diff --git a/.github/dependabot.yml b/.github/dependabot.yml index e577070f8f4b1..7ffe773060c4f 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -69,7 +69,7 @@ updates: - dependency-name: org.hibernate.validator:* - dependency-name: org.hibernate.search:* # Test dependencies - - dependency-name: net.sourceforge.htmlunit:htmlunit + - dependency-name: org.htmlunit:htmlunit - dependency-name: io.rest-assured:* - dependency-name: org.hamcrest:hamcrest - dependency-name: org.junit:junit-bom diff --git a/.github/workflows/preview-teardown.yml b/.github/workflows/preview-teardown.yml index 6782862321364..02cbc42b22df2 100644 --- a/.github/workflows/preview-teardown.yml +++ b/.github/workflows/preview-teardown.yml @@ -15,7 +15,7 @@ jobs: id: deploy run: npx surge teardown https://quarkus-pr-main-${{ github.event.number }}-preview.surge.sh --token ${{ secrets.SURGE_TOKEN }} || true - name: Update PR status comment - uses: actions-cool/maintain-one-comment@v3.1.1 + uses: actions-cool/maintain-one-comment@v3.2.0 with: token: ${{ secrets.GITHUB_TOKEN }} body: | diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml index 32208ac9fb5d6..c10cebf499923 100644 --- a/.github/workflows/preview.yml +++ b/.github/workflows/preview.yml @@ -96,7 +96,7 @@ jobs: id: deploy run: npx surge ./_site --domain https://quarkus-pr-main-${{ steps.pr.outputs.id }}-preview.surge.sh --token ${{ secrets.SURGE_TOKEN }} - name: Update PR status comment on success - uses: actions-cool/maintain-one-comment@v3.1.1 + uses: actions-cool/maintain-one-comment@v3.2.0 with: token: ${{ secrets.GITHUB_TOKEN }} body: | @@ -111,7 +111,7 @@ jobs: number: ${{ steps.pr.outputs.id }} - name: Update PR status comment on failure if: ${{ failure() }} - uses: actions-cool/maintain-one-comment@v3.1.1 + uses: actions-cool/maintain-one-comment@v3.2.0 with: token: ${{ secrets.GITHUB_TOKEN }} body: | diff --git a/.mvn/extensions.xml b/.mvn/extensions.xml index 44bda03e6155f..14d6835f2e102 100644 --- a/.mvn/extensions.xml +++ b/.mvn/extensions.xml @@ -2,7 +2,7 @@ com.gradle develocity-maven-extension - 1.21.2 + 1.21.4 com.gradle diff --git a/bom/application/pom.xml b/bom/application/pom.xml index 8363ef2462269..249a85cdee5eb 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -17,16 +17,16 @@ 2.0.2 1.78.1 1.0.2.5 - 1.0.18 + 1.0.19 5.0.0 3.0.2 - 3.1.8 + 3.2.0 1.3.2 1 1.1.6 2.1.5.Final 3.1.2.Final - 6.2.8.Final + 6.2.9.Final 0.33.0 0.2.4 0.1.15 @@ -35,7 +35,7 @@ 1.32.0-alpha 1.21.0-alpha 5.2.2.Final - 1.12.4 + 1.12.5 2.1.12 0.22.0 21.3 @@ -55,9 +55,9 @@ 4.1.0 4.0.0 3.10.0 - 2.8.3 + 2.8.4 6.3.0 - 4.5.1 + 4.5.2 2.1.0 1.0.13 3.0.1 @@ -92,7 +92,7 @@ 23.1.2 1.8.0 - 2.17.0 + 2.17.1 1.0.0.Final 3.14.0 1.17.0 @@ -101,17 +101,17 @@ bytebuddy.version (just below), hibernate-orm.version-for-documentation (in docs/pom.xml) and both hibernate-orm.version and antlr.version in build-parent/pom.xml WARNING again for diffs that don't provide enough context: when updating, see above --> - 6.5.0.Final - 1.14.12 + 6.5.2.Final + 1.14.15 6.0.6.Final 2.3.0.Final 8.0.1.Final 7.1.1.Final 7.0.1.Final - 2.3 + 2.4 8.0.0.Final - 8.13.2 + 8.13.4 2.2.21 2.2.5.Final 2.2.2.Final @@ -119,7 +119,8 @@ 2.0.0.Final 1.7.0.Final 1.0.1.Final - 2.4.1.Final + 2.4.2.Final + 2.1.4.SP1 3.6.1.Final 4.5.7 4.5.14 @@ -129,7 +130,7 @@ 2.3.2 2.2.224 42.7.3 - 3.3.3 + 3.4.0 8.3.0 12.6.1.jre11 1.6.7 @@ -139,13 +140,13 @@ 1.2.6 2.2 5.10.2 - 15.0.2.Final - 5.0.3.Final + 15.0.4.Final + 5.0.4.Final 3.1.5 4.1.108.Final 1.16.0 1.0.4 - 3.5.3.Final + 3.6.0.Final 2.6.0 3.7.0 1.8.0 @@ -155,18 +156,18 @@ 2.13.14 1.2.3 3.11.5 - 2.15.2 + 2.15.3 3.1.0 1.0.0 - 1.9.23 - 1.8.0 + 2.0.0 + 1.8.1 0.27.0 1.6.2 4.1.2 3.2.0 4.2.1 3.0.6.Final - 10.12.0 + 10.13.0 3.0.3 4.27.0 @@ -176,37 +177,37 @@ 4.11.1 1.8.0 0.34.1 - 3.25.9 + 3.25.10 0.3.0 - 4.13.0 + 4.13.1 5.2.SP7 2.1.SP2 5.4.Final 2.1.SP1 - 5.11.0 + 5.12.0 5.8.0 4.13.0 2.0.3.Final - 23.0.7 + 24.0.4 1.15.1 3.43.0 2.27.1 0.27.0 - 1.44.1 + 1.44.2 2.1 - 4.7.5 + 4.7.6 1.1.0 1.26.1 - 1.11.0 - 2.10.1 + 1.12.0 + 2.11.0 1.1.2.Final 2.23.1 - 1.3.0.Final + 1.3.1.Final 1.11.3 2.5.10.Final 0.1.18.Final - 1.19.7 - 3.3.5 + 1.19.8 + 3.3.6 2.0.0 1.4.5 @@ -216,7 +217,7 @@ 6.9.0.202403050737-r 0.15.0 - 9.37.3 + 9.39.1 0.9.6 0.0.6 0.1.3 @@ -2653,6 +2654,16 @@ quarkus-container-image-docker-deployment ${project.version} + + io.quarkus + quarkus-container-image-podman + ${project.version} + + + io.quarkus + quarkus-container-image-podman-deployment + ${project.version} + io.quarkus quarkus-container-image-jib @@ -2678,6 +2689,16 @@ quarkus-container-image-deployment ${project.version} + + io.quarkus + quarkus-container-image-docker-common + ${project.version} + + + io.quarkus + quarkus-container-image-docker-common-deployment + ${project.version} + io.quarkus quarkus-container-image @@ -4796,6 +4817,11 @@ pom + + org.jboss.marshalling + jboss-marshalling + ${jboss-marshalling.version} + org.jboss.threads jboss-threads diff --git a/bom/dev-ui/pom.xml b/bom/dev-ui/pom.xml index 68c3577523629..8c304dd392210 100644 --- a/bom/dev-ui/pom.xml +++ b/bom/dev-ui/pom.xml @@ -13,7 +13,7 @@ Dependency management for dev-ui. Importable by third party extension developers. - 24.3.11 + 24.3.13 3.1.3 4.0.5 3.1.3 @@ -21,17 +21,18 @@ 2.0.7 2.0.4 2.1.2 - 2.0.6 + 2.0.7 3.5.1 1.11.2 1.4.0 1.7.5 1.7.0 5.5.0 - 1.0.13 - 1.9.0 + 1.10.0 2.4.0 - + 1.0.16 + 1.0.0 + 2.15.3 17.7.2 8.0.1 @@ -268,6 +269,14 @@ runtime + + + org.mvnpm.at.mvnpm + qomponent + ${qomponent.version} + runtime + + org.mvnpm diff --git a/build-parent/pom.xml b/build-parent/pom.xml index 844e917e867ef..a997b9305eeac 100644 --- a/build-parent/pom.xml +++ b/build-parent/pom.xml @@ -20,10 +20,10 @@ 3.12.1 - 1.9.23 + 2.0.0 1.9.20 2.13.12 - 4.9.0 + 4.9.1 ${scala-maven-plugin.version} @@ -33,12 +33,12 @@ ${version.surefire.plugin} - 3.1.8 + 3.2.0 1.0.0 - 2.5.12 - 2.70.0 - 3.25.9 + 2.5.13 + 4.1.0 + 3.25.10 2.0.3.Final 6.0.1 @@ -107,7 +107,7 @@ - 23.0.7 + 24.0.4 19.0.3 quay.io/keycloak/keycloak:${keycloak.version} quay.io/keycloak/keycloak:${keycloak.wildfly.version}-legacy @@ -116,7 +116,7 @@ 3.25.3 - 3.5.4 + 3.6.0 7.3.0 @@ -137,7 +137,7 @@ 0.14.7 0.26.1 - 3.5.0 + 3.6.0 0.14.5 0.4.5 @@ -315,7 +315,7 @@ - net.sourceforge.htmlunit + org.htmlunit htmlunit ${htmlunit.version} @@ -653,7 +653,7 @@ org.apache.groovy groovy - 4.0.20 + 4.0.21 diff --git a/core/deployment/src/main/java/io/quarkus/deployment/Capability.java b/core/deployment/src/main/java/io/quarkus/deployment/Capability.java index 1a0ca32b00b75..0260ddb493d89 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/Capability.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/Capability.java @@ -101,6 +101,7 @@ public interface Capability { String METRICS = QUARKUS_PREFIX + ".metrics"; String CONTAINER_IMAGE_JIB = QUARKUS_PREFIX + ".container.image.jib"; String CONTAINER_IMAGE_DOCKER = QUARKUS_PREFIX + ".container.image.docker"; + String CONTAINER_IMAGE_PODMAN = QUARKUS_PREFIX + ".container.image.podman"; String CONTAINER_IMAGE_OPENSHIFT = QUARKUS_PREFIX + ".container.image.openshift"; String CONTAINER_IMAGE_BUILDPACK = QUARKUS_PREFIX + ".container.image.buildpack"; String HIBERNATE_ORM = QUARKUS_PREFIX + ".hibernate.orm"; diff --git a/core/deployment/src/main/java/io/quarkus/deployment/IsContainerRuntimeWorking.java b/core/deployment/src/main/java/io/quarkus/deployment/IsContainerRuntimeWorking.java new file mode 100644 index 0000000000000..4194d9d1a5628 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/IsContainerRuntimeWorking.java @@ -0,0 +1,168 @@ +package io.quarkus.deployment; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.BooleanSupplier; +import java.util.function.Supplier; + +import org.jboss.logging.Logger; + +import io.quarkus.deployment.console.StartupLogCompressor; + +public abstract class IsContainerRuntimeWorking implements BooleanSupplier { + private static final Logger LOGGER = Logger.getLogger(IsContainerRuntimeWorking.class); + private static final int DOCKER_HOST_CHECK_TIMEOUT = 1000; + + private final List strategies; + + protected IsContainerRuntimeWorking(List strategies) { + this.strategies = strategies; + } + + @Override + public boolean getAsBoolean() { + for (Strategy strategy : strategies) { + LOGGER.debugf("Checking container runtime Environment using strategy %s", strategy.getClass().getName()); + Result result = strategy.get(); + + if (result == Result.AVAILABLE) { + return true; + } + } + return false; + } + + protected interface Strategy extends Supplier { + + } + + /** + * Delegates the check to testcontainers (if the latter is on the classpath) + */ + protected static class TestContainersStrategy implements Strategy { + private final boolean silent; + + protected TestContainersStrategy(boolean silent) { + this.silent = silent; + } + + @Override + public Result get() { + // Testcontainers uses the Unreliables library to test if docker is started + // this runs in threads that start with 'ducttape' + StartupLogCompressor compressor = new StartupLogCompressor("Checking Docker Environment", Optional.empty(), null, + (s) -> s.getName().startsWith("ducttape")); + try { + Class dockerClientFactoryClass = Thread.currentThread().getContextClassLoader() + .loadClass("org.testcontainers.DockerClientFactory"); + Object dockerClientFactoryInstance = dockerClientFactoryClass.getMethod("instance").invoke(null); + + Class configurationClass = Thread.currentThread().getContextClassLoader() + .loadClass("org.testcontainers.utility.TestcontainersConfiguration"); + Object configurationInstance = configurationClass.getMethod("getInstance").invoke(null); + String oldReusePropertyValue = (String) configurationClass + .getMethod("getEnvVarOrUserProperty", String.class, String.class) + .invoke(configurationInstance, "testcontainers.reuse.enable", "false"); // use the default provided in TestcontainersConfiguration#environmentSupportsReuse + Method updateUserConfigMethod = configurationClass.getMethod("updateUserConfig", String.class, String.class); + // this will ensure that testcontainers does not start ryuk - see https://github.com/quarkusio/quarkus/issues/25852 for why this is important + updateUserConfigMethod.invoke(configurationInstance, "testcontainers.reuse.enable", "true"); + + // ensure that Testcontainers doesn't take previous failures into account + Class dockerClientProviderStrategyClass = Thread.currentThread().getContextClassLoader() + .loadClass("org.testcontainers.dockerclient.DockerClientProviderStrategy"); + Field failFastAlwaysField = dockerClientProviderStrategyClass.getDeclaredField("FAIL_FAST_ALWAYS"); + failFastAlwaysField.setAccessible(true); + AtomicBoolean failFastAlways = (AtomicBoolean) failFastAlwaysField.get(null); + failFastAlways.set(false); + + boolean isAvailable = (boolean) dockerClientFactoryClass.getMethod("isDockerAvailable") + .invoke(dockerClientFactoryInstance); + if (!isAvailable) { + compressor.closeAndDumpCaptured(); + } + + // restore the previous value + updateUserConfigMethod.invoke(configurationInstance, "testcontainers.reuse.enable", oldReusePropertyValue); + return isAvailable ? Result.AVAILABLE : Result.UNAVAILABLE; + } catch (ClassNotFoundException | NoSuchMethodException | InvocationTargetException | IllegalAccessException + | NoSuchFieldException e) { + if (!silent) { + compressor.closeAndDumpCaptured(); + LOGGER.debug("Unable to use Testcontainers to determine if Docker is working", e); + } + return Result.UNKNOWN; + } finally { + compressor.close(); + } + } + } + + /** + * Detection using a remote host socket + * We don't want to pull in the docker API here, so we just see if the DOCKER_HOST is set + * and if we can connect to it. + * We can't actually verify it is docker listening on the other end. + * Furthermore, this does not support Unix Sockets + */ + protected static class DockerHostStrategy implements Strategy { + private static final String UNIX_SCHEME = "unix"; + + @Override + public Result get() { + String dockerHost = System.getenv("DOCKER_HOST"); + + if (dockerHost == null) { + return Result.UNKNOWN; + } + + try { + URI dockerHostUri = new URI(dockerHost); + + if (UNIX_SCHEME.equals(dockerHostUri.getScheme())) { + // Java 11 does not support connecting to Unix sockets so for now let's use a naive approach + Path dockerSocketPath = Path.of(dockerHostUri.getPath()); + + if (Files.isWritable(dockerSocketPath)) { + return Result.AVAILABLE; + } else { + LOGGER.warnf( + "Unix socket defined in DOCKER_HOST %s is not writable, make sure Docker is running on the specified host", + dockerHost); + } + } else { + try (Socket s = new Socket()) { + s.connect(new InetSocketAddress(dockerHostUri.getHost(), dockerHostUri.getPort()), + DOCKER_HOST_CHECK_TIMEOUT); + return Result.AVAILABLE; + } catch (IOException e) { + LOGGER.warnf( + "Unable to connect to DOCKER_HOST URI %s, make sure Docker is running on the specified host", + dockerHost); + } + } + } catch (URISyntaxException | IllegalArgumentException e) { + LOGGER.warnf("Unable to parse DOCKER_HOST URI %s, it will be ignored for working Docker detection", + dockerHost); + } + + return Result.UNKNOWN; + } + } + + protected enum Result { + AVAILABLE, + UNAVAILABLE, + UNKNOWN + } +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/IsDockerWorking.java b/core/deployment/src/main/java/io/quarkus/deployment/IsDockerWorking.java index e739ca5a7c51f..1efd20d2da382 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/IsDockerWorking.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/IsDockerWorking.java @@ -3,175 +3,20 @@ import static io.quarkus.deployment.util.ContainerRuntimeUtil.ContainerRuntime.UNAVAILABLE; -import java.io.IOException; -import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.net.InetSocketAddress; -import java.net.Socket; -import java.net.URI; -import java.net.URISyntaxException; -import java.nio.file.Files; -import java.nio.file.Path; import java.util.List; -import java.util.Optional; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.function.BooleanSupplier; -import java.util.function.Supplier; -import org.jboss.logging.Logger; - -import io.quarkus.deployment.console.StartupLogCompressor; import io.quarkus.deployment.util.ContainerRuntimeUtil; -public class IsDockerWorking implements BooleanSupplier { - - private static final Logger LOGGER = Logger.getLogger(IsDockerWorking.class.getName()); - public static final int DOCKER_HOST_CHECK_TIMEOUT = 1000; - - private final List strategies; - +public class IsDockerWorking extends IsContainerRuntimeWorking { public IsDockerWorking() { this(false); } public IsDockerWorking(boolean silent) { - this.strategies = List.of(new TestContainersStrategy(silent), new DockerHostStrategy(), new DockerBinaryStrategy()); - } - - @Override - public boolean getAsBoolean() { - for (Strategy strategy : strategies) { - LOGGER.debugf("Checking Docker Environment using strategy %s", strategy.getClass().getName()); - Result result = strategy.get(); - if (result == Result.AVAILABLE) { - return true; - } - } - return false; - } - - private interface Strategy extends Supplier { - - } - - /** - * Delegates the check to testcontainers (if the latter is on the classpath) - */ - private static class TestContainersStrategy implements Strategy { - - private final boolean silent; - - private TestContainersStrategy(boolean silent) { - this.silent = silent; - } - - @Override - public Result get() { - // Testcontainers uses the Unreliables library to test if docker is started - // this runs in threads that start with 'ducttape' - StartupLogCompressor compressor = new StartupLogCompressor("Checking Docker Environment", Optional.empty(), null, - (s) -> s.getName().startsWith("ducttape")); - try { - Class dockerClientFactoryClass = Thread.currentThread().getContextClassLoader() - .loadClass("org.testcontainers.DockerClientFactory"); - Object dockerClientFactoryInstance = dockerClientFactoryClass.getMethod("instance").invoke(null); - - Class configurationClass = Thread.currentThread().getContextClassLoader() - .loadClass("org.testcontainers.utility.TestcontainersConfiguration"); - Object configurationInstance = configurationClass.getMethod("getInstance").invoke(null); - String oldReusePropertyValue = (String) configurationClass - .getMethod("getEnvVarOrUserProperty", String.class, String.class) - .invoke(configurationInstance, "testcontainers.reuse.enable", "false"); // use the default provided in TestcontainersConfiguration#environmentSupportsReuse - Method updateUserConfigMethod = configurationClass.getMethod("updateUserConfig", String.class, String.class); - // this will ensure that testcontainers does not start ryuk - see https://github.com/quarkusio/quarkus/issues/25852 for why this is important - updateUserConfigMethod.invoke(configurationInstance, "testcontainers.reuse.enable", "true"); - - // ensure that Testcontainers doesn't take previous failures into account - Class dockerClientProviderStrategyClass = Thread.currentThread().getContextClassLoader() - .loadClass("org.testcontainers.dockerclient.DockerClientProviderStrategy"); - Field failFastAlwaysField = dockerClientProviderStrategyClass.getDeclaredField("FAIL_FAST_ALWAYS"); - failFastAlwaysField.setAccessible(true); - AtomicBoolean failFastAlways = (AtomicBoolean) failFastAlwaysField.get(null); - failFastAlways.set(false); - - boolean isAvailable = (boolean) dockerClientFactoryClass.getMethod("isDockerAvailable") - .invoke(dockerClientFactoryInstance); - if (!isAvailable) { - compressor.closeAndDumpCaptured(); - } - - // restore the previous value - updateUserConfigMethod.invoke(configurationInstance, "testcontainers.reuse.enable", oldReusePropertyValue); - return isAvailable ? Result.AVAILABLE : Result.UNAVAILABLE; - } catch (ClassNotFoundException | NoSuchMethodException | InvocationTargetException | IllegalAccessException - | NoSuchFieldException e) { - if (!silent) { - compressor.closeAndDumpCaptured(); - LOGGER.debug("Unable to use Testcontainers to determine if Docker is working", e); - } - return Result.UNKNOWN; - } finally { - compressor.close(); - } - } - } - - /** - * Detection using a remote host socket - * We don't want to pull in the docker API here, so we just see if the DOCKER_HOST is set - * and if we can connect to it. - * We can't actually verify it is docker listening on the other end. - * Furthermore, this does not support Unix Sockets - */ - private static class DockerHostStrategy implements Strategy { - - private static final String UNIX_SCHEME = "unix"; - - @Override - public Result get() { - String dockerHost = System.getenv("DOCKER_HOST"); - - if (dockerHost == null) { - return Result.UNKNOWN; - } - - try { - URI dockerHostUri = new URI(dockerHost); - - if (UNIX_SCHEME.equals(dockerHostUri.getScheme())) { - // Java 11 does not support connecting to Unix sockets so for now let's use a naive approach - Path dockerSocketPath = Path.of(dockerHostUri.getPath()); - - if (Files.isWritable(dockerSocketPath)) { - return Result.AVAILABLE; - } else { - LOGGER.warnf( - "Unix socket defined in DOCKER_HOST %s is not writable, make sure Docker is running on the specified host", - dockerHost); - } - } else { - try (Socket s = new Socket()) { - s.connect(new InetSocketAddress(dockerHostUri.getHost(), dockerHostUri.getPort()), - DOCKER_HOST_CHECK_TIMEOUT); - return Result.AVAILABLE; - } catch (IOException e) { - LOGGER.warnf( - "Unable to connect to DOCKER_HOST URI %s, make sure Docker is running on the specified host", - dockerHost); - } - } - } catch (URISyntaxException | IllegalArgumentException e) { - LOGGER.warnf("Unable to parse DOCKER_HOST URI %s, it will be ignored for working Docker detection", - dockerHost); - } - - return Result.UNKNOWN; - } + super(List.of(new TestContainersStrategy(silent), new DockerHostStrategy(), new DockerBinaryStrategy())); } private static class DockerBinaryStrategy implements Strategy { - @Override public Result get() { if (ContainerRuntimeUtil.detectContainerRuntime(false) != UNAVAILABLE) { @@ -182,10 +27,4 @@ public Result get() { } } - - private enum Result { - AVAILABLE, - UNAVAILABLE, - UNKNOWN - } } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/IsPodmanWorking.java b/core/deployment/src/main/java/io/quarkus/deployment/IsPodmanWorking.java new file mode 100644 index 0000000000000..2a6fce41c656d --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/IsPodmanWorking.java @@ -0,0 +1,27 @@ +package io.quarkus.deployment; + +import static io.quarkus.deployment.util.ContainerRuntimeUtil.ContainerRuntime.UNAVAILABLE; + +import java.util.List; + +import io.quarkus.deployment.util.ContainerRuntimeUtil; + +public class IsPodmanWorking extends IsContainerRuntimeWorking { + public IsPodmanWorking() { + this(false); + } + + public IsPodmanWorking(boolean silent) { + super(List.of( + new TestContainersStrategy(silent), + new DockerHostStrategy(), + new PodmanBinaryStrategy())); + } + + private static class PodmanBinaryStrategy implements Strategy { + @Override + public Result get() { + return (ContainerRuntimeUtil.detectContainerRuntime(false) != UNAVAILABLE) ? Result.AVAILABLE : Result.UNKNOWN; + } + } +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/PodmanStatusProcessor.java b/core/deployment/src/main/java/io/quarkus/deployment/PodmanStatusProcessor.java new file mode 100644 index 0000000000000..1106a75e83835 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/PodmanStatusProcessor.java @@ -0,0 +1,12 @@ +package io.quarkus.deployment; + +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.builditem.LaunchModeBuildItem; +import io.quarkus.deployment.builditem.PodmanStatusBuildItem; + +public class PodmanStatusProcessor { + @BuildStep + PodmanStatusBuildItem isPodmanWorking(LaunchModeBuildItem launchMode) { + return new PodmanStatusBuildItem(new IsPodmanWorking(launchMode.getLaunchMode().isDevOrTest())); + } +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/builditem/ContainerRuntimeStatusBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/builditem/ContainerRuntimeStatusBuildItem.java new file mode 100644 index 0000000000000..1fc6684a9b0da --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/builditem/ContainerRuntimeStatusBuildItem.java @@ -0,0 +1,25 @@ +package io.quarkus.deployment.builditem; + +import io.quarkus.builder.item.SimpleBuildItem; +import io.quarkus.deployment.IsContainerRuntimeWorking; + +public abstract class ContainerRuntimeStatusBuildItem extends SimpleBuildItem { + private final IsContainerRuntimeWorking isContainerRuntimeWorking; + private Boolean cachedStatus; + + protected ContainerRuntimeStatusBuildItem(IsContainerRuntimeWorking isContainerRuntimeWorking) { + this.isContainerRuntimeWorking = isContainerRuntimeWorking; + } + + public boolean isContainerRuntimeAvailable() { + if (cachedStatus == null) { + synchronized (this) { + if (cachedStatus == null) { + cachedStatus = isContainerRuntimeWorking.getAsBoolean(); + } + } + } + + return cachedStatus; + } +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/builditem/DockerStatusBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/builditem/DockerStatusBuildItem.java index 9309683477926..aff9a5f305a90 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/builditem/DockerStatusBuildItem.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/builditem/DockerStatusBuildItem.java @@ -1,21 +1,17 @@ package io.quarkus.deployment.builditem; -import io.quarkus.builder.item.SimpleBuildItem; import io.quarkus.deployment.IsDockerWorking; -public final class DockerStatusBuildItem extends SimpleBuildItem { - - private final IsDockerWorking isDockerWorking; - private Boolean cachedStatus; - +public final class DockerStatusBuildItem extends ContainerRuntimeStatusBuildItem { public DockerStatusBuildItem(IsDockerWorking isDockerWorking) { - this.isDockerWorking = isDockerWorking; + super(isDockerWorking); } - public synchronized boolean isDockerAvailable() { - if (cachedStatus == null) { - cachedStatus = isDockerWorking.getAsBoolean(); - } - return cachedStatus; + /** + * @deprecated Use {@link #isContainerRuntimeAvailable()} instead + */ + @Deprecated(forRemoval = true) + public boolean isDockerAvailable() { + return isContainerRuntimeAvailable(); } } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/builditem/PodmanStatusBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/builditem/PodmanStatusBuildItem.java new file mode 100644 index 0000000000000..be1546fc3b881 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/builditem/PodmanStatusBuildItem.java @@ -0,0 +1,9 @@ +package io.quarkus.deployment.builditem; + +import io.quarkus.deployment.IsPodmanWorking; + +public final class PodmanStatusBuildItem extends ContainerRuntimeStatusBuildItem { + public PodmanStatusBuildItem(IsPodmanWorking isPodmanWorking) { + super(isPodmanWorking); + } +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/configuration/ClassLoadingConfig.java b/core/deployment/src/main/java/io/quarkus/deployment/configuration/ClassLoadingConfig.java index fd90753000775..9f6c050e6bf05 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/configuration/ClassLoadingConfig.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/configuration/ClassLoadingConfig.java @@ -5,6 +5,7 @@ import java.util.Optional; import java.util.Set; +import io.quarkus.runtime.annotations.ConfigDocMapKey; import io.quarkus.runtime.annotations.ConfigItem; import io.quarkus.runtime.annotations.ConfigPhase; import io.quarkus.runtime.annotations.ConfigRoot; @@ -71,6 +72,7 @@ public class ClassLoadingConfig { * Note that for technical reasons this is not supported when running with JBang. */ @ConfigItem + @ConfigDocMapKey("group-id:artifact-id") public Map> removedResources; } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/IsolatedRemoteDevModeMain.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/IsolatedRemoteDevModeMain.java index 6f54adaa0dd11..1ec369cd140fb 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/dev/IsolatedRemoteDevModeMain.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/IsolatedRemoteDevModeMain.java @@ -253,14 +253,19 @@ private Closeable doConnect() { @Override public Map apply(Set fileNames) { Map ret = new HashMap<>(); - for (String i : fileNames) { + for (String filename : fileNames) { try { - Path resolvedPath = appRoot.resolve(i); + Path resolvedPath = appRoot.resolve(filename); + // Ensure that path stays inside appRoot + if (!resolvedPath.startsWith(appRoot)) { + log.errorf("Attempted to access %s outside of %s", resolvedPath, appRoot); + continue; + } if (!Files.isDirectory(resolvedPath)) { - ret.put(i, Files.readAllBytes(resolvedPath)); + ret.put(filename, Files.readAllBytes(resolvedPath)); } } catch (IOException e) { - log.error("Failed to read file " + i, e); + log.error("Failed to read file " + filename, e); } } return ret; diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/RuntimeUpdatesProcessor.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/RuntimeUpdatesProcessor.java index de09063b148bb..46b3ba3b1862c 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/dev/RuntimeUpdatesProcessor.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/RuntimeUpdatesProcessor.java @@ -398,7 +398,11 @@ public Throwable getDeploymentProblem() { @Override public void setRemoteProblem(Throwable throwable) { compileProblem = throwable; - getCompileOutput().setMessage(throwable.getMessage()); + if (throwable == null) { + getCompileOutput().setMessage(null); + } else { + getCompileOutput().setMessage(throwable.getMessage()); + } } private StatusLine getCompileOutput() { @@ -561,9 +565,7 @@ public boolean doScan(boolean userInitiated, boolean forceRestart) { return true; } else if (!filesChanged.isEmpty()) { try { - for (Consumer> consumer : noRestartChangesConsumers) { - consumer.accept(filesChanged); - } + notifyExtensions(filesChanged); hotReloadProblem = null; getCompileOutput().setMessage(null); } catch (Throwable t) { @@ -585,6 +587,30 @@ public boolean doScan(boolean userInitiated, boolean forceRestart) { } } + /** + * This notifies registered extensions of "no-restart" changed files. + * + * @param noRestartChangedFiles the Set of changed files + */ + public void notifyExtensions(Set noRestartChangedFiles) { + if (lastStartIndex == null) { + // we don't notify extensions if the application never started + return; + } + scanLock.lock(); + codeGenLock.lock(); + try { + + for (Consumer> consumer : noRestartChangesConsumers) { + consumer.accept(noRestartChangedFiles); + } + } finally { + scanLock.unlock(); + codeGenLock.unlock(); + } + + } + public boolean instrumentationEnabled() { if (instrumentationEnabled != null) { return instrumentationEnabled; diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestConfig.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestConfig.java index f60c75fadc376..5c82f54fe447d 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestConfig.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestConfig.java @@ -5,6 +5,7 @@ import java.util.Map; import java.util.Optional; +import io.quarkus.runtime.annotations.ConfigDocMapKey; import io.quarkus.runtime.annotations.ConfigGroup; import io.quarkus.runtime.annotations.ConfigItem; import io.quarkus.runtime.annotations.ConfigRoot; @@ -186,6 +187,7 @@ public class TestConfig { * Additional environment variables to be set in the process that {@code @QuarkusIntegrationTest} launches. */ @ConfigItem + @ConfigDocMapKey("environment-variable-name") Map env; /** @@ -292,18 +294,21 @@ public static class Container { * Set additional ports to be exposed when @QuarkusIntegration needs to launch the application in a container. */ @ConfigItem + @ConfigDocMapKey("host-port") Map additionalExposedPorts; /** * A set of labels to add to the launched container */ @ConfigItem + @ConfigDocMapKey("label-name") Map labels; /** * A set of volume mounts to add to the launched container */ @ConfigItem + @ConfigDocMapKey("host-path") Map volumeMounts; } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/index/IndexingUtil.java b/core/deployment/src/main/java/io/quarkus/deployment/index/IndexingUtil.java index c3be954fafd0e..824774535ea83 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/index/IndexingUtil.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/index/IndexingUtil.java @@ -1,8 +1,5 @@ package io.quarkus.deployment.index; -import static io.quarkus.bootstrap.classloading.JarClassPathElement.JAVA_VERSION; -import static io.quarkus.bootstrap.classloading.JarClassPathElement.META_INF_VERSIONS; - import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; @@ -43,9 +40,23 @@ public class IndexingUtil { public static final String JANDEX_INDEX = "META-INF/jandex.idx"; + private static final String META_INF_VERSIONS = "META-INF/versions/"; + + private static final int JAVA_VERSION; + // At least Jandex 2.1 is needed private static final int REQUIRED_INDEX_VERSION = 8; + static { + int version = 8; + try { + version = Runtime.version().version().get(0); + } catch (Exception e) { + //version 8 + } + JAVA_VERSION = version; + } + public static Index indexJar(Path path) throws IOException { return indexJar(path.toFile(), Collections.emptySet()); } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/NativeConfig.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/NativeConfig.java index badd6a7c63343..310090fd7c1ee 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/NativeConfig.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/NativeConfig.java @@ -219,6 +219,14 @@ public interface NativeConfig { */ Optional pie(); + /** + * Generate instructions for a specific machine type. Defaults to {@code x86-64-v3} on AMD64 and {@code armv8-a} on AArch64. + * Use {@code compatibility} for best compatibility, or {@code native} for best performance if a native executable is + * deployed on the same machine or on a machine with the same CPU features. + * A list of all available machine types is available by executing {@code native-image -march=list} + */ + Optional march(); + /** * If this build is done using a remote docker daemon. */ diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/PackageConfig.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/PackageConfig.java index b46a36750613c..57c9f32039113 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/PackageConfig.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/PackageConfig.java @@ -9,6 +9,7 @@ import io.quarkus.maven.dependency.GACT; import io.quarkus.runtime.annotations.ConfigDocDefault; +import io.quarkus.runtime.annotations.ConfigDocMapKey; import io.quarkus.runtime.annotations.ConfigGroup; import io.quarkus.runtime.annotations.ConfigRoot; import io.smallrye.config.ConfigMapping; @@ -246,6 +247,7 @@ interface ManifestConfig { * quarkus.package.jar.manifest.attributes."Entry-key1"=Value1 * quarkus.package.jar.manifest.attributes."Entry-key2"=Value2 */ + @ConfigDocMapKey("attribute-name") Map attributes(); /** @@ -254,6 +256,7 @@ interface ManifestConfig { * quarkus.package.jar.manifest.sections."Section-Name"."Entry-Key1"=Value1 * quarkus.package.jar.manifest.sections."Section-Name"."Entry-Key2"=Value2 */ + @ConfigDocMapKey("section-name") Map> sections(); } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildStep.java index 5f863d2301c03..bf483fae4621f 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildStep.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildStep.java @@ -906,6 +906,9 @@ public NativeImageInvokerInfo build() { if (nativeConfig.enableVmInspection()) { addExperimentalVMOption(nativeImageArgs, "-H:+AllowVMInspection"); } + if (nativeConfig.march().isPresent()) { + nativeImageArgs.add("-march=" + nativeConfig.march().get()); + } List monitoringOptions = new ArrayList<>(); monitoringOptions.add(NativeConfig.MonitoringOption.HEAPDUMP); diff --git a/core/deployment/src/main/java/io/quarkus/deployment/recording/BytecodeRecorderImpl.java b/core/deployment/src/main/java/io/quarkus/deployment/recording/BytecodeRecorderImpl.java index ab33e0d209d4b..651d21b79eddc 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/recording/BytecodeRecorderImpl.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/recording/BytecodeRecorderImpl.java @@ -1200,6 +1200,7 @@ public void prepare(MethodContext context) { Set handledProperties = new HashSet<>(); Property[] desc = PropertyUtils.getPropertyDescriptors(param); + FieldsHelper fieldsHelper = new FieldsHelper(param.getClass()); for (Property i : desc) { if (!i.getDeclaringClass().getPackageName().startsWith("java.")) { // check if the getter is ignored @@ -1207,13 +1208,9 @@ public void prepare(MethodContext context) { continue; } // check if the matching field is ignored - try { - Field field = param.getClass().getDeclaredField(i.getName()); - if (ignoreField(field)) { - continue; - } - } catch (NoSuchFieldException ignored) { - + Field field = fieldsHelper.getDeclaredField(i.getName()); + if (field != null && ignoreField(field)) { + continue; } } Integer ctorParamIndex = constructorParamNameMap.remove(i.name); diff --git a/core/deployment/src/main/java/io/quarkus/deployment/recording/FieldsHelper.java b/core/deployment/src/main/java/io/quarkus/deployment/recording/FieldsHelper.java new file mode 100644 index 0000000000000..9c14b226b2f87 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/recording/FieldsHelper.java @@ -0,0 +1,23 @@ +package io.quarkus.deployment.recording; + +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.Map; + +final class FieldsHelper { + + private final Map fields; + + public FieldsHelper(final Class aClass) { + final Field[] declaredFields = aClass.getDeclaredFields(); + this.fields = new HashMap<>(declaredFields.length); + for (Field field : declaredFields) { + this.fields.put(field.getName(), field); + } + } + + //Returns the matching Field, or null if not existing + public Field getDeclaredField(final String name) { + return fields.get(name); + } +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/recording/PropertyUtils.java b/core/deployment/src/main/java/io/quarkus/deployment/recording/PropertyUtils.java index 25959c196c0a2..da8594e88766a 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/recording/PropertyUtils.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/recording/PropertyUtils.java @@ -43,7 +43,7 @@ public Property[] apply(Class type) { if (existingGetter == null || existingGetter.getReturnType().isAssignableFrom(i.getReturnType())) { getters.put(name, i); } - } else if (i.getName().startsWith("is") && i.getName().length() > 3 && i.getParameterCount() == 0 + } else if (i.getName().startsWith("is") && i.getName().length() > 2 && i.getParameterCount() == 0 && (i.getReturnType() == boolean.class || i.getReturnType() == Boolean.class)) { String name = Character.toLowerCase(i.getName().charAt(2)) + i.getName().substring(3); isGetters.put(name, i); diff --git a/core/deployment/src/main/java/io/quarkus/deployment/recording/RecordingAnnotationsUtil.java b/core/deployment/src/main/java/io/quarkus/deployment/recording/RecordingAnnotationsUtil.java index fe118e7c30a5c..5260c28d3b053 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/recording/RecordingAnnotationsUtil.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/recording/RecordingAnnotationsUtil.java @@ -4,7 +4,6 @@ import java.lang.reflect.AccessibleObject; import java.lang.reflect.Constructor; import java.util.HashSet; -import java.util.List; import java.util.ServiceLoader; import java.util.Set; @@ -13,8 +12,8 @@ final class RecordingAnnotationsUtil { - static final List> IGNORED_PROPERTY_ANNOTATIONS; - static final List> RECORDABLE_CONSTRUCTOR_ANNOTATIONS; + private static final Class[] IGNORED_PROPERTY_ANNOTATIONS; + private static final Class[] RECORDABLE_CONSTRUCTOR_ANNOTATIONS; static { Set> ignoredPropertyAnnotations = new HashSet<>(); @@ -33,30 +32,32 @@ final class RecordingAnnotationsUtil { } } - IGNORED_PROPERTY_ANNOTATIONS = List.copyOf(ignoredPropertyAnnotations); - RECORDABLE_CONSTRUCTOR_ANNOTATIONS = List.copyOf(recordableConstructorAnnotations); + IGNORED_PROPERTY_ANNOTATIONS = ignoredPropertyAnnotations.toArray(new Class[0]); + RECORDABLE_CONSTRUCTOR_ANNOTATIONS = recordableConstructorAnnotations.toArray(new Class[0]); } private RecordingAnnotationsUtil() { } - static boolean isIgnored(AccessibleObject object) { - for (int i = 0; i < IGNORED_PROPERTY_ANNOTATIONS.size(); i++) { - Class annotation = IGNORED_PROPERTY_ANNOTATIONS.get(i); - if (object.isAnnotationPresent(annotation)) { - return true; - } - } - return false; + static boolean isIgnored(final AccessibleObject object) { + return annotationsMatch(object.getDeclaredAnnotations(), IGNORED_PROPERTY_ANNOTATIONS); } - static boolean isRecordableConstructor(Constructor ctor) { - for (int i = 0; i < RECORDABLE_CONSTRUCTOR_ANNOTATIONS.size(); i++) { - Class annotation = RECORDABLE_CONSTRUCTOR_ANNOTATIONS.get(i); - if (ctor.isAnnotationPresent(annotation)) { - return true; + static boolean isRecordableConstructor(final Constructor ctor) { + return annotationsMatch(ctor.getDeclaredAnnotations(), RECORDABLE_CONSTRUCTOR_ANNOTATIONS); + } + + private static boolean annotationsMatch( + final Annotation[] declaredAnnotations, + final Class[] typesToCheck) { + for (Class annotation : typesToCheck) { + for (Annotation declaredAnnotation : declaredAnnotations) { + if (declaredAnnotation.annotationType().equals(annotation)) { + return true; + } } } return false; } + } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/util/ContainerRuntimeUtil.java b/core/deployment/src/main/java/io/quarkus/deployment/util/ContainerRuntimeUtil.java index ff6452d4199f2..10d33810a02a0 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/util/ContainerRuntimeUtil.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/util/ContainerRuntimeUtil.java @@ -5,6 +5,8 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.TimeUnit; import java.util.function.Predicate; import java.util.regex.Pattern; @@ -44,13 +46,21 @@ public static ContainerRuntime detectContainerRuntime() { return detectContainerRuntime(true); } + public static ContainerRuntime detectContainerRuntime(List orderToCheckRuntimes) { + return detectContainerRuntime(true, orderToCheckRuntimes); + } + public static ContainerRuntime detectContainerRuntime(boolean required) { + return detectContainerRuntime(required, List.of(ContainerRuntime.DOCKER, ContainerRuntime.PODMAN)); + } + + public static ContainerRuntime detectContainerRuntime(boolean required, List orderToCheckRuntimes) { ContainerRuntime containerRuntime = loadContainerRuntimeFromSystemProperty(); if (containerRuntime != null) { return containerRuntime; } - final ContainerRuntime containerRuntimeEnvironment = getContainerRuntimeEnvironment(); + final ContainerRuntime containerRuntimeEnvironment = getContainerRuntimeEnvironment(orderToCheckRuntimes); if (containerRuntimeEnvironment == ContainerRuntime.UNAVAILABLE) { storeContainerRuntimeInSystemProperty(ContainerRuntime.UNAVAILABLE); @@ -70,47 +80,58 @@ public static ContainerRuntime detectContainerRuntime(boolean required) { return containerRuntime; } - private static ContainerRuntime getContainerRuntimeEnvironment() { + private static ContainerRuntime getContainerRuntimeEnvironment(List orderToCheckRuntimes) { // Docker version 19.03.14, build 5eb3275d40 - String dockerVersionOutput; - boolean dockerAvailable; + // Check if Podman is installed // podman version 2.1.1 - String podmanVersionOutput; - boolean podmanAvailable; + var runtimesToCheck = new ArrayList<>(orderToCheckRuntimes.stream().distinct().toList()); + runtimesToCheck.retainAll(List.of(ContainerRuntime.DOCKER, ContainerRuntime.PODMAN)); if (CONTAINER_EXECUTABLE != null) { - if (CONTAINER_EXECUTABLE.trim().equalsIgnoreCase("docker")) { - dockerVersionOutput = getVersionOutputFor(ContainerRuntime.DOCKER); - dockerAvailable = dockerVersionOutput.contains("Docker version"); - if (dockerAvailable) { - return ContainerRuntime.DOCKER; - } - } - if (CONTAINER_EXECUTABLE.trim().equalsIgnoreCase("podman")) { - podmanVersionOutput = getVersionOutputFor(ContainerRuntime.PODMAN); - podmanAvailable = PODMAN_PATTERN.matcher(podmanVersionOutput).matches(); - if (podmanAvailable) { - return ContainerRuntime.PODMAN; - } + var runtime = runtimesToCheck.stream() + .filter(containerRuntime -> CONTAINER_EXECUTABLE.trim() + .equalsIgnoreCase(containerRuntime.getExecutableName())) + .findFirst() + .filter(r -> { + var versionOutput = getVersionOutputFor(r); + + return switch (r) { + case DOCKER, DOCKER_ROOTLESS -> versionOutput.contains("Docker version"); + case PODMAN, PODMAN_ROOTLESS -> PODMAN_PATTERN.matcher(versionOutput).matches(); + default -> false; + }; + }); + + if (runtime.isPresent()) { + return runtime.get(); + } else { + log.warn("quarkus.native.container-runtime config property must be set to either podman or docker " + + "and the executable must be available. Ignoring it."); } - log.warn("quarkus.native.container-runtime config property must be set to either podman or docker " + - "and the executable must be available. Ignoring it."); } - dockerVersionOutput = getVersionOutputFor(ContainerRuntime.DOCKER); - dockerAvailable = dockerVersionOutput.contains("Docker version"); - if (dockerAvailable) { - // Check if "docker" is an alias to "podman" - if (PODMAN_PATTERN.matcher(dockerVersionOutput).matches()) { - return ContainerRuntime.PODMAN; + for (var runtime : runtimesToCheck) { + var versionOutput = getVersionOutputFor(runtime); + + switch (runtime) { + case DOCKER: + case DOCKER_ROOTLESS: + var dockerAvailable = versionOutput.contains("Docker version"); + if (dockerAvailable) { + // Check if "docker" is an alias to podman + return PODMAN_PATTERN.matcher(versionOutput).matches() ? ContainerRuntime.PODMAN + : ContainerRuntime.DOCKER; + } + break; + + case PODMAN: + case PODMAN_ROOTLESS: + if (PODMAN_PATTERN.matcher(versionOutput).matches()) { + return ContainerRuntime.PODMAN; + } + break; } - return ContainerRuntime.DOCKER; - } - podmanVersionOutput = getVersionOutputFor(ContainerRuntime.PODMAN); - podmanAvailable = PODMAN_PATTERN.matcher(podmanVersionOutput).matches(); - if (podmanAvailable) { - return ContainerRuntime.PODMAN; } return ContainerRuntime.UNAVAILABLE; diff --git a/core/deployment/src/main/java/io/quarkus/deployment/util/WebJarUtil.java b/core/deployment/src/main/java/io/quarkus/deployment/util/WebJarUtil.java deleted file mode 100644 index 9beb7259aabcc..0000000000000 --- a/core/deployment/src/main/java/io/quarkus/deployment/util/WebJarUtil.java +++ /dev/null @@ -1,551 +0,0 @@ -package io.quarkus.deployment.util; - -import java.io.ByteArrayInputStream; -import java.io.Closeable; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.net.URL; -import java.nio.channels.FileChannel; -import java.nio.channels.FileLock; -import java.nio.charset.StandardCharsets; -import java.nio.file.DirectoryStream; -import java.nio.file.FileVisitResult; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.SimpleFileVisitor; -import java.nio.file.attribute.BasicFileAttributes; -import java.util.Arrays; -import java.util.Enumeration; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Scanner; -import java.util.Set; -import java.util.jar.JarEntry; -import java.util.jar.JarFile; - -import org.eclipse.microprofile.config.Config; -import org.eclipse.microprofile.config.ConfigProvider; -import org.jboss.logging.Logger; - -import io.quarkus.bootstrap.util.IoUtils; -import io.quarkus.builder.Version; -import io.quarkus.deployment.builditem.LaunchModeBuildItem; -import io.quarkus.deployment.builditem.LiveReloadBuildItem; -import io.quarkus.deployment.pkg.builditem.CurateOutcomeBuildItem; -import io.quarkus.maven.dependency.ResolvedDependency; -import io.quarkus.paths.PathCollection; -import io.quarkus.runtime.LaunchMode; -import io.smallrye.common.io.jar.JarFiles; - -/** - * Utility for Web resource related operations - * - * @deprecated Use WebJarBuildItem and WebJarResultsBuildItem instead. - */ -@Deprecated(forRemoval = true) -public class WebJarUtil { - - private static final Logger LOG = Logger.getLogger(WebJarUtil.class); - - private static final String TMP_DIR = System.getProperty("java.io.tmpdir"); - private static final String CUSTOM_MEDIA_FOLDER = "META-INF/branding/"; - private static final List IGNORE_LIST = Arrays.asList("logo.png", "favicon.ico", "style.css"); - private static final String CSS = ".css"; - private static final String SNAPSHOT_VERSION = "-SNAPSHOT"; - - private WebJarUtil() { - } - - public static void hotReloadBrandingChanges(CurateOutcomeBuildItem curateOutcomeBuildItem, - LaunchModeBuildItem launchMode, - ResolvedDependency resourcesArtifact, - Set hotReloadChanges) throws IOException { - - hotReloadBrandingChanges(curateOutcomeBuildItem, launchMode, resourcesArtifact, hotReloadChanges, true); - } - - public static void hotReloadBrandingChanges(CurateOutcomeBuildItem curateOutcomeBuildItem, - LaunchModeBuildItem launchMode, - ResolvedDependency resourcesArtifact, - Set hotReloadChanges, - boolean useDefaultQuarkusBranding) throws IOException { - - if (launchMode.getLaunchMode().equals(LaunchMode.DEVELOPMENT) && hotReloadChanges != null - && !hotReloadChanges.isEmpty()) { - for (String changedResource : hotReloadChanges) { - if (changedResource.startsWith(CUSTOM_MEDIA_FOLDER)) { - ClassLoader classLoader = WebJarUtil.class.getClassLoader(); - final ResolvedDependency userApplication = curateOutcomeBuildItem.getApplicationModel().getAppArtifact(); - String fileName = changedResource.replace(CUSTOM_MEDIA_FOLDER, ""); - // a branding file has changed ! - String modulename = getModuleOverrideName(resourcesArtifact, fileName); - if (IGNORE_LIST.contains(fileName) - && isOverride(userApplication.getResolvedPaths(), classLoader, fileName, modulename)) { - Path deploymentPath = createResourcesDirectory(userApplication, resourcesArtifact); - Path filePath = deploymentPath.resolve(fileName); - try (InputStream initialOverride = getOverride(userApplication.getResolvedPaths(), classLoader, - fileName, modulename, useDefaultQuarkusBranding); - InputStream override = insertVariables(userApplication, initialOverride, - fileName)) { - if (override != null) { - createFile(override, filePath); - } - } - } - } - } - } - } - - public static Path copyResourcesForDevOrTest(LiveReloadBuildItem liveReloadBuildItem, - CurateOutcomeBuildItem curateOutcomeBuildItem, - LaunchModeBuildItem launchMode, - ResolvedDependency resourcesArtifact, - String rootFolderInJar) - throws IOException { - return copyResourcesForDevOrTest(liveReloadBuildItem, curateOutcomeBuildItem, launchMode, resourcesArtifact, - rootFolderInJar, true, false); - } - - @Deprecated - public static Path copyResourcesForDevOrTest(LiveReloadBuildItem liveReloadBuildItem, - CurateOutcomeBuildItem curateOutcomeBuildItem, - LaunchModeBuildItem launchMode, - ResolvedDependency resourcesArtifact, - String rootFolderInJar, boolean useDefaultQuarkusBranding) - throws IOException { - return copyResourcesForDevOrTest(liveReloadBuildItem, curateOutcomeBuildItem, launchMode, resourcesArtifact, - rootFolderInJar, useDefaultQuarkusBranding, false); - } - - public static Path copyResourcesForDevOrTest(LiveReloadBuildItem liveReloadBuildItem, - CurateOutcomeBuildItem curateOutcomeBuildItem, - LaunchModeBuildItem launchMode, - ResolvedDependency resourcesArtifact, - String rootFolderInJar, - boolean useDefaultQuarkusBranding, boolean onlyCopyNonArtifactFiles) - throws IOException { - - rootFolderInJar = normalizeRootFolderInJar(rootFolderInJar); - final ResolvedDependency userApplication = curateOutcomeBuildItem.getApplicationModel().getAppArtifact(); - - Path deploymentPath = createResourcesDirectory(userApplication, resourcesArtifact); - - // Clean if not in dev mode or if the resources jar is a snapshot version - if (!launchMode.getLaunchMode().equals(LaunchMode.DEVELOPMENT) - || resourcesArtifact.getVersion().contains(SNAPSHOT_VERSION) - || (launchMode.getLaunchMode().equals(LaunchMode.DEVELOPMENT) && !liveReloadBuildItem.isLiveReload())) { - - IoUtils.createOrEmptyDir(deploymentPath); - } - - if (isEmpty(deploymentPath)) { - ClassLoader classLoader = WebJarUtil.class.getClassLoader(); - for (Path p : resourcesArtifact.getResolvedPaths()) { - File artifactFile = p.toFile(); - if (artifactFile.isFile()) { - // case of a jar file - try (JarFile jarFile = JarFiles.create(artifactFile)) { - Enumeration entries = jarFile.entries(); - while (entries.hasMoreElements()) { - JarEntry entry = entries.nextElement(); - if (entry.getName().startsWith(rootFolderInJar)) { - String fileName = entry.getName().replace(rootFolderInJar, ""); - Path filePath = deploymentPath.resolve(fileName); - if (entry.isDirectory()) { - Files.createDirectories(filePath); - } else { - boolean overrideFileCreated = false; - String modulename = getModuleOverrideName(resourcesArtifact, fileName); - if (IGNORE_LIST.contains(fileName) - && isOverride(userApplication.getResolvedPaths(), classLoader, fileName, - modulename)) { - try (InsertVariableResult overrideInsertResult = insertVariablesWithResult( - userApplication, - getOverride(userApplication.getResolvedPaths(), classLoader, - fileName, modulename, useDefaultQuarkusBranding), - fileName)) { - if (overrideInsertResult.inputStream != null) { - createFile(overrideInsertResult.inputStream, filePath); // Override (either developer supplied or Quarkus) - overrideFileCreated = true; - } - } - } - - if (!overrideFileCreated) { - try (InputStream entryInputStream = jarFile.getInputStream(entry); - InsertVariableResult entryInsertResult = insertVariablesWithResult( - userApplication, - entryInputStream, - fileName)) { - if (!onlyCopyNonArtifactFiles || entryInsertResult.changed) { - createFile(entryInsertResult.inputStream, filePath); - } - } - } - } - } - } - } - } else { - // case of a directory - Path rootFolderToCopy = p.resolve(rootFolderInJar); - if (!Files.isDirectory(rootFolderToCopy)) { - continue; - } - - Files.walkFileTree(rootFolderToCopy, new SimpleFileVisitor() { - @Override - public FileVisitResult preVisitDirectory(final Path dir, - final BasicFileAttributes attrs) throws IOException { - Files.createDirectories(deploymentPath.resolve(rootFolderToCopy.relativize(dir))); - return FileVisitResult.CONTINUE; - } - - @Override - public FileVisitResult visitFile(final Path file, - final BasicFileAttributes attrs) throws IOException { - String fileName = rootFolderToCopy.relativize(file).toString(); - Path targetFilePath = deploymentPath.resolve(rootFolderToCopy.relativize(file)); - - String modulename = getModuleOverrideName(resourcesArtifact, fileName); - boolean overrideFileCreated = false; - if (IGNORE_LIST.contains(fileName) - && isOverride(userApplication.getResolvedPaths(), classLoader, fileName, modulename)) { - try (InsertVariableResult insertVariableResult = insertVariablesWithResult(userApplication, - getOverride(userApplication.getResolvedPaths(), classLoader, - fileName, modulename, useDefaultQuarkusBranding), - fileName)) { - if (insertVariableResult.inputStream != null) { - overrideFileCreated = true; - createFile(insertVariableResult.inputStream, targetFilePath); // Override (either developer supplied or Quarkus) - } - } - } - - if (!overrideFileCreated) { - try (InsertVariableResult insertVariableResult = insertVariablesWithResult(userApplication, - Files.newInputStream(file), fileName)) { - if (!onlyCopyNonArtifactFiles || insertVariableResult.changed) { - createFile(insertVariableResult.inputStream, targetFilePath); // Override (either developer supplied or Quarkus) - } - } - } - - return FileVisitResult.CONTINUE; - } - }); - } - } - } - return deploymentPath; - } - - public static Map copyResourcesForProduction(CurateOutcomeBuildItem curateOutcomeBuildItem, - ResolvedDependency artifact, - String rootFolderInJar) throws IOException { - return copyResourcesForProduction(curateOutcomeBuildItem, artifact, rootFolderInJar, true); - } - - public static Map copyResourcesForProduction(CurateOutcomeBuildItem curateOutcomeBuildItem, - ResolvedDependency artifact, - String rootFolderInJar, - boolean useDefaultQuarkusBranding) throws IOException { - rootFolderInJar = normalizeRootFolderInJar(rootFolderInJar); - final ResolvedDependency userApplication = curateOutcomeBuildItem.getApplicationModel().getAppArtifact(); - - Map map = new HashMap<>(); - //we are including in a production artifact - //just stick the files in the generated output - //we could do this for dev mode as well but then we need to extract them every time - - ClassLoader classLoader = WebJarUtil.class.getClassLoader(); - for (Path p : artifact.getResolvedPaths()) { - File artifactFile = p.toFile(); - try (JarFile jarFile = JarFiles.create(artifactFile)) { - Enumeration entries = jarFile.entries(); - while (entries.hasMoreElements()) { - JarEntry entry = entries.nextElement(); - if (entry.getName().startsWith(rootFolderInJar) && !entry.isDirectory()) { - String filename = entry.getName().replace(rootFolderInJar, ""); - try (InputStream inputStream = insertVariables(userApplication, jarFile.getInputStream(entry), - filename)) { - byte[] content = null; - String modulename = getModuleOverrideName(artifact, filename); - if (IGNORE_LIST.contains(filename) - && isOverride(userApplication.getResolvedPaths(), classLoader, filename, modulename)) { - try (InputStream initialOverride = getOverride(userApplication.getResolvedPaths(), classLoader, - filename, modulename, useDefaultQuarkusBranding); - InputStream resourceAsStream = insertVariables(userApplication, initialOverride, - filename)) { - if (resourceAsStream != null) { - content = IoUtil.readBytes(resourceAsStream); // Override (either developer supplied or Quarkus) - } - } - } - if (content == null) { - content = FileUtil.readFileContents(inputStream); - } - - map.put(filename, content); - } - } - } - } - } - return map; - } - - public static void updateFile(Path original, byte[] newContent) throws IOException { - try (ByteArrayInputStream bais = new ByteArrayInputStream(newContent)) { - createFile(bais, original); - } - } - - public static void updateUrl(Path original, String path, String lineStartsWith, String format) throws IOException { - String content = Files.readString(original); - String result = updateUrl(content, path, lineStartsWith, format); - if (result != null && !result.equals(content)) { - Files.write(original, result.getBytes(StandardCharsets.UTF_8)); - } - } - - public static String updateUrl(String original, String path, String lineStartsWith, String format) { - try (Scanner scanner = new Scanner(original)) { - while (scanner.hasNextLine()) { - String line = scanner.nextLine(); - if (line.trim().startsWith(lineStartsWith)) { - String newLine = String.format(format, path); - return original.replace(line.trim(), newLine); - } - } - } - - return original; - } - - public static ResolvedDependency getAppArtifact(CurateOutcomeBuildItem curateOutcomeBuildItem, String groupId, - String artifactId) { - for (ResolvedDependency dep : curateOutcomeBuildItem.getApplicationModel().getDependencies()) { - if (dep.getArtifactId().equals(artifactId) - && dep.getGroupId().equals(groupId)) { - return dep; - } - } - throw new RuntimeException("Could not find artifact " + groupId + ":" + artifactId - + " among the application dependencies"); - } - - private static void copyFile(ResolvedDependency appArtifact, Path file, String fileName, Path targetFilePath) - throws IOException { - InputStream providedContent = pathToStream(file).orElse(null); - if (providedContent != null) { - Files.copy(insertVariables(appArtifact, providedContent, fileName), targetFilePath); - } - } - - private static String getModuleOverrideName(ResolvedDependency artifact, String filename) { - String type = filename.substring(filename.lastIndexOf(".")); - return artifact.getArtifactId() + type; - } - - private static InputStream getOverride(PathCollection paths, ClassLoader classLoader, String filename, String modulename, - boolean useDefaultQuarkusBranding) { - - // First check if the developer supplied the files - InputStream overrideStream = getCustomOverride(paths, filename, modulename); - if (overrideStream == null && useDefaultQuarkusBranding) { - // Else check if Quarkus has a default branding - overrideStream = getQuarkusOverride(classLoader, filename, modulename); - } - return overrideStream; - } - - private static InputStream insertVariables(ResolvedDependency appArtifact, InputStream is, - String filename) - throws IOException { - return insertVariablesWithResult(appArtifact, is, filename).inputStream; - } - - private static InsertVariableResult insertVariablesWithResult(ResolvedDependency appArtifact, InputStream is, - String filename) - throws IOException { - // Allow replacement of certain values in css - if (filename.endsWith(CSS)) { - Config c = ConfigProvider.getConfig(); - - String applicationName = c.getOptionalValue("quarkus.application.name", String.class) - .orElse(appArtifact.getArtifactId()); - - String applicationVersion = c.getOptionalValue("quarkus.application.version", String.class) - .orElse(appArtifact.getVersion()); - - String oldContents = new String(IoUtil.readBytes(is)); - String contents = replaceHeaderVars(oldContents, applicationName, applicationVersion); - - contents = contents.replace("{applicationHeader}", getUIHeader(c, applicationName, applicationVersion)); - - is = new ByteArrayInputStream(contents.getBytes()); - return new InsertVariableResult(is, - contents.length() != oldContents.length() || !contents.equals(oldContents)); - } - - return new InsertVariableResult(is, false); - } - - private static String getUIHeader(Config c, String applicationName, String applicationVersion) { - String applicationHeader = c.getOptionalValue("quarkus.application.ui-header", String.class).orElse(""); - return replaceHeaderVars(applicationHeader, applicationName, applicationVersion); - } - - private static String replaceHeaderVars(String contents, String applicationName, String applicationVersion) { - contents = contents.replace("{applicationName}", applicationName); - contents = contents.replace("{applicationVersion}", applicationVersion); - contents = contents.replace("{quarkusVersion}", Version.getVersion()); - return contents; - } - - private static InputStream getCustomOverride(PathCollection paths, String filename, String modulename) { - // Check if the developer supplied the files - Path customOverridePath = getCustomOverridePath(paths, filename, modulename); - if (customOverridePath != null) { - return pathToStream(customOverridePath).orElse(null); - } - return null; - } - - private static Path getCustomOverridePath(PathCollection paths, String filename, String modulename) { - - // First check if the developer supplied the files - for (Path root : paths) { - Path customModuleOverride = root.resolve(CUSTOM_MEDIA_FOLDER + modulename); - if (Files.exists(customModuleOverride)) { - return customModuleOverride; - } - Path customOverride = root.resolve(CUSTOM_MEDIA_FOLDER + filename); - if (Files.exists(customOverride)) { - return customOverride; - } - } - return null; - } - - private static InputStream getQuarkusOverride(ClassLoader classLoader, String filename, String modulename) { - // Allow quarkus per module override - InputStream stream = classLoader.getResourceAsStream(CUSTOM_MEDIA_FOLDER + modulename); - if (stream != null) { - return stream; - } - - return classLoader.getResourceAsStream(CUSTOM_MEDIA_FOLDER + filename); - } - - private static boolean isOverride(PathCollection paths, ClassLoader classLoader, String filename, String modulename) { - // Check if quarkus override this. - return isQuarkusOverride(classLoader, filename, modulename) || isCustomOverride(paths, filename, modulename); - } - - private static boolean isQuarkusOverride(ClassLoader classLoader, String filename, String modulename) { - // Check if quarkus override this. - return fileExistInClasspath(classLoader, CUSTOM_MEDIA_FOLDER + modulename) - || fileExistInClasspath(classLoader, CUSTOM_MEDIA_FOLDER + filename); - } - - private static boolean isCustomOverride(PathCollection paths, String filename, String modulename) { - for (Path root : paths) { - Path customModuleOverride = root.resolve(CUSTOM_MEDIA_FOLDER + modulename); - if (Files.exists(customModuleOverride)) { - return true; - } - Path customOverride = root.resolve(CUSTOM_MEDIA_FOLDER + filename); - return Files.exists(customOverride); - } - - return false; - } - - private static boolean fileExistInClasspath(ClassLoader classLoader, String filename) { - URL u = classLoader.getResource(filename); - return u != null; - } - - private static Optional pathToStream(Path path) { - if (Files.exists(path)) { - try { - return Optional.of(Files.newInputStream(path)); - } catch (IOException ex) { - LOG.warn("Could not read override file [" + path + "] - " + ex.getMessage()); - } - } - return Optional.empty(); - } - - private static void createFile(InputStream source, Path targetFile) throws IOException { - FileLock lock = null; - FileOutputStream fos = null; - try { - fos = new FileOutputStream(targetFile.toString()); - FileChannel channel = fos.getChannel(); - lock = channel.tryLock(); - if (lock != null) { - IoUtils.copy(fos, source); - } - } finally { - if (lock != null) { - lock.release(); - } - if (fos != null) { - fos.close(); - } - } - } - - public static Path createResourcesDirectory(ResolvedDependency userApplication, ResolvedDependency resourcesArtifact) { - try { - Path path = Paths.get(TMP_DIR, "quarkus", userApplication.getGroupId(), - userApplication.getArtifactId(), resourcesArtifact.getGroupId(), - resourcesArtifact.getArtifactId(), resourcesArtifact.getVersion()); - - Files.createDirectories(path); - return path; - } catch (IOException ex) { - throw new RuntimeException(ex); - } - } - - private static boolean isEmpty(final Path directory) throws IOException { - try (DirectoryStream dirStream = Files.newDirectoryStream(directory)) { - return !dirStream.iterator().hasNext(); - } - } - - private static String normalizeRootFolderInJar(String rootFolderInJar) { - if (rootFolderInJar.endsWith("/")) { - return rootFolderInJar; - } - - return rootFolderInJar + "/"; - } - - private static class InsertVariableResult implements Closeable { - final InputStream inputStream; - final boolean changed; - - public InsertVariableResult(InputStream inputStream, boolean changed) { - this.inputStream = inputStream; - this.changed = changed; - } - - @Override - public void close() throws IOException { - if (inputStream != null) { - inputStream.close(); - } - } - } -} diff --git a/core/deployment/src/test/java/io/quarkus/deployment/pkg/TestNativeConfig.java b/core/deployment/src/test/java/io/quarkus/deployment/pkg/TestNativeConfig.java index 2cf219198444a..0a0f28227bb26 100644 --- a/core/deployment/src/test/java/io/quarkus/deployment/pkg/TestNativeConfig.java +++ b/core/deployment/src/test/java/io/quarkus/deployment/pkg/TestNativeConfig.java @@ -150,6 +150,11 @@ public Optional pie() { return Optional.empty(); } + @Override + public Optional march() { + return Optional.empty(); + } + @Override public boolean remoteContainerBuild() { return false; diff --git a/core/deployment/src/test/java/io/quarkus/runner/classloading/DirectoryClassPathElementTestCase.java b/core/deployment/src/test/java/io/quarkus/runner/classloading/DirectoryClassPathElementTestCase.java deleted file mode 100644 index 01cebd094c6a6..0000000000000 --- a/core/deployment/src/test/java/io/quarkus/runner/classloading/DirectoryClassPathElementTestCase.java +++ /dev/null @@ -1,66 +0,0 @@ -package io.quarkus.runner.classloading; - -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Arrays; -import java.util.HashSet; -import java.util.Locale; -import java.util.Set; - -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; - -import io.quarkus.bootstrap.classloading.ClassPathResource; -import io.quarkus.bootstrap.classloading.DirectoryClassPathElement; -import io.quarkus.deployment.util.FileUtil; - -public class DirectoryClassPathElementTestCase { - - static Path root; - - @BeforeAll - public static void before() throws Exception { - root = Files.createTempDirectory("quarkus-test"); - Files.write(root.resolve("a.txt"), "A file".getBytes(StandardCharsets.UTF_8)); - Files.write(root.resolve("b.txt"), "another file".getBytes(StandardCharsets.UTF_8)); - Files.createDirectories(root.resolve("foo")); - Files.write(root.resolve("foo/sub.txt"), "subdir file".getBytes(StandardCharsets.UTF_8)); - } - - @AfterAll - public static void after() throws Exception { - FileUtil.deleteDirectory(root); - } - - @Test - public void testGetAllResources() { - DirectoryClassPathElement f = new DirectoryClassPathElement(root, true); - Set res = f.getProvidedResources(); - Assertions.assertEquals(4, res.size()); - Assertions.assertEquals(new HashSet<>(Arrays.asList("a.txt", "b.txt", "foo", "foo/sub.txt")), res); - } - - @Test - public void testGetResource() { - DirectoryClassPathElement f = new DirectoryClassPathElement(root, true); - ClassPathResource res = f.getResource("foo/sub.txt"); - Assertions.assertNotNull(res); - Assertions.assertEquals("subdir file", new String(res.getData(), StandardCharsets.UTF_8)); - } - - @Test - public void testInvalidPath() { - final String invalidPath; - if (System.getProperty("os.name").toLowerCase(Locale.ENGLISH).contains("windows")) { - invalidPath = "D:\\*"; - } else { - invalidPath = "hello\u0000world"; - } - final DirectoryClassPathElement classPathElement = new DirectoryClassPathElement(root, true); - final ClassPathResource resource = classPathElement.getResource(invalidPath); - Assertions.assertNull(resource, "DirectoryClassPathElement wasn't expected to return a resource for an invalid path"); - } -} diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigDocItemFinder.java b/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigDocItemFinder.java index 7e550d58ad3ff..a79513b5c46bd 100644 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigDocItemFinder.java +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigDocItemFinder.java @@ -24,7 +24,6 @@ import static io.quarkus.annotation.processor.generate_doc.DocGeneratorUtil.getKnownGenericType; import static io.quarkus.annotation.processor.generate_doc.DocGeneratorUtil.hyphenate; import static io.quarkus.annotation.processor.generate_doc.DocGeneratorUtil.hyphenateEnumValue; -import static io.quarkus.annotation.processor.generate_doc.DocGeneratorUtil.stringifyType; import static java.util.Collections.emptyList; import static java.util.stream.Collectors.toList; import static javax.lang.model.element.Modifier.ABSTRACT; @@ -292,23 +291,21 @@ private List recursivelyFindConfigItems(Element element, String r // FIXME: this is super dodgy: we should check the type!! if (typeArguments.size() == 2) { type = getType(typeArguments.get(1)); + List additionalNames; + if (unnamedMapKey) { + additionalNames = List + .of(name + String.format(NAMED_MAP_CONFIG_ITEM_FORMAT, configDocMapKey)); + } else { + name += String.format(NAMED_MAP_CONFIG_ITEM_FORMAT, configDocMapKey); + additionalNames = emptyList(); + } if (isConfigGroup(type)) { - List additionalNames; - if (unnamedMapKey) { - additionalNames = List - .of(name + String.format(NAMED_MAP_CONFIG_ITEM_FORMAT, configDocMapKey)); - } else { - name += String.format(NAMED_MAP_CONFIG_ITEM_FORMAT, configDocMapKey); - additionalNames = emptyList(); - } List groupConfigItems = readConfigGroupItems(configPhase, rootName, name, additionalNames, type, configSection, true, generateSeparateConfigGroupDocsFiles, configMapping); DocGeneratorUtil.appendConfigItemsIntoExistingOnes(configDocItems, groupConfigItems); continue; } else { - type = BACK_TICK + stringifyType(declaredType) + BACK_TICK; - configDocKey.setPassThroughMap(true); configDocKey.setWithinAMap(true); } } else { diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigDocKey.java b/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigDocKey.java index f4044599f8441..7c021425621eb 100644 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigDocKey.java +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigDocKey.java @@ -22,7 +22,6 @@ final public class ConfigDocKey implements ConfigDocElement, Comparable acceptedValues; private boolean optional; private boolean list; - private boolean passThroughMap; private boolean withinAConfigGroup; // if a key is "quarkus.kubernetes.part-of", then the value of this would be "kubernetes" private String topLevelGrouping; @@ -167,14 +166,6 @@ public void setDocMapKey(String docMapKey) { this.docMapKey = docMapKey; } - public boolean isPassThroughMap() { - return passThroughMap; - } - - public void setPassThroughMap(boolean passThroughMap) { - this.passThroughMap = passThroughMap; - } - public boolean isWithinAConfigGroup() { return withinAConfigGroup; } @@ -231,7 +222,6 @@ public boolean equals(Object o) { return withinAMap == that.withinAMap && optional == that.optional && list == that.list && - passThroughMap == that.passThroughMap && withinAConfigGroup == that.withinAConfigGroup && Objects.equals(type, that.type) && Objects.equals(key, that.key) && @@ -247,7 +237,7 @@ public boolean equals(Object o) { @Override public int hashCode() { return Objects.hash(type, key, configDoc, withinAMap, defaultValue, javaDocSiteLink, docMapKey, configPhase, - acceptedValues, optional, list, passThroughMap, withinAConfigGroup, topLevelGrouping); + acceptedValues, optional, list, withinAConfigGroup, topLevelGrouping); } @Override @@ -264,7 +254,6 @@ public String toString() { ", acceptedValues=" + acceptedValues + ", optional=" + optional + ", list=" + list + - ", passThroughMap=" + passThroughMap + ", withinAConfigGroup=" + withinAConfigGroup + ", topLevelGrouping='" + topLevelGrouping + '\'' + '}'; diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/SummaryTableDocFormatter.java b/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/SummaryTableDocFormatter.java index dd3756daa3bfd..ce604d33eb1de 100644 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/SummaryTableDocFormatter.java +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/SummaryTableDocFormatter.java @@ -111,8 +111,7 @@ public void format(Writer writer, ConfigDocKey configDocKey) throws IOException String required = configDocKey.isOptional() || !defaultValue.isEmpty() ? "" : "required icon:exclamation-circle[title=Configuration property is required]"; String key = configDocKey.getKey(); - String configKeyAnchor = configDocKey.isPassThroughMap() ? getAnchor(key + Constants.DASH + configDocKey.getDocMapKey()) - : getAnchor(key); + String configKeyAnchor = getAnchor(key); String anchor = anchorPrefix + configKeyAnchor; StringBuilder keys = new StringBuilder(); diff --git a/core/runtime/src/main/java/io/quarkus/runtime/ResettableSystemProperties.java b/core/runtime/src/main/java/io/quarkus/runtime/ResettableSystemProperties.java new file mode 100644 index 0000000000000..80fa859ebf0b3 --- /dev/null +++ b/core/runtime/src/main/java/io/quarkus/runtime/ResettableSystemProperties.java @@ -0,0 +1,47 @@ +package io.quarkus.runtime; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +/** + * Utility that allows for setting system properties when it's created and resetting them when it's closed. + * This is meant to be used in try-with-resources statements + */ +public class ResettableSystemProperties implements AutoCloseable { + + private final Map toRestore; + + public ResettableSystemProperties(Map toSet) { + Objects.requireNonNull(toSet); + if (toSet.isEmpty()) { + toRestore = Collections.emptyMap(); + return; + } + toRestore = new HashMap<>(); + for (var entry : toSet.entrySet()) { + String oldValue = System.setProperty(entry.getKey(), entry.getValue()); + toRestore.put(entry.getKey(), oldValue); + } + } + + public static ResettableSystemProperties of(String name, String value) { + return new ResettableSystemProperties(Map.of(name, value)); + } + + public static ResettableSystemProperties empty() { + return new ResettableSystemProperties(Collections.emptyMap()); + } + + @Override + public void close() { + for (var entry : toRestore.entrySet()) { + if (entry.getValue() != null) { + System.setProperty(entry.getKey(), entry.getValue()); + } else { + System.clearProperty(entry.getKey()); + } + } + } +} diff --git a/core/runtime/src/test/java/io/quarkus/runtime/ResettableSystemPropertiesTest.java b/core/runtime/src/test/java/io/quarkus/runtime/ResettableSystemPropertiesTest.java new file mode 100644 index 0000000000000..82e4d03ce5692 --- /dev/null +++ b/core/runtime/src/test/java/io/quarkus/runtime/ResettableSystemPropertiesTest.java @@ -0,0 +1,42 @@ +package io.quarkus.runtime; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Map; + +import org.junit.jupiter.api.Test; + +class ResettableSystemPropertiesTest { + + @Test + public void happyPath() { + System.setProperty("prop1", "val1"); + assertThat(System.getProperty("prop1")).isEqualTo("val1"); + try (var ignored = new ResettableSystemProperties( + Map.of("prop1", "val11", "prop2", "val2"))) { + assertThat(System.getProperty("prop1")).isEqualTo("val11"); + assertThat(System.getProperty("prop2")).isEqualTo("val2"); + } + assertThat(System.getProperty("prop1")).isEqualTo("val1"); + assertThat(System.getProperties()).doesNotContainKey("prop2"); + } + + @Test + public void exceptionThrown() { + System.setProperty("prop1", "val1"); + int initCount = System.getProperties().size(); + assertThat(System.getProperty("prop1")).isEqualTo("val1"); + try (var ignored = new ResettableSystemProperties( + Map.of("prop1", "val11", "prop2", "val2"))) { + assertThat(System.getProperty("prop1")).isEqualTo("val11"); + assertThat(System.getProperty("prop2")).isEqualTo("val2"); + + throw new RuntimeException("dummy"); + } catch (Exception ignored) { + + } + assertThat(System.getProperty("prop1")).isEqualTo("val1"); + assertThat(System.getProperties()).doesNotContainKey("prop2"); + assertThat(System.getProperties().size()).isEqualTo(initCount); + } +} diff --git a/devtools/bom-descriptor-json/pom.xml b/devtools/bom-descriptor-json/pom.xml index a9ec968e653b5..c7e2908451a7f 100644 --- a/devtools/bom-descriptor-json/pom.xml +++ b/devtools/bom-descriptor-json/pom.xml @@ -382,6 +382,19 @@ + + io.quarkus + quarkus-container-image-docker-common + ${project.version} + pom + test + + + * + * + + + io.quarkus quarkus-container-image-jib @@ -408,6 +421,19 @@ + + io.quarkus + quarkus-container-image-podman + ${project.version} + pom + test + + + * + * + + + io.quarkus quarkus-core diff --git a/devtools/cli/src/main/java/io/quarkus/cli/build/GradleRunner.java b/devtools/cli/src/main/java/io/quarkus/cli/build/GradleRunner.java index eab5511cb5170..afe7c34c796ab 100644 --- a/devtools/cli/src/main/java/io/quarkus/cli/build/GradleRunner.java +++ b/devtools/cli/src/main/java/io/quarkus/cli/build/GradleRunner.java @@ -204,7 +204,7 @@ public BuildCommandArgs prepareAction(String action, BuildOptions buildOptions, if (buildOptions.buildNative) { args.add("-Dquarkus.native.enabled=true"); - args.add("-Dquarkus.jar.enabled=false"); + args.add("-Dquarkus.package.jar.enabled=false"); } if (buildOptions.skipTests()) { setSkipTests(args); diff --git a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/extension/QuarkusPluginExtension.java b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/extension/QuarkusPluginExtension.java index 718a53f8798f9..c4c543fee145b 100644 --- a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/extension/QuarkusPluginExtension.java +++ b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/extension/QuarkusPluginExtension.java @@ -270,10 +270,14 @@ public ListProperty getCachingRelevantProperties() { } public void set(String name, @Nullable String value) { - quarkusBuildProperties.put(String.format("quarkus.%s", name), value); + quarkusBuildProperties.put(addQuarkusBuildPropertyPrefix(name), value); } - public void set(String name, Property value) { - quarkusBuildProperties.put(String.format("quarkus.%s", name), value); + public void set(String name, Provider value) { + quarkusBuildProperties.put(addQuarkusBuildPropertyPrefix(name), value); + } + + private String addQuarkusBuildPropertyPrefix(String name) { + return String.format("quarkus.%s", name); } } diff --git a/devtools/gradle/gradle-application-plugin/src/test/java/io/quarkus/gradle/extension/QuarkusExtensionTest.java b/devtools/gradle/gradle-application-plugin/src/test/java/io/quarkus/gradle/extension/QuarkusExtensionTest.java index 922f37a7bd82b..68668a71071d6 100644 --- a/devtools/gradle/gradle-application-plugin/src/test/java/io/quarkus/gradle/extension/QuarkusExtensionTest.java +++ b/devtools/gradle/gradle-application-plugin/src/test/java/io/quarkus/gradle/extension/QuarkusExtensionTest.java @@ -1,6 +1,8 @@ package io.quarkus.gradle.extension; import static io.quarkus.gradle.QuarkusPlugin.EXTENSION_NAME; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; import org.gradle.api.Project; import org.gradle.testfixtures.ProjectBuilder; @@ -15,4 +17,16 @@ public void extensionInstantiates() { QuarkusPluginExtension extension = project.getExtensions().create(EXTENSION_NAME, QuarkusPluginExtension.class, project); } + + @Test + void prefixesBuildProperty() { + Project project = ProjectBuilder.builder().build(); + project.getPluginManager().apply("java"); + QuarkusPluginExtension extension = project.getExtensions() + .create(EXTENSION_NAME, QuarkusPluginExtension.class, project); + + extension.set("test.args", "value"); + + assertThat(extension.getQuarkusBuildProperties().get()).containsExactly(entry("quarkus.test.args", "value")); + } } diff --git a/devtools/gradle/gradle/libs.versions.toml b/devtools/gradle/gradle/libs.versions.toml index c320a4e60e504..61cab662a4b13 100644 --- a/devtools/gradle/gradle/libs.versions.toml +++ b/devtools/gradle/gradle/libs.versions.toml @@ -2,8 +2,8 @@ plugin-publish = "1.2.1" # updating Kotlin here makes QuarkusPluginTest > shouldNotFailOnProjectDependenciesWithoutMain(Path) fail -kotlin = "1.9.24" -smallrye-config = "3.7.1" +kotlin = "2.0.0" +smallrye-config = "3.8.2" junit5 = "5.10.2" assertj = "3.25.3" diff --git a/devtools/gradle/settings.gradle.kts b/devtools/gradle/settings.gradle.kts index 799510aa4e2fb..ba308861ddeaf 100644 --- a/devtools/gradle/settings.gradle.kts +++ b/devtools/gradle/settings.gradle.kts @@ -1,5 +1,5 @@ plugins { - id("com.gradle.develocity") version "3.17.3" + id("com.gradle.develocity") version "3.17.4" } develocity { diff --git a/docs/pom.xml b/docs/pom.xml index bc89de3357558..bec1fb8c038d4 100644 --- a/docs/pom.xml +++ b/docs/pom.xml @@ -398,6 +398,19 @@ + + io.quarkus + quarkus-container-image-docker-common-deployment + ${project.version} + pom + test + + + * + * + + + io.quarkus quarkus-container-image-jib-deployment @@ -424,6 +437,19 @@ + + io.quarkus + quarkus-container-image-podman-deployment + ${project.version} + pom + test + + + * + * + + + io.quarkus quarkus-core-deployment diff --git a/docs/src/main/asciidoc/amqp.adoc b/docs/src/main/asciidoc/amqp.adoc index d36d6391cb059..b61117c1d8e44 100644 --- a/docs/src/main/asciidoc/amqp.adoc +++ b/docs/src/main/asciidoc/amqp.adoc @@ -151,9 +151,10 @@ Quarkus has built-in capabilities to deal with JSON AMQP messages. [NOTE] .@RegisterForReflection ==== -The `@RegisterForReflection` annotation instructs Quarkus to include the class (including fields and methods) when building the native executable. -This will be useful later when we run the applications as native executables inside containers. -Without, the native compilation would remove the fields and methods during the dead-code elimination phase. +The `@RegisterForReflection` annotation instructs Quarkus to keep the class, its fields, and methods when creating a native executable. +This is crucial when we later run our applications as native executables within containers. +Without this annotation, the native compilation process would discard the fields and methods during the dead-code elimination phase, which would lead to runtime errors. +More details about the `@RegisterForReflection` annotation can be found on the xref:writing-native-applications-tips.adoc#registerForReflection[native application tips] page. ==== == Sending quote request diff --git a/docs/src/main/asciidoc/cache.adoc b/docs/src/main/asciidoc/cache.adoc index c927e03a3cd9b..b27b6eba003fc 100644 --- a/docs/src/main/asciidoc/cache.adoc +++ b/docs/src/main/asciidoc/cache.adoc @@ -1075,3 +1075,4 @@ When you encounter this error, you can easily fix it by adding the following ann <1> It is an array, so you can register several cache implementations in one go if your configuration requires several of them. This annotation will register the cache implementation classes for reflection and this will include the classes into the native executable. +More details about the `@RegisterForReflection` annotation can be found on the xref:writing-native-applications-tips.adoc#registerForReflection[native application tips] page. \ No newline at end of file diff --git a/docs/src/main/asciidoc/cdi-reference.adoc b/docs/src/main/asciidoc/cdi-reference.adoc index b05bd50da4e29..29f1938ead089 100644 --- a/docs/src/main/asciidoc/cdi-reference.adoc +++ b/docs/src/main/asciidoc/cdi-reference.adoc @@ -1069,6 +1069,47 @@ public class NoopAsyncObserverExceptionHandler implements AsyncObserverException } ---- +[[reactive_pitfalls]] +== Pitfalls with Reactive Programming + +CDI is a purely synchronous framework. +Its notion of asynchrony is very limited and based solely on thread pools and thread offloading. +Therefore, there is a number of pitfalls when using CDI together with reactive programming. + +=== Detecting When Blocking Is Allowed + +The `io.quarkus.runtime.BlockingOperationControl#isBlockingAllowed()` method can be used to detect whether blocking is allowed on the current thread. +When it is not, and you need to perform a blocking operation, you have to offload it to another thread. +The easiest way is to use the `Vertx.executeBlocking()` method: + +[source,java] +---- +import io.quarkus.runtime.BlockingOperationControl; + +@ApplicationScoped +public class MyBean { + @Inject + Vertx vertx; + + @PostConstruct + void init() { + if (BlockingOperationControl.isBlockingAllowed()) { + somethingThatBlocks(); + } else { + vertx.executeBlocking(() -> { + somethingThatBlocks(); + return null; + }); + } + } + + void somethingThatBlocks() { + // use the file system or JDBC, call a REST service, etc. + Thread.sleep(5000); + } +} +---- + [[build_time_apis]] == Build Time Extensions diff --git a/docs/src/main/asciidoc/container-image.adoc b/docs/src/main/asciidoc/container-image.adoc index 2ec5a802577ca..b08fcf1ab843f 100644 --- a/docs/src/main/asciidoc/container-image.adoc +++ b/docs/src/main/asciidoc/container-image.adoc @@ -6,14 +6,15 @@ https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc = Container Images include::_attributes.adoc[] :categories: cloud -:summary: Learn how to build and push container images with Jib, OpenShift or Docker as part of the Quarkus build. +:summary: Learn how to build and push container images with Jib, OpenShift, Docker, or Podman as part of the Quarkus build. :topics: devops,cloud -:extensions: io.quarkus:quarkus-container-image-openshift,io.quarkus:quarkus-container-image-jib,io.quarkus:quarkus-container-image-docker,io.quarkus:quarkus-container-image-buildpack +:extensions: io.quarkus:quarkus-container-image-openshift,io.quarkus:quarkus-container-image-jib,io.quarkus:quarkus-container-image-docker,io.quarkus:quarkus-container-image-podman,io.quarkus:quarkus-container-image-buildpack Quarkus provides extensions for building (and pushing) container images. Currently, it supports: - <<#jib,Jib>> - <<#docker,Docker>> +- <<#podman,Podman>> - <<#openshift,OpenShift>> - <<#buildpack,Buildpack>> @@ -120,6 +121,16 @@ The `quarkus-container-image-docker` extension is capable of https://docs.docker NOTE: `docker buildx build` ONLY supports https://docs.docker.com/engine/reference/commandline/buildx_build/#load[loading the result of a build] to `docker images` when building for a single platform. Therefore, if you specify more than one argument in the `quarkus.docker.buildx.platform` property, the resulting images will not be loaded into `docker images`. If `quarkus.docker.buildx.platform` is omitted or if only a single platform is specified, it will then be loaded into `docker images`. +[[podman]] +=== Podman + +The extension `quarkus-container-image-podman` uses https://podman.io/[Podman] and the generated `Dockerfiles` under `src/main/docker` in order to perform container builds. + +To use this feature, add the following extension to your project. + +:add-extension-extensions: container-image-podman +include::{includes}/devtools/extension-add.adoc[] + [[openshift]] === OpenShift @@ -204,7 +215,7 @@ NOTE: If no registry is set (using `quarkus.container-image.registry`) then `doc It does not make sense to use multiple extension as part of the same build. When multiple container image extensions are present, an error will be raised to inform the user. The user can either remove the unneeded extensions or select one using `application.properties`. -For example, if both `container-image-docker` and `container-image-openshift` are present and the user needs to use `container-image-docker`: +For example, if both `container-image-docker` and `container-image-podman` are present and the user needs to use `container-image-docker`: [source,properties] ---- @@ -255,6 +266,13 @@ In addition to the generic container image options, the `container-image-docker` include::{generated-dir}/config/quarkus-container-image-docker.adoc[opts=optional, leveloffset=+1] +[[PodmanOptions]] +=== Podman Options + +In addition to the generic container image options, the `container-image-podman` also provides the following options: + +include::{generated-dir}/config/quarkus-container-image-podman.adoc[opts=optional, leveloffset=+1] + === OpenShift Options In addition to the generic container image options, the `container-image-openshift` also provides the following options: diff --git a/docs/src/main/asciidoc/datasource.adoc b/docs/src/main/asciidoc/datasource.adoc index a56046cb64efd..8ced678291ef0 100644 --- a/docs/src/main/asciidoc/datasource.adoc +++ b/docs/src/main/asciidoc/datasource.adoc @@ -101,7 +101,9 @@ For more information about pool size adjustment properties, see the <>, +but <> might not. +. Make sure your database server is configured to enable XA. +. Enable XA support explicitly for each relevant datasource by setting +<> to `xa`. + +Using XA, a rollback in one datasource will trigger a rollback in every other datasource enrolled in the transaction. + +[NOTE] +==== +XA transactions on reactive datasources are not supported at the moment. +==== + +[NOTE] +==== +If your transaction involves other, non-datasource resources, +keep in mind *those* resources might not support XA transactions, +or might require additional configuration. +==== + +If XA cannot be enabled for one of your datasources: + +* Be aware that enabling XA for all datasources _except one_ (and only one) is still supported +through https://www.narayana.io/docs/project/index.html#d5e857[Last Resource Commit Optimization (LRCO)]. +* If you do not need a rollback for one datasource to trigger a rollback for other datasources, +consider splitting your code into multiple transactions. +To that end, use xref:transaction.adoc#programmatic-approach[`QuarkusTransaction.requiringNew()`]/xref:transaction.adoc#declarative-approach[`@Transactional(REQUIRES_NEW)`] (preferably) +or xref:transaction.adoc#legacy-api-approach[`UserTransaction`] (for more complex use cases). + +[CAUTION] +==== +As a last resort, and for compatibility with Quarkus 3.8 and earlier, +you may allow unsafe transaction handling across multiple non-XA datasources +by setting `quarkus.transaction-manager.unsafe-multiple-last-resources` to `allow`. + +With this property set to `allow`, a transaction rollback +could possibly be applied to only some of the non-XA datasources, +with other non-XA datasources having already committed their changes, +leaving your overall system in an inconsistent state. + +Alternatively, you can allow the same unsafe behavior, +but with warnings when it is taken advantage of: + +* setting the property to `warn-each` +would result in logging a warning on *each* offending transaction. +* setting the property to `warn-first` +would result in logging a warning on the *first* offending transaction. + +We do not recommend using this configuration property, +and we plan to remove it in the future, +so you should plan fixing your application accordingly. +If you think your use case of this feature is valid and this option should be kept around, +open an issue in the https://github.com/quarkusio/quarkus/issues/new?assignees=&labels=kind%2Fenhancement&projects=&template=feature_request.yml[Quarkus tracker] +explaining why. +==== + == Datasource integrations === Datasource health check @@ -606,6 +688,7 @@ However, the Quarkus Derby extension allows native compilation of the Derby JDBC * Embedding H2 within your native image is not recommended. Consider using an alternative approach, for example, using a remote connection to a separate database instead. +ifndef::no-deprecated-test-resource[] ==== Run an integration test . Add a dependency on the artifacts providing the additional tools that are under the following Maven coordinates: @@ -642,6 +725,7 @@ public class TestResources { quarkus.datasource.db-kind=h2 quarkus.datasource.jdbc.url=jdbc:h2:tcp://localhost/mem:test ---- +endif::no-deprecated-test-resource[] [[datasource-reference]] == References @@ -834,9 +918,11 @@ a|* JDBC: `org.postgresql.Driver` |`reactive-pg-client` |`io.vertx.pgclient.spi.PgDriver` +ifndef::no-quarkus-reactive-db2-client[] |`db2` |`reactive-db2-client` |`io.vertx.db2client.spi.DB2Driver` +endif::no-quarkus-reactive-db2-client[] |=== [TIP] @@ -850,9 +936,11 @@ This automatic resolution is applicable in most cases so that driver configurati include::{generated-dir}/config/quarkus-reactive-datasource.adoc[opts=optional, leveloffset=+1] +ifndef::no-quarkus-reactive-db2-client[] ==== Reactive DB2 configuration include::{generated-dir}/config/quarkus-reactive-db2-client.adoc[opts=optional, leveloffset=+1] +endif::no-quarkus-reactive-db2-client[] ==== Reactive MariaDB/MySQL specific configuration diff --git a/docs/src/main/asciidoc/dev-ui.adoc b/docs/src/main/asciidoc/dev-ui.adoc index e27cb0910d8a1..a2bcb930fc7f5 100644 --- a/docs/src/main/asciidoc/dev-ui.adoc +++ b/docs/src/main/asciidoc/dev-ui.adoc @@ -317,7 +317,7 @@ import { beans } from 'build-time-data'; import '@vaadin/grid'; // <1> import { columnBodyRenderer } from '@vaadin/grid/lit.js'; // <2> import '@vaadin/vertical-layout'; -import 'qui-badge'; // <3> +import '@qomponent/qui-badge'; // <3> /** * This component shows the Arc Beans @@ -459,17 +459,16 @@ customElements.define('qwc-arc-beans', QwcArcBeans); ---- <1> Import the Vaadin component you want to use <2> You can also import other functions if needed -<3> There are some internal UI components that you can use, described below +<3> You can also use any component in the Qomponent library, described below -===== Using internal UI components +===== Qomponent -Some https://github.com/quarkusio/quarkus/tree/main/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qui[internal UI components] (under the `qui` namespace) are available to make certain things easier: +We also include all components from the [Qomponent](https://github.com/qomponent) library -- Card -- Badge -- Alert -- Code block -- IDE Link +- [Card](https://www.npmjs.com/package/@qomponent/qui-card) +- [Badge](https://www.npmjs.com/package/@qomponent/qui-badge) +- [Alert](https://www.npmjs.com/package/@qomponent/qui-alert) +- [Code block](https://www.npmjs.com/package/@qomponent/qui-code-block) ====== Card @@ -477,20 +476,20 @@ Card component to display contents in a card [source,javascript] ---- -import 'qui-card'; +import '@qomponent/qui-card'; ---- [source,html] ---- - -
- My contents -
-
+ +
+
+ Hello +
+
+
---- -https://github.com/phillip-kruger/quarkus-jokes/blob/f572ed6f949de0c0b8cbfa99d7389ab5168fea65/deployment/src/main/resources/dev-ui/qwc-jokes-vaadin.js#L110[Example code] - ====== Badge Badge UI Component based on the https://vaadin.com/docs/latest/components/badge[vaadin themed] badge @@ -499,109 +498,99 @@ image::dev-ui-qui-badge-v2.png[alt=Dev UI Badge,role="center"] [source,javascript] ---- -import 'qui-badge'; +import '@qomponent/qui-badge'; ---- You can use any combination of small, primary, pill, with icon and clickable with any level of `default`, `success`, `warning`, `error`, `contrast`, or set your own colors. [source,html] ---- -
-

Badges

-

Badges wrap the Vaadin theme in a component. - See https://vaadin.com/docs/latest/components/badge for more info. -

-
- -
-
- Default - Success - Warning - Error - Contrast - Custom colours -
-
-
- -
-
- Default primary - Success primary - Warning primary - Error primary - Contrast primary - Custom colours -
-
-
- -
-
- Default pill - Success pill - Warning pill - Error pill - Contrast pill - Custom colours -
-
-
- -
-
- - Default icon - - - Success icon - - - Warning icon - - - Error icon - - - Contrast icon - - - Custom colours - -
-
-
- -
-
- - - - - - -
-
-
- -
-
- this._info()}>Default - this._success()}>Success - this._warning()}>Warning - this._error()}>Error - this._contrast()}>Contrast - this._info()}>Custom colours -
-
-
+
+

Tiny

+
+ Default + Success + Warning + Error + Contrast + Custom colors +
+ +

Small

+
+ Default + Success + Warning + Error + Contrast + Custom colors +
+ +

Primary

+
+ Default primary + Success primary + Warning primary + Error primary + Contrast primary + Custom colors +
+ +

Pill

+
+ Default pill + Success pill + Warning pill + Error pill + Contrast pill + Custom colors +
+ +

With Icon

+
+ + Default icon + + + Success icon + + + Warning icon + + + Error icon + + + Contrast icon + + + Custom colors + +
+ +

Icon only

+
+ + + + + + +
+ +

Clickable

+
+ Default + Success + Warning + Error + Contrast + Custom colors
+
---- -https://github.com/phillip-kruger/quarkus-jokes/blob/f572ed6f949de0c0b8cbfa99d7389ab5168fea65/deployment/src/main/resources/dev-ui/qwc-jokes-vaadin.js#L214[Example code] - ====== Alert Alerts are modeled around the Bootstrap alerts. Click https://getbootstrap.com/docs/4.0/components/alerts[here] for more info. @@ -612,69 +601,62 @@ image::dev-ui-qui-alert-v2.png[alt=Dev UI Alert,role="center"] [source,javascript] ---- -import 'qui-alert'; +import '@qomponent/qui-alert'; ---- [source,html] ---- -
-
- Info alert - Success alert - Warning alert - Error alert -
- Permanent Info alert - Permanent Success alert - Permanent Warning alert - Permanent Error alert -
- Center Info alert - Center Success alert - Center Warning alert - Center Error alert -
- Info alert with icon - Success alert with icon - Warning alert with icon - Error alert with icon -
- Info alert with custom icon - Success alert with custom icon - Warning alert with custom icon - Error alert with custom icon -
- Small Info alert with icon - Small Success alert with icon - Small Warning alert with icon - Small Error alert with icon -
- Info alert with markup
quarkus.io
- Success alert with markup
quarkus.io
- Warning alert with markup
quarkus.io
- Error alert with markup
quarkus.io
-
- Primary Info alert with icon - Primary Success alert with icon - Primary Warning alert with icon - Primary Error alert with icon -
- Info alert with title - Success alert with title - Warning alert with title - Error alert with title -
- Info alert with title and icon - Success alert with title and icon - Warning alert with title and icon - Error alert with title and icon -
-
+Info alert +Success alert +Warning alert +Error alert + +Permanent Info alert +Permanent Success alert +Permanent Warning alert +Permanent Error alert + +Center Info alert +Center Success alert +Center Warning alert +Center Error alert + +Info alert with icon +Success alert with icon +Warning alert with icon +Error alert with icon + +Info alert with custom icon +Success alert with custom icon +Warning alert with custom icon +Error alert with custom icon + +Small Info alert with icon +Small Success alert with icon +Small Warning alert with icon +Small Error alert with icon + +Info alert with markup
quarkus.io
+Success alert with markup
quarkus.io
+Warning alert with markup
quarkus.io
+Error alert with markup
quarkus.io
+ +Primary Info alert with icon +Primary Success alert with icon +Primary Warning alert with icon +Primary Error alert with icon + +Info alert with title +Success alert with title +Warning alert with title +Error alert with title + +Info alert with title and icon +Success alert with title and icon +Warning alert with title and icon +Error alert with title and icon ---- -https://github.com/phillip-kruger/quarkus-jokes/blob/f572ed6f949de0c0b8cbfa99d7389ab5168fea65/deployment/src/main/resources/dev-ui/qwc-jokes-vaadin.js#L316[Example code] - - ====== Code block Display a code block. This component is aware of the theme and will use the correct codemirror theme to match the light/dark mode. @@ -684,21 +666,18 @@ image::dev-ui-qui-code-block-v2.png[alt=Dev UI Code Block,role="center"] [source,javascript] ---- -import '@quarkus-webcomponents/codeblock'; +import '@qomponent/qui-code-block'; ---- [source,html] ---- -
- - -
; + + + foo = bar + + ---- -https://github.com/quarkusio/quarkus/blob/05800d2a74601247a465f91f50d18c4075fb7fe6/extensions/kubernetes/vanilla/deployment/src/main/resources/dev-ui/qwc-kubernetes-manifest.js#L102[Example code] - Or fetching the contents from a URL: [source,html] @@ -711,8 +690,6 @@ Or fetching the contents from a URL:
---- -https://github.com/quarkusio/quarkus/blob/05800d2a74601247a465f91f50d18c4075fb7fe6/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-external-page.js#L118[Example code] - To make sure that the code block adopt the correct code-mirror theme (based on the current one in Dev UI), you can do the following: [source,javascript] @@ -741,7 +718,22 @@ Now you can get the current theme, so add the `theme` property to your code bloc
---- -====== IDE link +Modes: + - properties + - js + - java + - xml + - json + - yaml + - sql + - html + - css + - sass + - markdown + +==== Internal components + +===== IDE link Creates a link to a resource (like a Java source file) that can be opened in the user's IDE (if we could detect the IDE). diff --git a/docs/src/main/asciidoc/getting-started-testing.adoc b/docs/src/main/asciidoc/getting-started-testing.adoc index b9e0d99c7d2b7..25a273ea77dca 100644 --- a/docs/src/main/asciidoc/getting-started-testing.adoc +++ b/docs/src/main/asciidoc/getting-started-testing.adoc @@ -1261,8 +1261,8 @@ This is covered in the xref:building-native-image.adoc[Native Executable Guide]. `@QuarkusIntegrationTest` should be used to launch and test the artifact produced by the Quarkus build, and supports testing a jar (of whichever type), a native image or container image. Put simply, this means that if the result of a Quarkus build (`mvn package` or `gradle build`) is a jar, that jar will be launched as `java -jar ...` and tests run against it. If instead a native image was built, then the application is launched as `./application ...` and again the tests run against the running application. -Finally, if a container image was created during the build (by including the `quarkus-container-image-jib` or `quarkus-container-image-docker` extensions and having the -`quarkus.container-image.build=true` property configured), then a container is created and run (this requires the `docker` executable being present). +Finally, if a container image was created during the build (by including the `quarkus-container-image-jib`, `quarkus-container-image-docker`, or `container-image-podman` extensions and having the +`quarkus.container-image.build=true` property configured), then a container is created and run (this requires the `docker` or `podman` executable being present). This is a black box test that supports the same set features and has the same limitations. diff --git a/docs/src/main/asciidoc/hibernate-orm.adoc b/docs/src/main/asciidoc/hibernate-orm.adoc index def7a4f8bf355..febded3b4cb11 100644 --- a/docs/src/main/asciidoc/hibernate-orm.adoc +++ b/docs/src/main/asciidoc/hibernate-orm.adoc @@ -1132,7 +1132,7 @@ quarkus.hibernate-orm.database.generation=none # Enable SCHEMA approach and use default datasource quarkus.hibernate-orm.multitenant=SCHEMA # You could use a non-default datasource by using the following setting -# quarkus.hibernate-orm.multitenant-schema-datasource=other +# quarkus.hibernate-orm.datasource=other # The default data source used for all tenant schemas quarkus.datasource.db-kind=postgresql diff --git a/docs/src/main/asciidoc/hibernate-reactive.adoc b/docs/src/main/asciidoc/hibernate-reactive.adoc index 52d2f412e0d66..c0e32ee9b0475 100644 --- a/docs/src/main/asciidoc/hibernate-reactive.adoc +++ b/docs/src/main/asciidoc/hibernate-reactive.adoc @@ -191,7 +191,7 @@ and will have it use the default datasource. The configuration properties listed here allow you to override such defaults, and customize and tune various aspects. Hibernate Reactive uses the same properties you would use for Hibernate ORM. You will notice that some properties -contain `jdbc` in the name but there is not JDBC in Hibernate Reactive, these are simply legacy property names. +contain `jdbc` in the name but there is no JDBC in Hibernate Reactive, these are simply legacy property names. include::{generated-dir}/config/quarkus-hibernate-orm.adoc[opts=optional, leveloffset=+2] diff --git a/docs/src/main/asciidoc/images/messaging-quarkus.png b/docs/src/main/asciidoc/images/messaging-quarkus.png new file mode 100644 index 0000000000000..bd4f6cbe90c5e Binary files /dev/null and b/docs/src/main/asciidoc/images/messaging-quarkus.png differ diff --git a/docs/src/main/asciidoc/infinispan-client-reference.adoc b/docs/src/main/asciidoc/infinispan-client-reference.adoc index ec935c29916a2..7084c2e4ae3d9 100644 --- a/docs/src/main/asciidoc/infinispan-client-reference.adoc +++ b/docs/src/main/asciidoc/infinispan-client-reference.adoc @@ -739,8 +739,8 @@ When a method annotated with `@CacheInvalidateAll` is invoked, Infinispan will r == Querying The Infinispan client supports both indexed and non-indexed search as long as the -`ProtoStreamMarshaller` is configured above. This allows the user to query based on the -properties of the proto schema. *Indexed queries are preferred for performance reasons*. +`ProtoStreamMarshaller` is configured above. This allows the user to query on *keys* or +*values* based on the properties of the proto schema. *Indexed queries are preferred for performance reasons*. .XML [source,xml,options="nowrap",subs=attributes+,role="primary"] diff --git a/docs/src/main/asciidoc/javascript/config.js b/docs/src/main/asciidoc/javascript/config.js index e72b1c18a794e..26107d70d9b52 100644 --- a/docs/src/main/asciidoc/javascript/config.js +++ b/docs/src/main/asciidoc/javascript/config.js @@ -13,7 +13,7 @@ if(tables){ var input = caption.firstElementChild.lastElementChild; input.addEventListener("keyup", initiateSearch); input.addEventListener("input", initiateSearch); - input.attributes.removeNamedItem('disabled'); + if (input.attributes.disabled) input.attributes.removeNamedItem('disabled'); inputs[input.id] = {"table": table}; } @@ -24,7 +24,7 @@ if(tables){ const decoration = td.firstElementChild.lastElementChild.firstElementChild; const iconDecoration = decoration.children.item(0); const collapsibleSpan = decoration.children.item(1); - const descDiv = td.firstElementChild.children.item(1); + const descDiv = td.firstElementChild.querySelector(".description"); const collapsibleHandler = makeCollapsibleHandler(descDiv, td, row, collapsibleSpan, iconDecoration); row.addEventListener('click', collapsibleHandler); } diff --git a/docs/src/main/asciidoc/kafka-dev-services.adoc b/docs/src/main/asciidoc/kafka-dev-services.adoc index 2937185d7bfe3..8b61148eaf3e2 100644 --- a/docs/src/main/asciidoc/kafka-dev-services.adoc +++ b/docs/src/main/asciidoc/kafka-dev-services.adoc @@ -82,7 +82,7 @@ For Strimzi, you can select any image with a Kafka version which has Kraft suppo [source, properties] ---- -quarkus.kafka.devservices.image-name=quay.io/strimzi-test-container/test-container:0.105.0-kafka-3.6.0 +quarkus.kafka.devservices.image-name=quay.io/strimzi-test-container/test-container:0.106.0-kafka-3.7.0 ---- == Configuring Kafka topics diff --git a/docs/src/main/asciidoc/kafka-getting-started.adoc b/docs/src/main/asciidoc/kafka-getting-started.adoc index 86c795b6e7062..3e722be1774b0 100644 --- a/docs/src/main/asciidoc/kafka-getting-started.adoc +++ b/docs/src/main/asciidoc/kafka-getting-started.adoc @@ -408,7 +408,7 @@ version: '3.5' services: zookeeper: - image: quay.io/strimzi/kafka:0.39.0-kafka-3.6.1 + image: quay.io/strimzi/kafka:0.41.0-kafka-3.7.0 command: [ "sh", "-c", "bin/zookeeper-server-start.sh config/zookeeper.properties" @@ -421,7 +421,7 @@ services: - kafka-quickstart-network kafka: - image: quay.io/strimzi/kafka:0.39.0-kafka-3.6.1 + image: quay.io/strimzi/kafka:0.41.0-kafka-3.7.0 command: [ "sh", "-c", "bin/kafka-server-start.sh config/server.properties --override listeners=$${KAFKA_LISTENERS} --override advertised.listeners=$${KAFKA_ADVERTISED_LISTENERS} --override zookeeper.connect=$${KAFKA_ZOOKEEPER_CONNECT}" diff --git a/docs/src/main/asciidoc/kafka-schema-registry-avro.adoc b/docs/src/main/asciidoc/kafka-schema-registry-avro.adoc index 6a7766b8034fd..2800ef99f00b3 100644 --- a/docs/src/main/asciidoc/kafka-schema-registry-avro.adoc +++ b/docs/src/main/asciidoc/kafka-schema-registry-avro.adoc @@ -324,7 +324,7 @@ version: '2' services: zookeeper: - image: quay.io/strimzi/kafka:0.39.0-kafka-3.6.1 + image: quay.io/strimzi/kafka:0.41.0-kafka-3.7.0 command: [ "sh", "-c", "bin/zookeeper-server-start.sh config/zookeeper.properties" @@ -335,7 +335,7 @@ services: LOG_DIR: /tmp/logs kafka: - image: quay.io/strimzi/kafka:0.39.0-kafka-3.6.1 + image: quay.io/strimzi/kafka:0.41.0-kafka-3.7.0 command: [ "sh", "-c", "bin/kafka-server-start.sh config/server.properties --override listeners=$${KAFKA_LISTENERS} --override advertised.listeners=$${KAFKA_ADVERTISED_LISTENERS} --override zookeeper.connect=$${KAFKA_ZOOKEEPER_CONNECT}" diff --git a/docs/src/main/asciidoc/kafka-schema-registry-json-schema.adoc b/docs/src/main/asciidoc/kafka-schema-registry-json-schema.adoc index 63d540d191a59..5dc85d8a6b331 100644 --- a/docs/src/main/asciidoc/kafka-schema-registry-json-schema.adoc +++ b/docs/src/main/asciidoc/kafka-schema-registry-json-schema.adoc @@ -352,7 +352,7 @@ version: '2' services: zookeeper: - image: quay.io/strimzi/kafka:0.39.0-kafka-3.6.1 + image: quay.io/strimzi/kafka:0.41.0-kafka-3.7.0 command: [ "sh", "-c", "bin/zookeeper-server-start.sh config/zookeeper.properties" @@ -363,7 +363,7 @@ services: LOG_DIR: /tmp/logs kafka: - image: quay.io/strimzi/kafka:0.39.0-kafka-3.6.1 + image: quay.io/strimzi/kafka:0.41.0-kafka-3.7.0 command: [ "sh", "-c", "bin/kafka-server-start.sh config/server.properties --override listeners=$${KAFKA_LISTENERS} --override advertised.listeners=$${KAFKA_ADVERTISED_LISTENERS} --override zookeeper.connect=$${KAFKA_ZOOKEEPER_CONNECT}" diff --git a/docs/src/main/asciidoc/kafka-streams.adoc b/docs/src/main/asciidoc/kafka-streams.adoc index db79bb2e9557e..1119b939acdce 100644 --- a/docs/src/main/asciidoc/kafka-streams.adoc +++ b/docs/src/main/asciidoc/kafka-streams.adoc @@ -499,7 +499,7 @@ version: '3.5' services: zookeeper: - image: quay.io/strimzi/kafka:0.39.0-kafka-3.6.1 + image: quay.io/strimzi/kafka:0.41.0-kafka-3.7.0 command: [ "sh", "-c", "bin/zookeeper-server-start.sh config/zookeeper.properties" @@ -511,7 +511,7 @@ services: networks: - kafkastreams-network kafka: - image: quay.io/strimzi/kafka:0.39.0-kafka-3.6.1 + image: quay.io/strimzi/kafka:0.41.0-kafka-3.7.0 command: [ "sh", "-c", "bin/kafka-server-start.sh config/server.properties --override listeners=$${KAFKA_LISTENERS} --override advertised.listeners=$${KAFKA_ADVERTISED_LISTENERS} --override zookeeper.connect=$${KAFKA_ZOOKEEPER_CONNECT} --override num.partitions=$${KAFKA_NUM_PARTITIONS}" diff --git a/docs/src/main/asciidoc/kafka.adoc b/docs/src/main/asciidoc/kafka.adoc index ff532b0c198ab..909db7f28647c 100644 --- a/docs/src/main/asciidoc/kafka.adoc +++ b/docs/src/main/asciidoc/kafka.adoc @@ -472,6 +472,12 @@ To use this failure handler, the bean must be exposed with the `@Identifier` qua The handler is called with details of the deserialization, including the action represented as `Uni`. On the deserialization `Uni` failure strategies like retry, providing a fallback value or applying timeout can be implemented. +If you don’t configure a deserialization failure handler and a deserialization failure happens, the application is marked unhealthy. +You can also ignore the failure, which will log the exception and produce a `null` value. +To enable this behavior, set the `mp.messaging.incoming.$channel.fail-on-deserialization-failure` attribute to `false`. + +If the `fail-on-deserialization-failure` attribute is set to `false` and the `failure-strategy` attribute is `dead-letter-queue` the failed record will be sent to the corresponding *dead letter queue* topic. + === Consumer Groups In Kafka, a consumer group is a set of consumers which cooperate to consume data from a topic. @@ -2423,7 +2429,7 @@ The configuration of the created Kafka broker can be customized using `@Resource [source,java] ---- @QuarkusTestResource(value = KafkaCompanionResource.class, initArgs = { - @ResourceArg(name = "strimzi.kafka.image", value = "quay.io/strimzi-test-container/test-container:0.105.0-kafka-3.6.0"), // Image name + @ResourceArg(name = "strimzi.kafka.image", value = "quay.io/strimzi-test-container/test-container:0.106.0-kafka-3.7.0"), // Image name @ResourceArg(name = "kafka.port", value = "9092"), // Fixed port for kafka, by default it will be exposed on a random port @ResourceArg(name = "kraft", value = "true"), // Enable Kraft mode @ResourceArg(name = "num.partitions", value = "3"), // Other custom broker configurations diff --git a/docs/src/main/asciidoc/messaging.adoc b/docs/src/main/asciidoc/messaging.adoc new file mode 100644 index 0000000000000..2361154650bc1 --- /dev/null +++ b/docs/src/main/asciidoc/messaging.adoc @@ -0,0 +1,778 @@ +//// +This guide is maintained in the main Quarkus repository +and pull requests should be submitted there: +https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc +//// += Quarkus Messaging Extensions +include::_attributes.adoc[] +:categories: messaging +:topics: messaging,reactive-messaging +:extensions: io.quarkus:quarkus-messaging +:rm_blocking_annotation: https://javadoc.io/doc/io.smallrye.reactive/smallrye-reactive-messaging-api/latest/io/smallrye/reactive/messaging/annotations/Blocking.html +:rm_blocking_docs: http://smallrye.io/smallrye-reactive-messaging/4.8.0/concepts/blocking/ +:rm_doc: http://smallrye.io/smallrye-reactive-messaging/ +:rm_doc_method_signatures: https://smallrye.io/smallrye-reactive-messaging/latest/concepts/signatures/ +:rm_doc_emitter: https://smallrye.io/smallrye-reactive-messaging/latest/concepts/emitter/ +:mutiny: https://smallrye.io/smallrye-mutiny/latest/ +:eclipse-vertx: https://vertx.io/ +:camel-smallrye-reactive-messaging: https://camel.apache.org/camel-quarkus/3.8.x/reference/extensions/smallrye-reactive-messaging.html +:nats-jetstream: https://docs.quarkiverse.io/quarkus-reactive-messsaging-nats-jetstream/dev/index.html +:solace-quarkus: https://solacelabs.github.io/solace-quarkus/ +:http-websocket-connector: https://quarkus.io/extensions/io.quarkiverse.reactivemessaging.http/quarkus-reactive-messaging-http/ + +Event-driven messaging systems have become the backbone of most modern applications, +enabling the building of message-driven microservices or complex data streaming pipelines. + +Quarkus offers a comprehensive suite of messaging extensions designed to synchronize with leading messaging technologies effortlessly. +This empowers developers to concentrate on crafting the core application logic, liberating them from the necessity to delve into the complexities of individual APIs and messaging infrastructures. + +image::messaging-quarkus.png[Quarkus Messaging] + +This page focuses on common features and the development model for all messaging extensions. + +Some of these extensions are maintained in the core Quarkus repository: + +* *Messaging*: The core extension defines the basic concepts and APIs to develop messaging applications +* xref:kafka.adoc[*Messaging - Kafka Connector*] +* xref:pulsar.adoc[*Messaging - Pulsar Connector*] +* xref:rabbitmq-reference.adoc[*Messaging - RabbitMQ Connector*] +* xref:amqp-reference.adoc[*Messaging - AMQP 1.0 Connector*] +* *Messaging - MQTT Connector* + +Some extensions are contributed and maintained by the community: + +* link:{camel-smallrye-reactive-messaging}[Camel Smallrye Reactive Messaging] +* link:{nats-jetstream}[Nats Jetstream Connector] +* link:{solace-quarkus}[Solace Messaging Connector] +* link:{http-websocket-connector}[Reactive HTTP and WebSocket Connector] +* AWS SQS Connector + +Other connectors, such as the *JMS Connector* or the *Google PubSub Connector*, do not benefit from the same level of integration and require more manual configuration to set up. + +On the other hand, some messaging-related extensions propose low-level provider-specific integrations. +The level of support covered on this page DOES NOT involve these low-level extensions. +A non-exhaustive list of this kind of extension are the following: + +* link:https://quarkus.io/guides/kafka-streams[Kafka Streams Extension] +* link:https://docs.quarkiverse.io/quarkus-rabbitmq-client/dev/index.html[RabbitMQ Client] +* link:https://docs.quarkiverse.io/quarkus-hivemq-client/dev/index.html[HiveMQ Client] +* link:https://docs.quarkiverse.io/quarkus-artemis/dev/quarkus-artemis-jms.html[Artemis Core & JMS] +* link:https://docs.quarkiverse.io/quarkus-google-cloud-services/main/pubsub.html[Google Cloud Pubsub] + +== Quarkus Messaging Development Model +Quarkus simplifies message-driven application development by establishing a uniform model for publishing, consuming, and processing messages, regardless of whether the underlying broker technology uses message queuing or event streaming. +Built upon the MicroProfile Reactive Messaging specification, Quarkus Messaging extensions ensure seamless integration with these technologies. +Importantly, proficiency in reactive programming is NOT a prerequisite for leveraging these capabilities. + +The Reactive Messaging specification defines a CDI-based programming model for implementing event-driven and message-driven applications. +Using a small set of annotations, CDI beans become building blocks for implementing interactions with message brokers. +These interactions happen through _channels_ where application components read and write messages. + +_Channels_ are identified by a unique name and declared using a set of annotations. + +=== `@Incoming` and `@Outgoing` annotations +`@Incoming` and `@Outgoing` method annotations define _channels_ allowing to consume messages from and produce messages to the message broker: + +[source, java] +---- +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +@ApplicationScoped +public class MessageProcessingBean { + + @Incoming("source") + @Outgoing("sink") + public String process(String consumedPayload) { + // Process the incoming message payload and return an updated payload + return consumedPayload.toUpperCase(); + } + +} +---- + +`@Outgoing` can be used by itself on a method to generate messages: + +[source, java] +---- +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +@ApplicationScoped +public class MessageGeneratorBean { + + @Outgoing("sink") + public Multi generate() { + return Multi.createFrom().items("a", "b", "c"); + } + +} +---- + +`@Incoming` can be used by itself to consume messages: + +[source, java] +---- +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +@ApplicationScoped +public class MessageProcessingBean { + + @Incoming("source") + public void process(String consumedPayload) { + // process the payload + consumedPayload.toUpperCase(); + } + +} +---- + +[IMPORTANT] +==== +Note that you should not call methods annotated with `@Incoming` and/or `@Outgoing` directly from your code. +They are invoked by the framework. +Having user code invoking them would not have the expected outcome. +==== + +You can read more on supported method signatures in the link:{rm_doc_method_signatures}[SmallRye Reactive Messaging – Supported signatures]. + +=== Emitters and `@Channel` annotation + +An application often needs to combine messaging with other parts of the application, ex. produce messages from HTTP endpoints, or stream consumed messages as a response. + +To send messages from imperative code to a specific channel, you need to inject an `Emitter` object identified by the `@Channel` annotation: + +[source, java] +---- +import org.eclipse.microprofile.reactive.messaging.Channel; +import org.eclipse.microprofile.reactive.messaging.Emitter; + +@ApplicationScoped +@Path("/") +public class MyImperativeBean { + + @Channel("prices") + Emitter emitter; + + @GET + @Path("/send") + public CompletionStage send(double d) { + return emitter.send(d); + } +} +---- + +The `@Channel` annotation lets you indicate to which channel you will send your payloads or messages. +The `Emitter` allows buffering messages sent to the channel. + +For more control, using link:{mutiny}[Mutiny] APIs, you can use the `MutinyEmitter` emitter interface: + +[source, java] +---- +import io.smallrye.mutiny.Multi; +import org.eclipse.microprofile.reactive.messaging.Channel; +import org.eclipse.microprofile.reactive.messaging.MutinyEmitter; + +@ApplicationScoped +@Path("/") +public class MyImperativeBean { + + @Channel("prices") + MutinyEmitter emitter; + + @GET + @Path("/send") + public void send(double d) { + emitter.sendAndAwait(d); + } + +} +---- + +The `@Channel` annotation can also be used to inject the stream of messages from an incoming channel: + +[source, java] +---- +import org.eclipse.microprofile.reactive.messaging.Channel; + +@ApplicationScoped +@Path("/") +public class SseResource { + + @Channel("prices") + Multi prices; + + @GET + @Path("/prices") + @RestStreamElementType(MediaType.TEXT_PLAIN) + public Multi stream() { + return prices; + } + +} +---- + +When consuming messages with `@Channel`, the application code is responsible for subscribing to the stream. +In the example above, the Quarkus REST (formerly RESTEasy Reactive) endpoint handles that for you. + +You can read more on the emitters and channels in the link:{rm_doc_emitter}[SmallRye Reactive Messaging – Emitter and Channels] documentation. + +=== Messages and Metadata +A `Message` is an envelope around a payload. +In the examples above only payloads were used, but every payload is wrapped around a `Message` internally in Quarkus Messaging. + +The `Message` interface associates a payload of type `` with `Metadata`, +a set of arbitrary objects and asynchronous actions for acknowledgement (ack) and negative acknowledgement (nack). + +[source, java] +---- +import org.eclipse.microprofile.reactive.messaging.Message; + +@Incoming("source") +@Outgoing("sink") +public Message process(Message consumed) { + // Access the metadata + MyMetadata my = consumed.getMetadata(MyMetadata.class).get(); + // Process the incoming message and return an updated message + return consumed.withPayload(consumed.getPayload().toUpperCase()); +} +---- + +A message is acknowledged back to the broker when its processing or reception has been successful. +Acknowledgements between messages are chained, meaning that when processing a message, +the acknowledgement of an outgoing message triggers the acknowledgement of incoming message(s). +In most cases, acks and nacks are managed for you and connectors allow you to configure different strategies per channel. +So, you usually don't need to interact with the `Message` interface directly. +Only advanced use cases require dealing with the Message directly. + +Accessing the `Metadata`, on the other hand, can be practical in many cases. +Connectors add specific metadata objects to the message to give access to the message headers, properties, and other connector-specific information. +You do not need to interact with the `Message` interface to access connector-specific metadata. +You can simply inject the metadata object as a method parameter after the payload parameter: + +[source, java] +---- +import org.eclipse.microprofile.reactive.messaging.Metadata; +@Incoming("source") +@Outgoing("sink") +public String process(String payload, MyMetadata my) { + // Access the metadata + Map props = my.getProperties(); + // Process the payload and return an updated payload + return payload.toUpperCase(); +} +---- + +Depending on the connector, payload types available to consume in processing methods differ. +You can implement a custom `MessageConverter` to transform the payload to a type that is accepted by your application. + +=== Channel configuration + +Channel attributes can be configured using the `mp.messaging.incoming.` and `mp.messaging.outgoing.` configuration properties. + +For example, to configure the Kafka connector to consume messages from the `my-topic` topic with a custom deserializer: + +[source, properties] +---- +mp.messaging.incoming.source.connector=smallrye-kafka +mp.messaging.incoming.source.topic=my-topic +mp.messaging.incoming.source.value.deserializer=org.apache.kafka.common.serialization.StringDeserializer +mp.messaging.incoming.source.auto.offset.reset=earliest +---- + +The `connector` attribute is required for all channels and specifies the connector to use. +You can omit this configuration if you have a single connector on your classpath, as Quarkus will automatically select the connector. + +Global channel attributes can be configured using the connector name: + +[source, properties] +---- +mp.messaging.connector.smallrye-kafka.bootstrap.servers=localhost:9092 +---- + +Connector-specific attributes are listed in connector documentation. + +=== Channel wiring and Messaging patterns + +At startup time, Quarkus analyzes declared channels to wire them together and verify that all channels are connected. +Concretely, each channel creates a _reactive stream_ of messages connected to another channel's _reactive stream_ of messages. +Adhering to the reactive stream protocol, the back-pressure mechanism is enforced between channels, allowing to control application resource usage and not over-commit and overloading part of the system. + +On the flip side it is NOT possible to create new channels programmatically at runtime. +There are, however, many patterns that let you implement most, if not all, messaging and integration use cases: + +[IMPORTANT] +==== +Some messaging technologies allow consumers to subscribe to a set of topics or queues, and producers to send messages to a specific topic on message basis. +If you are sure you need to configure and create clients dynamically at runtime, you should consider using the low-level clients directly. +==== + +==== Internal Channels + +In some use cases, it is convenient to use messaging patterns to transfer messages inside the same application. +When you don't connect a channel to a messaging backend, i.e. a connector, everything happens internally to the application, +and the streams are created by chaining methods together. +Each chain is still a reactive stream and enforces the back-pressure protocol. + +The framework verifies that the producer/consumer chain is complete, +meaning that if the application writes messages into an internal channel (using a method with only `@Outgoing`, or an `Emitter`), +it must also consume the messages from within the application (using a method with only `@Incoming` or using an unmanaged stream). + +==== Enable/Disable channels + +All defined channels are enabled by default, but it is possible to disable a channel with the configuration: + +[source, properties] +---- +mp.messaging.incoming.my-channel.enabled=false +---- + +This can be used alongside Quarkus build profiles to enable/disable channels based on some build-time condition, such as the the target environment. +You need to make sure of two things when disabling a channel: + +- the disabled channel usage is located in a bean that can be filtered out at build time, +- that without the channel, the remaining channels still work correctly. + +[source, java] +---- +@ApplicationScoped +@IfBuildProfile("my-profile") +public class MyProfileBean { + + @Outgoing("my-channel") + public Multi generate() { + return Multi.createFrom().items("a", "b", "c"); + } + +} +---- + +==== Multiple Outgoings and `@Broadcast` + +By default, messages transmitted in a channel are only dispatched to a single consumer. +Having multiple consumers is considered an error and is reported at deployment time. + +The `@Broadcast` annotation changes this behavior and indicates that messages transiting in the channel are dispatched to all the consumers. +`@Broadcast` must be used with the `@Outgoing` annotation: + +[source, java] +---- +import org.eclipse.microprofile.reactive.messaging.Broadcast; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +@Incoming("in") +@Outgoing("out") +@Broadcast +public int increment(int i) { + return i + 1; +} + +@Incoming("out") +public void consume1(int i) { + //... +} + +@Incoming("out") +public void consume2(int i) { + //... +} +---- + +Similarly to `@Broadcast`, you can use `@Outgoing` annotation multiple times on the same method to indicate that the method produces messages to multiple channels: + +[source, java] +---- +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +@Incoming("in") +@Outgoing("out1") +@Outgoing("out2") +public String process(String s) { + // send messages from channel in to both channels out1 and out2 + return s.toUpperCase(); +} +---- + +Using Multiple Outgoings can be useful for implementing fan-out patterns, in which a single message is processed by multiple target channels. + +You can selectively dispatch messages to multiple outgoings by returning `Targeted` from the processing method: + +[source, java] +---- +@Incoming("in") +@Outgoing("out1") +@Outgoing("out2") +@Outgoing("out3") +public Targeted process(double price) { + // send messages from channel-in to both channel-out1 and channel-out2 + Targeted targeted = Targeted.of("out1", "Price: " + price, "out2", "Quote: " + price); + if (price > 90.0) { + return targeted.with("out3", price); + } + return targeted; +} +---- + +==== Multiple Incomings and `@Merge` + +By default, a single producer can transmit messages in a channel. +Having multiple producers is considered erroneous and is reported at deployment time. +The `@Merge` annotation changes this behavior and indicates that a channel can have multiple producers. +`@Merge` must be used with the `@Incoming` annotation: + +[source, java] +---- +@Incoming("in1") +@Outgoing("out") +public int increment(int i) { + return i + 1; +} + +@Incoming("in2") +@Outgoing("out") +public int multiply(int i) { + return i * 2; +} + +@Incoming("out") +@Merge +public void getAll(int i) { + //... +} +---- + +Similarly to `@Merge`, you can use `@Incoming` annotation multiple times on the same method to indicate that the method consumes messages from multiple channels: + +[source, java] +---- +@Incoming("in1") +@Incoming("in2") +public String process(String s) { + // get messages from channel-1 and channel-2 + return s.toUpperCase(); +} +---- + + +==== Stream Processing + +In some advanced scenarios, you can manipulate directly the stream of messages instead of each individual message. + +Using link:{mutiny}[Mutiny APIs] in incoming and outgoing signatures allow you to process the stream of messages: + +[source, java] +---- +import io.smallrye.mutiny.Multi; +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +@ApplicationScoped +public class StreamProcessor { + + @Incoming("source") + @Outgoing("sink") + public Multi process(Multi in) { + return in.map(String::toUpperCase); + } + +} +---- + +== Execution Model + +Quarkus Messaging sits on top of the xref:quarkus-reactive-architecture.adoc#engine[reactive engine] of Quarkus and leverages link:{eclipse-vertx}[Eclipse Vert.x] to dispatch messages for processing. +It supports three execution modes: + +* *Event-loop*, where messages are dispatched on the Vert.x I/O thread. +Remember that you should not perform blocking operations on the event loop. +* *Worker-threads*, where messages are dispatched on a worker thread pool. +* *Virtual-threads*, where messages are dispatched on a virtual thread (requires Java 21+). +As virtual threads are not pooled, a new virtual thread is created for each message. +Please refer to the dedicated xref:messaging-virtual-threads.adoc[Quarkus Virtual Thread support] guide for more information. + +Quarkus chooses the default execution mode based on the method signature. +If the method signature is _synchronous_, messages are dispatched on *worker threads* otherwise it defaults to *event-loop*: + +|=== +|Method signature |Default execution mode + +|@Incoming("source") +void process(String payload) +|Worker-threads + +|@Incoming("source") +Uni process(String payload) +|Event-loop + +|@Incoming("source") +CompletionStage process(Message message) +|Event-loop + +|@Incoming("source") +@Outgoing("sink") +Multi process(Multi in) +| Stream-processing methods are executed at startup, then each message is dispatched on event loop. + +|=== + +Fine-grained control over the execution model is possible using annotations: + +* link:{rm_blocking_annotation}[`@Blocking`] will force the method to be executed on a worker thread pool. +The default pool of worker threads is shared between all channels. +Using `@Blocking("my-custom-pool")` you can configure channels with a custom thread pool. +The configuration property `smallrye.messaging.worker.my-custom-pool.max-concurrency` specifies the maximum number of threads in the pool. +You can read more on the blocking processing in link:{rm_blocking_docs}[SmallRye Reactive Messaging documentation]. +* `@NonBlocking` will force the method to be executed on the event-loop thread. +* `@RunOnVirtualThread` will force the method to be executed on a virtual thread. +To leverage the lightweight nature of virtual threads, the default maximum concurrency for methods annotated with `@RunOnVirtualThread` is 1024. +This can be changed by setting the `smallrye.messaging.worker..max-concurrency` configuration property +or using together with the `@Blocking("my-custom-pool")` annotation. + +The presence of `@Transactional` annotation implies blocking execution. + +In messaging applications, produced and consumed messages constitute an ordered stream of events, +either enforced by the broker (inside a topic or a queue) +or by the order of reception and emission in the application. +To preserve this order, Quarkus Messaging dispatches messages sequentially by default. +You can override this behavior by using `@Blocking(ordered = false)` or `@RunOnVirtualThread` annotation. + +=== Incoming Channel Concurrency + +Some connectors support configuring the concurrency level of incoming channels. + +[source, properties] +---- +mp.messaging.incoming.my-channel.concurrency=4 +---- + +This creates four copies of the incoming channel under the hood, wiring them to the same processing method. +Depending on the broker technology, this can be useful to increase the application's throughput by processing multiple messages concurrently +while still preserving the partial order of messages received in different copies. +This is the case, for example, for Kafka, where multiple consumers can consume different topic partitions. + +== Health Checks + +Together with the Smallrye Health extension, Quarkus Messaging extensions provide health check support per channel. +The implementation of _startup_, _readiness_ and _liveness_ checks depends on the connector. +Some connectors allow configuring the health check behavior or disabling them completely or per channel. + +Channel health checks can be disabled using `quarkus.messaging.health..enabled` or per health check type, +ex. `quarkus.messaging.health..liveness.enabled`. + +Setting the `quarkus.messaging.health.enabled` configuration property to `false` completely disables the messaging health checks. + +== Observability + +=== Micrometer Metrics + +Quarkus Messaging extensions provide simple but useful metrics to monitor the health of the messaging system. +The xref:telemetry-micrometer.adoc[Micrometer extension] exposes these metrics. + +The following metrics can be gathered per channel, identified with the `channel` tag: + +* `quarkus.messaging.message.count` : The number of messages produced or received +* `quarkus.messaging.message.acks` : The number of messages processed successfully +* `quarkus.messaging.message.failures` : The number of messages processed with failures +* `quarkus.messaging.message.duration` : The duration of the message processing + +For backwards compatibility reasons, channel metrics are not enabled by default and can be enabled with: `smallrye.messaging.observation.enabled=true`. + +=== OpenTelemetry Tracing + +Some Quarkus Messaging connectors integrate out-of-the-box with OpenTelemetry Tracing. +When the xref:opentelemetry.adoc[OpenTelemetry extension] is present, outgoing messages propagate the current tracing span. +On incoming channels, if a received message contains tracing information, the message processing inherits the message span as parent. + +You can disable tracing for a specific channel using the following configuration: + +[source, properties] +---- +mp.messaging.incoming.data.tracing-enabled=false +---- + +== Testing + +=== Testing with Dev Services + +Most Quarkus Messaging extensions provide a Dev Service to simplify the development and testing of applications. +The Dev Service creates a broker instance configured to work out-of-the-box with the Quarkus Messaging extension. + +During testing Quarkus creates a separate brok er instance to run the tests against it. + +You can read more about Dev Services in the xref:dev-services.adoc[Dev Services] guide, including a list of Dev Services provided by platform extensions. + +=== Testing with InMemoryConnector + +It can be useful to test the application without starting a broker. +To achieve this, you can _switch_ the channels managed by a connector to _in-memory_. + +IMPORTANT: This approach only works for JVM tests. It cannot be used for native tests (because they do not support injection). + +Let's say we want to test the following sample application: + +[source, java] +---- +import org.eclipse.microprofile.reactive.messaging.Channel; +import org.eclipse.microprofile.reactive.messaging.Emitter; +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +@ApplicationScoped +public class MyMessagingApplication { + + @Inject + @Channel("words-out") + Emitter emitter; + + public void sendMessage(String out) { + emitter.send(out); + } + + @Incoming("words-in") + @Outgoing("uppercase") + public Message toUpperCase(Message message) { + return message.withPayload(message.getPayload().toUpperCase()); + } + +} +---- + +First, add the following test dependency to your application: + +[source,xml,role="primary asciidoc-tabs-target-sync-cli asciidoc-tabs-target-sync-maven"] +.pom.xml +---- + + io.smallrye.reactive + smallrye-reactive-messaging-in-memory + test + +---- + +[source,gradle,role="secondary asciidoc-tabs-target-sync-gradle"] +.build.gradle +---- +testImplementation("io.smallrye.reactive:smallrye-reactive-messaging-in-memory") +---- + +Then, create a Quarkus Test Resource as follows: + +[source, java] +---- +public class InMemoryConnectorLifecycleManager implements QuarkusTestResourceLifecycleManager { + + @Override + public Map start() { + Map env = new HashMap<>(); + Map props1 = InMemoryConnector.switchIncomingChannelsToInMemory("words-in"); // <1> + Map props2 = InMemoryConnector.switchOutgoingChannelsToInMemory("uppercase"); // <2> + Map props3 = InMemoryConnector.switchOutgoingChannelsToInMemory("words-out"); // <3> + env.putAll(props1); + env.putAll(props2); + env.putAll(props3); + return env; // <4> + } + + @Override + public void stop() { + InMemoryConnector.clear(); // <5> + } +} +---- +<1> Switch the incoming channel `words-in` (consumed messages) to in-memory. +<2> Switch the outgoing channel `words-out` (produced messages) to in-memory. +<3> Switch the outgoing channel `uppercase` (processed messages) to in-memory. +<4> Builds and returns a `Map` containing all the properties required to configure the application to use in-memory channels. +<5> When the test stops, clear the `InMemoryConnector` (discard all the received and sent messages) + +Create a `@QuarkusTest` using the test resource created above: + +[source, java] +---- +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.junit.QuarkusTest; +import io.smallrye.reactive.messaging.memory.InMemoryConnector; +import io.smallrye.reactive.messaging.memory.InMemorySink; +import io.smallrye.reactive.messaging.memory.InMemorySource; + +import org.eclipse.microprofile.reactive.messaging.spi.Connector; +import org.junit.jupiter.api.Test; + +import jakarta.inject.Inject; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.awaitility.Awaitility.await; + +@QuarkusTest +@QuarkusTestResource(InMemoryConnectorLifecycleManager.class) +class MyMessagingApplicationTest { + + @Inject + @Connector("smallrye-in-memory") + InMemoryConnector connector; // <1> + + @Inject + MyMessagingApplication app; + + @Test + void test() { + InMemorySink wordsOut = connector.sink("words-out"); // <2> + InMemorySource wordsIn = connector.source("words-in"); // <3> + InMemorySink uppercaseOut = connector.sink("uppercase"); // <4> + + app.sendMessage("Hello"); // <5> + assertEquals("Hello", wordsOut.received().get(0).getPayload()); // <6> + + wordsIn.send("Bonjour"); // <7> + await().untilAsserted(() -> assertEquals("BONJOUR", uppercaseOut.received().get(0).getPayload())); // <8> + } +} + +---- +<1> Inject the in-memory connector in your test class, using the `@Connector` or `@Any` qualifier. +<2> Retrieve the outgoing channel (`words-out`) - the channel must have been switched to in-memory in the test resource. +<3> Retrieve the incoming channel (`words-in`) +<4> Retrieve the outgoing channel (`uppercase`) +<5> Use the injected application bean to call `sendMessage` method to send a message using the emitter with the channel `words-out`. +<6> Use the `received` method on `words-out` in-memory channel to check the message produced by the application. +<7> Use the `send` mwthod on `words-in` in-memory channel to send a message. +The application will process this message and send a message to `uppercase` channel. +<8> Use the `received` method on `uppercase` channel to check the messages produced by the application. + +[IMPORTANT] +==== +In-memory connector is solely intended for testing purposes. +There are some caveats to consider when using the in-memory connector: + +- The in-memory connector only transmits objects (payloads or configured messages) sent using the `InMemorySource#send` method. +Messages received by the application methods won't contain connector-specific metadata. +- By default, in-memory channels dispatch messages on the caller thread of the `InMemorySource#send` method, which would be the main thread in unit tests. +However, most of the other connectors handle context propagation dispatching messages on separate duplicated Vert.x contexts. + +The `quarkus-test-vertx` dependency provides the `@io.quarkus.test.vertx.RunOnVertxContext` annotation, which when used on a test method, executes the test on a Vert.x context. + +If your tests are dependent on context propagation, you can configure the in-memory connector channels with `run-on-vertx-context` attribute to dispatch events, including messages and acknowledgements, on a Vert.x context. +Alternatively you can switch this behaviour using the `InMemorySource#runOnVertxContext` method. + +==== + +== Going further + +This guide shows the general principles of Quarkus Messaging extensions. + +If you want to go further, you can check the link:{rm_doc}[SmallRye Reactive Messaging] documentation, +which has in-depth documentation for each of these concepts and more. diff --git a/docs/src/main/asciidoc/mongodb.adoc b/docs/src/main/asciidoc/mongodb.adoc index 6093e86a9af16..cd069975a5293 100644 --- a/docs/src/main/asciidoc/mongodb.adoc +++ b/docs/src/main/asciidoc/mongodb.adoc @@ -701,7 +701,7 @@ Currently, Quarkus doesn't support link:https://docs.mongodb.com/manual/core/sec ==== If you encounter the following error when running your application in native mode: + `Failed to encode 'MyObject'. Encoding 'myVariable' errored with: Can't find a codec for class org.acme.MyVariable.` + -This means that the `org.acme.MyVariable` class is not known to GraalVM, the remedy is to add the `@RegisterForReflection` annotation to your `MyVariable class`. +This means that the `org.acme.MyVariable` class is not known to GraalVM, the remedy is to add the `@RegisterForReflection` annotation to your `MyVariable` class. More details about the `@RegisterForReflection` annotation can be found on the xref:writing-native-applications-tips.adoc#registerForReflection[native application tips] page. ==== diff --git a/docs/src/main/asciidoc/native-reference.adoc b/docs/src/main/asciidoc/native-reference.adoc index 09cc190e96950..8c7d741a5a8b1 100644 --- a/docs/src/main/asciidoc/native-reference.adoc +++ b/docs/src/main/asciidoc/native-reference.adoc @@ -2255,7 +2255,7 @@ To work around that issue, add the following line to the `application.properties [source, properties] ---- -quarkus.native.additional-build-args=-march=compatibility +quarkus.native.march=compatibility ---- Then, rebuild your native executable. diff --git a/docs/src/main/asciidoc/opentelemetry.adoc b/docs/src/main/asciidoc/opentelemetry.adoc index 5462114043b5b..6f08bda91c7ab 100644 --- a/docs/src/main/asciidoc/opentelemetry.adoc +++ b/docs/src/main/asciidoc/opentelemetry.adoc @@ -398,6 +398,32 @@ public class CustomConfiguration { } ---- +==== End User attributes + +When enabled, Quarkus adds OpenTelemetry End User attributes as Span attributes. +Before you enable this feature, verify that Quarkus Security extension is present and configured. +More information about the Quarkus Security can be found in the xref:security-overview.adoc[Quarkus Security overview]. + +The attributes are only added when authentication has already happened on a best-efforts basis. +Whether the End User attributes are added as Span attributes depends on authentication and authorization configuration of your Quarkus application. +If you create custom Spans prior to the authentication, Quarkus cannot add the End User attributes to them. +Quarkus is only able to add the attributes to the Span that is current after the authentication has been finished. +Another important consideration regarding custom Spans is active CDI request context that is used to propagate Quarkus `SecurityIdentity`. +In principle, Quarkus is able to add the End User attributes when the CDI request context has been activated for you before the custom Spans are created. + +[source,application.properties] +---- +quarkus.otel.traces.eusp.enabled=true <1> +quarkus.http.auth.proactive=true <2> +---- +<1> Enable the End User Attributes feature so that the `SecurityIdentity` principal and roles are added as Span attributes. +The End User attributes are personally identifiable information, therefore make sure you want to export them before you enable this feature. +<2> Optionally enable proactive authentication. +The best possible results are achieved when proactive authentication is enabled because the authentication happens sooner. +A good way to determine whether proactive authentication should be enabled in your Quarkus application is to read the Quarkus xref:security-proactive-authentication.adoc[Proactive authentication] guide. + +IMPORTANT: This feature is not supported when a custom xref:security-customization.adoc#jaxrs-security-context[Jakarta REST SecurityContexts] is used. + [[sampler]] === Sampler A https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk.md#sampling[sampler] decides whether a trace should be discarded or forwarded, effectively managing noise and reducing overhead by limiting the number of collected traces sent to the collector. diff --git a/docs/src/main/asciidoc/podman.adoc b/docs/src/main/asciidoc/podman.adoc index a8eae2551625d..05a21f8faeb26 100644 --- a/docs/src/main/asciidoc/podman.adoc +++ b/docs/src/main/asciidoc/podman.adoc @@ -73,7 +73,7 @@ With the above rootless setup on Linux, you will need to configure clients, such [source,bash] ---- -export DOCKER_HOST=$(podman info --format '{{.Host.RemoteSocket.Path}}') +export DOCKER_HOST=unix://$(podman info --format '{{.Host.RemoteSocket.Path}}') ---- == Other Linux settings diff --git a/docs/src/main/asciidoc/pulsar.adoc b/docs/src/main/asciidoc/pulsar.adoc index eb2e86d2ad85a..fa61691f869cc 100644 --- a/docs/src/main/asciidoc/pulsar.adoc +++ b/docs/src/main/asciidoc/pulsar.adoc @@ -384,7 +384,7 @@ Note that dead letter topic can be used in different message redelivery methods, ---- mp.messaging.incoming.data.failure-strategy=reconsume-later mp.messaging.incoming.data.reconsumeLater.delay=5000 -mp.messaging.incoming.data.enableRetry=true +mp.messaging.incoming.data.retryEnable=true mp.messaging.incoming.data.negativeAck.redeliveryBackoff=1000,60000,2 ---- diff --git a/docs/src/main/asciidoc/qute-reference.adoc b/docs/src/main/asciidoc/qute-reference.adoc index 8b31a7c50f139..dff500a62efb2 100644 --- a/docs/src/main/asciidoc/qute-reference.adoc +++ b/docs/src/main/asciidoc/qute-reference.adoc @@ -2538,7 +2538,7 @@ There are several ways to solve this problem: ** In this case, an optimized value resolver is generated automatically and used at runtime ** This is the preferred solution * Annotate the model class with <> - a specialized value resolver is generated and used at runtime -* Annotate the model class with `@io.quarkus.runtime.annotations.RegisterForReflection` to make the reflection-based value resolver work +* Annotate the model class with `@io.quarkus.runtime.annotations.RegisterForReflection` to make the reflection-based value resolver work. More details about the `@RegisterForReflection` annotation can be found on the xref:writing-native-applications-tips.adoc#registerForReflection[native application tips] page. [[rest_integration]] diff --git a/docs/src/main/asciidoc/rabbitmq.adoc b/docs/src/main/asciidoc/rabbitmq.adoc index 7e271f83876c1..67c23e1502072 100644 --- a/docs/src/main/asciidoc/rabbitmq.adoc +++ b/docs/src/main/asciidoc/rabbitmq.adoc @@ -173,9 +173,10 @@ Quarkus has built-in capabilities to deal with JSON RabbitMQ messages. [NOTE] .@RegisterForReflection ==== -The `@RegisterForReflection` annotation instructs Quarkus to include the class (including fields and methods) when building the native executable. -This will be useful later when we run the applications as native executables inside containers. -Without, the native compilation would remove the fields and methods during the dead-code elimination phase. +The `@RegisterForReflection` annotation instructs Quarkus to keep the class, its fields, and methods when creating a native executable. +This is crucial when we later run our applications as native executables within containers. +Without this annotation, the native compilation process would discard the fields and methods during the dead-code elimination phase, which would lead to runtime errors. +More details about the `@RegisterForReflection` annotation can be found on the xref:writing-native-applications-tips.adoc#registerForReflection[native application tips] page. ==== == Sending quote request diff --git a/docs/src/main/asciidoc/security-authentication-mechanisms.adoc b/docs/src/main/asciidoc/security-authentication-mechanisms.adoc index 30425666b7763..ead7ae4238a0e 100644 --- a/docs/src/main/asciidoc/security-authentication-mechanisms.adoc +++ b/docs/src/main/asciidoc/security-authentication-mechanisms.adoc @@ -35,13 +35,18 @@ The following table maps specific authentication requirements to a supported mec |Username and password |xref:security-basic-authentication.adoc[Basic], <> -|Bearer access token |xref:security-oidc-bearer-token-authentication.adoc[OIDC Bearer token authentication], xref:security-jwt.adoc[JWT], xref:security-oauth2.adoc[OAuth2] +|Bearer access token |xref:security-oidc-bearer-token-authentication.adoc[OIDC Bearer token authentication], xref:security-jwt.adoc[JWT] +ifndef::no-quarkus-elytron-security-oauth2[] +, xref:security-oauth2.adoc[OAuth2] +endif::no-quarkus-elytron-security-oauth2[] |Single sign-on (SSO) |xref:security-oidc-code-flow-authentication.adoc[OIDC Code Flow], <> |Client certificate |<> +ifndef::no-webauthn-authentication[] |WebAuthn |xref:security-webauthn.adoc[WebAuthn] +endif::no-webauthn-authentication[] |Kerberos ticket |link:https://quarkiverse.github.io/quarkiverse-docs/quarkus-kerberos/dev/index.html[Kerberos] |==== @@ -96,7 +101,7 @@ quarkus.http.auth.form.error-page= # Define testing user quarkus.security.users.embedded.enabled=true quarkus.security.users.embedded.plain-text=true -quarkus.security.users.embedded.users.alice=alice +quarkus.security.users.embedded.users.alice=alice quarkus.security.users.embedded.roles.alice=user ---- @@ -312,17 +317,23 @@ For more information about customizing `SecurityIdentity`, see the xref:security Quarkus Security also supports the following authentication mechanisms through extensions: +ifndef::no-webauthn-authentication[] * <> +endif::no-webauthn-authentication[] * <> * <> +ifndef::no-quarkus-elytron-security-oauth2[] * <> +endif::no-quarkus-elytron-security-oauth2[] +ifndef::no-webauthn-authentication[] [[webauthn-authentication]] === WebAuthn authentication https://webauthn.guide/[WebAuthn] is an authentication mechanism that replaces passwords. When you write a service for registering new users, or logging them in, instead of asking for a password, you can use WebAuthn, which replaces the password. For more information, see the xref:security-webauthn.adoc[Secure a Quarkus application by using the WebAuthn authentication mechanism] guide. +endif::no-webauthn-authentication[] [[openid-connect-authentication]] === OpenID Connect authentication @@ -357,7 +368,9 @@ For more information about OIDC authentication and authorization methods that yo |Multiple tenants that can support the Bearer token authentication or Authorization Code Flow mechanisms|xref:security-openid-connect-multitenancy.adoc[Using OpenID Connect (OIDC) multi-tenancy] |Securing Quarkus with commonly used OpenID Connect providers|xref:security-openid-connect-providers.adoc[Configuring well-known OpenID Connect providers] |Using Keycloak to centralize authorization |xref:security-keycloak-authorization.adoc[Using OpenID Connect (OIDC) and Keycloak to centralize authorization] +ifndef::no-quarkus-keycloak-admin-client[] |Configuring Keycloak programmatically |xref:security-keycloak-admin-client.adoc[Using the Keycloak admin client] +endif::no-quarkus-keycloak-admin-client[] |==== [NOTE] @@ -386,12 +399,15 @@ For example, it can be a public endpoint or be protected with mTLS. In this scenario, you do not need to protect your Quarkus endpoint by using the Quarkus OpenID Connect adapter. ==== +ifndef::no-quarkus-oidc-token-propagation[] The `quarkus-resteasy-client-oidc-token-propagation` extension requires the `quarkus-oidc` extension. It provides Jakarta REST `TokenCredentialRequestFilter`, which sets the OpenID Connect Bearer token or Authorization Code Flow access token as the `Bearer` scheme value of the HTTP `Authorization` header. This filter can be registered with MicroProfile REST client implementations injected into the current Quarkus endpoint, which must be protected by using the Quarkus OIDC adapter. This filter can propagate the access token to the downstream services. For more information, see the xref:security-openid-connect-client.adoc[OpenID Connect client and token propagation quickstart] and xref:security-openid-connect-client-reference.adoc[OpenID Connect (OIDC) and OAuth2 client and filters reference] guides. +endif::no-quarkus-oidc-token-propagation[] + [[smallrye-jwt-authentication]] === SmallRye JWT authentication @@ -404,6 +420,7 @@ It represents them as `org.eclipse.microprofile.jwt.JsonWebToken`. For more information, see the xref:security-jwt.adoc[Using JWT RBAC] guide. +ifndef::no-quarkus-elytron-security-oauth2[] [[oauth2-authentication]] === OAuth2 authentication @@ -411,6 +428,7 @@ For more information, see the xref:security-jwt.adoc[Using JWT RBAC] guide. `quarkus-elytron-security-oauth2` is based on `Elytron` and is primarily intended for introspecting opaque tokens remotely. For more information, see the Quarkus xref:security-oauth2.adoc[Using OAuth2] guide. +endif::no-quarkus-elytron-security-oauth2[] [[oidc-jwt-oauth2-comparison]] == Choosing between OpenID Connect, SmallRye JWT, and OAuth2 authentication mechanisms @@ -425,13 +443,20 @@ In both cases, `quarkus-oidc` requires a connection to the specified OpenID Conn * If the user authentication requires Authorization Code flow, or you need to support multiple tenants, use `quarkus-oidc`. `quarkus-oidc` can also request user information by using both Authorization Code Flow and Bearer access tokens. -* If your bearer tokens must be verified, use `quarkus-oidc`, `quarkus-smallrye-jwt`, or `quarkus-elytron-security-oauth2`. +ifndef::no-quarkus-elytron-security-oauth2[] +* If your bearer tokens must be verified, use `quarkus-oidc`, `quarkus-elytron-security-oauth2`, or `quarkus-smallrye-jwt`. +endif::no-quarkus-elytron-security-oauth2[] +ifdef::no-quarkus-elytron-security-oauth2[] +* If your bearer tokens must be verified, use `quarkus-oidc` or `quarkus-smallrye-jwt`. +endif::no-quarkus-elytron-security-oauth2[] * If your bearer tokens are in a JSON web token (JWT) format, you can use any extensions in the preceding list. Both `quarkus-oidc` and `quarkus-smallrye-jwt` support refreshing the `JsonWebKey` (JWK) set when the OpenID Connect provider rotates the keys. Therefore, if remote token introspection must be avoided or is unsupported by the providers, use `quarkus-oidc` or `quarkus-smallrye-jwt` to verify JWT tokens. -* To introspect the JWT tokens remotely, you can use either `quarkus-oidc` or `quarkus-elytron-security-oauth2` because they support verifying the opaque or binary tokens by using remote introspection. +* To introspect the JWT tokens remotely, you can use `quarkus-oidc` +ifndef::no-quarkus-elytron-security-oauth2[or `quarkus-elytron-security-oauth2`] +for verifying the opaque or binary tokens by using remote introspection. `quarkus-smallrye-jwt` does not support the remote introspection of both opaque or JWT tokens but instead relies on the locally available keys that are usually retrieved from the OpenID Connect provider. * `quarkus-oidc` and `quarkus-smallrye-jwt` support the JWT and opaque token injection into the endpoint code. @@ -442,9 +467,10 @@ All extensions can have the tokens injected as `Principal`. `quarkus-oidc` uses only the JWK-formatted keys that are part of a JWK set, whereas `quarkus-smallrye-jwt` supports PEM keys. * `quarkus-smallrye-jwt` handles locally signed, inner-signed-and-encrypted, and encrypted tokens. -In contrast, although `quarkus-oidc` and `quarkus-elytron-security-oauth2` can also verify such tokens, they treat them as opaque tokens and verify them through remote introspection. +ifndef::no-quarkus-elytron-security-oauth2[In contrast, although `quarkus-oidc` and `quarkus-elytron-security-oauth2` can also verify such tokens, they treat them as opaque tokens and verify them through remote introspection.] +ifdef::no-quarkus-elytron-security-oauth2[In contrast, although `quarkus-oidc` can also verify such tokens, it treats them as opaque tokens and verifies them through remote introspection.] -* If you need a lightweight library for the remote introspection of opaque or JWT tokens, use `quarkus-elytron-security-oauth2`. +ifndef::no-quarkus-elytron-security-oauth2[* If you need a lightweight library for the remote introspection of opaque or JWT tokens, use `quarkus-elytron-security-oauth2`.] [NOTE] ==== @@ -459,26 +485,80 @@ Nonetheless, the providers effectively delegate most of the token-associated sta [[table]] .Token authentication mechanism comparison |=== -^|Feature required 3+^| Authentication mechanism - -^| ^s|`quarkus-oidc` ^s|`quarkus-smallrye-jwt` ^s| `quarkus-elytron-security-oauth2` - -s|Bearer JWT verification ^|Local verification or introspection ^|Local verification ^|Introspection - -s|Bearer opaque token verification ^|Introspection ^|No ^|Introspection -s|Refreshing `JsonWebKey` set to verify JWT tokens ^|Yes ^|Yes ^|No -s|Represent token as `Principal` ^|Yes ^|Yes ^|Yes -s|Inject JWT as MP JWT ^|Yes ^|Yes ^|No - -s|Authorization code flow ^| Yes ^|No ^|No -s|Multi-tenancy ^| Yes ^|No ^|No -s|User information support ^| Yes ^|No ^|No -s|PEM key format support ^|No ^|Yes ^|No - -s|SecretKey support ^|No ^|In JSON Web Key (JWK) format ^|No -s|Inner-signed and encrypted or encrypted tokens ^|Introspection ^|Local verification ^|Introspection -s|Custom token verification ^|No ^|With injected JWT parser ^|No -s|JWT as a cookie support ^|No ^|Yes ^|Yes +// Display four columns +ifndef::no-quarkus-elytron-security-oauth2[ ^|Feature required 3+^| Authentication mechanism] +// Display three columns and hide the quarkus-elytron-security-oauth2 column. +ifdef::no-quarkus-elytron-security-oauth2[ ^|Feature required 2+^| Authentication mechanism] + +^| +^s|`quarkus-oidc` +^s|`quarkus-smallrye-jwt` +ifndef::no-quarkus-elytron-security-oauth2[ ^s|`quarkus-elytron-security-oauth2`] + +s|Bearer JWT verification +^|Local verification or introspection +^|Local verification +ifndef::no-quarkus-elytron-security-oauth2[ ^|Introspection] + +s|Bearer opaque token verification +^|Introspection +^|No +ifndef::no-quarkus-elytron-security-oauth2[ ^|Introspection] + +s|Refreshing `JsonWebKey` set to verify JWT tokens +^|Yes +^|Yes +ifndef::no-quarkus-elytron-security-oauth2[ ^|No] + +s|Represent token as `Principal` +^|Yes +^|Yes +ifndef::no-quarkus-elytron-security-oauth2[ ^|Yes] + +s|Inject JWT as MP JWT +^|Yes +^|Yes +ifndef::no-quarkus-elytron-security-oauth2[ ^|No] + +s|Authorization code flow +^| Yes +^|No +ifndef::no-quarkus-elytron-security-oauth2[ ^|No] + +s|Multi-tenancy +^| Yes +^|No +ifndef::no-quarkus-elytron-security-oauth2[ ^|No] + +s|User information support +^| Yes +^|No +ifndef::no-quarkus-elytron-security-oauth2[ ^|No] + +s|PEM key format support +^|No +^|Yes +ifndef::no-quarkus-elytron-security-oauth2[ ^|No] + +s|SecretKey support +^|No +^|In JSON Web Key (JWK) format +ifndef::no-quarkus-elytron-security-oauth2[ ^|No] + +s|Inner-signed and encrypted or encrypted tokens +^|Introspection +^|Local verification +ifndef::no-quarkus-elytron-security-oauth2[ ^|Introspection] + +s|Custom token verification +^|No +^|With injected JWT parser +ifndef::no-quarkus-elytron-security-oauth2[ ^|No] + +s|JWT as a cookie support +^|No +^|Yes +ifndef::no-quarkus-elytron-security-oauth2[ ^|Yes] |=== [[combining-authentication-mechanisms]] @@ -560,13 +640,29 @@ public class HelloResource { |=== ^|Authentication mechanism^| Annotation -s|Basic authentication mechanism ^|`io.quarkus.vertx.http.runtime.security.annotation.BasicAuthentication` -s|Form-based authentication mechanism ^|`io.quarkus.vertx.http.runtime.security.annotation.FormAuthentication` -s|Mutual TLS authentication mechanism ^|`io.quarkus.vertx.http.runtime.security.annotation.MTLSAuthentication` -s|WebAuthn authentication mechanism ^|`io.quarkus.security.webauthn.WebAuthn` -s|Bearer token authentication mechanism ^|`io.quarkus.oidc.BearerTokenAuthentication` -s|OIDC authorization code flow mechanism ^|`io.quarkus.oidc.AuthorizationCodeFlow` -s|SmallRye JWT authentication mechanism ^|`io.quarkus.smallrye.jwt.runtime.auth.BearerTokenAuthentication` +s|Basic authentication mechanism +^|`io.quarkus.vertx.http.runtime.security.annotation.BasicAuthentication` + +s|Form-based authentication mechanism +^|`io.quarkus.vertx.http.runtime.security.annotation.FormAuthentication` + +s|Mutual TLS authentication mechanism +^|`io.quarkus.vertx.http.runtime.security.annotation.MTLSAuthentication` + +ifndef::no-webauthn-authentication[] +s|WebAuthn authentication mechanism +^|`io.quarkus.security.webauthn.WebAuthn` +endif::no-webauthn-authentication[] + +s|Bearer token authentication mechanism +^|`io.quarkus.oidc.BearerTokenAuthentication` + +s|OIDC authorization code flow mechanism +^|`io.quarkus.oidc.AuthorizationCodeFlow` + +s|SmallRye JWT authentication mechanism +^|`io.quarkus.smallrye.jwt.runtime.auth.BearerTokenAuthentication` + |=== TIP: Quarkus automatically secures endpoints annotated with the authentication mechanism annotation. When no standard security annotation is present on the REST endpoint and resource, the `io.quarkus.security.Authenticated` annotation is added for you. diff --git a/docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc b/docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc index 5f3f37c8a39ae..fdae898edb3e0 100644 --- a/docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc +++ b/docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc @@ -882,7 +882,7 @@ public class MediaLibraryPermission extends LibraryPermission { } ---- -<1> When building a native executable, the permission class must be registered for reflection unless it is also used in at least one `io.quarkus.security.PermissionsAllowed#name` parameter. +<1> When building a native executable, the permission class must be registered for reflection unless it is also used in at least one `io.quarkus.security.PermissionsAllowed#name` parameter. More details about the `@RegisterForReflection` annotation can be found on the xref:writing-native-applications-tips.adoc#registerForReflection[native application tips] page. <2> We want to pass the `MediaLibrary` instance to the `LibraryPermission` constructor. [source,properties] diff --git a/docs/src/main/asciidoc/security-basic-authentication-howto.adoc b/docs/src/main/asciidoc/security-basic-authentication-howto.adoc index 015127dbfb430..e346a2f31d64e 100644 --- a/docs/src/main/asciidoc/security-basic-authentication-howto.adoc +++ b/docs/src/main/asciidoc/security-basic-authentication-howto.adoc @@ -18,7 +18,12 @@ Enable xref:security-basic-authentication.adoc[Basic authentication] for your Qu * You have installed at least one extension that provides an `IdentityProvider` based on username and password. For example: -** xref:security-jpa.adoc[Quarkus Security Jakarta Persistence extensions (`security-jpa` or `security-jpa-reactive`)] +ifndef::no-quarkus-security-jpa-reactive[] +** xref:security-jpa.adoc[Quarkus Security Jakarta Persistence extensions (`quarkus-security-jpa` or `quarkus-security-jpa-reactive`)] +endif::no-quarkus-security-jpa-reactive[] +ifdef::no-quarkus-security-jpa-reactive[] +** xref:security-jpa.adoc[Quarkus Security Jakarta Persistence extension (`quarkus-security-jpa`)] +endif::no-quarkus-security-jpa-reactive[] ** xref:security-properties.adoc[Elytron security properties file extension `(quarkus-elytron-security-properties-file)`] ** xref:security-jdbc.adoc[Elytron security JDBC extension `(quarkus-elytron-security-jdbc)`] diff --git a/docs/src/main/asciidoc/security-getting-started-tutorial.adoc b/docs/src/main/asciidoc/security-getting-started-tutorial.adoc index 29311a068cf7d..5dcab363d539b 100644 --- a/docs/src/main/asciidoc/security-getting-started-tutorial.adoc +++ b/docs/src/main/asciidoc/security-getting-started-tutorial.adoc @@ -54,12 +54,20 @@ You can find the solution in the `security-jpa-quickstart` link:{quickstarts-tre == Create and verify the Maven project -For Quarkus Security to be able to map your security source to Jakarta Persistence entities, ensure that the Maven project in this tutorial includes the `security-jpa` or `security-jpa-reactive` extension. +ifndef::no-quarkus-security-jpa-reactive[] +For Quarkus Security to be able to map your security source to Jakarta Persistence entities, ensure that the Maven project in this tutorial includes the `quarkus-security-jpa` or `quarkus-security-jpa-reactive` extension. +endif::no-quarkus-security-jpa-reactive[] +ifdef::no-quarkus-security-jpa-reactive[] +For Quarkus Security to be able to map your security source to Jakarta Persistence entities, ensure that the Maven project in this tutorial includes the `quarkus-security-jpa` extension. +endif::no-quarkus-security-jpa-reactive[] [NOTE] ==== -xref:hibernate-orm-panache.adoc[Hibernate ORM with Panache] is used to store your user identities, but you can also use xref:hibernate-orm.adoc[Hibernate ORM] with the `security-jpa` extension. -Both xref:hibernate-reactive.adoc[Hibernate Reactive] and xref:hibernate-reactive-panache.adoc[Hibernate Reactive with Panache] can be used with the `security-jpa-reactive` extension. +xref:hibernate-orm-panache.adoc[Hibernate ORM with Panache] is used to store your user identities, but you can also use xref:hibernate-orm.adoc[Hibernate ORM] with the `quarkus-security-jpa` extension. + +ifndef::no-quarkus-security-jpa-reactive[] +Both xref:hibernate-reactive.adoc[Hibernate Reactive] and xref:hibernate-reactive-panache.adoc[Hibernate Reactive with Panache] can be used with the `quarkus-security-jpa-reactive` extension. +endif::no-quarkus-security-jpa-reactive[] You must also add your preferred database connector library. The instructions in this example tutorial use a PostgreSQL database for the identity store. @@ -86,18 +94,20 @@ include::{includes}/devtools/create-app.adoc[] :add-extension-extensions: security-jpa include::{includes}/devtools/extension-add.adoc[] ==== +ifndef::no-quarkus-security-jpa-reactive[] ** To add the Security Jakarta Persistence extension to an existing Maven project with Hibernate Reactive, run the following command from your project base directory: + ==== :add-extension-extensions: security-jpa-reactive include::{includes}/devtools/extension-add.adoc[] ==== +endif::no-quarkus-security-jpa-reactive[] === Verify the quarkus-security-jpa dependency -After you have run either of the preceding commands to create the Maven project, verify that the `security-jpa` dependency was added to your project build XML file. +After you have run either of the preceding commands to create the Maven project, verify that the `quarkus-security-jpa` dependency was added to your project build XML file. -* To verify the `security-jpa` extension, check for the following configuration: +* To verify the `quarkus-security-jpa` extension, check for the following configuration: + ==== [source,xml,role="primary asciidoc-tabs-target-sync-cli asciidoc-tabs-target-sync-maven"] @@ -115,7 +125,8 @@ After you have run either of the preceding commands to create the Maven project, implementation("io.quarkus:quarkus-security-jpa") ---- ==== -* To verify the `security-jpa-reactive` extension, check for the following configuration: +ifndef::no-quarkus-security-jpa-reactive[] +* To verify the `quarkus-security-jpa-reactive` extension, check for the following configuration: + ==== [source,xml,role="primary asciidoc-tabs-target-sync-cli asciidoc-tabs-target-sync-maven"] @@ -133,6 +144,7 @@ implementation("io.quarkus:quarkus-security-jpa") implementation("io.quarkus:quarkus-security-jpa-reactive") ---- ==== +endif::no-quarkus-security-jpa-reactive[] == Write the application @@ -266,7 +278,7 @@ public class User extends PanacheEntity { ---- -The `security-jpa` extension only initializes if a single entity is annotated with `@UserDefinition`. +The `quarkus-security-jpa` extension only initializes if a single entity is annotated with `@UserDefinition`. <1> The `@UserDefinition` annotation must be present on a single entity, either a regular Hibernate ORM entity or a Hibernate ORM with Panache entity. <2> Indicates the field used for the username. @@ -280,12 +292,13 @@ You can configure it to use plain text or custom passwords. ==== Don’t forget to set up the Panache and PostgreSQL JDBC driver, please see xref:hibernate-orm-panache.adoc#setting-up-and-configuring-hibernate-orm-with-panache[Setting up and configuring Hibernate ORM with Panache] for more information. ==== - +ifndef::no-quarkus-security-jpa-reactive[] [NOTE] ==== Hibernate Reactive Panache uses `io.quarkus.hibernate.reactive.panache.PanacheEntity` instead of `io.quarkus.hibernate.orm.panache.PanacheEntity`. For more information, see link:{quickstarts-tree-url}/security-jpa-reactive-quickstart/src/main/java/org/acme/elytron/security/jpa/reactive/User.java[User file]. ==== +endif::no-quarkus-security-jpa-reactive[] == Configure the application @@ -299,7 +312,7 @@ When secure access is required, and no other authentication mechanisms are enabl Therefore, in this tutorial, you do not need to set the property `quarkus.http.auth.basic` to `true`. ==== + -. Configure at least one data source in the `application.properties` file so the `security-jpa` extension can access your database. +. Configure at least one data source in the `application.properties` file so the `quarkus-security-jpa` extension can access your database. For example: + ==== @@ -318,9 +331,10 @@ quarkus.hibernate-orm.database.generation=drop-and-create + . To initialize the database with users and roles, implement the `Startup` class, as outlined in the following code snippet: +ifndef::no-quarkus-security-jpa-reactive[] [NOTE] ==== -* The URLs of Reactive datasources that are used by the `security-jpa-reactive` extension are set with the `quarkus.datasource.reactive.url` +* The URLs of Reactive datasources that are used by the `quarkus-security-jpa-reactive` extension are set with the `quarkus.datasource.reactive.url` configuration property and not the `quarkus.datasource.jdbc.url` configuration property typically used by JDBC datasources. + [source,properties] @@ -333,6 +347,7 @@ link:https://hibernate.org/orm/[Hibernate ORM] automatically creates the databas This approach is suitable for development but is not recommended for production. Therefore, adjustments are needed in a production environment. ==== +endif::no-quarkus-security-jpa-reactive[] [source,java] ---- @@ -362,7 +377,7 @@ The preceding example demonstrates how the application can be protected and iden [IMPORTANT] ==== In a production environment, do not store plain text passwords. -As a result, the `security-jpa` defaults to using bcrypt-hashed passwords. +As a result, the `quarkus-security-jpa` defaults to using bcrypt-hashed passwords. ==== == Test your application by using Dev Services for PostgreSQL diff --git a/docs/src/main/asciidoc/security-jpa.adoc b/docs/src/main/asciidoc/security-jpa.adoc index d114c46cf9666..0eefd1783de9e 100644 --- a/docs/src/main/asciidoc/security-jpa.adoc +++ b/docs/src/main/asciidoc/security-jpa.adoc @@ -78,12 +78,12 @@ public class User extends PanacheEntity { ---- -The `security-jpa` extension initializes only if a single entity is annotated with `@UserDefinition`. +The `quarkus-security-jpa` extension initializes only if a single entity is annotated with `@UserDefinition`. <1> The `@UserDefinition` annotation must be present on a single entity, either a regular Hibernate ORM entity or a Hibernate ORM with Panache entity. <2> Indicates the field used for the username. <3> Indicates the field used for the password. -By default, `security-jpa` uses bcrypt-hashed passwords, or you can configure plain text or custom passwords instead. +By default, `quarkus-security-jpa` uses bcrypt-hashed passwords, or you can configure plain text or custom passwords instead. <4> This indicates the comma-separated list of roles added to the target principal representation attributes. <5> This method lets you add users while hashing passwords with the proper `bcrypt` hash. diff --git a/docs/src/main/asciidoc/security-jwt.adoc b/docs/src/main/asciidoc/security-jwt.adoc index 7015c4be95a35..101fd589580c6 100644 --- a/docs/src/main/asciidoc/security-jwt.adoc +++ b/docs/src/main/asciidoc/security-jwt.adoc @@ -1106,6 +1106,7 @@ SmallRye JWT provides more properties which can be used to customize the token p |smallrye.jwt.keystore.verify.key.alias||This property has to be set to identify a public verification key which will be extracted from `KeyStore` from a matching certificate if `mp.jwt.verify.publickey.location` points to a `KeyStore` file. |smallrye.jwt.keystore.decrypt.key.alias||This property has to be set to identify a private decryption key if `mp.jwt.decrypt.key.location` points to a `KeyStore` file. |smallrye.jwt.keystore.decrypt.key.password||This property may be set if a private decryption key's password in `KeyStore` is different to `smallrye.jwt.keystore.password` when `mp.jwt.decrypt.key.location` points to a `KeyStore` file. +|smallrye.jwt.resolve-remote-keys-at-startup|false|Set this property to true to resolve the remote keys at the application startup. |=== == References diff --git a/docs/src/main/asciidoc/security-oidc-auth0-tutorial.adoc b/docs/src/main/asciidoc/security-oidc-auth0-tutorial.adoc index c44e4346d19eb..6e1ffbb1d991f 100644 --- a/docs/src/main/asciidoc/security-oidc-auth0-tutorial.adoc +++ b/docs/src/main/asciidoc/security-oidc-auth0-tutorial.adoc @@ -887,6 +887,7 @@ Open a browser, access http://localhost:8080/hello and get the name displayed in To confirm the permission is correctly enforced, change it to `echo.name`: `@PermissionsAllowed("echo.name")`. Clear the browser cache, access http://localhost:8080/hello again and you will get `403` reported by `ApiEchoService`. Now revert it back to `@PermissionsAllowed("echo:name")`. +ifndef::no-deprecated-test-resource[] == Integration testing You have already used OIDC DevUI SPA to login to Auth0 and test the Quarkus endpoint with the access token, updating the endpoint code along the way. @@ -1035,6 +1036,7 @@ image::auth0-test-success.png[Auth0 test success] By the way, if you like, you can run the tests in Continuous mode directly from DevUI: image::auth0-continuous-testing.png[Auth0 Continuous testing] +endif::no-deprecated-test-resource[] [[production-mode]] == Production mode 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 6b2f9b06a891a..67717da4065b1 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 @@ -228,13 +228,14 @@ docker run --name keycloak -e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=ad For more information, see the Keycloak documentation about link:https://www.keycloak.org/docs/latest/server_admin/index.html#configuring-realms[creating and configuring a new realm]. - +ifndef::no-quarkus-keycloak-admin-client[] [NOTE] ==== If you want to use the Keycloak Admin Client to configure your server from your application, you need to include either the `quarkus-keycloak-admin-rest-client` or the `quarkus-keycloak-admin-resteasy-client` (if the application uses `quarkus-rest-client`) extension. For more information, see the xref:security-keycloak-admin-client.adoc[Quarkus Keycloak Admin Client] guide. - ==== +endif::no-quarkus-keycloak-admin-client[] + [[keycloak-dev-mode]] @@ -367,4 +368,6 @@ For information about writing integration tests that depend on `Dev Services for * xref:security-jwt-build.adoc[Sign and encrypt JWT tokens with SmallRye JWT Build] * xref:security-authentication-mechanisms.adoc#combining-authentication-mechanisms[Combining authentication mechanisms] * xref:security-overview.adoc[Quarkus Security overview] +ifndef::no-quarkus-keycloak-admin-client[] * xref:security-keycloak-admin-client.adoc[Quarkus Keycloak Admin Client] +endif::no-quarkus-keycloak-admin-client[] diff --git a/docs/src/main/asciidoc/security-oidc-bearer-token-authentication.adoc b/docs/src/main/asciidoc/security-oidc-bearer-token-authentication.adoc index b1c4bd9180b8f..577ca17069967 100644 --- a/docs/src/main/asciidoc/security-oidc-bearer-token-authentication.adoc +++ b/docs/src/main/asciidoc/security-oidc-bearer-token-authentication.adoc @@ -855,6 +855,7 @@ public class NativeBearerTokenAuthenticationIT extends BearerTokenAuthentication For more information about initializing and configuring Dev Services for Keycloak, see the xref:security-openid-connect-dev-services.adoc[Dev Services for Keycloak] guide. +ifndef::no-deprecated-test-resource[] [[integration-testing-keycloak]] ==== `KeycloakTestResourceLifecycleManager` @@ -957,6 +958,7 @@ By default: By default, `KeycloakTestResourceLifecycleManager` uses HTTPS to initialize a Keycloak instance, and this can be disabled by using `keycloak.use.https=false`. The default realm name is `quarkus`, and the client id is `quarkus-service-app`. If you want to customize these values, set the `keycloak.realm` and `keycloak.service.client` system properties. +endif::no-deprecated-test-resource[] [[integration-testing-public-key]] ==== Local public key @@ -1295,7 +1297,7 @@ import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import io.quarkus.oidc.AccessTokenCredential; -import io.quarkus.oidc.TenantFeature; +import io.quarkus.oidc.Tenant; import io.quarkus.oidc.TenantIdentityProvider; import io.quarkus.security.identity.SecurityIdentity; import io.quarkus.vertx.ConsumeEvent; @@ -1304,7 +1306,7 @@ import io.smallrye.common.annotation.Blocking; @ApplicationScoped public class OrderService { - @TenantFeature("tenantId") + @Tenant("tenantId") @Inject TenantIdentityProvider identityProvider; @@ -1321,14 +1323,14 @@ public class OrderService { } ---- -<1> For the default tenant, the `TenantFeature` qualifier is optional. +<1> For the default tenant, the `Tenant` qualifier is optional. <2> Executes token verification and converts the token to a `SecurityIdentity`. [NOTE] ==== When the provider is used during an HTTP request, the tenant configuration can be resolved as described in the xref:security-openid-connect-multitenancy.adoc[Using OpenID Connect Multi-Tenancy] guide. -However, when there is no active HTTP request, you must select the tenant explicitly with the `io.quarkus.oidc.TenantFeature` qualifier. +However, when there is no active HTTP request, you must select the tenant explicitly with the `io.quarkus.oidc.Tenant` qualifier. ==== [WARNING] @@ -1356,5 +1358,7 @@ For more information, see xref:security-oidc-code-flow-authentication#oidc-reque * xref:security-authentication-mechanisms.adoc#oidc-jwt-oauth2-comparison[Choosing between OpenID Connect, SmallRye JWT, and OAuth2 authentication mechanisms] * xref:security-authentication-mechanisms.adoc#combining-authentication-mechanisms[Combining authentication mechanisms] * xref:security-overview.adoc[Quarkus Security overview] +ifndef::no-quarkus-keycloak-admin-client[] * xref:security-keycloak-admin-client.adoc[Quarkus Keycloak Admin Client] +endif::no-quarkus-keycloak-admin-client[] * xref:security-openid-connect-multitenancy.adoc[Using OpenID Connect Multi-Tenancy] diff --git a/docs/src/main/asciidoc/security-oidc-code-flow-authentication-tutorial.adoc b/docs/src/main/asciidoc/security-oidc-code-flow-authentication-tutorial.adoc index 4ccd40e661a68..2a8c6219970bf 100644 --- a/docs/src/main/asciidoc/security-oidc-code-flow-authentication-tutorial.adoc +++ b/docs/src/main/asciidoc/security-oidc-code-flow-authentication-tutorial.adoc @@ -278,7 +278,9 @@ After you have completed this tutorial, explore xref:security-oidc-bearer-token- * xref:security-openid-connect-dev-services.adoc[Dev Services for Keycloak] * xref:security-jwt-build.adoc[Sign and encrypt JWT tokens with SmallRye JWT Build] * xref:security-authentication-mechanisms.adoc#oidc-jwt-oauth2-comparison[Choosing between OpenID Connect, SmallRye JWT, and OAuth2 authentication mechanisms] +ifndef::no-quarkus-keycloak-admin-client[] * xref:security-keycloak-admin-client.adoc[Quarkus Keycloak Admin Client] +endif::no-quarkus-keycloak-admin-client[] * https://www.keycloak.org/documentation.html[Keycloak Documentation] * xref:security-oidc-auth0-tutorial.adoc[Protect Quarkus web application by using Auth0 OpenID Connect provider] * https://openid.net/connect/[OpenID Connect] diff --git a/docs/src/main/asciidoc/security-oidc-code-flow-authentication.adoc b/docs/src/main/asciidoc/security-oidc-code-flow-authentication.adoc index e35ca4f0aade5..8972e1bc8c9d3 100644 --- a/docs/src/main/asciidoc/security-oidc-code-flow-authentication.adoc +++ b/docs/src/main/asciidoc/security-oidc-code-flow-authentication.adoc @@ -385,6 +385,7 @@ For example, `quarkus.oidc.authentication.redirect-path=/service/callback`, and If `quarkus.oidc.authentication.redirect-path` is set, but you need the original request URL to be restored after the user is redirected back to a unique callback URL, for example, `http://localhost:8080/service/callback`, set `quarkus.oidc.authentication.restore-path-after-redirect` property to `true`. This will restore the request URL such as `http://localhost:8080/service/1`. +[[customize-authentication-requests]] ==== Customizing authentication requests By default, only the `response_type` (set to `code`), `scope` (set to `openid`), `client_id`, `redirect_uri`, and `state` properties are passed as HTTP query parameters to the OIDC provider's authorization endpoint when the user is redirected to it to authenticate. @@ -398,6 +399,8 @@ The following example shows how you can work around this issue: quarkus.oidc.authentication.extra-params.response_mode=query ---- +See also the <> section explaining how a custom `OidcRedirectFilter` can be used to customize OIDC redirects, including those to the OIDC authorization endpoint. + ==== Customizing the authentication error response When the user is redirected to the OIDC authorization endpoint to authenticate and, if necessary, authorize the Quarkus application, this redirect request might fail, for example, when an invalid scope is included in the redirect URI. @@ -422,6 +425,133 @@ For example, if it is set to '/error' and the current request URI is `https://lo To prevent the user from being redirected to this page to be re-authenticated, ensure that this error endpoint is a public resource. ==== +[[oidc-redirect-filters]] +=== OIDC redirect filters + +You can register one or more `io.quarkus.oidc.OidcRedirectFilter` implementations to filter OIDC redirects to OIDC authorization and logout endpoints but also local redirects to custom error and session expired pages. Custom `OidcRedirectFilter` can add additional query parameters, response headers and set new cookies. + +For example, the following simple custom `OidcRedirectFilter` adds an additional query parameter and a custom response header for all redirect requests that can be done by Quarkus OIDC: + +[source,java] +---- +package io.quarkus.it.keycloak; + +import jakarta.enterprise.context.ApplicationScoped; + +import io.quarkus.arc.Unremovable; +import io.quarkus.oidc.OidcRedirectFilter; + +@ApplicationScoped +@Unremovable +public class GlobalOidcRedirectFilter implements OidcRedirectFilter { + + @Override + public void filter(OidcRedirectContext context) { + if (context.redirectUri().contains("/session-expired-page")) { + context.additionalQueryParams().add("redirect-filtered", "true,"); <1> + context.routingContext().response().putHeader("Redirect-Filtered", "true"); <2> + } + } + +} +---- +<1> Add an additional query parameter. Note the queury names and values are URL-encoded by Quarkus OIDC, a `redirect-filtered=true%20C` query parameter is added to the redirect URI in this case. +<2> Add a custom HTTP response header. + +See also the <> section how to configure additional query parameters for OIDC authorization point. + +Custom `OidcRedirectFilter` for local error and session expired pages can also create secure cookies to help with generating such pages. + +For example, let's assume you need to redirect the current user whose session has expired to a custom session expired page available at `http://localhost:8080/session-expired-page`. The following custom `OidcRedirectFilter` encrypts the user name in a custom `session_expired` cookie using an OIDC tenant client secret: + +[source,java] +---- +package io.quarkus.it.keycloak; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.jwt.Claims; + +import io.quarkus.arc.Unremovable; +import io.quarkus.oidc.AuthorizationCodeTokens; +import io.quarkus.oidc.OidcRedirectFilter; +import io.quarkus.oidc.Redirect; +import io.quarkus.oidc.TenantFeature; +import io.quarkus.oidc.runtime.OidcUtils; +import io.smallrye.jwt.build.Jwt; + +@ApplicationScoped +@Unremovable +@TenantFeature("tenant-refresh") +@Redirect(Location.SESSION_EXPIRED_PAGE) <1> +public class SessionExpiredOidcRedirectFilter implements OidcRedirectFilter { + + @Override + public void filter(OidcRedirectContext context) { + + if (context.redirectUri().contains("/session-expired-page")) { + AuthorizationCodeTokens tokens = context.routingContext().get(AuthorizationCodeTokens.class.getName()); <2> + String userName = OidcUtils.decodeJwtContent(tokens.getIdToken()).getString(Claims.preferred_username.name()); <3> + String jwe = Jwt.preferredUserName(userName).jwe() + .encryptWithSecret(context.oidcTenantConfig().credentials.secret.get()); <4> + OidcUtils.createCookie(context.routingContext(), context.oidcTenantConfig(), "session_expired", + jwe + "|" + context.oidcTenantConfig().tenantId.get(), 10); <5> + } + } +} + +---- +<1> Make sure this redirect filter is only called during a redirect to the session expired page. +<2> Access `AuthorizationCodeTokens` tokens associated with the now expired session as a `RoutingContext` attribute. +<3> Decode ID token claims and get a user name. +<4> Save the user name in a JWT token encrypted with the current OIDC tenant's client secret. +<5> Create a custom `session_expired` cookie valid for 5 seconds which joins the encrypted token and a tenant id using a "|" separator. Recording a tenant id in a custom cookie can help to generate correct session expired pages in a multi-tenant OIDC setup. + +Next, a public JAX-RS resource which generates session expired pages can use this cookie to create a page tailored for this user and the corresponding OIDC tenant, for example: + +[source,java] +---- +package io.quarkus.it.keycloak; + +import jakarta.inject.Inject; +import jakarta.ws.rs.CookieParam; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +import org.eclipse.microprofile.jwt.Claims; +import org.eclipse.microprofile.jwt.JsonWebToken; + +import io.quarkus.oidc.OidcTenantConfig; +import io.quarkus.oidc.runtime.TenantConfigBean; +import io.smallrye.jwt.auth.principal.DefaultJWTParser; +import io.vertx.ext.web.RoutingContext; + +@Path("/session-expired-page") +public class SessionExpiredResource { + + @Inject + TenantConfigBean tenantConfig; <1> + + @GET + public String sessionExpired(@CookieParam("session_expired") String sessionExpired) throws Exception { + // Cookie format: jwt| + + String[] pair = sessionExpired.split("\\|"); <2> + OidcTenantConfig oidcConfig = tenantConfig.getStaticTenantsConfig().get(pair[1]).getOidcTenantConfig(); <3> + JsonWebToken jwt = new DefaultJWTParser().decrypt(pair[0], oidcConfig.credentials.secret.get()); <4> + OidcUtils.removeCookie(context, oidcConfig, "session_expired"); <5> + return jwt.getClaim(Claims.preferred_username) + ", your session has expired. " + + "Please login again at http://localhost:8081/" + oidcConfig.tenantId.get(); <6> + } +} +---- +<1> Inject `TenantConfigBean` which can be used to access all the current OIDC tenant configurations. +<2> Split the custom cookie value into 2 parts, first part is the encrypted token, last part is the tenant id. +<3> Get the OIDC tenant configuration. +<4> Decrypt the cookie value using the OIDC tenant's client secret. +<5> Remove the custom cookie. +<6> Use the username in the decrypted token and the tenant id to generate the service expired page response. + === Accessing authorization data You can access information about authorization in different ways. @@ -515,7 +645,7 @@ Set the `quarkus.oidc.authentication.user-info-required=true` property to reques A request is sent to the OIDC provider `UserInfo` endpoint by using the access token returned with the authorization code grant response, and an `io.quarkus.oidc.UserInfo` (a simple `jakarta.json.JsonObject` wrapper) object is created. `io.quarkus.oidc.UserInfo` can be injected or accessed as a SecurityIdentity `userinfo` attribute. -`quarkus.oidc.authentication.user-info-required` is automatically enabled if one of these conditions is met: +`quarkus.oidc.authentication.user-info-required` is automatically enabled if one of these conditions is met: - if `quarkus.oidc.roles.source` is set to `userinfo` or `quarkus.oidc.token.verify-access-token-with-user-info` is set to `true` or `quarkus.oidc.authentication.id-token-required` is set to `false`, the current OIDC tenant must support a UserInfo endpoint in these cases. @@ -661,8 +791,11 @@ OIDC `CodeAuthenticationMechanism` uses the default `io.quarkus.oidc.TokenStateM It makes Quarkus OIDC endpoints completely stateless and it is recommended to follow this strategy to achieve the best scalability results. -See the <> and <> sections of this guide for alternative approaches to storing tokens. -For example, storing tokens in the database or other server-side storage, if you prefer and have good reasons for storing the token state on the server. +ifndef::no-quarkus-oidc-db-token-state-manager[] +Refer to the <> section of this guide for information on storing tokens in the database or other server-side storage solutions. This approach is suitable if you prefer and have compelling reasons to store the token state on the server. +endif::no-quarkus-oidc-db-token-state-manager[] + +See the <> section for alternative methods of token storage. This is ideal for those seeking customized solutions for token state management, especially when standard server-side storage does not meet your specific requirements. You can configure the default `TokenStateManager` to avoid saving an access token in the session cookie and to only keep ID and refresh tokens or a single ID token only. @@ -688,8 +821,10 @@ In such cases, use the `quarkus.oidc.token-state-manager.strategy` property to c If your chosen session cookie strategy combines tokens and generates a large session cookie value that is greater than 4KB, some browsers might not be able to handle such cookie sizes. This can occur when the ID, access, and refresh tokens are JWT tokens and the selected strategy is `keep-all-tokens` or with ID and refresh tokens when the strategy is `id-refresh-token`. To work around this issue, you can set `quarkus.oidc.token-state-manager.split-tokens=true` to create a unique session token for each token. +ifndef::no-quarkus-oidc-db-token-state-manager[] An alternative solution is to have the tokens saved in the database. For more information, see <>. +endif::no-quarkus-oidc-db-token-state-manager[] The default `TokenStateManager` encrypts the tokens before storing them in the session cookie. The following example shows how you configure it to split the tokens and encrypt them: @@ -781,6 +916,7 @@ public class CustomTokenStateManager implements TokenStateManager { For information about the default `TokenStateManager` storing tokens in an encrypted session cookie, see <>. +ifndef::no-quarkus-oidc-db-token-state-manager[] For information about the custom Quarkus `TokenStateManager` implementation storing tokens in a database, see <>. [[db-token-state-manager]] @@ -882,6 +1018,7 @@ public class OidcDbTokenStateManagerEntity { <1> The Hibernate ORM extension will only create this table for you when the database schema is generated. For more information, refer to the xref:hibernate-orm.adoc[Hibernate ORM] guide. <2> You can choose a column length depending on the length of your tokens. +endif::no-quarkus-oidc-db-token-state-manager[] === Logout and expiration @@ -1110,6 +1247,8 @@ When the session can not be refreshed, the currently authenticated user is redir Instead, you can request that the user is redirected to a public, application specific session expired page first. This page informs the user that the session has now expired and advise to re-authenticate by following a link to a secured application welcome page. The user clicks on the link and Quarkus OIDC enforces a redirect to the OIDC provider to re-authenticate. Use `quarkus.oidc.authentication.session-expired-page` relative path property, if you'd like to do it. For example, setting `quarkus.oidc.authentication.session-expired-page=/session-expired-page` will ensure that the user whose session has expired is redirected to `http://localhost:8080/session-expired-page`, assuming the application is available at `http://localhost:8080`. + +See also the <> section explaining how a custom `OidcRedirectFilter` can be used to customize OIDC redirects, including those to the session expired pages. ==== @@ -1564,6 +1703,7 @@ testImplementation("net.sourceforge.htmlunit:htmlunit") testImplementation("io.quarkus:quarkus-junit5") ---- +ifndef::no-deprecated-test-resource[] [[integration-testing-wiremock]] === Wiremock @@ -1605,10 +1745,10 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import org.junit.jupiter.api.Test; -import com.gargoylesoftware.htmlunit.SilentCssErrorHandler; -import com.gargoylesoftware.htmlunit.WebClient; -import com.gargoylesoftware.htmlunit.html.HtmlForm; -import com.gargoylesoftware.htmlunit.html.HtmlPage; +import org.htmlunit.SilentCssErrorHandler; +import org.htmlunit.WebClient; +import org.htmlunit.html.HtmlForm; +import org.htmlunit.html.HtmlPage; import io.quarkus.test.common.QuarkusTestResource; import io.quarkus.test.junit.QuarkusTest; @@ -1650,6 +1790,7 @@ The user `admin` has the `user` and `admin` roles by default - it can be customi Additionally, `OidcWiremockTestResource` sets the token issuer and audience to `https://service.example.com`, which can be customized with `quarkus.test.oidc.token.issuer` and `quarkus.test.oidc.token.audience` system properties. `OidcWiremockTestResource` can be used to emulate all OIDC providers. +endif::no-deprecated-test-resource[] [[integration-testing-keycloak-devservices]] === Dev Services for Keycloak @@ -1686,6 +1827,7 @@ public class CodeFlowAuthorizationTest { } ---- +ifndef::no-deprecated-test-resource[] [[integration-testing-keycloak]] === Using KeycloakTestResourceLifecycleManager @@ -1750,6 +1892,7 @@ The user `admin` has the `user` and `admin` roles by default - it can be customi By default, `KeycloakTestResourceLifecycleManager` uses HTTPS to initialize a Keycloak instance that can be disabled by specifying `keycloak.use.https=false`. The default realm name is `quarkus` and client id is `quarkus-web-app` - set `keycloak.realm` and `keycloak.web-app.client` system properties to customize the values if needed. +endif::no-deprecated-test-resource[] [[integration-testing-security-annotation]] === TestSecurity annotation @@ -1795,4 +1938,3 @@ From the `quarkus dev` console, type `j` to change the application global log le * https://www.keycloak.org/documentation.html[Keycloak documentation] * https://openid.net/connect/[OpenID Connect] * https://tools.ietf.org/html/rfc7519[JSON Web Token] - diff --git a/docs/src/main/asciidoc/security-oidc-configuration-properties-reference.adoc b/docs/src/main/asciidoc/security-oidc-configuration-properties-reference.adoc index 438a9f72a3ec4..e76df52eb6375 100644 --- a/docs/src/main/asciidoc/security-oidc-configuration-properties-reference.adoc +++ b/docs/src/main/asciidoc/security-oidc-configuration-properties-reference.adoc @@ -19,14 +19,8 @@ include::{generated-dir}/config/quarkus-oidc.adoc[opts=optional, leveloffset=+1] * xref:security-oidc-bearer-token-authentication.adoc[OIDC Bearer token authentication] * xref:security-oidc-bearer-token-authentication-tutorial.adoc[Protect a service application by using OpenID Connect (OIDC) Bearer token authentication] -// * https://www.keycloak.org/documentation.html[Keycloak Documentation] * https://openid.net/connect/[OpenID Connect] -// * https://tools.ietf.org/html/rfc7519[JSON Web Token] * xref:security-openid-connect-client-reference.adoc[OpenID Connect and OAuth2 Client and Filters Reference Guide] -// * xref:security-openid-connect-dev-services.adoc[Dev Services for Keycloak] -// * xref:security-jwt-build.adoc[Sign and encrypt JWT tokens with SmallRye JWT Build] * xref:security-authentication-mechanisms.adoc#oidc-jwt-oauth2-comparison[Choosing between OpenID Connect, SmallRye JWT, and OAuth2 authentication mechanisms] * xref:security-authentication-mechanisms.adoc#combining-authentication-mechanisms[Combining authentication mechanisms] * xref:security-overview.adoc[Quarkus Security] -// * xref:security-keycloak-admin-client.adoc[Quarkus Keycloak Admin Client] -// TASK - Select some references and eliminate the rest. diff --git a/docs/src/main/asciidoc/security-openid-connect-client-reference.adoc b/docs/src/main/asciidoc/security-openid-connect-client-reference.adoc index db214a15dbe15..6bceac124346a 100644 --- a/docs/src/main/asciidoc/security-openid-connect-client-reference.adoc +++ b/docs/src/main/asciidoc/security-openid-connect-client-reference.adoc @@ -15,7 +15,11 @@ You can use Quarkus extensions for OpenID Connect and OAuth 2.0 access token man This includes the following: - Using `quarkus-oidc-client`, `quarkus-rest-client-oidc-filter` and `quarkus-resteasy-client-oidc-filter` extensions to acquire and refresh access tokens from OpenID Connect and OAuth 2.0 compliant Authorization Servers such as link:https://www.keycloak.org[Keycloak]. + +ifndef::no-quarkus-oidc-token-propagation[] + - Using `quarkus-rest-client-oidc-token-propagation` and `quarkus-resteasy-client-oidc-token-propagation` extensions to propagate the current `Bearer` or `Authorization Code Flow` access tokens. +endif::no-quarkus-oidc-token-propagation[] The access tokens managed by these extensions can be used as HTTP Authorization Bearer tokens to access the remote services. @@ -1097,6 +1101,7 @@ public class OidcRequestCustomizer implements OidcRequestFilter { } ---- +ifndef::no-quarkus-oidc-token-propagation-reactive[] [[token-propagation-reactive]] == Token Propagation Reactive @@ -1172,7 +1177,9 @@ quarkus.oidc-token-propagation.exchange-token=true ---- `AccessTokenRequestReactiveFilter` uses a default `OidcClient` by default. A named `OidcClient` can be selected with a `quarkus.oidc-token-propagation-reactive.client-name` configuration property or with the `io.quarkus.oidc.token.propagation.AccessToken#exchangeTokenClient` annotation attribute. +endif::no-quarkus-oidc-token-propagation-reactive[] +ifndef::no-quarkus-oidc-token-propagation[] [[token-propagation]] == Token Propagation @@ -1187,6 +1194,7 @@ However, the direct end-to-end Bearer token propagation should be avoided. For e Additionally, a complex application might need to exchange or update the tokens before propagating them. For example, the access context might be different when `Service A` is accessing `Service B`. In this case, `Service A` might be granted a narrow or completely different set of scopes to access `Service B`. The following sections show how `AccessTokenRequestFilter` and `JsonWebTokenRequestFilter` can help. +endif::no-quarkus-oidc-token-propagation[] === RestClient AccessTokenRequestFilter @@ -1328,6 +1336,7 @@ As mentioned, use `AccessTokenRequestFilter` if you work with Keycloak or an Ope You can generate the tokens as described in xref:security-oidc-bearer-token-authentication.adoc#integration-testing[OpenID Connect Bearer Token Integration testing] section. Prepare the REST test endpoints. You can have the test front-end endpoint, which uses the injected MP REST client with a registered token propagation filter, call the downstream endpoint. For example, see the `integration-tests/resteasy-client-oidc-token-propagation` in the `main` Quarkus repository. +ifndef::no-quarkus-oidc-token-propagation[] [[reactive-token-propagation]] == Token Propagation Reactive @@ -1345,8 +1354,10 @@ The `quarkus-rest-client-resteasy-client-oidc-token-propagation` extension provi The `quarkus-rest-client-resteasy-client-oidc-token-propagation` extension (as opposed to the non-reactive `quarkus-resteasy-client-oidc-token-propagation` extension) does not currently support the exchanging or resigning of the tokens before the propagation. However, these features might be added in the future. +endif::no-quarkus-oidc-token-propagation[] -[[oidc-client-graphql-client]] +ifndef::no-quarkus-oidc-client-graphql[] +[[quarkus-oidc-client-graphql]] == GraphQL client integration The `quarkus-oidc-client-graphql` extension provides a way to integrate an OIDC client into xref:smallrye-graphql-client.adoc[GraphQL clients] paralleling the approach used with REST clients. @@ -1401,6 +1412,7 @@ Uni tokenUni = oidcClients.getClient("OIDC_CLIENT_NAME") builder.dynamicHeader("Authorization", tokenUni); VertxDynamicGraphQLClient client = builder.build(); ---- +endif::no-quarkus-oidc-client-graphql[] [[configuration-reference]] == Configuration reference @@ -1409,9 +1421,11 @@ VertxDynamicGraphQLClient client = builder.build(); include::{generated-dir}/config/quarkus-oidc-client.adoc[opts=optional, leveloffset=+1] +ifndef::no-quarkus-oidc-token-propagation-reactive[] === OIDC token propagation include::{generated-dir}/config/quarkus-oidc-token-propagation-reactive.adoc[opts=optional, leveloffset=+1] +endif::no-quarkus-oidc-token-propagation-reactive[] == References diff --git a/docs/src/main/asciidoc/security-openid-connect-client.adoc b/docs/src/main/asciidoc/security-openid-connect-client.adoc index 6594912214038..9876031aee273 100644 --- a/docs/src/main/asciidoc/security-openid-connect-client.adoc +++ b/docs/src/main/asciidoc/security-openid-connect-client.adoc @@ -68,6 +68,7 @@ The solution is in the `security-openid-connect-client-quickstart` link:{quickst First, you need a new project. Create a new project with the following command: +ifndef::no-quarkus-oidc-token-propagation[] :create-app-artifact-id: security-openid-connect-client-quickstart :create-app-extensions: oidc,rest-client-oidc-filter,rest-client-oidc-token-propagation,rest include::{includes}/devtools/create-app.adoc[] @@ -78,6 +79,20 @@ If you already have your Quarkus project configured, you can add these extension :add-extension-extensions: oidc,rest-client-oidc-filter,rest-client-oidc-token-propagation,rest include::{includes}/devtools/extension-add.adoc[] +endif::no-quarkus-oidc-token-propagation[] + +ifdef::no-quarkus-oidc-token-propagation[] +:create-app-artifact-id: security-openid-connect-client-quickstart +:create-app-extensions: oidc,rest-client-oidc-filter,rest +include::{includes}/devtools/create-app.adoc[] + +It generates a Maven project, importing the `oidc`, `rest-client-oidc-filter`, and `rest` extensions. + +If you already have your Quarkus project configured, you can add these extensions to your project by running the following command in your project base directory: + +:add-extension-extensions: oidc,rest-client-oidc-filter,rest +include::{includes}/devtools/extension-add.adoc[] +endif::no-quarkus-oidc-token-propagation[] It adds the following extensions to your build file: @@ -92,21 +107,31 @@ It adds the following extensions to your build file: io.quarkus quarkus-rest-client-oidc-filter
+ifndef::no-quarkus-oidc-token-propagation[] io.quarkus quarkus-rest-client-oidc-token-propagation +endif::no-quarkus-oidc-token-propagation[] io.quarkus quarkus-rest ---- -[source,gradle,role="secondary asciidoc-tabs-target-sync-gradle"] .build.gradle +ifndef::no-quarkus-oidc-token-propagation[] +[source,gradle,role="secondary asciidoc-tabs-target-sync-gradle"] ---- implementation("io.quarkus:quarkus-oidc,rest-client-oidc-filter,rest-client-oidc-token-propagation,rest") ---- +endif::no-quarkus-oidc-token-propagation[] +ifdef::no-quarkus-oidc-token-propagation[] +[source,gradle,role="secondary asciidoc-tabs-target-sync-gradle"] +---- +implementation("io.quarkus:quarkus-oidc,rest-client-oidc-filter,rest") +---- +endif::no-quarkus-oidc-token-propagation[] == Writing the application @@ -155,11 +180,13 @@ public class ProtectedResource { `ProtectedResource` returns a name from both `userName()` and `adminName()` methods. The name is extracted from the current `JsonWebToken`. -Next, add three REST clients: +Next, add the following REST clients: 1. `RestClientWithOidcClientFilter`, which uses an OIDC client filter provided by the `quarkus-rest-client-oidc-filter` extension to get and propagate an access token. 2. `RestClientWithTokenHeaderParam`, which accepts a token already acquired by the programmatically created OidcClient as an HTTP `Authorization` header value. +ifndef::no-quarkus-oidc-token-propagation[] 3. `RestClientWithTokenPropagationFilter`, which uses an OIDC token propagation filter provided by the `quarkus-rest-client-oidc-token-propagation` extension to get and propagate an access token. +endif::no-quarkus-oidc-token-propagation[] Add the `RestClientWithOidcClientFilter` REST client: @@ -217,7 +244,7 @@ public interface RestClientWithTokenHeaderParam { @Produces("text/plain") @Path("userName") Uni getUserName(@HeaderParam("Authorization") String authorization); <1> - + @GET @Produces("text/plain") @Path("adminName") @@ -226,6 +253,7 @@ public interface RestClientWithTokenHeaderParam { ---- <1> `RestClientWithTokenHeaderParam` REST client expects that the tokens will be passed to it as HTTP `Authorization` header values. +ifndef::no-quarkus-oidc-token-propagation[] Add the `RestClientWithTokenPropagationFilter` REST client: [source,java] @@ -263,6 +291,8 @@ public interface RestClientWithTokenPropagationFilter { IMPORTANT: Do not use the `RestClientWithOidcClientFilter` and `RestClientWithTokenPropagationFilter` interfaces in the same REST client because they can conflict, leading to issues. For example, the OIDC client filter can override the token from the OIDC token propagation filter, or the propagation filter might not work correctly if it attempts to propagate a token when none is available, expecting the OIDC client filter to obtain a new token instead. +endif::no-quarkus-oidc-token-propagation[] + Also, add `OidcClientCreator` to create an OIDC client programmatically at startup. `OidcClientCreator` supports `RestClientWithTokenHeaderParam` REST client calls: @@ -340,12 +370,12 @@ public class FrontendResource { @Inject @RestClient RestClientWithOidcClientFilter restClientWithOidcClientFilter; <1> - + @Inject @RestClient RestClientWithTokenPropagationFilter restClientWithTokenPropagationFilter; <2> - @Inject + @Inject OidcClientCreator oidcClientCreator; TokensHelper tokenHelper = new TokensHelper(); <5> @Inject @@ -387,7 +417,7 @@ public class FrontendResource { return tokenHelper.getTokens(oidcClientCreator.getOidcClient()).onItem() .transformToUni(tokens -> restClientWithTokenHeaderParam.getUserName("Bearer " + tokens.getAccessToken())); } - + @GET @Path("admin-name-with-oidc-client-token-header-param") @Produces("text/plain") @@ -403,7 +433,7 @@ public class FrontendResource { Tokens tokens = tokenHelper.getTokens(oidcClientCreator.getOidcClient()).await().indefinitely(); return restClientWithTokenHeaderParam.getUserName("Bearer " + tokens.getAccessToken()).await().indefinitely(); } - + @GET @Path("admin-name-with-oidc-client-token-header-param-blocking") @Produces("text/plain") @@ -411,7 +441,7 @@ public class FrontendResource { Tokens tokens = tokenHelper.getTokens(oidcClientCreator.getOidcClient()).await().indefinitely(); return restClientWithTokenHeaderParam.getAdminName("Bearer " + tokens.getAccessToken()).await().indefinitely(); } - + } ---- <1> `FrontendResource` uses the injected `RestClientWithOidcClientFilter` REST client with the OIDC client filter to get and propagate an access token to `ProtectedResource` when either `/frontend/user-name-with-oidc-client-token` or `/frontend/admin-name-with-oidc-client-token` is called. @@ -663,7 +693,7 @@ curl -i -X GET \ In contrast with the preceding command, this command returns a `403` status code. -Next, test that the programmatically created OIDC client correctly acquires and propagates the token with `RestClientWithTokenHeaderParam` both in reactive and imperative (blocking) modes. +Next, test that the programmatically created OIDC client correctly acquires and propagates the token with `RestClientWithTokenHeaderParam` both in reactive and imperative (blocking) modes. Call the `/user-name-with-oidc-client-token-header-param`. This command returns the `200` status code and the name `alice`: diff --git a/docs/src/main/asciidoc/security-openid-connect-dev-services.adoc b/docs/src/main/asciidoc/security-openid-connect-dev-services.adoc index fd3d37c2468ab..32ebe11900fc0 100644 --- a/docs/src/main/asciidoc/security-openid-connect-dev-services.adoc +++ b/docs/src/main/asciidoc/security-openid-connect-dev-services.adoc @@ -258,7 +258,7 @@ For more information, see xref:security-oidc-bearer-token-authentication.adoc#in [[keycloak-initialization]] === Keycloak initialization -The `quay.io/keycloak/keycloak:23.0.7` image which contains a Keycloak distribution powered by Quarkus is used to start a container by default. +The `quay.io/keycloak/keycloak:24.0.4` image which contains a Keycloak distribution powered by Quarkus is used to start a container by default. `quarkus.keycloak.devservices.image-name` can be used to change the Keycloak image name. For example, set it to `quay.io/keycloak/keycloak:19.0.3-legacy` to use a Keycloak distribution powered by WildFly. Be aware that a Quarkus-based Keycloak distribution is only available starting from Keycloak `20.0.0`. diff --git a/docs/src/main/asciidoc/security-openid-connect-multitenancy.adoc b/docs/src/main/asciidoc/security-openid-connect-multitenancy.adoc index cb2ea8024881c..0cca72b4f7dad 100644 --- a/docs/src/main/asciidoc/security-openid-connect-multitenancy.adoc +++ b/docs/src/main/asciidoc/security-openid-connect-multitenancy.adoc @@ -410,6 +410,7 @@ After a little while, you can run this binary directly: == Test the application +ifndef::no-deprecated-test-resource[] === Use Dev Services for Keycloak xref:security-openid-connect-dev-services.adoc[Dev Services for Keycloak] is recommended for the integration testing against Keycloak. @@ -478,13 +479,12 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.IOException; +import org.htmlunit.SilentCssErrorHandler; +import org.htmlunit.WebClient; +import org.htmlunit.html.HtmlForm; +import org.htmlunit.html.HtmlPage; import org.junit.jupiter.api.Test; -import com.gargoylesoftware.htmlunit.SilentCssErrorHandler; -import com.gargoylesoftware.htmlunit.WebClient; -import com.gargoylesoftware.htmlunit.html.HtmlForm; -import com.gargoylesoftware.htmlunit.html.HtmlPage; - import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.keycloak.client.KeycloakTestClient; import io.restassured.RestAssured; @@ -562,6 +562,7 @@ public class CodeFlowIT extends CodeFlowTest { ---- For more information about how it is initialized and configured, see xref:security-openid-connect-dev-services.adoc[Dev Services for Keycloak]. +endif::no-deprecated-test-resource[] === Use the browser diff --git a/docs/src/main/asciidoc/security-overview.adoc b/docs/src/main/asciidoc/security-overview.adoc index c4620b815fba1..5750dbd9a897c 100644 --- a/docs/src/main/asciidoc/security-overview.adoc +++ b/docs/src/main/asciidoc/security-overview.adoc @@ -17,8 +17,12 @@ Before building security into your Quarkus applications, learn about the xref:se == Key features of Quarkus Security The Quarkus Security framework provides built-in security authentication mechanisms for Basic, Form-based, and mutual TLS (mTLS) authentication. +ifndef::no-webauthn-authentication[] You can also use other well-known xref:security-authentication-mechanisms.adoc#other-supported-authentication-mechanisms[authentication mechanisms], such as OpenID Connect (OIDC) and WebAuthn. - +endif::no-webauthn-authentication[] +ifdef::no-webauthn-authentication[] +You can also use other well-known xref:security-authentication-mechanisms.adoc#other-supported-authentication-mechanisms[authentication mechanisms], such as OpenID Connect (OIDC). +endif::no-webauthn-authentication[] Authentication mechanisms depend on xref:security-identity-providers.adoc[Identity providers] to verify the authentication credentials and map them to a `SecurityIdentity` instance with the username, roles, original authentication credentials, and other attributes. {project-name} also includes built-in security to allow for role-based access control (RBAC) based on the common security annotations `@RolesAllowed`, `@DenyAll`, `@PermitAll` on REST endpoints, and Contexts and Dependency Injection (CDI) beans. @@ -53,6 +57,12 @@ For guidance on testing Quarkus Security features and ensuring that your Quarkus == More about security features in Quarkus +=== WebSockets Next security + +The `quarkus-websockets-next` extension provides a modern, efficient implementation of the WebSocket API. +It also provides an integration with Quarkus security. +For more information, see the xref:websockets-next-reference.adoc#websocket-next-security[Security] section of the Quarkus "WebSockets Next reference" guide. + [[cross-origin-resource-sharing]] === Cross-origin resource sharing diff --git a/docs/src/main/asciidoc/smallrye-graphql-client.adoc b/docs/src/main/asciidoc/smallrye-graphql-client.adoc index 4aa0a118e0d6b..bfe200446348c 100644 --- a/docs/src/main/asciidoc/smallrye-graphql-client.adoc +++ b/docs/src/main/asciidoc/smallrye-graphql-client.adoc @@ -363,7 +363,8 @@ This example showed how to use both the dynamic and typesafe GraphQL clients to GraphQL service and explained the difference between the client types. == References +ifndef::no-quarkus-oidc-client-graphql[] +* xref:security-openid-connect-client-reference.adoc#quarkus-oidc-client-graphql[Integrating OIDC clients into GraphQL clients] +endif::no-quarkus-oidc-client-graphql[] -* xref:security-openid-connect-client-reference.adoc#oidc-client-graphql-client[Integrating OIDC clients into GraphQL clients] * https://smallrye.io/smallrye-graphql/latest/[Upstream SmallRye GraphQL Client documentation] - diff --git a/docs/src/main/asciidoc/telemetry-opentracing-to-otel-tutorial.adoc b/docs/src/main/asciidoc/telemetry-opentracing-to-otel-tutorial.adoc index 5f381e63294ec..35cfd903c8466 100644 --- a/docs/src/main/asciidoc/telemetry-opentracing-to-otel-tutorial.adoc +++ b/docs/src/main/asciidoc/telemetry-opentracing-to-otel-tutorial.adoc @@ -302,7 +302,7 @@ innerSpan.setTag("error.message", e.getMessage());``` innerSpan.recordException(e);``` |- -|Baggage carried by SpanContext in the Span | Baggage is an independent signal propagated in parallel with the OTel Context +|Baggage carried by SpanContext in the Span |Baggage is an independent signal propagated in parallel with the OTel Context, it's not part of it. |=== diff --git a/docs/src/main/asciidoc/websockets-next-reference.adoc b/docs/src/main/asciidoc/websockets-next-reference.adoc index 3e76df74bf969..55203bc86b1a2 100644 --- a/docs/src/main/asciidoc/websockets-next-reference.adoc +++ b/docs/src/main/asciidoc/websockets-next-reference.adoc @@ -16,6 +16,8 @@ include::_attributes.adoc[] include::{includes}/extension-status.adoc[] +The `quarkus-websockets-next` extension provides a modern declarative API to define WebSocket server and client endpoints. + == The WebSocket protocol The _WebSocket_ protocol, documented in the https://datatracker.ietf.org/doc/html/rfc6455[RFC6455], establishes a standardized method for creating a bidirectional communication channel between a client and a server through a single TCP connection. @@ -457,6 +459,10 @@ The method that declares a most-specific supertype of the actual exception is se NOTE: The `@io.quarkus.websockets.next.OnError` annotation can be also used to declare a global error handler, i.e. a method that is not declared on a WebSocket endpoint. Such a method may not accept `@PathParam` paremeters. Error handlers declared on an endpoint take precedence over the global error handlers. +When an error occurs but no error handler can handle the failure, Quarkus uses the strategy specified by `quarkus.websockets-next.server.unhandled-failure-strategy` and `quarkus.websockets-next.client.unhandled-failure-strategy`, respectively. +By default, the connection is closed. +Alternatively, an error message can be logged or no operation performed. + == Access to the WebSocketConnection The `io.quarkus.websockets.next.WebSocketConnection` object represents the WebSocket connection. @@ -574,6 +580,67 @@ void pong(Buffer data) { } ---- +[[websocket-next-security]] +== Security + +WebSocket endpoint callback methods can be secured with security annotations such as `io.quarkus.security.Authenticated`, +`jakarta.annotation.security.RolesAllowed` and other annotations listed in the xref:security-authorize-web-endpoints-reference.adoc#standard-security-annotations[Supported security annotations] documentation. + +For example: + +[source, java] +---- +package io.quarkus.websockets.next.test.security; + +import jakarta.annotation.security.RolesAllowed; +import jakarta.inject.Inject; + +import io.quarkus.security.ForbiddenException; +import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.websockets.next.OnError; +import io.quarkus.websockets.next.OnOpen; +import io.quarkus.websockets.next.OnTextMessage; +import io.quarkus.websockets.next.WebSocket; + +@WebSocket(path = "/end") +public class Endpoint { + + @Inject + SecurityIdentity currentIdentity; + + @OnOpen + String open() { + return "ready"; + } + + @RolesAllowed("admin") + @OnTextMessage + String echo(String message) { <1> + return message; + } + + @OnError + String error(ForbiddenException t) { <2> + return "forbidden:" + currentIdentity.getPrincipal().getName(); + } +} +---- +<1> The echo callback method can only be invoked if the current security identity has an `admin` role. +<2> The error handler is invoked in case of the authorization failure. + +`SecurityIdentity` is initially created during a secure HTTP upgrade and associated with the websocket connection. + +Currently, for an HTTP upgrade be secured, users must configure an HTTP policy protecting the HTTP upgrade path. +For example, to secure the `open()` method in the above websocket endpoint, one can add the following authentication policy: + +[source,properties] +---- +quarkus.http.auth.permission.secured.paths=/end +quarkus.http.auth.permission.secured.policy=authenticated +---- + +Other options for securing HTTP upgrade requests, such as using the security annotations, will be explored in the future. + [[websocket-next-configuration-reference]] == Configuration reference diff --git a/docs/src/main/asciidoc/writing-native-applications-tips.adoc b/docs/src/main/asciidoc/writing-native-applications-tips.adoc index 04afc5df02b1b..c7b6f86c8419a 100644 --- a/docs/src/main/asciidoc/writing-native-applications-tips.adoc +++ b/docs/src/main/asciidoc/writing-native-applications-tips.adoc @@ -197,6 +197,8 @@ public class MyReflectionConfiguration { } ---- +Note: By default the `@RegisterForReflection` annotation will also registered any potential nested classes for reflection. If you want to avoid this behavior, you can set the `ignoreNested` attribute to `true`. + ==== Using a configuration file You can also use a configuration file to register classes for reflection, if you prefer relying on the GraalVM infrastructure. diff --git a/docs/sync-web-site.sh b/docs/sync-web-site.sh index d7892c714e392..f65b3d3f8abfa 100755 --- a/docs/sync-web-site.sh +++ b/docs/sync-web-site.sh @@ -38,9 +38,9 @@ if [ -z $TARGET_DIR ]; then GIT_OPTIONS="--depth=1" fi if [ -n "${RELEASE_GITHUB_TOKEN}" ]; then - git clone -b develop --single-branch $GIT_OPTIONS https://github.com/quarkusio/quarkusio.github.io.git ${TARGET_DIR} + git clone --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} + git clone --single-branch $GIT_OPTIONS git@github.com:quarkusio/quarkusio.github.io.git ${TARGET_DIR} fi fi @@ -148,7 +148,7 @@ then cd target/web-site git add -A git commit -m "Sync web site with Quarkus documentation" - git push origin develop + git push origin main echo "Web Site updated - wait for CI build" else echo " diff --git a/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/DataSourceJdbcRuntimeConfig.java b/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/DataSourceJdbcRuntimeConfig.java index 382c20ccaa992..392903d7d3105 100644 --- a/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/DataSourceJdbcRuntimeConfig.java +++ b/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/DataSourceJdbcRuntimeConfig.java @@ -8,6 +8,7 @@ import io.agroal.api.configuration.AgroalConnectionFactoryConfiguration; import io.agroal.api.configuration.AgroalConnectionPoolConfiguration; import io.quarkus.runtime.annotations.ConfigDocDefault; +import io.quarkus.runtime.annotations.ConfigDocMapKey; import io.quarkus.runtime.annotations.ConfigGroup; import io.smallrye.config.WithDefault; import io.smallrye.config.WithName; @@ -131,6 +132,7 @@ public interface DataSourceJdbcRuntimeConfig { /** * Other unspecified properties to be passed to the JDBC driver when creating new connections. */ + @ConfigDocMapKey("property-key") Map additionalJdbcProperties(); /** diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/AnnotationsTransformerBuildItem.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/AnnotationsTransformerBuildItem.java index e4d803349d668..f926db37e782e 100644 --- a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/AnnotationsTransformerBuildItem.java +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/AnnotationsTransformerBuildItem.java @@ -1,6 +1,7 @@ package io.quarkus.arc.deployment; import org.jboss.jandex.AnnotationTarget; +import org.jboss.jandex.AnnotationTransformation; import io.quarkus.arc.processor.AnnotationsTransformer; import io.quarkus.builder.item.MultiBuildItem; @@ -19,13 +20,32 @@ */ public final class AnnotationsTransformerBuildItem extends MultiBuildItem { - private final AnnotationsTransformer transformer; + private final AnnotationTransformation transformer; + /** + * @deprecated use {@link #AnnotationsTransformerBuildItem(AnnotationTransformation)} + */ + @Deprecated(forRemoval = true) public AnnotationsTransformerBuildItem(AnnotationsTransformer transformer) { this.transformer = transformer; } + public AnnotationsTransformerBuildItem(AnnotationTransformation transformation) { + this.transformer = transformation; + } + + /** + * @deprecated use {@link #getAnnotationTransformation()} + */ + @Deprecated(forRemoval = true) public AnnotationsTransformer getAnnotationsTransformer() { + if (transformer instanceof AnnotationsTransformer) { + return (AnnotationsTransformer) transformer; + } + throw new UnsupportedOperationException("AnnotationTransformation is not an AnnotationsTransformer: " + transformer); + } + + public AnnotationTransformation getAnnotationTransformation() { return transformer; } diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ArcProcessor.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ArcProcessor.java index 0ad8d634aac41..4904043e48831 100644 --- a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ArcProcessor.java +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ArcProcessor.java @@ -212,7 +212,7 @@ protected DotName getDotName(DotName dotName) { applicationClassPredicateProducer.produce(new CompletedApplicationClassPredicateBuildItem(applicationClassPredicate)); builder.setApplicationClassPredicate(applicationClassPredicate); - builder.addAnnotationTransformer(new AnnotationsTransformer() { + builder.addAnnotationTransformation(new AnnotationsTransformer() { @Override public boolean appliesTo(AnnotationTarget.Kind kind) { @@ -259,7 +259,7 @@ public void transform(TransformationContext transformationContext) { resourceAnnotations.stream().map(ResourceAnnotationBuildItem::getName).collect(Collectors.toList())); // register all annotation transformers for (AnnotationsTransformerBuildItem transformer : annotationTransformers) { - builder.addAnnotationTransformer(transformer.getAnnotationsTransformer()); + builder.addAnnotationTransformation(transformer.getAnnotationTransformation()); } // register all injection point transformers for (InjectionPointTransformerBuildItem transformer : injectionPointTransformers) { diff --git a/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/context/optimized/OptimizeContextsAutoTest.java b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/context/optimized/OptimizeContextsAutoTest.java index 666c38dbc79f3..56be28ce16c3b 100644 --- a/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/context/optimized/OptimizeContextsAutoTest.java +++ b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/context/optimized/OptimizeContextsAutoTest.java @@ -10,6 +10,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; +import io.quarkus.arc.Arc; import io.quarkus.arc.ComponentsProvider; import io.quarkus.test.QuarkusUnitTest; @@ -29,7 +30,8 @@ public void testContexts() { assertTrue(bean.ping()); for (ComponentsProvider componentsProvider : ServiceLoader.load(ComponentsProvider.class)) { // We have less than 1000 beans - assertFalse(componentsProvider.getComponents().getContextInstances().isEmpty()); + assertFalse(componentsProvider.getComponents(Arc.container().getCurrentContextFactory()).getContextInstances() + .isEmpty()); } } } diff --git a/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/context/optimized/OptimizeContextsDisabledTest.java b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/context/optimized/OptimizeContextsDisabledTest.java index b1b611c81312c..64fc8c86de6ae 100644 --- a/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/context/optimized/OptimizeContextsDisabledTest.java +++ b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/context/optimized/OptimizeContextsDisabledTest.java @@ -9,6 +9,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; +import io.quarkus.arc.Arc; import io.quarkus.arc.ComponentsProvider; import io.quarkus.test.QuarkusUnitTest; @@ -27,7 +28,8 @@ public class OptimizeContextsDisabledTest { public void testContexts() { assertTrue(bean.ping()); for (ComponentsProvider componentsProvider : ServiceLoader.load(ComponentsProvider.class)) { - assertTrue(componentsProvider.getComponents().getContextInstances().isEmpty()); + assertTrue(componentsProvider.getComponents(Arc.container().getCurrentContextFactory()).getContextInstances() + .isEmpty()); } } diff --git a/extensions/arc/runtime/src/main/java/io/quarkus/arc/lookup/LookupIfProperty.java b/extensions/arc/runtime/src/main/java/io/quarkus/arc/lookup/LookupIfProperty.java index 6e86cc5bf12d5..a7de40972f6e8 100644 --- a/extensions/arc/runtime/src/main/java/io/quarkus/arc/lookup/LookupIfProperty.java +++ b/extensions/arc/runtime/src/main/java/io/quarkus/arc/lookup/LookupIfProperty.java @@ -30,7 +30,7 @@ * } * * {@literal @ApplicationScoped} - * class ServiceBar { + * class ServiceBar implements Service { * * public String name() { * return "bar"; diff --git a/extensions/arc/runtime/src/main/java/io/quarkus/arc/lookup/LookupUnlessProperty.java b/extensions/arc/runtime/src/main/java/io/quarkus/arc/lookup/LookupUnlessProperty.java index 5afcb7e4d18f1..ec1f7072afe27 100644 --- a/extensions/arc/runtime/src/main/java/io/quarkus/arc/lookup/LookupUnlessProperty.java +++ b/extensions/arc/runtime/src/main/java/io/quarkus/arc/lookup/LookupUnlessProperty.java @@ -30,7 +30,7 @@ * } * * {@literal @ApplicationScoped} - * class ServiceBar { + * class ServiceBar implements Service { * * public String name() { * return "bar"; diff --git a/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/ArcRecorder.java b/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/ArcRecorder.java index 4c1ecac85a712..ff8ccc90061d6 100644 --- a/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/ArcRecorder.java +++ b/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/ArcRecorder.java @@ -51,6 +51,7 @@ public ArcContainer initContainer(ShutdownContext shutdown, RuntimeValue> HttpContent requestContent = LastHttpContent.EMPTY_LAST_CONTENT; if (request.getBody().isPresent()) { - ByteBuf body = Unpooled.wrappedBuffer(request.getBody().get().getBytes()); + ByteBuf body = Unpooled.wrappedBuffer(request.getBody().get().getBytes(StandardCharsets.UTF_8)); requestContent = new DefaultLastHttpContent(body); } diff --git a/extensions/azure-functions/deployment/src/main/java/io/quarkus/azure/functions/deployment/AzureFunctionsConfig.java b/extensions/azure-functions/deployment/src/main/java/io/quarkus/azure/functions/deployment/AzureFunctionsConfig.java index 7e324cbee2d0b..4a29e13e3cca5 100644 --- a/extensions/azure-functions/deployment/src/main/java/io/quarkus/azure/functions/deployment/AzureFunctionsConfig.java +++ b/extensions/azure-functions/deployment/src/main/java/io/quarkus/azure/functions/deployment/AzureFunctionsConfig.java @@ -26,6 +26,7 @@ import com.microsoft.azure.toolkit.lib.common.exception.InvalidConfigurationException; import com.microsoft.azure.toolkit.lib.common.model.Region; +import io.quarkus.runtime.annotations.ConfigDocMapKey; import io.quarkus.runtime.annotations.ConfigGroup; import io.quarkus.runtime.annotations.ConfigItem; import io.quarkus.runtime.annotations.ConfigPhase; @@ -117,6 +118,7 @@ public class AzureFunctionsConfig { * Specifies the application settings for your Azure Functions, which are defined in name-value pairs */ @ConfigItem + @ConfigDocMapKey("setting-name") public Map appSettings = Collections.emptyMap(); @ConfigGroup diff --git a/extensions/container-image/container-image-buildpack/deployment/src/main/java/io/quarkus/container/image/buildpack/deployment/BuildpackConfig.java b/extensions/container-image/container-image-buildpack/deployment/src/main/java/io/quarkus/container/image/buildpack/deployment/BuildpackConfig.java index 4f6452bfb0fa1..d5ff64ec4c910 100644 --- a/extensions/container-image/container-image-buildpack/deployment/src/main/java/io/quarkus/container/image/buildpack/deployment/BuildpackConfig.java +++ b/extensions/container-image/container-image-buildpack/deployment/src/main/java/io/quarkus/container/image/buildpack/deployment/BuildpackConfig.java @@ -3,6 +3,7 @@ import java.util.Map; import java.util.Optional; +import io.quarkus.runtime.annotations.ConfigDocMapKey; import io.quarkus.runtime.annotations.ConfigItem; import io.quarkus.runtime.annotations.ConfigPhase; import io.quarkus.runtime.annotations.ConfigRoot; @@ -26,6 +27,7 @@ public class BuildpackConfig { * Environment key/values to pass to buildpacks. */ @ConfigItem + @ConfigDocMapKey("environment-variable-name") public Map builderEnv; /** diff --git a/extensions/container-image/container-image-docker-common/deployment/pom.xml b/extensions/container-image/container-image-docker-common/deployment/pom.xml new file mode 100644 index 0000000000000..6f553dc401722 --- /dev/null +++ b/extensions/container-image/container-image-docker-common/deployment/pom.xml @@ -0,0 +1,47 @@ + + + + quarkus-container-image-docker-common-parent + io.quarkus + 999-SNAPSHOT + + 4.0.0 + + quarkus-container-image-docker-common-deployment + Quarkus - Container Image - Docker Common - Deployment + + + + io.quarkus + quarkus-container-image-docker-common + + + io.quarkus + quarkus-container-image-deployment + + + org.assertj + assertj-core + test + + + + + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + + diff --git a/extensions/container-image/container-image-docker-common/deployment/src/main/java/io/quarkus/container/image/docker/common/deployment/CommonConfig.java b/extensions/container-image/container-image-docker-common/deployment/src/main/java/io/quarkus/container/image/docker/common/deployment/CommonConfig.java new file mode 100644 index 0000000000000..426240ae842ed --- /dev/null +++ b/extensions/container-image/container-image-docker-common/deployment/src/main/java/io/quarkus/container/image/docker/common/deployment/CommonConfig.java @@ -0,0 +1,56 @@ +package io.quarkus.container.image.docker.common.deployment; + +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import io.quarkus.runtime.annotations.ConfigDocDefault; +import io.quarkus.runtime.annotations.ConfigDocMapKey; + +public interface CommonConfig { + /** + * Path to the JVM Dockerfile. + * If set to an absolute path then the absolute path will be used, otherwise the path + * will be considered relative to the project root. + * If not set src/main/docker/Dockerfile.jvm will be used. + */ + @ConfigDocDefault("src/main/docker/Dockerfile.jvm") + Optional dockerfileJvmPath(); + + /** + * Path to the native Dockerfile. + * If set to an absolute path then the absolute path will be used, otherwise the path + * will be considered relative to the project root. + * If not set src/main/docker/Dockerfile.native will be used. + */ + @ConfigDocDefault("src/main/docker/Dockerfile.native") + Optional dockerfileNativePath(); + + /** + * Build args passed to docker via {@code --build-arg} + */ + @ConfigDocMapKey("arg-name") + Map buildArgs(); + + /** + * Images to consider as cache sources. Values are passed to {@code docker build}/{@code podman build} via the + * {@code cache-from} option + */ + Optional> cacheFrom(); + + /** + * The networking mode for the RUN instructions during build + */ + Optional network(); + + /** + * Name of binary used to execute the docker/podman commands. + * This setting can override the global container runtime detection. + */ + Optional executableName(); + + /** + * Additional arbitrary arguments passed to the executable when building the container image. + */ + Optional> additionalArgs(); +} diff --git a/extensions/container-image/container-image-docker-common/deployment/src/main/java/io/quarkus/container/image/docker/common/deployment/CommonProcessor.java b/extensions/container-image/container-image-docker-common/deployment/src/main/java/io/quarkus/container/image/docker/common/deployment/CommonProcessor.java new file mode 100644 index 0000000000000..ff9e2bbdbc5a7 --- /dev/null +++ b/extensions/container-image/container-image-docker-common/deployment/src/main/java/io/quarkus/container/image/docker/common/deployment/CommonProcessor.java @@ -0,0 +1,343 @@ +package io.quarkus.container.image.docker.common.deployment; + +import static io.quarkus.container.image.deployment.util.EnablementUtil.buildContainerImageNeeded; +import static io.quarkus.container.image.deployment.util.EnablementUtil.pushContainerImageNeeded; +import static io.quarkus.container.util.PathsUtil.findMainSourcesRoot; +import static io.quarkus.deployment.util.ContainerRuntimeUtil.detectContainerRuntime; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Stream; + +import org.jboss.logging.Logger; + +import io.quarkus.container.image.deployment.ContainerImageConfig; +import io.quarkus.container.image.deployment.util.NativeBinaryUtil; +import io.quarkus.container.spi.ContainerImageBuildRequestBuildItem; +import io.quarkus.container.spi.ContainerImageBuilderBuildItem; +import io.quarkus.container.spi.ContainerImageInfoBuildItem; +import io.quarkus.container.spi.ContainerImagePushRequestBuildItem; +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.builditem.ContainerRuntimeStatusBuildItem; +import io.quarkus.deployment.pkg.PackageConfig; +import io.quarkus.deployment.pkg.PackageConfig.JarConfig.JarType; +import io.quarkus.deployment.pkg.builditem.ArtifactResultBuildItem; +import io.quarkus.deployment.pkg.builditem.NativeImageBuildItem; +import io.quarkus.deployment.pkg.builditem.OutputTargetBuildItem; +import io.quarkus.deployment.util.ContainerRuntimeUtil.ContainerRuntime; +import io.quarkus.deployment.util.ExecUtil; + +public abstract class CommonProcessor { + private static final Logger LOGGER = Logger.getLogger(CommonProcessor.class); + protected static final String DOCKERFILE_JVM = "Dockerfile.jvm"; + protected static final String DOCKERFILE_LEGACY_JAR = "Dockerfile.legacy-jar"; + protected static final String DOCKERFILE_NATIVE = "Dockerfile.native"; + protected static final String DOCKER_DIRECTORY_NAME = "docker"; + + protected abstract String getProcessorImplementation(); + + protected abstract String createContainerImage(ContainerImageConfig containerImageConfig, C config, + ContainerImageInfoBuildItem containerImageInfo, OutputTargetBuildItem out, DockerfilePaths dockerfilePaths, + boolean buildContainerImage, boolean pushContainerImage, PackageConfig packageConfig, String executableName); + + protected void buildFromJar(C config, + ContainerRuntimeStatusBuildItem containerRuntimeStatusBuildItem, + ContainerImageConfig containerImageConfig, + OutputTargetBuildItem out, + ContainerImageInfoBuildItem containerImageInfo, + Optional buildRequest, + Optional pushRequest, + BuildProducer artifactResultProducer, + BuildProducer containerImageBuilder, + PackageConfig packageConfig, + ContainerRuntime containerRuntime) { + + var buildContainerImage = buildContainerImageNeeded(containerImageConfig, buildRequest); + var pushContainerImage = pushContainerImageNeeded(containerImageConfig, pushRequest); + + if (buildContainerImage || pushContainerImage) { + if (!containerRuntimeStatusBuildItem.isContainerRuntimeAvailable()) { + throw new RuntimeException( + "Unable to build container image. Please check your %s installation." + .formatted(getProcessorImplementation())); + } + + var dockerfilePaths = getDockerfilePaths(config, false, packageConfig, out); + var dockerfileBaseInformation = DockerFileBaseInformationProvider.impl() + .determine(dockerfilePaths.dockerfilePath()); + + if (dockerfileBaseInformation.isPresent() && (dockerfileBaseInformation.get().javaVersion() < 17)) { + throw new IllegalStateException( + "The project is built with Java 17 or higher, but the selected Dockerfile (%s) is using a lower Java version in the base image (%s). Please ensure you are using the proper base image in the Dockerfile." + .formatted( + dockerfilePaths.dockerfilePath().toAbsolutePath(), + dockerfileBaseInformation.get().baseImage())); + } + + if (buildContainerImage) { + LOGGER.infof("Starting (local) container image build for jar using %s", getProcessorImplementation()); + } + + var executableName = getExecutableName(config, containerRuntime); + var builtContainerImage = createContainerImage(containerImageConfig, config, containerImageInfo, out, + dockerfilePaths, buildContainerImage, pushContainerImage, packageConfig, executableName); + + // a pull is not required when using this image locally because the strategy always builds the container image + // locally before pushing it to the registry + artifactResultProducer.produce( + new ArtifactResultBuildItem( + null, + "jar-container", + Map.of( + "container-image", builtContainerImage, + "pull-required", "false"))); + + containerImageBuilder.produce(new ContainerImageBuilderBuildItem(getProcessorImplementation())); + } + } + + protected void buildFromNativeImage(C config, + ContainerRuntimeStatusBuildItem containerRuntimeStatusBuildItem, + ContainerImageConfig containerImageConfig, + ContainerImageInfoBuildItem containerImage, + Optional buildRequest, + Optional pushRequest, + OutputTargetBuildItem out, + BuildProducer artifactResultProducer, + BuildProducer containerImageBuilder, + PackageConfig packageConfig, + NativeImageBuildItem nativeImage, + ContainerRuntime containerRuntime) { + + var buildContainerImage = buildContainerImageNeeded(containerImageConfig, buildRequest); + var pushContainerImage = pushContainerImageNeeded(containerImageConfig, pushRequest); + + if (buildContainerImage || pushContainerImage) { + if (!containerRuntimeStatusBuildItem.isContainerRuntimeAvailable()) { + throw new RuntimeException( + "Unable to build container image. Please check your %s installation." + .formatted(getProcessorImplementation())); + } + + if (!NativeBinaryUtil.nativeIsLinuxBinary(nativeImage)) { + throw new RuntimeException( + "The native binary produced by the build is not a Linux binary and therefore cannot be used in a Linux container image. Consider adding \"quarkus.native.container-build=true\" to your configuration."); + } + + if (buildContainerImage) { + LOGGER.infof("Starting (local) container image build for jar using %s", getProcessorImplementation()); + } + + var executableName = getExecutableName(config, containerRuntime); + var dockerfilePaths = getDockerfilePaths(config, true, packageConfig, out); + var builtContainerImage = createContainerImage(containerImageConfig, config, containerImage, out, dockerfilePaths, + buildContainerImage, pushContainerImage, packageConfig, executableName); + + // a pull is not required when using this image locally because the strategy always builds the container image + // locally before pushing it to the registry + artifactResultProducer.produce( + new ArtifactResultBuildItem( + null, + "native-container", + Map.of( + "container-image", builtContainerImage, + "pull-required", "false"))); + + containerImageBuilder.produce(new ContainerImageBuilderBuildItem(getProcessorImplementation())); + } + } + + protected void loginToRegistryIfNeeded(ContainerImageConfig containerImageConfig, + ContainerImageInfoBuildItem containerImageInfo, + String executableName) { + + var registry = containerImageInfo.getRegistry() + .orElseGet(() -> { + LOGGER.info("No container image registry was set, so 'docker.io' will be used"); + return "docker.io"; + }); + + // Check if we need to login first + if (containerImageConfig.username.isPresent() && containerImageConfig.password.isPresent()) { + var loginSuccessful = ExecUtil.exec(executableName, "login", registry, "-u", containerImageConfig.username.get(), + "-p", containerImageConfig.password.get()); + + if (!loginSuccessful) { + throw containerRuntimeException(executableName, + new String[] { "-u", containerImageConfig.username.get(), "-p", "********" }); + } + } + } + + protected List getContainerCommonBuildArgs(String image, + DockerfilePaths dockerfilePaths, + ContainerImageConfig containerImageConfig, + C config, + boolean addImageAsTag) { + + var args = new ArrayList(6 + config.buildArgs().size() + config.additionalArgs().map(List::size).orElse(0)); + args.addAll(List.of("build", "-f", dockerfilePaths.dockerfilePath().toAbsolutePath().toString())); + + config.buildArgs().forEach((k, v) -> args.addAll(List.of("--build-arg", "%s=%s".formatted(k, v)))); + containerImageConfig.labels.forEach((k, v) -> args.addAll(List.of("--label", "%s=%s".formatted(k, v)))); + config.cacheFrom() + .filter(cacheFrom -> !cacheFrom.isEmpty()) + .ifPresent(cacheFrom -> args.addAll(List.of("--cache-from", String.join(",", cacheFrom)))); + config.network().ifPresent(network -> args.addAll(List.of("--network", network))); + config.additionalArgs().ifPresent(args::addAll); + + if (addImageAsTag) { + args.addAll(List.of("-t", image)); + } + + return args; + } + + protected void createAdditionalTags(String image, List additionalImageTags, String executableName) { + additionalImageTags.stream() + .map(additionalTag -> new String[] { "tag", image, additionalTag }) + .forEach(tagArgs -> { + LOGGER.infof("Running '%s %s'", executableName, String.join(" ", tagArgs)); + var tagSuccessful = ExecUtil.exec(executableName, tagArgs); + + if (!tagSuccessful) { + throw containerRuntimeException(executableName, tagArgs); + } + }); + } + + protected void pushImages(ContainerImageInfoBuildItem containerImageInfo, String executableName) { + Stream.concat(containerImageInfo.getAdditionalImageTags().stream(), Stream.of(containerImageInfo.getImage())) + .forEach(imageToPush -> pushImage(imageToPush, executableName)); + } + + protected void pushImage(String image, String executableName) { + String[] pushArgs = { "push", image }; + var pushSuccessful = ExecUtil.exec(executableName, pushArgs); + + if (!pushSuccessful) { + throw containerRuntimeException(executableName, pushArgs); + } + + LOGGER.infof("Successfully pushed %s image %s", getProcessorImplementation(), image); + } + + protected void buildImage(ContainerImageInfoBuildItem containerImageInfo, + OutputTargetBuildItem out, + String executableName, + String[] args, + boolean createAdditionalTags) { + + LOGGER.infof("Executing the following command to build image: '%s %s'", executableName, + String.join(" ", args)); + var buildSuccessful = ExecUtil.exec(out.getOutputDirectory().toFile(), executableName, args); + + if (!buildSuccessful) { + throw containerRuntimeException(executableName, args); + } + + if (createAdditionalTags && !containerImageInfo.getAdditionalImageTags().isEmpty()) { + createAdditionalTags(containerImageInfo.getImage(), containerImageInfo.getAdditionalImageTags(), + executableName); + } + } + + protected RuntimeException containerRuntimeException(String executableName, String[] args) { + return new RuntimeException( + "Execution of '%s %s' failed. See %s output for more details" + .formatted( + executableName, + String.join(" ", args), + getProcessorImplementation())); + } + + private String getExecutableName(C config, ContainerRuntime containerRuntime) { + return config.executableName() + .orElseGet(() -> detectContainerRuntime(List.of(containerRuntime)).getExecutableName()); + } + + private DockerfilePaths getDockerfilePaths(C config, + boolean forNative, + PackageConfig packageConfig, + OutputTargetBuildItem out) { + + var outputDirectory = out.getOutputDirectory(); + + if (forNative) { + return config.dockerfileNativePath() + .map(dockerfileNativePath -> ProvidedDockerfile.get(Paths.get(dockerfileNativePath), outputDirectory)) + .orElseGet(() -> DockerfileDetectionResult.detect(DOCKERFILE_NATIVE, outputDirectory)); + } else { + return config.dockerfileJvmPath() + .map(dockerfileJvmPath -> ProvidedDockerfile.get(Paths.get(dockerfileJvmPath), outputDirectory)) + .orElseGet(() -> (packageConfig.jar().type() == JarType.LEGACY_JAR) + ? DockerfileDetectionResult.detect(DOCKERFILE_LEGACY_JAR, outputDirectory) + : DockerfileDetectionResult.detect(DOCKERFILE_JVM, outputDirectory)); + } + } + + protected interface DockerfilePaths { + Path dockerfilePath(); + + Path dockerExecutionPath(); + } + + protected record DockerfileDetectionResult(Path dockerfilePath, Path dockerExecutionPath) implements DockerfilePaths { + protected static DockerfilePaths detect(String resource, Path outputDirectory) { + var dockerfileToExecutionRoot = findDockerfileRoot(outputDirectory); + if (dockerfileToExecutionRoot == null) { + throw new IllegalStateException( + "Unable to find root of Dockerfile files. Consider adding 'src/main/docker/' to your project root."); + } + + var dockerFilePath = dockerfileToExecutionRoot.getKey().resolve(resource); + if (!Files.exists(dockerFilePath)) { + throw new IllegalStateException( + "Unable to find Dockerfile %s in %s" + .formatted(resource, dockerfileToExecutionRoot.getKey().toAbsolutePath())); + } + + return new DockerfileDetectionResult(dockerFilePath, dockerfileToExecutionRoot.getValue()); + } + + private static Map.Entry findDockerfileRoot(Path outputDirectory) { + var mainSourcesRoot = findMainSourcesRoot(outputDirectory); + if (mainSourcesRoot == null) { + return null; + } + + var dockerfilesRoot = mainSourcesRoot.getKey().resolve(DOCKER_DIRECTORY_NAME); + if (!dockerfilesRoot.toFile().exists()) { + return null; + } + + return new AbstractMap.SimpleEntry<>(dockerfilesRoot, mainSourcesRoot.getValue()); + } + } + + protected record ProvidedDockerfile(Path dockerfilePath, Path dockerExecutionPath) implements DockerfilePaths { + protected static DockerfilePaths get(Path dockerfilePath, Path outputDirectory) { + var mainSourcesRoot = findMainSourcesRoot(outputDirectory); + + if (mainSourcesRoot == null) { + throw new IllegalStateException("Unable to determine project root"); + } + + var executionPath = mainSourcesRoot.getValue(); + var effectiveDockerfilePath = dockerfilePath.isAbsolute() ? dockerfilePath : executionPath.resolve(dockerfilePath); + + if (!effectiveDockerfilePath.toFile().exists()) { + throw new IllegalArgumentException( + "Specified Dockerfile path %s does not exist".formatted(effectiveDockerfilePath.toAbsolutePath())); + } + + return new ProvidedDockerfile(effectiveDockerfilePath, executionPath); + } + } +} diff --git a/extensions/container-image/container-image-docker/deployment/src/main/java/io/quarkus/container/image/docker/deployment/DockerFileBaseInformationProvider.java b/extensions/container-image/container-image-docker-common/deployment/src/main/java/io/quarkus/container/image/docker/common/deployment/DockerFileBaseInformationProvider.java similarity index 51% rename from extensions/container-image/container-image-docker/deployment/src/main/java/io/quarkus/container/image/docker/deployment/DockerFileBaseInformationProvider.java rename to extensions/container-image/container-image-docker-common/deployment/src/main/java/io/quarkus/container/image/docker/common/deployment/DockerFileBaseInformationProvider.java index 262cffd8c8a95..f80fdf5c5e950 100644 --- a/extensions/container-image/container-image-docker/deployment/src/main/java/io/quarkus/container/image/docker/deployment/DockerFileBaseInformationProvider.java +++ b/extensions/container-image/container-image-docker-common/deployment/src/main/java/io/quarkus/container/image/docker/common/deployment/DockerFileBaseInformationProvider.java @@ -1,10 +1,10 @@ -package io.quarkus.container.image.docker.deployment; +package io.quarkus.container.image.docker.common.deployment; import java.nio.file.Path; import java.util.List; import java.util.Optional; -interface DockerFileBaseInformationProvider { +public interface DockerFileBaseInformationProvider { Optional determine(Path dockerFile); @@ -16,32 +16,19 @@ static DockerFileBaseInformationProvider impl() { @Override public Optional determine(Path dockerFile) { - for (DockerFileBaseInformationProvider delegate : delegates) { - Optional result = delegate.determine(dockerFile); + for (var delegate : delegates) { + var result = delegate.determine(dockerFile); + if (result.isPresent()) { return result; } } + return Optional.empty(); } }; } - class DockerFileBaseInformation { - private final int javaVersion; - private final String baseImage; - - public DockerFileBaseInformation(String baseImage, int javaVersion) { - this.javaVersion = javaVersion; - this.baseImage = baseImage; - } - - public int getJavaVersion() { - return javaVersion; - } - - public String getBaseImage() { - return baseImage; - } + record DockerFileBaseInformation(String baseImage, int javaVersion) { } } diff --git a/extensions/container-image/container-image-docker/deployment/src/main/java/io/quarkus/container/image/docker/deployment/RedHatOpenJDKRuntimeBaseProvider.java b/extensions/container-image/container-image-docker-common/deployment/src/main/java/io/quarkus/container/image/docker/common/deployment/RedHatOpenJDKRuntimeBaseProvider.java similarity index 90% rename from extensions/container-image/container-image-docker/deployment/src/main/java/io/quarkus/container/image/docker/deployment/RedHatOpenJDKRuntimeBaseProvider.java rename to extensions/container-image/container-image-docker-common/deployment/src/main/java/io/quarkus/container/image/docker/common/deployment/RedHatOpenJDKRuntimeBaseProvider.java index 1a955893486e1..0c15a1640a245 100644 --- a/extensions/container-image/container-image-docker/deployment/src/main/java/io/quarkus/container/image/docker/deployment/RedHatOpenJDKRuntimeBaseProvider.java +++ b/extensions/container-image/container-image-docker-common/deployment/src/main/java/io/quarkus/container/image/docker/common/deployment/RedHatOpenJDKRuntimeBaseProvider.java @@ -1,4 +1,4 @@ -package io.quarkus.container.image.docker.deployment; +package io.quarkus.container.image.docker.common.deployment; import java.io.IOException; import java.nio.file.Files; @@ -12,8 +12,7 @@ * Can extract information from Dockerfile that uses {@code registry.access.redhat.com/ubi8/openjdk-$d-runtime:$d.$d} as the * base image */ -class RedHatOpenJDKRuntimeBaseProvider - implements DockerFileBaseInformationProvider { +class RedHatOpenJDKRuntimeBaseProvider implements DockerFileBaseInformationProvider { @Override public Optional determine(Path dockerFile) { diff --git a/extensions/container-image/container-image-docker/deployment/src/main/java/io/quarkus/container/image/docker/deployment/UbiMinimalBaseProvider.java b/extensions/container-image/container-image-docker-common/deployment/src/main/java/io/quarkus/container/image/docker/common/deployment/UbiMinimalBaseProvider.java similarity index 94% rename from extensions/container-image/container-image-docker/deployment/src/main/java/io/quarkus/container/image/docker/deployment/UbiMinimalBaseProvider.java rename to extensions/container-image/container-image-docker-common/deployment/src/main/java/io/quarkus/container/image/docker/common/deployment/UbiMinimalBaseProvider.java index 1ad6adc24f6a7..4ac17527960e3 100644 --- a/extensions/container-image/container-image-docker/deployment/src/main/java/io/quarkus/container/image/docker/deployment/UbiMinimalBaseProvider.java +++ b/extensions/container-image/container-image-docker-common/deployment/src/main/java/io/quarkus/container/image/docker/common/deployment/UbiMinimalBaseProvider.java @@ -1,4 +1,4 @@ -package io.quarkus.container.image.docker.deployment; +package io.quarkus.container.image.docker.common.deployment; import java.io.IOException; import java.nio.file.Files; @@ -14,8 +14,7 @@ * Can extract information from Dockerfile that uses {@code registry.access.redhat.com/ubi8/ubi-minimal:$d.$d} as the * base image */ -class UbiMinimalBaseProvider - implements DockerFileBaseInformationProvider { +class UbiMinimalBaseProvider implements DockerFileBaseInformationProvider { public static final String UBI_MINIMAL_PREFIX = "registry.access.redhat.com/ubi8/ubi-minimal"; diff --git a/extensions/container-image/container-image-docker/deployment/src/test/java/io/quarkus/container/image/docker/deployment/RedHatOpenJDKRuntimeBaseProviderTest.java b/extensions/container-image/container-image-docker-common/deployment/src/test/java/io/quarkus/container/image/docker/common/deployment/RedHatOpenJDKRuntimeBaseProviderTest.java similarity index 64% rename from extensions/container-image/container-image-docker/deployment/src/test/java/io/quarkus/container/image/docker/deployment/RedHatOpenJDKRuntimeBaseProviderTest.java rename to extensions/container-image/container-image-docker-common/deployment/src/test/java/io/quarkus/container/image/docker/common/deployment/RedHatOpenJDKRuntimeBaseProviderTest.java index d09507d72b2de..119f4f81eac8c 100644 --- a/extensions/container-image/container-image-docker/deployment/src/test/java/io/quarkus/container/image/docker/deployment/RedHatOpenJDKRuntimeBaseProviderTest.java +++ b/extensions/container-image/container-image-docker-common/deployment/src/test/java/io/quarkus/container/image/docker/common/deployment/RedHatOpenJDKRuntimeBaseProviderTest.java @@ -1,6 +1,6 @@ -package io.quarkus.container.image.docker.deployment; +package io.quarkus.container.image.docker.common.deployment; -import static io.quarkus.container.image.docker.deployment.TestUtil.getPath; +import static io.quarkus.container.image.docker.common.deployment.TestUtil.getPath; import static org.assertj.core.api.Assertions.assertThat; import java.nio.file.Path; @@ -16,8 +16,8 @@ void testImageWithJava17() { Path path = getPath("openjdk-17-runtime"); var result = sut.determine(path); assertThat(result).hasValueSatisfying(v -> { - assertThat(v.getBaseImage()).isEqualTo("registry.access.redhat.com/ubi8/openjdk-17-runtime:1.19"); - assertThat(v.getJavaVersion()).isEqualTo(17); + assertThat(v.baseImage()).isEqualTo("registry.access.redhat.com/ubi8/openjdk-17-runtime:1.19"); + assertThat(v.javaVersion()).isEqualTo(17); }); } @@ -26,8 +26,8 @@ void testImageWithJava21() { Path path = getPath("openjdk-21-runtime"); var result = sut.determine(path); assertThat(result).hasValueSatisfying(v -> { - assertThat(v.getBaseImage()).isEqualTo("registry.access.redhat.com/ubi8/openjdk-21-runtime:1.19"); - assertThat(v.getJavaVersion()).isEqualTo(21); + assertThat(v.baseImage()).isEqualTo("registry.access.redhat.com/ubi8/openjdk-21-runtime:1.19"); + assertThat(v.javaVersion()).isEqualTo(21); }); } diff --git a/extensions/container-image/container-image-docker/deployment/src/test/java/io/quarkus/container/image/docker/deployment/TestUtil.java b/extensions/container-image/container-image-docker-common/deployment/src/test/java/io/quarkus/container/image/docker/common/deployment/TestUtil.java similarity index 87% rename from extensions/container-image/container-image-docker/deployment/src/test/java/io/quarkus/container/image/docker/deployment/TestUtil.java rename to extensions/container-image/container-image-docker-common/deployment/src/test/java/io/quarkus/container/image/docker/common/deployment/TestUtil.java index ad45ce736c77a..180b4940429f8 100644 --- a/extensions/container-image/container-image-docker/deployment/src/test/java/io/quarkus/container/image/docker/deployment/TestUtil.java +++ b/extensions/container-image/container-image-docker-common/deployment/src/test/java/io/quarkus/container/image/docker/common/deployment/TestUtil.java @@ -1,4 +1,4 @@ -package io.quarkus.container.image.docker.deployment; +package io.quarkus.container.image.docker.common.deployment; import java.net.URISyntaxException; import java.nio.file.Path; diff --git a/extensions/container-image/container-image-docker/deployment/src/test/java/io/quarkus/container/image/docker/deployment/UbiMinimalBaseProviderTest.java b/extensions/container-image/container-image-docker-common/deployment/src/test/java/io/quarkus/container/image/docker/common/deployment/UbiMinimalBaseProviderTest.java similarity index 64% rename from extensions/container-image/container-image-docker/deployment/src/test/java/io/quarkus/container/image/docker/deployment/UbiMinimalBaseProviderTest.java rename to extensions/container-image/container-image-docker-common/deployment/src/test/java/io/quarkus/container/image/docker/common/deployment/UbiMinimalBaseProviderTest.java index a1b4b9d6747a4..784f6fc3b1bd1 100644 --- a/extensions/container-image/container-image-docker/deployment/src/test/java/io/quarkus/container/image/docker/deployment/UbiMinimalBaseProviderTest.java +++ b/extensions/container-image/container-image-docker-common/deployment/src/test/java/io/quarkus/container/image/docker/common/deployment/UbiMinimalBaseProviderTest.java @@ -1,6 +1,6 @@ -package io.quarkus.container.image.docker.deployment; +package io.quarkus.container.image.docker.common.deployment; -import static io.quarkus.container.image.docker.deployment.TestUtil.getPath; +import static io.quarkus.container.image.docker.common.deployment.TestUtil.getPath; import static org.assertj.core.api.Assertions.assertThat; import java.nio.file.Path; @@ -16,8 +16,8 @@ void testImageWithJava17() { Path path = getPath("ubi-java17"); var result = sut.determine(path); assertThat(result).hasValueSatisfying(v -> { - assertThat(v.getBaseImage()).isEqualTo("registry.access.redhat.com/ubi8/ubi-minimal:8.9"); - assertThat(v.getJavaVersion()).isEqualTo(17); + assertThat(v.baseImage()).isEqualTo("registry.access.redhat.com/ubi8/ubi-minimal:8.9"); + assertThat(v.javaVersion()).isEqualTo(17); }); } @@ -26,8 +26,8 @@ void testImageWithJava21() { Path path = getPath("ubi-java21"); var result = sut.determine(path); assertThat(result).hasValueSatisfying(v -> { - assertThat(v.getBaseImage()).isEqualTo("registry.access.redhat.com/ubi8/ubi-minimal:8.9"); - assertThat(v.getJavaVersion()).isEqualTo(21); + assertThat(v.baseImage()).isEqualTo("registry.access.redhat.com/ubi8/ubi-minimal:8.9"); + assertThat(v.javaVersion()).isEqualTo(21); }); } diff --git a/extensions/container-image/container-image-docker/deployment/src/test/resources/openjdk-17-runtime b/extensions/container-image/container-image-docker-common/deployment/src/test/resources/openjdk-17-runtime similarity index 100% rename from extensions/container-image/container-image-docker/deployment/src/test/resources/openjdk-17-runtime rename to extensions/container-image/container-image-docker-common/deployment/src/test/resources/openjdk-17-runtime diff --git a/extensions/container-image/container-image-docker/deployment/src/test/resources/openjdk-21-runtime b/extensions/container-image/container-image-docker-common/deployment/src/test/resources/openjdk-21-runtime similarity index 100% rename from extensions/container-image/container-image-docker/deployment/src/test/resources/openjdk-21-runtime rename to extensions/container-image/container-image-docker-common/deployment/src/test/resources/openjdk-21-runtime diff --git a/extensions/container-image/container-image-docker/deployment/src/test/resources/ubi-java17 b/extensions/container-image/container-image-docker-common/deployment/src/test/resources/ubi-java17 similarity index 100% rename from extensions/container-image/container-image-docker/deployment/src/test/resources/ubi-java17 rename to extensions/container-image/container-image-docker-common/deployment/src/test/resources/ubi-java17 diff --git a/extensions/container-image/container-image-docker/deployment/src/test/resources/ubi-java21 b/extensions/container-image/container-image-docker-common/deployment/src/test/resources/ubi-java21 similarity index 100% rename from extensions/container-image/container-image-docker/deployment/src/test/resources/ubi-java21 rename to extensions/container-image/container-image-docker-common/deployment/src/test/resources/ubi-java21 diff --git a/extensions/container-image/container-image-docker-common/pom.xml b/extensions/container-image/container-image-docker-common/pom.xml new file mode 100644 index 0000000000000..c115d50d4603d --- /dev/null +++ b/extensions/container-image/container-image-docker-common/pom.xml @@ -0,0 +1,20 @@ + + + + quarkus-container-image-parent + io.quarkus + 999-SNAPSHOT + + 4.0.0 + + quarkus-container-image-docker-common-parent + Quarkus - Container Image - Docker Common - Parent + pom + + deployment + runtime + + + diff --git a/extensions/container-image/container-image-docker-common/runtime/pom.xml b/extensions/container-image/container-image-docker-common/runtime/pom.xml new file mode 100644 index 0000000000000..c7b6cebce33b9 --- /dev/null +++ b/extensions/container-image/container-image-docker-common/runtime/pom.xml @@ -0,0 +1,43 @@ + + + 4.0.0 + + + quarkus-container-image-docker-common-parent + io.quarkus + 999-SNAPSHOT + + + quarkus-container-image-docker-common + Quarkus - Container Image - Docker Common + Build container images of your application using Docker APIs + + + + io.quarkus + quarkus-container-image + + + + + + io.quarkus + quarkus-extension-maven-plugin + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + + diff --git a/extensions/container-image/container-image-docker-common/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/container-image/container-image-docker-common/runtime/src/main/resources/META-INF/quarkus-extension.yaml new file mode 100644 index 0000000000000..3e7ecf140c725 --- /dev/null +++ b/extensions/container-image/container-image-docker-common/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -0,0 +1,15 @@ +--- +artifact: ${project.groupId}:${project.artifactId}:${project.version} +name: "Container Image - Docker common" +metadata: + keywords: + - "container" + - "image" + - "docker" + categories: + - "cloud" + status: "preview" + unlisted: true + config: + - "quarkus.docker." + - "quarkus.podman." \ No newline at end of file diff --git a/extensions/container-image/container-image-docker/deployment/pom.xml b/extensions/container-image/container-image-docker/deployment/pom.xml index 0985cbc99c3b0..1d6fb564a0cea 100644 --- a/extensions/container-image/container-image-docker/deployment/pom.xml +++ b/extensions/container-image/container-image-docker/deployment/pom.xml @@ -20,7 +20,7 @@ io.quarkus - quarkus-container-image-deployment + quarkus-container-image-docker-common-deployment org.assertj diff --git a/extensions/container-image/container-image-docker/deployment/src/main/java/io/quarkus/container/image/docker/deployment/DockerConfig.java b/extensions/container-image/container-image-docker/deployment/src/main/java/io/quarkus/container/image/docker/deployment/DockerConfig.java index cf06381a8aca5..ed802507fd777 100644 --- a/extensions/container-image/container-image-docker/deployment/src/main/java/io/quarkus/container/image/docker/deployment/DockerConfig.java +++ b/extensions/container-image/container-image-docker/deployment/src/main/java/io/quarkus/container/image/docker/deployment/DockerConfig.java @@ -1,76 +1,23 @@ package io.quarkus.container.image.docker.deployment; import java.util.List; -import java.util.Map; import java.util.Optional; -import io.quarkus.runtime.annotations.ConfigDocDefault; +import io.quarkus.container.image.docker.common.deployment.CommonConfig; import io.quarkus.runtime.annotations.ConfigDocSection; import io.quarkus.runtime.annotations.ConfigGroup; -import io.quarkus.runtime.annotations.ConfigItem; import io.quarkus.runtime.annotations.ConfigPhase; import io.quarkus.runtime.annotations.ConfigRoot; +import io.smallrye.config.ConfigMapping; @ConfigRoot(phase = ConfigPhase.BUILD_TIME) -public class DockerConfig { - - /** - * Path to the JVM Dockerfile. - * If set to an absolute path then the absolute path will be used, otherwise the path - * will be considered relative to the project root. - * If not set src/main/docker/Dockerfile.jvm will be used. - */ - @ConfigItem - @ConfigDocDefault("src/main/docker/Dockerfile.jvm") - public Optional dockerfileJvmPath; - - /** - * Path to the native Dockerfile. - * If set to an absolute path then the absolute path will be used, otherwise the path - * will be considered relative to the project root. - * If not set src/main/docker/Dockerfile.native will be used. - */ - @ConfigItem - @ConfigDocDefault("src/main/docker/Dockerfile.native") - public Optional dockerfileNativePath; - - /** - * Build args passed to docker via {@code --build-arg} - */ - @ConfigItem - public Map buildArgs; - - /** - * Images to consider as cache sources. Values are passed to {@code docker build} via the {@code cache-from} option - */ - @ConfigItem - public Optional> cacheFrom; - - /** - * The networking mode for the RUN instructions during build - */ - @ConfigItem - public Optional network; - - /** - * Name of binary used to execute the docker commands. - * This setting can override the global container runtime detection. - */ - @ConfigItem - public Optional executableName; - - /** - * Additional arbitrary arguments passed to the executable when building the container image. - */ - @ConfigItem - public Optional> additionalArgs; - +@ConfigMapping(prefix = "quarkus.docker") +public interface DockerConfig extends CommonConfig { /** * Configuration for Docker Buildx options */ - @ConfigItem @ConfigDocSection - public DockerBuildxConfig buildx; + DockerBuildxConfig buildx(); /** * Configuration for Docker Buildx options. These are only relevant if using Docker Buildx @@ -80,13 +27,12 @@ public class DockerConfig { * If any of these configurations are set, it will add {@code buildx} to the {@code executableName}. */ @ConfigGroup - public static class DockerBuildxConfig { + interface DockerBuildxConfig { /** * Which platform(s) to target during the build. See * https://docs.docker.com/engine/reference/commandline/buildx_build/#platform */ - @ConfigItem - public Optional> platform; + Optional> platform(); /** * Sets the export action for the build result. See @@ -94,20 +40,18 @@ public static class DockerBuildxConfig { * absolute paths, * not relative from where the command is executed from. */ - @ConfigItem - public Optional output; + Optional output(); /** * Set type of progress output ({@code auto}, {@code plain}, {@code tty}). Use {@code plain} to show container output * (default “{@code auto}”). See https://docs.docker.com/engine/reference/commandline/buildx_build/#progress */ - @ConfigItem - public Optional progress; + Optional progress(); - boolean useBuildx() { - return platform.filter(p -> !p.isEmpty()).isPresent() || - output.isPresent() || - progress.isPresent(); + default boolean useBuildx() { + return platform().filter(p -> !p.isEmpty()).isPresent() || + output().isPresent() || + progress().isPresent(); } } } diff --git a/extensions/container-image/container-image-docker/deployment/src/main/java/io/quarkus/container/image/docker/deployment/DockerProcessor.java b/extensions/container-image/container-image-docker/deployment/src/main/java/io/quarkus/container/image/docker/deployment/DockerProcessor.java index 5c94f9c13715b..830e214559474 100644 --- a/extensions/container-image/container-image-docker/deployment/src/main/java/io/quarkus/container/image/docker/deployment/DockerProcessor.java +++ b/extensions/container-image/container-image-docker/deployment/src/main/java/io/quarkus/container/image/docker/deployment/DockerProcessor.java @@ -1,27 +1,13 @@ package io.quarkus.container.image.docker.deployment; -import static io.quarkus.container.image.deployment.util.EnablementUtil.buildContainerImageNeeded; -import static io.quarkus.container.image.deployment.util.EnablementUtil.pushContainerImageNeeded; -import static io.quarkus.container.util.PathsUtil.findMainSourcesRoot; -import static io.quarkus.deployment.pkg.PackageConfig.JarConfig.JarType.*; -import static io.quarkus.deployment.util.ContainerRuntimeUtil.detectContainerRuntime; - -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.AbstractMap; -import java.util.ArrayList; -import java.util.Arrays; import java.util.List; -import java.util.Map; import java.util.Optional; -import java.util.stream.Stream; import org.jboss.logging.Logger; import io.quarkus.container.image.deployment.ContainerImageConfig; -import io.quarkus.container.image.deployment.util.NativeBinaryUtil; +import io.quarkus.container.image.docker.common.deployment.CommonProcessor; import io.quarkus.container.spi.AvailableContainerImageExtensionBuildItem; import io.quarkus.container.spi.ContainerImageBuildRequestBuildItem; import io.quarkus.container.spi.ContainerImageBuilderBuildItem; @@ -40,18 +26,18 @@ import io.quarkus.deployment.pkg.builditem.OutputTargetBuildItem; import io.quarkus.deployment.pkg.builditem.UpxCompressedBuildItem; import io.quarkus.deployment.pkg.steps.NativeBuild; -import io.quarkus.deployment.util.ExecUtil; - -public class DockerProcessor { +import io.quarkus.deployment.util.ContainerRuntimeUtil.ContainerRuntime; - private static final Logger log = Logger.getLogger(DockerProcessor.class); +public class DockerProcessor extends CommonProcessor { + private static final Logger LOG = Logger.getLogger(DockerProcessor.class); private static final String DOCKER = "docker"; - private static final String DOCKERFILE_JVM = "Dockerfile.jvm"; - private static final String DOCKERFILE_LEGACY_JAR = "Dockerfile.legacy-jar"; - private static final String DOCKERFILE_NATIVE = "Dockerfile.native"; - private static final String DOCKER_DIRECTORY_NAME = "docker"; static final String DOCKER_CONTAINER_IMAGE_NAME = "docker"; + @Override + protected String getProcessorImplementation() { + return DOCKER; + } + @BuildStep public AvailableContainerImageExtensionBuildItem availability() { return new AvailableContainerImageExtensionBuildItem(DOCKER); @@ -63,53 +49,19 @@ public void dockerBuildFromJar(DockerConfig dockerConfig, ContainerImageConfig containerImageConfig, OutputTargetBuildItem out, ContainerImageInfoBuildItem containerImageInfo, - CompiledJavaVersionBuildItem compiledJavaVersion, + @SuppressWarnings("unused") CompiledJavaVersionBuildItem compiledJavaVersion, Optional buildRequest, Optional pushRequest, @SuppressWarnings("unused") Optional appCDSResult, // ensure docker build will be performed after AppCDS creation BuildProducer artifactResultProducer, BuildProducer containerImageBuilder, PackageConfig packageConfig, - @SuppressWarnings("unused") // used to ensure that the jar has been built - JarBuildItem jar) { - - boolean buildContainerImage = buildContainerImageNeeded(containerImageConfig, buildRequest); - boolean pushContainerImage = pushContainerImageNeeded(containerImageConfig, pushRequest); - if (!buildContainerImage && !pushContainerImage) { - return; - } + @SuppressWarnings("unused") JarBuildItem jar // used to ensure that the jar has been built + ) { - if (!dockerStatusBuildItem.isDockerAvailable()) { - throw new RuntimeException("Unable to build docker image. Please check your docker installation"); - } - - DockerfilePaths dockerfilePaths = getDockerfilePaths(dockerConfig, false, packageConfig, out); - DockerFileBaseInformationProvider dockerFileBaseInformationProvider = DockerFileBaseInformationProvider.impl(); - Optional dockerFileBaseInformation = dockerFileBaseInformationProvider - .determine(dockerfilePaths.getDockerfilePath()); - - if (dockerFileBaseInformation.isPresent() && (dockerFileBaseInformation.get().getJavaVersion() < 17)) { - throw new IllegalStateException( - String.format( - "The project is built with Java 17 or higher, but the selected Dockerfile (%s) is using a lower Java version in the base image (%s). Please ensure you are using the proper base image in the Dockerfile.", - dockerfilePaths.getDockerfilePath().toAbsolutePath(), - dockerFileBaseInformation.get().getBaseImage())); - } - - if (buildContainerImage) { - log.info("Starting (local) container image build for jar using docker."); - } - - String builtContainerImage = createContainerImage(containerImageConfig, dockerConfig, containerImageInfo, out, - false, - buildContainerImage, - pushContainerImage, packageConfig); - - // a pull is not required when using this image locally because the docker strategy always builds the container image - // locally before pushing it to the registry - artifactResultProducer.produce(new ArtifactResultBuildItem(null, "jar-container", - Map.of("container-image", builtContainerImage, "pull-required", "false"))); - containerImageBuilder.produce(new ContainerImageBuilderBuildItem(DOCKER)); + buildFromJar(dockerConfig, dockerStatusBuildItem, containerImageConfig, out, containerImageInfo, + buildRequest, pushRequest, artifactResultProducer, containerImageBuilder, packageConfig, + ContainerRuntime.DOCKER); } @BuildStep(onlyIf = { IsNormalNotRemoteDev.class, NativeBuild.class, DockerBuild.class }) @@ -120,49 +72,30 @@ public void dockerBuildFromNativeImage(DockerConfig dockerConfig, Optional buildRequest, Optional pushRequest, OutputTargetBuildItem out, - Optional upxCompressed, // used to ensure that we work with the compressed native binary if compression was enabled + @SuppressWarnings("unused") Optional upxCompressed, // used to ensure that we work with the compressed native binary if compression was enabled BuildProducer artifactResultProducer, BuildProducer containerImageBuilder, PackageConfig packageConfig, // used to ensure that the native binary has been built NativeImageBuildItem nativeImage) { - boolean buildContainerImage = buildContainerImageNeeded(containerImageConfig, buildRequest); - boolean pushContainerImage = pushContainerImageNeeded(containerImageConfig, pushRequest); - if (!buildContainerImage && !pushContainerImage) { - return; - } - - if (!dockerStatusBuildItem.isDockerAvailable()) { - throw new RuntimeException("Unable to build docker image. Please check your docker installation"); - } - - if (!NativeBinaryUtil.nativeIsLinuxBinary(nativeImage)) { - throw new RuntimeException( - "The native binary produced by the build is not a Linux binary and therefore cannot be used in a Linux container image. Consider adding \"quarkus.native.container-build=true\" to your configuration"); - } - - log.info("Starting (local) container image build for native binary using docker."); - - String builtContainerImage = createContainerImage(containerImageConfig, dockerConfig, containerImage, out, true, - buildContainerImage, - pushContainerImage, packageConfig); - - // a pull is not required when using this image locally because the docker strategy always builds the container image - // locally before pushing it to the registry - artifactResultProducer.produce(new ArtifactResultBuildItem(null, "native-container", - Map.of("container-image", builtContainerImage, "pull-required", "false"))); - - containerImageBuilder.produce(new ContainerImageBuilderBuildItem(DOCKER)); + buildFromNativeImage(dockerConfig, dockerStatusBuildItem, containerImageConfig, containerImage, + buildRequest, pushRequest, out, artifactResultProducer, containerImageBuilder, packageConfig, nativeImage, + ContainerRuntime.DOCKER); } - private String createContainerImage(ContainerImageConfig containerImageConfig, DockerConfig dockerConfig, + @Override + protected String createContainerImage(ContainerImageConfig containerImageConfig, + DockerConfig dockerConfig, ContainerImageInfoBuildItem containerImageInfo, - OutputTargetBuildItem out, boolean forNative, boolean buildContainerImage, + OutputTargetBuildItem out, + DockerfilePaths dockerfilePaths, + boolean buildContainerImage, boolean pushContainerImage, - PackageConfig packageConfig) { + PackageConfig packageConfig, + String executableName) { - boolean useBuildx = dockerConfig.buildx.useBuildx(); + boolean useBuildx = dockerConfig.buildx().useBuildx(); // useBuildx: Whether any of the buildx parameters are set // @@ -180,274 +113,90 @@ private String createContainerImage(ContainerImageConfig containerImageConfig, D // This is because when using buildx with more than one platform, the resulting images are not loaded into 'docker images'. // Therefore, a docker tag or docker push will not work after a docker build. - DockerfilePaths dockerfilePaths = getDockerfilePaths(dockerConfig, forNative, packageConfig, out); - String[] dockerArgs = getDockerArgs(containerImageInfo.getImage(), dockerfilePaths, containerImageConfig, dockerConfig, - containerImageInfo, pushContainerImage); - if (useBuildx && pushContainerImage) { // Needed because buildx will push all the images in a single step - loginToRegistryIfNeeded(containerImageConfig, containerImageInfo, dockerConfig); + loginToRegistryIfNeeded(containerImageConfig, containerImageInfo, executableName); } if (buildContainerImage) { - final String executableName = dockerConfig.executableName.orElse(detectContainerRuntime(true).getExecutableName()); - log.infof("Executing the following command to build docker image: '%s %s'", executableName, - String.join(" ", dockerArgs)); - boolean buildSuccessful = ExecUtil.exec(out.getOutputDirectory().toFile(), executableName, dockerArgs); - if (!buildSuccessful) { - throw dockerException(executableName, dockerArgs); - } + var dockerBuildArgs = getDockerBuildArgs(containerImageInfo.getImage(), dockerfilePaths, containerImageConfig, + dockerConfig, containerImageInfo, pushContainerImage, executableName); + + buildImage(containerImageInfo, out, executableName, dockerBuildArgs, false); - dockerConfig.buildx.platform - .filter(platform -> platform.size() > 1) + dockerConfig.buildx().platform() + .filter(platform -> !platform.isEmpty()) .ifPresentOrElse( - platform -> log.infof("Built container image %s (%s platform(s))\n", containerImageInfo.getImage(), + platform -> LOG.infof("Built container image %s (%s platform(s))\n", containerImageInfo.getImage(), String.join(",", platform)), - () -> log.infof("Built container image %s\n", containerImageInfo.getImage())); + () -> LOG.infof("Built container image %s\n", containerImageInfo.getImage())); - } - - if (!useBuildx && buildContainerImage) { // If we didn't use buildx, now we need to process any tags - if (!containerImageInfo.getAdditionalImageTags().isEmpty()) { - createAdditionalTags(containerImageInfo.getImage(), containerImageInfo.getAdditionalImageTags(), dockerConfig); + if (!useBuildx && !containerImageInfo.getAdditionalImageTags().isEmpty()) { + createAdditionalTags(containerImageInfo.getImage(), containerImageInfo.getAdditionalImageTags(), + executableName); } } if (!useBuildx && pushContainerImage) { // If not using buildx, push the images - loginToRegistryIfNeeded(containerImageConfig, containerImageInfo, dockerConfig); - - Stream.concat(containerImageInfo.getAdditionalImageTags().stream(), Stream.of(containerImageInfo.getImage())) - .forEach(imageToPush -> pushImage(imageToPush, dockerConfig)); + loginToRegistryIfNeeded(containerImageConfig, containerImageInfo, executableName); + pushImages(containerImageInfo, executableName); } return containerImageInfo.getImage(); } - private void loginToRegistryIfNeeded(ContainerImageConfig containerImageConfig, - ContainerImageInfoBuildItem containerImageInfo, DockerConfig dockerConfig) { - String registry = containerImageInfo.getRegistry() - .orElseGet(() -> { - log.info("No container image registry was set, so 'docker.io' will be used"); - return "docker.io"; - }); - - // Check if we need to login first - if (containerImageConfig.username.isPresent() && containerImageConfig.password.isPresent()) { - final String executableName = dockerConfig.executableName.orElse(detectContainerRuntime(true).getExecutableName()); - boolean loginSuccessful = ExecUtil.exec(executableName, "login", registry, "-u", - containerImageConfig.username.get(), - "-p" + containerImageConfig.password.get()); - if (!loginSuccessful) { - throw dockerException(executableName, - new String[] { "-u", containerImageConfig.username.get(), "-p", "********" }); - } - } - } + private String[] getDockerBuildArgs(String image, + DockerfilePaths dockerfilePaths, + ContainerImageConfig containerImageConfig, + DockerConfig dockerConfig, + ContainerImageInfoBuildItem containerImageInfo, + boolean pushImages, + String executableName) { - private String[] getDockerArgs(String image, DockerfilePaths dockerfilePaths, ContainerImageConfig containerImageConfig, - DockerConfig dockerConfig, ContainerImageInfoBuildItem containerImageInfo, boolean pushImages) { - List dockerArgs = new ArrayList<>(6 + dockerConfig.buildArgs.size() + dockerConfig.additionalArgs.map( - List::size).orElse(0)); - boolean useBuildx = dockerConfig.buildx.useBuildx(); + var dockerBuildArgs = getContainerCommonBuildArgs(image, dockerfilePaths, containerImageConfig, dockerConfig, true); + var buildx = dockerConfig.buildx(); + var useBuildx = buildx.useBuildx(); if (useBuildx) { - final String executableName = dockerConfig.executableName.orElse(detectContainerRuntime(true).getExecutableName()); // Check the executable. If not 'docker', then fail the build if (!DOCKER.equals(executableName)) { throw new IllegalArgumentException( - String.format( - "The 'buildx' properties are specific to 'executable-name=docker' and can not be used with " + - "the '%s' executable name. Either remove the `buildx` properties or the `executable-name` property.", - executableName)); + "The 'buildx' properties are specific to 'executable-name=docker' and can not be used with the '%s' executable name. Either remove the `buildx` properties or the `executable-name` property." + .formatted(executableName)); } - dockerArgs.add("buildx"); + dockerBuildArgs.add(0, "buildx"); } - dockerArgs.addAll(Arrays.asList("build", "-f", dockerfilePaths.getDockerfilePath().toAbsolutePath().toString())); - dockerConfig.buildx.platform + buildx.platform() .filter(platform -> !platform.isEmpty()) .ifPresent(platform -> { - dockerArgs.add("--platform"); - dockerArgs.add(String.join(",", platform)); + dockerBuildArgs.addAll(List.of("--platform", String.join(",", platform))); if (platform.size() == 1) { // Buildx only supports loading the image to the docker system if there is only 1 image - dockerArgs.add("--load"); + dockerBuildArgs.add("--load"); } }); - dockerConfig.buildx.progress.ifPresent(progress -> dockerArgs.addAll(List.of("--progress", progress))); - dockerConfig.buildx.output.ifPresent(output -> dockerArgs.addAll(List.of("--output", output))); - dockerConfig.buildArgs - .forEach((key, value) -> dockerArgs.addAll(Arrays.asList("--build-arg", String.format("%s=%s", key, value)))); - containerImageConfig.labels - .forEach((key, value) -> dockerArgs.addAll(Arrays.asList("--label", String.format("%s=%s", key, value)))); - dockerConfig.cacheFrom - .filter(cacheFrom -> !cacheFrom.isEmpty()) - .ifPresent(cacheFrom -> { - dockerArgs.add("--cache-from"); - dockerArgs.add(String.join(",", cacheFrom)); - }); - dockerConfig.network.ifPresent(network -> { - dockerArgs.add("--network"); - dockerArgs.add(network); - }); - dockerConfig.additionalArgs.ifPresent(dockerArgs::addAll); - dockerArgs.addAll(Arrays.asList("-t", image)); + + buildx.progress().ifPresent(progress -> dockerBuildArgs.addAll(List.of("--progress", progress))); + buildx.output().ifPresent(output -> dockerBuildArgs.addAll(List.of("--output", output))); if (useBuildx) { // When using buildx for multi-arch images, it wants to push in a single step // 1) Create all the additional tags containerImageInfo.getAdditionalImageTags() - .forEach(additionalImageTag -> dockerArgs.addAll(List.of("-t", additionalImageTag))); + .forEach(additionalImageTag -> dockerBuildArgs.addAll(List.of("-t", additionalImageTag))); if (pushImages) { // 2) Enable the --push flag - dockerArgs.add("--push"); - } - } - - dockerArgs.add(dockerfilePaths.getDockerExecutionPath().toAbsolutePath().toString()); - return dockerArgs.toArray(new String[0]); - } - - private void createAdditionalTags(String image, List additionalImageTags, DockerConfig dockerConfig) { - final String executableName = dockerConfig.executableName.orElse(detectContainerRuntime(true).getExecutableName()); - for (String additionalTag : additionalImageTags) { - String[] tagArgs = { "tag", image, additionalTag }; - boolean tagSuccessful = ExecUtil.exec(executableName, tagArgs); - if (!tagSuccessful) { - throw dockerException(executableName, tagArgs); + dockerBuildArgs.add("--push"); } } - } - - private void pushImage(String image, DockerConfig dockerConfig) { - final String executableName = dockerConfig.executableName.orElse(detectContainerRuntime(true).getExecutableName()); - String[] pushArgs = { "push", image }; - boolean pushSuccessful = ExecUtil.exec(executableName, pushArgs); - if (!pushSuccessful) { - throw dockerException(executableName, pushArgs); - } - log.info("Successfully pushed docker image " + image); - } - - private RuntimeException dockerException(String executableName, String[] dockerArgs) { - return new RuntimeException( - "Execution of '" + executableName + " " + String.join(" ", dockerArgs) - + "' failed. See docker output for more details"); - } - @SuppressWarnings("deprecation") // legacy JAR - private DockerfilePaths getDockerfilePaths(DockerConfig dockerConfig, boolean forNative, - PackageConfig packageConfig, - OutputTargetBuildItem outputTargetBuildItem) { - Path outputDirectory = outputTargetBuildItem.getOutputDirectory(); - if (forNative) { - if (dockerConfig.dockerfileNativePath.isPresent()) { - return ProvidedDockerfile.get(Paths.get(dockerConfig.dockerfileNativePath.get()), outputDirectory); - } else { - return DockerfileDetectionResult.detect(DOCKERFILE_NATIVE, outputDirectory); - } - } else { - if (dockerConfig.dockerfileJvmPath.isPresent()) { - return ProvidedDockerfile.get(Paths.get(dockerConfig.dockerfileJvmPath.get()), outputDirectory); - } else if (packageConfig.jar().type() == LEGACY_JAR) { - return DockerfileDetectionResult.detect(DOCKERFILE_LEGACY_JAR, outputDirectory); - } else { - return DockerfileDetectionResult.detect(DOCKERFILE_JVM, outputDirectory); - } - } + dockerBuildArgs.add(dockerfilePaths.dockerExecutionPath().toAbsolutePath().toString()); + return dockerBuildArgs.toArray(String[]::new); } - - private interface DockerfilePaths { - Path getDockerfilePath(); - - Path getDockerExecutionPath(); - } - - private static class DockerfileDetectionResult implements DockerfilePaths { - private final Path dockerfilePath; - private final Path dockerExecutionPath; - - private DockerfileDetectionResult(Path dockerfilePath, Path dockerExecutionPath) { - this.dockerfilePath = dockerfilePath; - this.dockerExecutionPath = dockerExecutionPath; - } - - public Path getDockerfilePath() { - return dockerfilePath; - } - - public Path getDockerExecutionPath() { - return dockerExecutionPath; - } - - static DockerfileDetectionResult detect(String resource, Path outputDirectory) { - Map.Entry dockerfileToExecutionRoot = findDockerfileRoot(outputDirectory); - if (dockerfileToExecutionRoot == null) { - throw new IllegalStateException( - "Unable to find root of Dockerfile files. Consider adding 'src/main/docker/' to your project root"); - } - Path dockerFilePath = dockerfileToExecutionRoot.getKey().resolve(resource); - if (!Files.exists(dockerFilePath)) { - throw new IllegalStateException( - "Unable to find Dockerfile " + resource + " in " - + dockerfileToExecutionRoot.getKey().toAbsolutePath()); - } - return new DockerfileDetectionResult(dockerFilePath, dockerfileToExecutionRoot.getValue()); - } - - private static Map.Entry findDockerfileRoot(Path outputDirectory) { - Map.Entry mainSourcesRoot = findMainSourcesRoot(outputDirectory); - if (mainSourcesRoot == null) { - return null; - } - Path dockerfilesRoot = mainSourcesRoot.getKey().resolve(DOCKER_DIRECTORY_NAME); - if (!dockerfilesRoot.toFile().exists()) { - return null; - } - return new AbstractMap.SimpleEntry<>(dockerfilesRoot, mainSourcesRoot.getValue()); - } - - } - - private static class ProvidedDockerfile implements DockerfilePaths { - private final Path dockerfilePath; - private final Path dockerExecutionPath; - - private ProvidedDockerfile(Path dockerfilePath, Path dockerExecutionPath) { - this.dockerfilePath = dockerfilePath; - this.dockerExecutionPath = dockerExecutionPath; - } - - public static ProvidedDockerfile get(Path dockerfilePath, Path outputDirectory) { - AbstractMap.SimpleEntry mainSourcesRoot = findMainSourcesRoot(outputDirectory); - if (mainSourcesRoot == null) { - throw new IllegalStateException("Unable to determine project root"); - } - Path effectiveDockerfilePath = dockerfilePath.isAbsolute() ? dockerfilePath - : mainSourcesRoot.getValue().resolve(dockerfilePath); - if (!effectiveDockerfilePath.toFile().exists()) { - throw new IllegalArgumentException( - "Specified Dockerfile path " + effectiveDockerfilePath.toAbsolutePath() + " does not exist"); - } - return new ProvidedDockerfile( - effectiveDockerfilePath, - mainSourcesRoot.getValue()); - } - - @Override - public Path getDockerfilePath() { - return dockerfilePath; - } - - @Override - public Path getDockerExecutionPath() { - return dockerExecutionPath; - } - } - } diff --git a/extensions/container-image/container-image-docker/pom.xml b/extensions/container-image/container-image-docker/pom.xml index 9e778d91943f8..b3eab9c5d2909 100644 --- a/extensions/container-image/container-image-docker/pom.xml +++ b/extensions/container-image/container-image-docker/pom.xml @@ -17,4 +17,4 @@ runtime - \ No newline at end of file + diff --git a/extensions/container-image/container-image-docker/runtime/pom.xml b/extensions/container-image/container-image-docker/runtime/pom.xml index 38b3aeccb159f..8e172cf9a7466 100644 --- a/extensions/container-image/container-image-docker/runtime/pom.xml +++ b/extensions/container-image/container-image-docker/runtime/pom.xml @@ -17,7 +17,7 @@ io.quarkus - quarkus-container-image + quarkus-container-image-docker-common @@ -49,4 +49,4 @@ - \ No newline at end of file + diff --git a/extensions/container-image/container-image-docker/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/container-image/container-image-docker/runtime/src/main/resources/META-INF/quarkus-extension.yaml index 163f70c3e2511..c7737a60750eb 100644 --- a/extensions/container-image/container-image-docker/runtime/src/main/resources/META-INF/quarkus-extension.yaml +++ b/extensions/container-image/container-image-docker/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -8,4 +8,6 @@ metadata: - "image" categories: - "cloud" - status: "preview" \ No newline at end of file + status: "preview" + config: + - "quarkus.docker." \ No newline at end of file diff --git a/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/ContainerImageJibConfig.java b/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/ContainerImageJibConfig.java index a19391b993afd..a6792a1248366 100644 --- a/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/ContainerImageJibConfig.java +++ b/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/ContainerImageJibConfig.java @@ -5,6 +5,7 @@ import java.util.Optional; import java.util.Set; +import io.quarkus.runtime.annotations.ConfigDocMapKey; import io.quarkus.runtime.annotations.ConfigItem; import io.quarkus.runtime.annotations.ConfigPhase; import io.quarkus.runtime.annotations.ConfigRoot; @@ -99,6 +100,7 @@ public class ContainerImageJibConfig { * Environment variables to add to the container image */ @ConfigItem + @ConfigDocMapKey("environment-variable-name") public Map environmentVariables; /** @@ -202,6 +204,7 @@ public class ContainerImageJibConfig { * when the container image is being built locally. */ @ConfigItem + @ConfigDocMapKey("environment-variable-name") public Map dockerEnvironment; /** diff --git a/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/JibProcessor.java b/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/JibProcessor.java index 20ba3aea728c9..01f72dc80cec1 100644 --- a/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/JibProcessor.java +++ b/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/JibProcessor.java @@ -84,6 +84,7 @@ import io.quarkus.deployment.util.ContainerRuntimeUtil; import io.quarkus.fs.util.ZipUtils; import io.quarkus.maven.dependency.ResolvedDependency; +import io.quarkus.runtime.ResettableSystemProperties; public class JibProcessor { @@ -255,15 +256,13 @@ private JibContainer containerize(ContainerImageConfig containerImageConfig, for (String additionalTag : containerImage.getAdditionalTags()) { containerizer.withAdditionalTag(additionalTag); } - String previousContextStorageSysProp = null; - try { - // Jib uses the Google HTTP Client under the hood which attempts to record traces via OpenCensus which is wired - // to delegate to OpenTelemetry. - // This can lead to problems with the Quarkus OpenTelemetry extension which expects Vert.x to be running, - // something that is not the case at build time, see https://github.com/quarkusio/quarkus/issues/22864. - previousContextStorageSysProp = System.setProperty(OPENTELEMETRY_CONTEXT_CONTEXT_STORAGE_PROVIDER_SYS_PROP, - "default"); + // Jib uses the Google HTTP Client under the hood which attempts to record traces via OpenCensus which is wired + // to delegate to OpenTelemetry. + // This can lead to problems with the Quarkus OpenTelemetry extension which expects Vert.x to be running, + // something that is not the case at build time, see https://github.com/quarkusio/quarkus/issues/22864. + try (var resettableSystemProperties = ResettableSystemProperties + .of(OPENTELEMETRY_CONTEXT_CONTEXT_STORAGE_PROVIDER_SYS_PROP, "default")) { JibContainer container = containerizeUnderLock(jibContainerBuilder, containerizer); log.infof("%s container image %s (%s)\n", containerImageConfig.isPushExplicitlyEnabled() ? "Pushed" : "Created", @@ -272,12 +271,6 @@ private JibContainer containerize(ContainerImageConfig containerImageConfig, return container; } catch (Exception e) { throw new RuntimeException("Unable to create container image", e); - } finally { - if (previousContextStorageSysProp == null) { - System.clearProperty(OPENTELEMETRY_CONTEXT_CONTEXT_STORAGE_PROVIDER_SYS_PROP); - } else { - System.setProperty(OPENTELEMETRY_CONTEXT_CONTEXT_STORAGE_PROVIDER_SYS_PROP, previousContextStorageSysProp); - } } } diff --git a/extensions/container-image/container-image-podman/deployment/pom.xml b/extensions/container-image/container-image-podman/deployment/pom.xml new file mode 100644 index 0000000000000..3db78bfc5e407 --- /dev/null +++ b/extensions/container-image/container-image-podman/deployment/pom.xml @@ -0,0 +1,49 @@ + + + 4.0.0 + + + io.quarkus + quarkus-container-image-podman-parent + 999-SNAPSHOT + + + quarkus-container-image-podman-deployment + Quarkus - Container Image - Podman - Deployment + + + + io.quarkus + quarkus-container-image-podman + + + io.quarkus + quarkus-container-image-docker-common-deployment + + + org.assertj + assertj-core + test + + + + + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + + + diff --git a/extensions/container-image/container-image-podman/deployment/src/main/java/io/quarkus/container/image/podman/deployment/PodmanBuild.java b/extensions/container-image/container-image-podman/deployment/src/main/java/io/quarkus/container/image/podman/deployment/PodmanBuild.java new file mode 100644 index 0000000000000..7e48b7fbbc493 --- /dev/null +++ b/extensions/container-image/container-image-podman/deployment/src/main/java/io/quarkus/container/image/podman/deployment/PodmanBuild.java @@ -0,0 +1,20 @@ +package io.quarkus.container.image.podman.deployment; + +import java.util.function.BooleanSupplier; + +import io.quarkus.container.image.deployment.ContainerImageConfig; + +public class PodmanBuild implements BooleanSupplier { + private final ContainerImageConfig containerImageConfig; + + public PodmanBuild(ContainerImageConfig containerImageConfig) { + this.containerImageConfig = containerImageConfig; + } + + @Override + public boolean getAsBoolean() { + return containerImageConfig.builder + .map(b -> b.equals(PodmanProcessor.PODMAN_CONTAINER_IMAGE_NAME)) + .orElse(true); + } +} diff --git a/extensions/container-image/container-image-podman/deployment/src/main/java/io/quarkus/container/image/podman/deployment/PodmanConfig.java b/extensions/container-image/container-image-podman/deployment/src/main/java/io/quarkus/container/image/podman/deployment/PodmanConfig.java new file mode 100644 index 0000000000000..c49c12715c78e --- /dev/null +++ b/extensions/container-image/container-image-podman/deployment/src/main/java/io/quarkus/container/image/podman/deployment/PodmanConfig.java @@ -0,0 +1,19 @@ +package io.quarkus.container.image.podman.deployment; + +import java.util.List; +import java.util.Optional; + +import io.quarkus.container.image.docker.common.deployment.CommonConfig; +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.annotations.ConfigRoot; +import io.smallrye.config.ConfigMapping; + +@ConfigRoot(phase = ConfigPhase.BUILD_TIME) +@ConfigMapping(prefix = "quarkus.podman") +public interface PodmanConfig extends CommonConfig { + /** + * Which platform(s) to target during the build. See + * https://docs.podman.io/en/latest/markdown/podman-build.1.html#platform-os-arch-variant + */ + Optional> platform(); +} diff --git a/extensions/container-image/container-image-podman/deployment/src/main/java/io/quarkus/container/image/podman/deployment/PodmanProcessor.java b/extensions/container-image/container-image-podman/deployment/src/main/java/io/quarkus/container/image/podman/deployment/PodmanProcessor.java new file mode 100644 index 0000000000000..6593bc944f93e --- /dev/null +++ b/extensions/container-image/container-image-podman/deployment/src/main/java/io/quarkus/container/image/podman/deployment/PodmanProcessor.java @@ -0,0 +1,186 @@ +package io.quarkus.container.image.podman.deployment; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Stream; + +import org.jboss.logging.Logger; + +import io.quarkus.container.image.deployment.ContainerImageConfig; +import io.quarkus.container.image.docker.common.deployment.CommonProcessor; +import io.quarkus.container.spi.AvailableContainerImageExtensionBuildItem; +import io.quarkus.container.spi.ContainerImageBuildRequestBuildItem; +import io.quarkus.container.spi.ContainerImageBuilderBuildItem; +import io.quarkus.container.spi.ContainerImageInfoBuildItem; +import io.quarkus.container.spi.ContainerImagePushRequestBuildItem; +import io.quarkus.deployment.IsNormalNotRemoteDev; +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.builditem.PodmanStatusBuildItem; +import io.quarkus.deployment.pkg.PackageConfig; +import io.quarkus.deployment.pkg.builditem.AppCDSResultBuildItem; +import io.quarkus.deployment.pkg.builditem.ArtifactResultBuildItem; +import io.quarkus.deployment.pkg.builditem.CompiledJavaVersionBuildItem; +import io.quarkus.deployment.pkg.builditem.JarBuildItem; +import io.quarkus.deployment.pkg.builditem.NativeImageBuildItem; +import io.quarkus.deployment.pkg.builditem.OutputTargetBuildItem; +import io.quarkus.deployment.pkg.builditem.UpxCompressedBuildItem; +import io.quarkus.deployment.pkg.steps.NativeBuild; +import io.quarkus.deployment.util.ContainerRuntimeUtil.ContainerRuntime; +import io.quarkus.deployment.util.ExecUtil; + +public class PodmanProcessor extends CommonProcessor { + private static final Logger LOG = Logger.getLogger(PodmanProcessor.class); + private static final String PODMAN = "podman"; + static final String PODMAN_CONTAINER_IMAGE_NAME = "podman"; + + @Override + protected String getProcessorImplementation() { + return PODMAN; + } + + @BuildStep + public AvailableContainerImageExtensionBuildItem availability() { + return new AvailableContainerImageExtensionBuildItem(PODMAN); + } + + @BuildStep(onlyIf = { IsNormalNotRemoteDev.class, PodmanBuild.class }, onlyIfNot = NativeBuild.class) + public void podmanBuildFromJar(PodmanConfig podmanConfig, + PodmanStatusBuildItem podmanStatusBuildItem, + ContainerImageConfig containerImageConfig, + OutputTargetBuildItem out, + ContainerImageInfoBuildItem containerImageInfo, + @SuppressWarnings("unused") CompiledJavaVersionBuildItem compiledJavaVersion, + Optional buildRequest, + Optional pushRequest, + @SuppressWarnings("unused") Optional appCDSResult, // ensure podman build will be performed after AppCDS creation + BuildProducer artifactResultProducer, + BuildProducer containerImageBuilder, + PackageConfig packageConfig, + @SuppressWarnings("unused") JarBuildItem jar) { + + buildFromJar(podmanConfig, podmanStatusBuildItem, containerImageConfig, out, containerImageInfo, buildRequest, + pushRequest, artifactResultProducer, containerImageBuilder, packageConfig, ContainerRuntime.PODMAN); + } + + @BuildStep(onlyIf = { IsNormalNotRemoteDev.class, NativeBuild.class, PodmanBuild.class }) + public void podmanBuildFromNativeImage(PodmanConfig podmanConfig, + PodmanStatusBuildItem podmanStatusBuildItem, + ContainerImageConfig containerImageConfig, + ContainerImageInfoBuildItem containerImage, + Optional buildRequest, + Optional pushRequest, + OutputTargetBuildItem out, + @SuppressWarnings("unused") Optional upxCompressed, // used to ensure that we work with the compressed native binary if compression was enabled + BuildProducer artifactResultProducer, + BuildProducer containerImageBuilder, + PackageConfig packageConfig, + // used to ensure that the native binary has been built + NativeImageBuildItem nativeImage) { + + buildFromNativeImage(podmanConfig, podmanStatusBuildItem, containerImageConfig, containerImage, + buildRequest, pushRequest, out, artifactResultProducer, containerImageBuilder, packageConfig, nativeImage, + ContainerRuntime.PODMAN); + } + + @Override + protected String createContainerImage(ContainerImageConfig containerImageConfig, + PodmanConfig podmanConfig, + ContainerImageInfoBuildItem containerImageInfo, + OutputTargetBuildItem out, + DockerfilePaths dockerfilePaths, + boolean buildContainerImage, + boolean pushContainerImage, + PackageConfig packageConfig, + String executableName) { + + // Following https://developers.redhat.com/articles/2023/11/03/how-build-multi-architecture-container-images#testing_multi_architecture_containers + // If we are building more than 1 platform, then the build needs to happen in 2 separate steps + // 1) podman manifest create + // 2) podman build --platform --manifest + + // Then when pushing you push the manifest, not the image: + // podman manifest push + + var isMultiPlatformBuild = isMultiPlatformBuild(podmanConfig); + var image = containerImageInfo.getImage(); + + if (isMultiPlatformBuild) { + createManifest(image, executableName); + } + + if (buildContainerImage) { + var podmanBuildArgs = getPodmanBuildArgs(image, dockerfilePaths, containerImageConfig, podmanConfig, + isMultiPlatformBuild); + buildImage(containerImageInfo, out, executableName, podmanBuildArgs, true); + } + + if (pushContainerImage) { + loginToRegistryIfNeeded(containerImageConfig, containerImageInfo, executableName); + + if (isMultiPlatformBuild) { + pushManifests(containerImageInfo, executableName); + } else { + pushImages(containerImageInfo, executableName); + } + } + + return image; + } + + private String[] getPodmanBuildArgs(String image, + DockerfilePaths dockerfilePaths, + ContainerImageConfig containerImageConfig, + PodmanConfig podmanConfig, + boolean isMultiPlatformBuild) { + + var podmanBuildArgs = getContainerCommonBuildArgs(image, dockerfilePaths, containerImageConfig, podmanConfig, + !isMultiPlatformBuild); + + podmanConfig.platform() + .filter(platform -> !platform.isEmpty()) + .ifPresent(platform -> { + podmanBuildArgs.addAll(List.of("--platform", String.join(",", platform))); + + if (isMultiPlatformBuild) { + podmanBuildArgs.addAll(List.of("--manifest", image)); + } + }); + + podmanBuildArgs.add(dockerfilePaths.dockerExecutionPath().toAbsolutePath().toString()); + return podmanBuildArgs.toArray(String[]::new); + } + + private void pushManifests(ContainerImageInfoBuildItem containerImageInfo, String executableName) { + Stream.concat(containerImageInfo.getAdditionalImageTags().stream(), Stream.of(containerImageInfo.getImage())) + .forEach(manifestToPush -> pushManifest(manifestToPush, executableName)); + } + + private void pushManifest(String image, String executableName) { + String[] pushArgs = { "manifest", "push", image }; + var pushSuccessful = ExecUtil.exec(executableName, pushArgs); + + if (!pushSuccessful) { + throw containerRuntimeException(executableName, pushArgs); + } + + LOG.infof("Successfully pushed podman manifest %s", image); + } + + private void createManifest(String image, String executableName) { + var manifestCreateArgs = new String[] { "manifest", "create", image }; + + LOG.infof("Running '%s %s'", executableName, String.join(" ", manifestCreateArgs)); + var createManifestSuccessful = ExecUtil.exec(executableName, manifestCreateArgs); + + if (!createManifestSuccessful) { + throw containerRuntimeException(executableName, manifestCreateArgs); + } + } + + private boolean isMultiPlatformBuild(PodmanConfig podmanConfig) { + return podmanConfig.platform() + .map(List::size) + .orElse(0) >= 2; + } +} diff --git a/extensions/container-image/container-image-podman/pom.xml b/extensions/container-image/container-image-podman/pom.xml new file mode 100644 index 0000000000000..57386b321fa80 --- /dev/null +++ b/extensions/container-image/container-image-podman/pom.xml @@ -0,0 +1,20 @@ + + + + quarkus-container-image-parent + io.quarkus + 999-SNAPSHOT + + 4.0.0 + + quarkus-container-image-podman-parent + Quarkus - Container Image - Podman - Parent + pom + + deployment + runtime + + + diff --git a/extensions/container-image/container-image-podman/runtime/pom.xml b/extensions/container-image/container-image-podman/runtime/pom.xml new file mode 100644 index 0000000000000..1bdbbdf23b8ac --- /dev/null +++ b/extensions/container-image/container-image-podman/runtime/pom.xml @@ -0,0 +1,52 @@ + + + 4.0.0 + + + io.quarkus + quarkus-container-image-podman-parent + 999-SNAPSHOT + + + quarkus-container-image-podman + Quarkus - Container Image - Podman + Build container images of your application using Podman + + + + io.quarkus + quarkus-container-image-docker-common + + + + + + + io.quarkus + quarkus-extension-maven-plugin + + + + io.quarkus.container.image.podman.deployment.PodmanBuild + io.quarkus.container.image.podman + + + + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + + diff --git a/extensions/container-image/container-image-podman/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/container-image/container-image-podman/runtime/src/main/resources/META-INF/quarkus-extension.yaml new file mode 100644 index 0000000000000..563a067358c16 --- /dev/null +++ b/extensions/container-image/container-image-podman/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -0,0 +1,14 @@ +--- +artifact: ${project.groupId}:${project.artifactId}:${project.version} +name: "Container Image Podman" +metadata: + keywords: + - "podman" + - "container" + - "image" + guide: "https://quarkus.io/guides/container-image" + categories: + - "cloud" + status: "preview" + config: + - "quarkus.podman." \ No newline at end of file diff --git a/extensions/container-image/deployment/src/main/java/io/quarkus/container/image/deployment/ContainerImageCapabilitiesUtil.java b/extensions/container-image/deployment/src/main/java/io/quarkus/container/image/deployment/ContainerImageCapabilitiesUtil.java index 5a4f027a0a3a3..53b344b27d956 100644 --- a/extensions/container-image/deployment/src/main/java/io/quarkus/container/image/deployment/ContainerImageCapabilitiesUtil.java +++ b/extensions/container-image/deployment/src/main/java/io/quarkus/container/image/deployment/ContainerImageCapabilitiesUtil.java @@ -13,12 +13,14 @@ public final class ContainerImageCapabilitiesUtil { public final static Map CAPABILITY_TO_EXTENSION_NAME = Map.of( Capability.CONTAINER_IMAGE_JIB, "quarkus-container-image-jib", Capability.CONTAINER_IMAGE_DOCKER, "quarkus-container-image-docker", + Capability.CONTAINER_IMAGE_PODMAN, "quarkus-container-image-podman", Capability.CONTAINER_IMAGE_OPENSHIFT, "quarkus-container-image-openshift", Capability.CONTAINER_IMAGE_BUILDPACK, "quarkus-container-image-buildpack"); private final static Map CAPABILITY_TO_BUILDER_NAME = Map.of( Capability.CONTAINER_IMAGE_JIB, "jib", Capability.CONTAINER_IMAGE_DOCKER, "docker", + Capability.CONTAINER_IMAGE_PODMAN, "podman", Capability.CONTAINER_IMAGE_OPENSHIFT, "openshift", Capability.CONTAINER_IMAGE_BUILDPACK, "buildpack"); diff --git a/extensions/container-image/deployment/src/main/java/io/quarkus/container/image/deployment/ContainerImageConfig.java b/extensions/container-image/deployment/src/main/java/io/quarkus/container/image/deployment/ContainerImageConfig.java index fc1af2b9dca50..b5ad69727968c 100644 --- a/extensions/container-image/deployment/src/main/java/io/quarkus/container/image/deployment/ContainerImageConfig.java +++ b/extensions/container-image/deployment/src/main/java/io/quarkus/container/image/deployment/ContainerImageConfig.java @@ -4,6 +4,7 @@ import java.util.Map; import java.util.Optional; +import io.quarkus.runtime.annotations.ConfigDocMapKey; import io.quarkus.runtime.annotations.ConfigItem; import io.quarkus.runtime.annotations.ConfigRoot; import io.quarkus.runtime.annotations.ConvertWith; @@ -42,6 +43,7 @@ public class ContainerImageConfig { * Custom labels to add to the generated image. */ @ConfigItem + @ConfigDocMapKey("label-name") public Map labels; /** @@ -89,7 +91,7 @@ public class ContainerImageConfig { public Optional push; /** - * The name of the container image extension to use (e.g. docker, jib, s2i). + * The name of the container image extension to use (e.g. docker, podman, jib, s2i). * The option will be used in case multiple extensions are present. */ @ConfigItem diff --git a/extensions/container-image/pom.xml b/extensions/container-image/pom.xml index 4e84fd15310c4..bb2474fd039f7 100644 --- a/extensions/container-image/pom.xml +++ b/extensions/container-image/pom.xml @@ -20,7 +20,9 @@ spi util container-image-buildpack + container-image-docker-common container-image-docker + container-image-podman container-image-jib container-image-openshift diff --git a/extensions/datasource/deployment/src/main/java/io/quarkus/datasource/deployment/devservices/DevServicesDatasourceProcessor.java b/extensions/datasource/deployment/src/main/java/io/quarkus/datasource/deployment/devservices/DevServicesDatasourceProcessor.java index a5ab0f15c4e3e..bde32b21a071a 100644 --- a/extensions/datasource/deployment/src/main/java/io/quarkus/datasource/deployment/devservices/DevServicesDatasourceProcessor.java +++ b/extensions/datasource/deployment/src/main/java/io/quarkus/datasource/deployment/devservices/DevServicesDatasourceProcessor.java @@ -249,8 +249,7 @@ private RunningDevService startDevDb( } if (devDbProvider.isDockerRequired() && !dockerStatusBuildItem.isDockerAvailable()) { - String message = "Please configure the datasource URL for " - + dataSourcePrettyName + String message = "Please configure the datasource URL for " + dataSourcePrettyName + " or ensure the Docker daemon is up and running."; if (launchMode == LaunchMode.TEST) { throw new IllegalStateException(message); diff --git a/extensions/datasource/runtime/src/main/java/io/quarkus/datasource/runtime/DevServicesBuildTimeConfig.java b/extensions/datasource/runtime/src/main/java/io/quarkus/datasource/runtime/DevServicesBuildTimeConfig.java index 3770832c23b21..13e1043e86f6c 100644 --- a/extensions/datasource/runtime/src/main/java/io/quarkus/datasource/runtime/DevServicesBuildTimeConfig.java +++ b/extensions/datasource/runtime/src/main/java/io/quarkus/datasource/runtime/DevServicesBuildTimeConfig.java @@ -4,6 +4,7 @@ import java.util.Optional; import java.util.OptionalInt; +import io.quarkus.runtime.annotations.ConfigDocMapKey; import io.quarkus.runtime.annotations.ConfigGroup; import io.quarkus.runtime.configuration.TrimmedStringConverter; import io.smallrye.config.WithConverter; @@ -33,6 +34,7 @@ public interface DevServicesBuildTimeConfig { /** * Environment variables that are passed to the container. */ + @ConfigDocMapKey("environment-variable-name") Map containerEnv(); /** @@ -41,11 +43,13 @@ public interface DevServicesBuildTimeConfig { * Properties defined here are database-specific * and are interpreted specifically in each database dev service implementation. */ + @ConfigDocMapKey("property-key") Map containerProperties(); /** * Generic properties that are added to the database connection URL. */ + @ConfigDocMapKey("property-key") Map properties(); /** @@ -97,6 +101,7 @@ public interface DevServicesBuildTimeConfig { *

* This has no effect if the provider is not a container-based database, such as H2 or Derby. */ + @ConfigDocMapKey("host-path") Map volumes(); /** diff --git a/extensions/elasticsearch-rest-client-common/deployment/src/main/java/io/quarkus/elasticsearch/restclient/common/deployment/ElasticsearchDevServicesBuildTimeConfig.java b/extensions/elasticsearch-rest-client-common/deployment/src/main/java/io/quarkus/elasticsearch/restclient/common/deployment/ElasticsearchDevServicesBuildTimeConfig.java index f468715df9cba..b5b1625001413 100644 --- a/extensions/elasticsearch-rest-client-common/deployment/src/main/java/io/quarkus/elasticsearch/restclient/common/deployment/ElasticsearchDevServicesBuildTimeConfig.java +++ b/extensions/elasticsearch-rest-client-common/deployment/src/main/java/io/quarkus/elasticsearch/restclient/common/deployment/ElasticsearchDevServicesBuildTimeConfig.java @@ -4,6 +4,7 @@ import java.util.Objects; import java.util.Optional; +import io.quarkus.runtime.annotations.ConfigDocMapKey; import io.quarkus.runtime.annotations.ConfigItem; import io.quarkus.runtime.annotations.ConfigPhase; import io.quarkus.runtime.annotations.ConfigRoot; @@ -96,6 +97,7 @@ public class ElasticsearchDevServicesBuildTimeConfig { * Environment variables that are passed to the container. */ @ConfigItem + @ConfigDocMapKey("environment-variable-name") public Map containerEnv; /** diff --git a/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayDataSourceRuntimeConfig.java b/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayDataSourceRuntimeConfig.java index 2e9d9aa3dd35e..772ba52458280 100644 --- a/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayDataSourceRuntimeConfig.java +++ b/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayDataSourceRuntimeConfig.java @@ -7,6 +7,7 @@ import java.util.Optional; import java.util.OptionalInt; +import io.quarkus.runtime.annotations.ConfigDocMapKey; import io.quarkus.runtime.annotations.ConfigGroup; import io.quarkus.runtime.annotations.ConfigItem; @@ -218,6 +219,7 @@ public static FlywayDataSourceRuntimeConfig defaultConfig() { * Sets the placeholders to replace in SQL migration scripts. */ @ConfigItem + @ConfigDocMapKey("placeholder-key") public Map placeholders = Collections.emptyMap(); /** diff --git a/extensions/hibernate-orm/deployment-spi/src/main/java/io/quarkus/hibernate/orm/deployment/spi/DatabaseKindDialectBuildItem.java b/extensions/hibernate-orm/deployment-spi/src/main/java/io/quarkus/hibernate/orm/deployment/spi/DatabaseKindDialectBuildItem.java index fe807866175cf..0087e4334b4f7 100644 --- a/extensions/hibernate-orm/deployment-spi/src/main/java/io/quarkus/hibernate/orm/deployment/spi/DatabaseKindDialectBuildItem.java +++ b/extensions/hibernate-orm/deployment-spi/src/main/java/io/quarkus/hibernate/orm/deployment/spi/DatabaseKindDialectBuildItem.java @@ -1,6 +1,7 @@ package io.quarkus.hibernate.orm.deployment.spi; import java.util.Optional; +import java.util.Set; import io.quarkus.builder.item.MultiBuildItem; @@ -9,15 +10,74 @@ */ public final class DatabaseKindDialectBuildItem extends MultiBuildItem { private final String dbKind; - private final String dialect; + private final Optional databaseProductName; + private final Optional dialect; + private final Set matchingDialects; private final Optional defaultDatabaseProductVersion; + /** + * @param dbKind The DB Kind set through {@code quarkus.datasource.db-kind} + * @param databaseProductName The corresponding database-product-name to set in Hibernate ORM. + * See {@code org.hibernate.dialect.Database} for information on how this name is resolved to a dialect. + * @param dialects The corresponding dialects in Hibernate ORM, + * to detect the dbKind when using database multi-tenancy. + */ + public static DatabaseKindDialectBuildItem forCoreDialect(String dbKind, String databaseProductName, + Set dialects) { + return new DatabaseKindDialectBuildItem(dbKind, Optional.empty(), Optional.of(databaseProductName), + dialects, Optional.empty()); + } + + /** + * @param dbKind The DB Kind set through {@code quarkus.datasource.db-kind} + * @param databaseProductName The corresponding database-product-name to set in Hibernate ORM. + * See {@code org.hibernate.dialect.Database} for information on how this name is resolved to a dialect. + * @param dialects The corresponding dialects in Hibernate ORM, + * to detect the dbKind when using database multi-tenancy. + * @param defaultDatabaseProductVersion The default database-product-version to set in Hibernate ORM. + * This is useful when the default version of the dialect in Hibernate ORM + * is lower than what we expect in Quarkus. + */ + public static DatabaseKindDialectBuildItem forCoreDialect(String dbKind, String databaseProductName, + Set dialects, String defaultDatabaseProductVersion) { + return new DatabaseKindDialectBuildItem(dbKind, Optional.empty(), Optional.of(databaseProductName), + dialects, Optional.of(defaultDatabaseProductVersion)); + } + + /** + * @param dbKind The DB Kind set through {@code quarkus.datasource.db-kind} + * @param dialect The corresponding dialect to set in Hibernate ORM. + * See {@code org.hibernate.dialect.Database} for information on how this name is resolved to a dialect. + */ + public static DatabaseKindDialectBuildItem forThirdPartyDialect(String dbKind, String dialect) { + return new DatabaseKindDialectBuildItem(dbKind, Optional.of(dialect), Optional.empty(), Set.of(dialect), + Optional.empty()); + } + /** * @param dbKind The DB Kind set through {@code quarkus.datasource.db-kind} * @param dialect The corresponding dialect to set in Hibernate ORM. + * See {@code org.hibernate.dialect.Database} for information on how this name is resolved to a dialect. + * @param defaultDatabaseProductVersion The default database-product-version to set in Hibernate ORM. + * This is useful when the default version of the dialect in Hibernate ORM + * is lower than what we expect in Quarkus. */ + public static DatabaseKindDialectBuildItem forThirdPartyDialect(String dbKind, String dialect, + String defaultDatabaseProductVersion) { + return new DatabaseKindDialectBuildItem(dbKind, Optional.of(dialect), Optional.empty(), + Set.of(dialect), Optional.of(defaultDatabaseProductVersion)); + } + + /** + * @param dbKind The DB Kind set through {@code quarkus.datasource.db-kind} + * @param dialect The corresponding dialect to set in Hibernate ORM. + * @deprecated Use {@link #forCoreDialect(String, String, Set)}(different arguments!) + * for core Hibernate ORM dialects to avoid warnings on startup, + * or {@link #forThirdPartyDialect(String, String)} for community or third-party dialects. + */ + @Deprecated public DatabaseKindDialectBuildItem(String dbKind, String dialect) { - this(dbKind, dialect, Optional.empty()); + this(dbKind, Optional.of(dialect), Optional.empty(), Set.of(dialect), Optional.empty()); } /** @@ -27,15 +87,22 @@ public DatabaseKindDialectBuildItem(String dbKind, String dialect) { * @param defaultDatabaseProductVersion The default database-product-version to set in Hibernate ORM. * This is useful when the default version of the dialect in Hibernate ORM * is lower than what we expect in Quarkus. + * @deprecated Use {@link #forCoreDialect(String, String, Set, String)}(different arguments!) + * for core Hibernate ORM dialects to avoid warnings on startup, + * or {@link #forThirdPartyDialect(String, String, String)} for community or third-party dialects. */ + @Deprecated public DatabaseKindDialectBuildItem(String dbKind, String dialect, String defaultDatabaseProductVersion) { - this(dbKind, dialect, Optional.of(defaultDatabaseProductVersion)); + this(dbKind, Optional.of(dialect), Optional.empty(), Set.of(dialect), Optional.of(defaultDatabaseProductVersion)); } - private DatabaseKindDialectBuildItem(String dbKind, String dialect, + private DatabaseKindDialectBuildItem(String dbKind, Optional dialect, + Optional databaseProductName, Set matchingDialects, Optional defaultDatabaseProductVersion) { this.dbKind = dbKind; this.dialect = dialect; + this.matchingDialects = matchingDialects; + this.databaseProductName = databaseProductName; this.defaultDatabaseProductVersion = defaultDatabaseProductVersion; } @@ -44,9 +111,21 @@ public String getDbKind() { } public String getDialect() { + return dialect.get(); + } + + public Optional getDialectOptional() { return dialect; } + public Set getMatchingDialects() { + return matchingDialects; + } + + public Optional getDatabaseProductName() { + return databaseProductName; + } + public Optional getDefaultDatabaseProductVersion() { return defaultDatabaseProductVersion; } diff --git a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateLogFilterBuildStep.java b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateLogFilterBuildStep.java index 25d5e14f47844..2dffe9d53f761 100644 --- a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateLogFilterBuildStep.java +++ b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateLogFilterBuildStep.java @@ -30,8 +30,6 @@ void setupLogFilters(BuildProducer filters) { // Silence incubating settings warnings as we will use some for compatibility filters.produce(new LogCleanupFilterBuildItem("org.hibernate.orm.incubating", "HHH90006001")); - // https://hibernate.atlassian.net/browse/HHH-16546 - filters.produce(new LogCleanupFilterBuildItem("org.hibernate.tuple.entity.EntityMetamodel", "HHH000157")); //This "deprecation" warning isn't practical for the specific Quarkus needs, as it reminds users they don't need //to set the 'hibernate.dialect' property, however it's being set by Quarkus buildsteps so they can't avoid it. diff --git a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmConfigPersistenceUnit.java b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmConfigPersistenceUnit.java index bdf64462f2836..1f29cbd83ca83 100644 --- a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmConfigPersistenceUnit.java +++ b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmConfigPersistenceUnit.java @@ -229,7 +229,10 @@ public interface HibernateOrmConfigPersistenceUnit { /** * Defines the name of the datasource to use in case of SCHEMA approach. The datasource of the persistence unit will be used * if not set. + * + * @deprecated Use {@link #datasource()} instead. */ + @Deprecated @WithConverter(TrimmedStringConverter.class) Optional multitenantSchemaDatasource(); diff --git a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmProcessor.java b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmProcessor.java index 54b5f4de5f5aa..656443066024a 100644 --- a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmProcessor.java +++ b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmProcessor.java @@ -162,24 +162,27 @@ public final class HibernateOrmProcessor { @BuildStep void registerHibernateOrmMetadataForCoreDialects( BuildProducer producer) { - producer.produce(new DatabaseKindDialectBuildItem(DatabaseKind.DB2, - "org.hibernate.dialect.DB2Dialect")); - producer.produce(new DatabaseKindDialectBuildItem(DatabaseKind.DERBY, - "org.hibernate.dialect.DerbyDialect")); - producer.produce(new DatabaseKindDialectBuildItem(DatabaseKind.H2, + producer.produce(DatabaseKindDialectBuildItem.forCoreDialect(DatabaseKind.DB2, "DB2", + Set.of("org.hibernate.dialect.DB2Dialect"))); + producer.produce(DatabaseKindDialectBuildItem.forCoreDialect(DatabaseKind.DERBY, "Apache Derby", + Set.of("org.hibernate.dialect.DerbyDialect"))); + producer.produce(DatabaseKindDialectBuildItem.forCoreDialect(DatabaseKind.H2, "H2", + Set.of("org.hibernate.dialect.H2Dialect"), // Using our own default version is extra important for H2 // See https://github.com/quarkusio/quarkus/issues/1886 - "org.hibernate.dialect.H2Dialect", DialectVersions.Defaults.H2)); - producer.produce(new DatabaseKindDialectBuildItem(DatabaseKind.MARIADB, - "org.hibernate.dialect.MariaDBDialect", DialectVersions.Defaults.MARIADB)); - producer.produce(new DatabaseKindDialectBuildItem(DatabaseKind.MSSQL, - "org.hibernate.dialect.SQLServerDialect", DialectVersions.Defaults.MSSQL)); - producer.produce(new DatabaseKindDialectBuildItem(DatabaseKind.MYSQL, - "org.hibernate.dialect.MySQLDialect")); - producer.produce(new DatabaseKindDialectBuildItem(DatabaseKind.ORACLE, - "org.hibernate.dialect.OracleDialect")); - producer.produce(new DatabaseKindDialectBuildItem(DatabaseKind.POSTGRESQL, - "org.hibernate.dialect.PostgreSQLDialect")); + DialectVersions.Defaults.H2)); + producer.produce(DatabaseKindDialectBuildItem.forCoreDialect(DatabaseKind.MARIADB, "MariaDB", + Set.of("org.hibernate.dialect.MariaDBDialect"), + DialectVersions.Defaults.MARIADB)); + producer.produce(DatabaseKindDialectBuildItem.forCoreDialect(DatabaseKind.MSSQL, "Microsoft SQL Server", + Set.of("org.hibernate.dialect.SQLServerDialect"), + DialectVersions.Defaults.MSSQL)); + producer.produce(DatabaseKindDialectBuildItem.forCoreDialect(DatabaseKind.MYSQL, "MySQL", + Set.of("org.hibernate.dialect.MySQLDialect"))); + producer.produce(DatabaseKindDialectBuildItem.forCoreDialect(DatabaseKind.ORACLE, "Oracle", + Set.of("org.hibernate.dialect.OracleDialect"))); + producer.produce(DatabaseKindDialectBuildItem.forCoreDialect(DatabaseKind.POSTGRESQL, "PostgreSQL", + Set.of("org.hibernate.dialect.PostgreSQLDialect"))); } @BuildStep @@ -667,33 +670,51 @@ public void multitenancy(HibernateOrmRecorder recorder, boolean multitenancyEnabled = false; for (PersistenceUnitDescriptorBuildItem persistenceUnitDescriptor : persistenceUnitDescriptors) { - if (persistenceUnitDescriptor.getConfig().getMultiTenancyStrategy() == MultiTenancyStrategy.NONE) { - continue; - } + String persistenceUnitConfigName = persistenceUnitDescriptor.getConfigurationName(); + var multitenancyStrategy = persistenceUnitDescriptor.getConfig().getMultiTenancyStrategy(); + switch (multitenancyStrategy) { + case NONE -> { + } + case DISCRIMINATOR -> multitenancyEnabled = true; + case DATABASE, SCHEMA -> { + multitenancyEnabled = true; + + String multiTenancySchemaDataSource = persistenceUnitDescriptor.getMultiTenancySchemaDataSource(); + Optional datasource; + if (multitenancyStrategy == MultiTenancyStrategy.SCHEMA && multiTenancySchemaDataSource != null) { + LOG.warnf("Configuration property '%1$s' is deprecated. Use '%2$s' instead.", + HibernateOrmRuntimeConfig.puPropertyKey(persistenceUnitConfigName, + "multitenant-schema-datasource"), + HibernateOrmRuntimeConfig.puPropertyKey(persistenceUnitConfigName, "datasource")); + datasource = Optional.of(multiTenancySchemaDataSource); + } else { + datasource = persistenceUnitDescriptor.getConfig().getDataSource(); + } - multitenancyEnabled = true; - - ExtendedBeanConfigurator configurator = SyntheticBeanBuildItem.configure(DataSourceTenantConnectionResolver.class) - .scope(ApplicationScoped.class) - .types(TenantConnectionResolver.class) - .setRuntimeInit() - .defaultBean() - .unremovable() - .supplier(recorder.dataSourceTenantConnectionResolver(persistenceUnitDescriptor.getPersistenceUnitName(), - persistenceUnitDescriptor.getConfig().getDataSource(), - persistenceUnitDescriptor.getConfig().getMultiTenancyStrategy(), - persistenceUnitDescriptor.getMultiTenancySchemaDataSource())); - - if (PersistenceUnitUtil.isDefaultPersistenceUnit(persistenceUnitDescriptor.getPersistenceUnitName())) { - configurator.addQualifier(Default.class); - } else { - configurator.addQualifier().annotation(DotNames.NAMED) - .addValue("value", persistenceUnitDescriptor.getPersistenceUnitName()).done(); - configurator.addQualifier().annotation(PersistenceUnit.class) - .addValue("value", persistenceUnitDescriptor.getPersistenceUnitName()).done(); - } + ExtendedBeanConfigurator configurator = SyntheticBeanBuildItem + .configure(DataSourceTenantConnectionResolver.class) + .scope(ApplicationScoped.class) + .types(TenantConnectionResolver.class) + .setRuntimeInit() + .defaultBean() + .unremovable() + .supplier(recorder.dataSourceTenantConnectionResolver( + persistenceUnitDescriptor.getPersistenceUnitName(), + datasource, + persistenceUnitDescriptor.getConfig().getMultiTenancyStrategy())); + + if (PersistenceUnitUtil.isDefaultPersistenceUnit(persistenceUnitDescriptor.getPersistenceUnitName())) { + configurator.addQualifier(Default.class); + } else { + configurator.addQualifier().annotation(DotNames.NAMED) + .addValue("value", persistenceUnitDescriptor.getPersistenceUnitName()).done(); + configurator.addQualifier().annotation(PersistenceUnit.class) + .addValue("value", persistenceUnitDescriptor.getPersistenceUnitName()).done(); + } - syntheticBeans.produce(configurator.done()); + syntheticBeans.produce(configurator.done()); + } + } } if (multitenancyEnabled) { @@ -1107,15 +1128,18 @@ private static void collectDialectConfig(String persistenceUnitName, } Optional dialect = explicitDialect; + Optional dbProductName = Optional.empty(); Optional dbProductVersion = explicitDbMinVersion; if (dbKind.isPresent() || explicitDialect.isPresent()) { for (DatabaseKindDialectBuildItem item : dbKindMetadataBuildItems) { if (dbKind.isPresent() && DatabaseKind.is(dbKind.get(), item.getDbKind()) // Set the default version based on the dialect when we don't have a datasource // (i.e. for database multi-tenancy) - || explicitDialect.isPresent() && explicitDialect.get().equals(item.getDialect())) { - if (explicitDialect.isEmpty()) { - dialect = Optional.of(item.getDialect()); + || explicitDialect.isPresent() && item.getMatchingDialects().contains(explicitDialect.get())) { + dbProductName = item.getDatabaseProductName(); + if (dbProductName.isEmpty() && explicitDialect.isEmpty()) { + // Use dialects only as a last resort, prefer product name or explicitly user-provided dialect + dialect = item.getDialectOptional(); } if (explicitDbMinVersion.isEmpty()) { dbProductVersion = item.getDefaultDatabaseProductVersion(); @@ -1123,7 +1147,7 @@ private static void collectDialectConfig(String persistenceUnitName, break; } } - if (dialect.isEmpty()) { + if (dialect.isEmpty() && dbProductName.isEmpty()) { throw new ConfigurationException( "The Hibernate ORM extension could not guess the dialect from the database kind '" + dbKind.get() + "'. Add an explicit '" @@ -1134,6 +1158,8 @@ private static void collectDialectConfig(String persistenceUnitName, if (dialect.isPresent()) { puPropertiesCollector.accept(AvailableSettings.DIALECT, dialect.get()); + } else if (dbProductName.isPresent()) { + puPropertiesCollector.accept(AvailableSettings.JAKARTA_HBM2DDL_DB_NAME, dbProductName.get()); } else { // We only get here with the database multi-tenancy strategy; see the initial check, up top. assert multiTenancyStrategy == MultiTenancyStrategy.DATABASE; @@ -1148,7 +1174,7 @@ private static void collectDialectConfig(String persistenceUnitName, if (persistenceUnitConfig.dialect().storageEngine().isPresent()) { // Only actually set the storage engines if MySQL or MariaDB - if (isMySQLOrMariaDB(dialect.get())) { + if (isMySQLOrMariaDB(dbKind, dialect)) { // The storage engine has to be set as a system property. // We record it so that we can later run checks (because we can only set a single value) storageEngineCollector.add(persistenceUnitConfig.dialect().storageEngine().get()); @@ -1609,9 +1635,15 @@ private static Class[] toArray(final Set> interfaces) { return interfaces.toArray(new Class[interfaces.size()]); } - private static boolean isMySQLOrMariaDB(String dialect) { - String lowercaseDialect = dialect.toLowerCase(Locale.ROOT); - return lowercaseDialect.contains("mysql") || lowercaseDialect.contains("mariadb"); + private static boolean isMySQLOrMariaDB(Optional dbKind, Optional dialect) { + if (dbKind.isPresent() && (DatabaseKind.isMySQL(dbKind.get()) || DatabaseKind.isMariaDB(dbKind.get()))) { + return true; + } + if (dialect.isPresent()) { + String lowercaseDialect = dialect.get().toLowerCase(Locale.ROOT); + return lowercaseDialect.contains("mysql") || lowercaseDialect.contains("mariadb"); + } + return false; } private static final class ProxyCache { diff --git a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/HibernateOrmRecorder.java b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/HibernateOrmRecorder.java index 5050d7e5c648e..01259ea60f49c 100644 --- a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/HibernateOrmRecorder.java +++ b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/HibernateOrmRecorder.java @@ -83,12 +83,11 @@ public void created(BeanContainer beanContainer) { public Supplier dataSourceTenantConnectionResolver(String persistenceUnitName, Optional dataSourceName, - MultiTenancyStrategy multiTenancyStrategy, String multiTenancySchemaDataSourceName) { + MultiTenancyStrategy multiTenancyStrategy) { return new Supplier() { @Override public DataSourceTenantConnectionResolver get() { - return new DataSourceTenantConnectionResolver(persistenceUnitName, dataSourceName, multiTenancyStrategy, - multiTenancySchemaDataSourceName); + return new DataSourceTenantConnectionResolver(persistenceUnitName, dataSourceName, multiTenancyStrategy); } }; } diff --git a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/tenant/DataSourceTenantConnectionResolver.java b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/tenant/DataSourceTenantConnectionResolver.java index 29b38ebebadbe..4bc6dbed953d0 100644 --- a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/tenant/DataSourceTenantConnectionResolver.java +++ b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/tenant/DataSourceTenantConnectionResolver.java @@ -11,7 +11,6 @@ import org.jboss.logging.Logger; import io.agroal.api.AgroalDataSource; -import io.agroal.api.configuration.AgroalDataSourceConfiguration; import io.quarkus.agroal.DataSource; import io.quarkus.arc.Arc; import io.quarkus.datasource.common.runtime.DataSourceUtil; @@ -35,17 +34,14 @@ public class DataSourceTenantConnectionResolver implements TenantConnectionResol private MultiTenancyStrategy multiTenancyStrategy; - private String multiTenancySchemaDataSourceName; - public DataSourceTenantConnectionResolver() { } public DataSourceTenantConnectionResolver(String persistenceUnitName, Optional dataSourceName, - MultiTenancyStrategy multiTenancyStrategy, String multiTenancySchemaDataSourceName) { + MultiTenancyStrategy multiTenancyStrategy) { this.persistenceUnitName = persistenceUnitName; this.dataSourceName = dataSourceName; this.multiTenancyStrategy = multiTenancyStrategy; - this.multiTenancySchemaDataSourceName = multiTenancySchemaDataSourceName; } @Override @@ -53,49 +49,28 @@ public ConnectionProvider resolve(String tenantId) { LOG.debugv("resolve((persistenceUnitName={0}, tenantIdentifier={1})", persistenceUnitName, tenantId); LOG.debugv("multitenancy strategy: {0}", multiTenancyStrategy); - AgroalDataSource dataSource = tenantDataSource(dataSourceName, tenantId, multiTenancyStrategy, - multiTenancySchemaDataSourceName); + AgroalDataSource dataSource = tenantDataSource(dataSourceName, tenantId, multiTenancyStrategy); if (dataSource == null) { throw new IllegalStateException( String.format(Locale.ROOT, "No instance of datasource found for persistence unit '%1$s' and tenant '%2$s'", persistenceUnitName, tenantId)); } - if (multiTenancyStrategy == MultiTenancyStrategy.SCHEMA) { - return new SchemaTenantConnectionProvider(tenantId, dataSource); - } - return new QuarkusConnectionProvider(dataSource); - } - - /** - * Create a new data source from the given configuration. - * - * @param config Configuration to use. - * - * @return New data source instance. - */ - private static AgroalDataSource createFrom(AgroalDataSourceConfiguration config) { - try { - return AgroalDataSource.from(config); - } catch (SQLException ex) { - throw new IllegalStateException("Failed to create a new data source based on the existing datasource configuration", - ex); - } + return switch (multiTenancyStrategy) { + case DATABASE -> new QuarkusConnectionProvider(dataSource); + case SCHEMA -> new SchemaTenantConnectionProvider(tenantId, dataSource); + default -> throw new IllegalStateException("Unexpected multitenancy strategy: " + multiTenancyStrategy); + }; } private static AgroalDataSource tenantDataSource(Optional dataSourceName, String tenantId, - MultiTenancyStrategy strategy, String multiTenancySchemaDataSourceName) { - if (strategy != MultiTenancyStrategy.SCHEMA) { - return Arc.container().instance(AgroalDataSource.class, new DataSource.DataSourceLiteral(tenantId)).get(); - } - - if (multiTenancySchemaDataSourceName == null) { - // The datasource name should always be present when using SCHEMA multi-tenancy; + MultiTenancyStrategy strategy) { + return switch (strategy) { + case DATABASE -> Arc.container().instance(AgroalDataSource.class, new DataSource.DataSourceLiteral(tenantId)).get(); + // The datasource name should always be present when using a multi-tenancy other than DATABASE; // we perform checks in HibernateOrmProcessor during the build. - AgroalDataSource dataSource = getDataSource(dataSourceName.get()); - return createFrom(dataSource.getConfiguration()); - } - - return getDataSource(multiTenancySchemaDataSourceName); + case SCHEMA -> getDataSource(dataSourceName.get()); + default -> throw new IllegalStateException("Unexpected multitenancy strategy: " + strategy); + }; } private static AgroalDataSource getDataSource(String dataSourceName) { diff --git a/extensions/hibernate-reactive/deployment/src/main/java/io/quarkus/hibernate/reactive/deployment/Dialects.java b/extensions/hibernate-reactive/deployment/src/main/java/io/quarkus/hibernate/reactive/deployment/Dialects.java deleted file mode 100644 index a699c1fabea6d..0000000000000 --- a/extensions/hibernate-reactive/deployment/src/main/java/io/quarkus/hibernate/reactive/deployment/Dialects.java +++ /dev/null @@ -1,34 +0,0 @@ -package io.quarkus.hibernate.reactive.deployment; - -import java.util.List; - -import io.quarkus.datasource.common.runtime.DatabaseKind; -import io.quarkus.hibernate.orm.deployment.spi.DatabaseKindDialectBuildItem; -import io.quarkus.hibernate.orm.runtime.HibernateOrmRuntimeConfig; -import io.quarkus.runtime.configuration.ConfigurationException; - -/** - * This used to be the approach before 6bf38240 in the Hibernate ORM extension as well. - * Align to ORM? TBD - */ -@Deprecated -final class Dialects { - - private Dialects() { - //utility - } - - public static String guessDialect(String persistenceUnitName, String resolvedDbKind, - List dbKindDialectBuildItems) { - for (DatabaseKindDialectBuildItem item : dbKindDialectBuildItems) { - if (DatabaseKind.is(resolvedDbKind, item.getDbKind())) { - return item.getDialect(); - } - } - - String error = "The Hibernate ORM extension could not guess the dialect from the database kind '" + resolvedDbKind - + "'. Add an explicit '" + HibernateOrmRuntimeConfig.puPropertyKey(persistenceUnitName, "dialect") - + "' property."; - throw new ConfigurationException(error); - } -} diff --git a/extensions/hibernate-reactive/deployment/src/main/java/io/quarkus/hibernate/reactive/deployment/HibernateReactiveProcessor.java b/extensions/hibernate-reactive/deployment/src/main/java/io/quarkus/hibernate/reactive/deployment/HibernateReactiveProcessor.java index 37303a0f579a1..cd55a007cab70 100644 --- a/extensions/hibernate-reactive/deployment/src/main/java/io/quarkus/hibernate/reactive/deployment/HibernateReactiveProcessor.java +++ b/extensions/hibernate-reactive/deployment/src/main/java/io/quarkus/hibernate/reactive/deployment/HibernateReactiveProcessor.java @@ -427,21 +427,24 @@ private static ParsedPersistenceXmlDescriptor generateReactivePersistenceUnit( return desc; } - private static void setDialectAndStorageEngine(Optional dbKindOptional, Optional explicitDialect, + private static void setDialectAndStorageEngine(Optional dbKind, Optional explicitDialect, Optional explicitDbMinVersion, List dbKindDialectBuildItems, Optional storageEngine, BuildProducer systemProperties, ParsedPersistenceXmlDescriptor desc) { final String persistenceUnitName = DEFAULT_PERSISTENCE_UNIT_NAME; Optional dialect = explicitDialect; + Optional dbProductName = Optional.empty(); Optional dbProductVersion = explicitDbMinVersion; - if (dbKindOptional.isPresent() || explicitDialect.isPresent()) { + if (dbKind.isPresent() || explicitDialect.isPresent()) { for (DatabaseKindDialectBuildItem item : dbKindDialectBuildItems) { - if (dbKindOptional.isPresent() && DatabaseKind.is(dbKindOptional.get(), item.getDbKind()) + if (dbKind.isPresent() && DatabaseKind.is(dbKind.get(), item.getDbKind()) // Set the default version based on the dialect when we don't have a datasource // (i.e. for database multi-tenancy) - || explicitDialect.isPresent() && explicitDialect.get().equals(item.getDialect())) { - if (explicitDialect.isEmpty()) { - dialect = Optional.of(item.getDialect()); + || explicitDialect.isPresent() && item.getMatchingDialects().contains(explicitDialect.get())) { + dbProductName = item.getDatabaseProductName(); + if (dbProductName.isEmpty() && explicitDialect.isEmpty()) { + // Use dialects only as a last resort, prefer product name or explicitly user-provided dialect + dialect = item.getDialectOptional(); } if (explicitDbMinVersion.isEmpty()) { dbProductVersion = item.getDefaultDatabaseProductVersion(); @@ -449,10 +452,10 @@ private static void setDialectAndStorageEngine(Optional dbKindOptional, break; } } - if (dialect.isEmpty()) { + if (dialect.isEmpty() && dbProductName.isEmpty()) { throw new ConfigurationException( "The Hibernate Reactive extension could not guess the dialect from the database kind '" - + dbKindOptional.get() + + dbKind.get() + "'. Add an explicit '" + HibernateOrmRuntimeConfig.puPropertyKey(persistenceUnitName, "dialect") + "' property."); @@ -461,6 +464,8 @@ private static void setDialectAndStorageEngine(Optional dbKindOptional, if (dialect.isPresent()) { desc.getProperties().setProperty(AvailableSettings.DIALECT, dialect.get()); + } else if (dbProductName.isPresent()) { + desc.getProperties().setProperty(AvailableSettings.JAKARTA_HBM2DDL_DB_NAME, dbProductName.get()); } else { // We only get here with the database multi-tenancy strategy; see the initial check, up top. throw new ConfigurationException(String.format(Locale.ROOT, @@ -472,15 +477,11 @@ private static void setDialectAndStorageEngine(Optional dbKindOptional, persistenceUnitName)); } - if (dbProductVersion.isPresent()) { - desc.getProperties().setProperty(JAKARTA_HBM2DDL_DB_VERSION, dbProductVersion.get()); - } - // The storage engine has to be set as a system property. if (storageEngine.isPresent()) { systemProperties.produce(new SystemPropertyBuildItem(STORAGE_ENGINE, storageEngine.get())); // Only actually set the storage engines if MySQL or MariaDB - if (isMySQLOrMariaDB(dialect.get())) { + if (isMySQLOrMariaDB(dbKind, dialect)) { systemProperties.produce(new SystemPropertyBuildItem(STORAGE_ENGINE, storageEngine.get())); } else { LOG.warnf("The storage engine set through configuration property '%1$s' is being ignored" @@ -489,11 +490,20 @@ private static void setDialectAndStorageEngine(Optional dbKindOptional, } } + if (dbProductVersion.isPresent()) { + desc.getProperties().setProperty(JAKARTA_HBM2DDL_DB_VERSION, dbProductVersion.get()); + } } - private static boolean isMySQLOrMariaDB(String dialect) { - String lowercaseDialect = dialect.toLowerCase(Locale.ROOT); - return lowercaseDialect.contains("mysql") || lowercaseDialect.contains("mariadb"); + private static boolean isMySQLOrMariaDB(Optional dbKind, Optional dialect) { + if (dbKind.isPresent() && (DatabaseKind.isMySQL(dbKind.get()) || DatabaseKind.isMariaDB(dbKind.get()))) { + return true; + } + if (dialect.isPresent()) { + String lowercaseDialect = dialect.get().toLowerCase(Locale.ROOT); + return lowercaseDialect.contains("mysql") || lowercaseDialect.contains("mariadb"); + } + return false; } private static void setMaxFetchDepth(ParsedPersistenceXmlDescriptor descriptor, OptionalInt maxFetchDepth) { diff --git a/extensions/infinispan-client/runtime/src/main/java/io/quarkus/infinispan/client/runtime/InfinispanDevServicesConfig.java b/extensions/infinispan-client/runtime/src/main/java/io/quarkus/infinispan/client/runtime/InfinispanDevServicesConfig.java index 83f70143409bd..e4368b0126886 100644 --- a/extensions/infinispan-client/runtime/src/main/java/io/quarkus/infinispan/client/runtime/InfinispanDevServicesConfig.java +++ b/extensions/infinispan-client/runtime/src/main/java/io/quarkus/infinispan/client/runtime/InfinispanDevServicesConfig.java @@ -6,6 +6,7 @@ import java.util.Optional; import java.util.OptionalInt; +import io.quarkus.runtime.annotations.ConfigDocMapKey; import io.quarkus.runtime.annotations.ConfigGroup; import io.quarkus.runtime.annotations.ConfigItem; @@ -124,6 +125,7 @@ public class InfinispanDevServicesConfig { * Environment variables that are passed to the container. */ @ConfigItem + @ConfigDocMapKey("environment-variable-name") public Map containerEnv; /** diff --git a/extensions/info/deployment/src/main/java/io/quarkus/info/deployment/InfoBuildTimeConfig.java b/extensions/info/deployment/src/main/java/io/quarkus/info/deployment/InfoBuildTimeConfig.java index 4990353262920..8dd651ba25ad4 100644 --- a/extensions/info/deployment/src/main/java/io/quarkus/info/deployment/InfoBuildTimeConfig.java +++ b/extensions/info/deployment/src/main/java/io/quarkus/info/deployment/InfoBuildTimeConfig.java @@ -2,6 +2,7 @@ import java.util.Map; +import io.quarkus.runtime.annotations.ConfigDocMapKey; import io.quarkus.runtime.annotations.ConfigPhase; import io.quarkus.runtime.annotations.ConfigRoot; import io.smallrye.config.ConfigMapping; @@ -76,6 +77,7 @@ interface Build { * Additional properties to be added to the build section */ @WithParentName + @ConfigDocMapKey("property-key") Map additionalProperties(); } diff --git a/extensions/info/deployment/src/main/resources/dev-ui/qwc-info.js b/extensions/info/deployment/src/main/resources/dev-ui/qwc-info.js index 5ba4c71f05d00..a89b011d4f67b 100644 --- a/extensions/info/deployment/src/main/resources/dev-ui/qwc-info.js +++ b/extensions/info/deployment/src/main/resources/dev-ui/qwc-info.js @@ -3,7 +3,7 @@ import {unsafeHTML} from 'lit/directives/unsafe-html.js'; import { columnBodyRenderer } from '@vaadin/grid/lit.js'; import { infoUrl } from 'build-time-data'; import '@vaadin/progress-bar'; -import 'qui-card'; +import '@qomponent/qui-card'; import '@vaadin/icon'; /** @@ -83,7 +83,7 @@ export class QwcInfo extends LitElement { _renderOsInfo(info){ if(info.os){ let os = info.os; - return html` + return html`

${this._renderOsIcon(os.name)} @@ -99,7 +99,7 @@ export class QwcInfo extends LitElement { _renderJavaInfo(info){ if(info.java){ let java = info.java; - return html` + return html`
@@ -126,7 +126,7 @@ export class QwcInfo extends LitElement { _renderGitInfo(info){ if(info.git){ let git = info.git; - return html` + return html`
@@ -162,7 +162,7 @@ export class QwcInfo extends LitElement { _renderBuildInfo(info){ if(info.build){ let build = info.build; - return html` + return html`
@@ -189,7 +189,7 @@ export class QwcInfo extends LitElement { for (const property of Object.keys(extInfo)){ rows.push(html``); } - cards.push(html` + cards.push(html`
Group${build.group}
${property}${extInfo[property]}
@@ -202,4 +202,4 @@ export class QwcInfo extends LitElement { } } } -customElements.define('qwc-info', QwcInfo); \ No newline at end of file +customElements.define('qwc-info', QwcInfo); diff --git a/extensions/jackson/deployment/src/test/java/io/quarkus/jackson/deployment/JacksonDefaultPoolTest.java b/extensions/jackson/deployment/src/test/java/io/quarkus/jackson/deployment/JacksonDefaultPoolTest.java deleted file mode 100644 index 715f9d62ff3ba..0000000000000 --- a/extensions/jackson/deployment/src/test/java/io/quarkus/jackson/deployment/JacksonDefaultPoolTest.java +++ /dev/null @@ -1,14 +0,0 @@ -package io.quarkus.jackson.deployment; - -import org.assertj.core.api.Assertions; -import org.junit.jupiter.api.Test; - -import com.fasterxml.jackson.core.util.JsonRecyclerPools; - -public class JacksonDefaultPoolTest { - - @Test - public void validateDefaultJacksonPool() { - Assertions.assertThat(JsonRecyclerPools.defaultPool()).isInstanceOf(JsonRecyclerPools.LockFreePool.class); - } -} diff --git a/extensions/jackson/runtime/src/main/java/io/quarkus/jackson/runtime/VertxHybridPoolObjectMapperCustomizer.java b/extensions/jackson/runtime/src/main/java/io/quarkus/jackson/runtime/VertxHybridPoolObjectMapperCustomizer.java index 3254837f6e54d..eb3bf36158d06 100644 --- a/extensions/jackson/runtime/src/main/java/io/quarkus/jackson/runtime/VertxHybridPoolObjectMapperCustomizer.java +++ b/extensions/jackson/runtime/src/main/java/io/quarkus/jackson/runtime/VertxHybridPoolObjectMapperCustomizer.java @@ -11,8 +11,9 @@ public class VertxHybridPoolObjectMapperCustomizer implements ObjectMapperCustom @Override public void customize(ObjectMapper objectMapper) { var existingMapperPool = objectMapper.getFactory()._getRecyclerPool(); - // JsonRecyclerPools.defaultPool() by default should create a LockFreePool - if (existingMapperPool instanceof JsonRecyclerPools.LockFreePool) { + // if the recycler pool in use is the default jackson one it means that user hasn't + // explicitly chosen any, so we can replace it with the vert.x virtual thread friendly one + if (existingMapperPool.getClass() == JsonRecyclerPools.defaultPool().getClass()) { objectMapper.getFactory().setRecyclerPool(HybridJacksonPool.getInstance()); } } diff --git a/extensions/jdbc/jdbc-h2/runtime/pom.xml b/extensions/jdbc/jdbc-h2/runtime/pom.xml index af1b7833ba93b..ef1752782a3aa 100644 --- a/extensions/jdbc/jdbc-h2/runtime/pom.xml +++ b/extensions/jdbc/jdbc-h2/runtime/pom.xml @@ -52,6 +52,9 @@ com.h2database:h2 + + io.quarkus.jdbc.h2 + diff --git a/extensions/jdbc/jdbc-oracle/deployment/src/main/java/io/quarkus/jdbc/oracle/deployment/OracleNativeImage.java b/extensions/jdbc/jdbc-oracle/deployment/src/main/java/io/quarkus/jdbc/oracle/deployment/OracleNativeImage.java index 9bfd954d11e47..8167b900a952e 100644 --- a/extensions/jdbc/jdbc-oracle/deployment/src/main/java/io/quarkus/jdbc/oracle/deployment/OracleNativeImage.java +++ b/extensions/jdbc/jdbc-oracle/deployment/src/main/java/io/quarkus/jdbc/oracle/deployment/OracleNativeImage.java @@ -2,6 +2,7 @@ import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.builditem.AdditionalIndexedClassesBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; /** @@ -14,7 +15,8 @@ public final class OracleNativeImage { * by reflection, as commonly expected. */ @BuildStep - void reflection(BuildProducer reflectiveClass) { + void reflection(BuildProducer reflectiveClass, + BuildProducer additionalIndexedClasses) { //Not strictly necessary when using Agroal, as it also registers //any JDBC driver being configured explicitly through its configuration. //We register it for the sake of people not using Agroal. @@ -23,6 +25,10 @@ void reflection(BuildProducer reflectiveClass) { final String driverName = "oracle.jdbc.driver.OracleDriver"; reflectiveClass.produce(ReflectiveClassBuildItem.builder(driverName).build()); + // This is needed when using XA and we use the `@RegisterForReflection` trick to make sure all nested classes are registered for reflection + additionalIndexedClasses + .produce(new AdditionalIndexedClassesBuildItem("io.quarkus.jdbc.oracle.runtime.graal.OracleReflections")); + // for ldap style jdbc urls. e.g. jdbc:oracle:thin:@ldap://oid:5000/mydb1,cn=OracleContext,dc=myco,dc=com // // Note that all JDK provided InitialContextFactory impls from the JDK registered via module descriptors diff --git a/extensions/jdbc/jdbc-oracle/runtime/src/main/java/io/quarkus/jdbc/oracle/runtime/graal/OracleReflections.java b/extensions/jdbc/jdbc-oracle/runtime/src/main/java/io/quarkus/jdbc/oracle/runtime/graal/OracleReflections.java new file mode 100644 index 0000000000000..f489dc605beea --- /dev/null +++ b/extensions/jdbc/jdbc-oracle/runtime/src/main/java/io/quarkus/jdbc/oracle/runtime/graal/OracleReflections.java @@ -0,0 +1,13 @@ +package io.quarkus.jdbc.oracle.runtime.graal; + +import io.quarkus.runtime.annotations.RegisterForReflection; + +/** + * We don't use a build item here as we also need to register all the nested classes and there's no way to do it easily with the + * build item for now. + */ +@RegisterForReflection(targets = { oracle.jdbc.xa.OracleXADataSource.class, + oracle.jdbc.datasource.impl.OracleDataSource.class }) +public class OracleReflections { + +} diff --git a/extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/KafkaDevServicesBuildTimeConfig.java b/extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/KafkaDevServicesBuildTimeConfig.java index 756662efdd317..d3bbc818fc103 100644 --- a/extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/KafkaDevServicesBuildTimeConfig.java +++ b/extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/KafkaDevServicesBuildTimeConfig.java @@ -4,6 +4,7 @@ import java.util.Map; import java.util.Optional; +import io.quarkus.runtime.annotations.ConfigDocMapKey; import io.quarkus.runtime.annotations.ConfigGroup; import io.quarkus.runtime.annotations.ConfigItem; @@ -47,8 +48,8 @@ public class KafkaDevServicesBuildTimeConfig { public Provider provider = Provider.REDPANDA; public enum Provider { - REDPANDA("docker.io/vectorized/redpanda:v22.3.4"), - STRIMZI("quay.io/strimzi-test-container/test-container:latest-kafka-3.2.1"), + REDPANDA("docker.io/vectorized/redpanda:v24.1.2"), + STRIMZI("quay.io/strimzi-test-container/test-container:latest-kafka-3.7.0"), KAFKA_NATIVE("quay.io/ogunalp/kafka-native:latest"); private final String defaultImageName; @@ -106,6 +107,7 @@ public String getDefaultImageName() { * The topic creation will not try to re-partition existing topics with different number of partitions. */ @ConfigItem + @ConfigDocMapKey("topic-name") public Map topicPartitions; /** @@ -120,6 +122,7 @@ public String getDefaultImageName() { * Environment variables that are passed to the container. */ @ConfigItem + @ConfigDocMapKey("environment-variable-name") public Map containerEnv; /** diff --git a/extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/KafkaProcessor.java b/extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/KafkaProcessor.java index 393c85fdf3cd2..41f3415a761c6 100644 --- a/extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/KafkaProcessor.java +++ b/extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/KafkaProcessor.java @@ -76,12 +76,12 @@ import io.quarkus.deployment.builditem.LogCategoryBuildItem; import io.quarkus.deployment.builditem.RunTimeConfigurationDefaultBuildItem; import io.quarkus.deployment.builditem.RuntimeConfigSetupCompleteBuildItem; +import io.quarkus.deployment.builditem.nativeimage.NativeImageConfigBuildItem; import io.quarkus.deployment.builditem.nativeimage.NativeImageProxyDefinitionBuildItem; import io.quarkus.deployment.builditem.nativeimage.NativeImageResourceBuildItem; import io.quarkus.deployment.builditem.nativeimage.NativeImageSecurityProviderBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassConditionBuildItem; -import io.quarkus.deployment.builditem.nativeimage.RuntimeInitializedClassBuildItem; import io.quarkus.deployment.builditem.nativeimage.ServiceProviderBuildItem; import io.quarkus.deployment.logging.LogCleanupFilterBuildItem; import io.quarkus.deployment.pkg.builditem.CurateOutcomeBuildItem; @@ -482,15 +482,16 @@ UnremovableBeanBuildItem ensureJsonParserAvailable() { } @BuildStep - public void registerRuntimeInitializedClasses(BuildProducer producer) { - // Classes using java.util.Random, which need to be runtime initialized - producer.produce( - new RuntimeInitializedClassBuildItem("org.apache.kafka.common.security.authenticator.SaslClientAuthenticator")); - producer.produce(new RuntimeInitializedClassBuildItem( - "org.apache.kafka.common.security.oauthbearer.internals.expiring.ExpiringCredentialRefreshingLogin")); - // VerificationKeyResolver is value on static map in OAuthBearerValidatorCallbackHandler - producer.produce(new RuntimeInitializedClassBuildItem( - "org.apache.kafka.common.security.oauthbearer.OAuthBearerValidatorCallbackHandler")); + NativeImageConfigBuildItem nativeImageConfiguration() { + NativeImageConfigBuildItem.Builder builder = NativeImageConfigBuildItem.builder() + // Classes using java.util.Random, which need to be runtime initialized + .addRuntimeInitializedClass("org.apache.kafka.common.security.authenticator.SaslClientAuthenticator") + .addRuntimeInitializedClass( + "org.apache.kafka.common.security.oauthbearer.internals.expiring.ExpiringCredentialRefreshingLogin") + // VerificationKeyResolver is value on static map in OAuthBearerValidatorCallbackHandler + .addRuntimeInitializedClass("org.apache.kafka.common.security.oauthbearer.OAuthBearerValidatorCallbackHandler") + .addRuntimeReinitializedClass("org.apache.kafka.shaded.com.google.protobuf.UnsafeUtil"); + return builder.build(); } @BuildStep diff --git a/extensions/kafka-client/runtime/src/main/java/io/quarkus/kafka/client/runtime/KafkaRuntimeConfigProducer.java b/extensions/kafka-client/runtime/src/main/java/io/quarkus/kafka/client/runtime/KafkaRuntimeConfigProducer.java index 504bc66168f4e..ba0f407ea5dec 100644 --- a/extensions/kafka-client/runtime/src/main/java/io/quarkus/kafka/client/runtime/KafkaRuntimeConfigProducer.java +++ b/extensions/kafka-client/runtime/src/main/java/io/quarkus/kafka/client/runtime/KafkaRuntimeConfigProducer.java @@ -35,6 +35,9 @@ public Map createKafkaRuntimeConfig(Config config, ApplicationCo if (!propertyNameLowerCase.startsWith(CONFIG_PREFIX) || propertyNameLowerCase.startsWith(UI_CONFIG_PREFIX)) { continue; } + if (propertyNameLowerCase.length() <= CONFIG_PREFIX.length()) { + continue; + } // Replace _ by . - This is because Kafka properties tend to use . and env variables use _ for every special // character. So, replace _ with . String effectivePropertyName = propertyNameLowerCase.substring(CONFIG_PREFIX.length() + 1).toLowerCase() diff --git a/extensions/kafka-client/runtime/src/main/java/io/smallrye/reactive/kafka/graal/KafkaSubstitutions.java b/extensions/kafka-client/runtime/src/main/java/io/quarkus/kafka/client/runtime/graal/KafkaSubstitutions.java similarity index 51% rename from extensions/kafka-client/runtime/src/main/java/io/smallrye/reactive/kafka/graal/KafkaSubstitutions.java rename to extensions/kafka-client/runtime/src/main/java/io/quarkus/kafka/client/runtime/graal/KafkaSubstitutions.java index 852c6ca247a6e..c93f9127eb827 100644 --- a/extensions/kafka-client/runtime/src/main/java/io/smallrye/reactive/kafka/graal/KafkaSubstitutions.java +++ b/extensions/kafka-client/runtime/src/main/java/io/quarkus/kafka/client/runtime/graal/KafkaSubstitutions.java @@ -1,10 +1,13 @@ -package io.smallrye.reactive.kafka.graal; +package io.quarkus.kafka.client.runtime.graal; +import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import com.oracle.svm.core.annotate.Substitute; import com.oracle.svm.core.annotate.TargetClass; +import sun.misc.Unsafe; + @TargetClass(className = "org.apache.kafka.common.network.SaslChannelBuilder") final class Target_org_apache_kafka_common_network_SaslChannelBuilder { @@ -17,6 +20,20 @@ private static String defaultKerberosRealm() throws ClassNotFoundException, NoSu } +@TargetClass(className = "org.apache.kafka.shaded.com.google.protobuf.UnsafeUtil") +final class Target_org_apache_kafka_shaded_com_google_protobuf_UnsafeUtil { + @Substitute + static sun.misc.Unsafe getUnsafe() { + try { + Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe"); + theUnsafe.setAccessible(true); + return (Unsafe) theUnsafe.get(null); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} + class KafkaSubstitutions { } diff --git a/extensions/kafka-client/runtime/src/main/java/io/smallrye/reactive/kafka/graal/StrimziSubstitutions.java b/extensions/kafka-client/runtime/src/main/java/io/quarkus/kafka/client/runtime/graal/StrimziSubstitutions.java similarity index 98% rename from extensions/kafka-client/runtime/src/main/java/io/smallrye/reactive/kafka/graal/StrimziSubstitutions.java rename to extensions/kafka-client/runtime/src/main/java/io/quarkus/kafka/client/runtime/graal/StrimziSubstitutions.java index 2219060f6b7a8..d16d81ed9bf54 100644 --- a/extensions/kafka-client/runtime/src/main/java/io/smallrye/reactive/kafka/graal/StrimziSubstitutions.java +++ b/extensions/kafka-client/runtime/src/main/java/io/quarkus/kafka/client/runtime/graal/StrimziSubstitutions.java @@ -1,4 +1,4 @@ -package io.smallrye.reactive.kafka.graal; +package io.quarkus.kafka.client.runtime.graal; import java.util.function.BooleanSupplier; diff --git a/extensions/kubernetes-client/runtime-internal/src/main/java/io/quarkus/kubernetes/client/runtime/KubernetesDevServicesBuildTimeConfig.java b/extensions/kubernetes-client/runtime-internal/src/main/java/io/quarkus/kubernetes/client/runtime/KubernetesDevServicesBuildTimeConfig.java index d32eebd2ab80a..1c5dd077006dc 100644 --- a/extensions/kubernetes-client/runtime-internal/src/main/java/io/quarkus/kubernetes/client/runtime/KubernetesDevServicesBuildTimeConfig.java +++ b/extensions/kubernetes-client/runtime-internal/src/main/java/io/quarkus/kubernetes/client/runtime/KubernetesDevServicesBuildTimeConfig.java @@ -3,6 +3,7 @@ import java.util.Map; import java.util.Optional; +import io.quarkus.runtime.annotations.ConfigDocMapKey; import io.smallrye.config.WithDefault; public interface KubernetesDevServicesBuildTimeConfig { @@ -67,6 +68,7 @@ public interface KubernetesDevServicesBuildTimeConfig { /** * Environment variables that are passed to the container. */ + @ConfigDocMapKey("environment-variable-name") Map containerEnv(); enum Flavor { diff --git a/extensions/kubernetes-client/runtime-internal/src/main/java/io/quarkus/kubernetes/client/runtime/QuarkusHttpClientFactory.java b/extensions/kubernetes-client/runtime-internal/src/main/java/io/quarkus/kubernetes/client/runtime/QuarkusHttpClientFactory.java index a54a030e5b170..ab8d71db34a07 100644 --- a/extensions/kubernetes-client/runtime-internal/src/main/java/io/quarkus/kubernetes/client/runtime/QuarkusHttpClientFactory.java +++ b/extensions/kubernetes-client/runtime-internal/src/main/java/io/quarkus/kubernetes/client/runtime/QuarkusHttpClientFactory.java @@ -9,6 +9,7 @@ import io.fabric8.kubernetes.client.Config; import io.fabric8.kubernetes.client.http.HttpClient; import io.fabric8.kubernetes.client.vertx.VertxHttpClientBuilder; +import io.quarkus.runtime.ResettableSystemProperties; import io.vertx.core.Vertx; import io.vertx.core.VertxOptions; import io.vertx.core.file.FileSystemOptions; @@ -35,21 +36,12 @@ private Vertx createVertxInstance() { // This is done using the DISABLE_DNS_RESOLVER_PROP_NAME system property. // The DNS resolver used by vert.x is configured during the (synchronous) initialization. // So, we just need to disable the async resolver around the Vert.x instance creation. - String originalValue = System.getProperty(DISABLE_DNS_RESOLVER_PROP_NAME); - Vertx vertx; - try { - System.setProperty(DISABLE_DNS_RESOLVER_PROP_NAME, "true"); - vertx = Vertx.vertx(new VertxOptions().setFileSystemOptions( + try (var resettableSystemProperties = ResettableSystemProperties.of( + DISABLE_DNS_RESOLVER_PROP_NAME, "true")) { + return Vertx.vertx(new VertxOptions().setFileSystemOptions( new FileSystemOptions().setFileCachingEnabled(false).setClassPathResolvingEnabled(false))); - } finally { - // Restore the original value - if (originalValue == null) { - System.clearProperty(DISABLE_DNS_RESOLVER_PROP_NAME); - } else { - System.setProperty(DISABLE_DNS_RESOLVER_PROP_NAME, originalValue); - } + } - return vertx; } @Override diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/ClusterRoleBindingConfig.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/ClusterRoleBindingConfig.java index de0eb2ad9f0f9..746d4ac101f6e 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/ClusterRoleBindingConfig.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/ClusterRoleBindingConfig.java @@ -3,6 +3,7 @@ import java.util.Map; import java.util.Optional; +import io.quarkus.runtime.annotations.ConfigDocMapKey; import io.quarkus.runtime.annotations.ConfigGroup; import io.quarkus.runtime.annotations.ConfigItem; @@ -20,6 +21,7 @@ public class ClusterRoleBindingConfig { * Labels to add into the RoleBinding resource. */ @ConfigItem + @ConfigDocMapKey("label-name") public Map labels; /** diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/ClusterRoleConfig.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/ClusterRoleConfig.java index 7ac12a2e19f92..91d1fa838babf 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/ClusterRoleConfig.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/ClusterRoleConfig.java @@ -3,6 +3,7 @@ import java.util.Map; import java.util.Optional; +import io.quarkus.runtime.annotations.ConfigDocMapKey; import io.quarkus.runtime.annotations.ConfigGroup; import io.quarkus.runtime.annotations.ConfigItem; @@ -19,6 +20,7 @@ public class ClusterRoleConfig { * Labels to add into the ClusterRole resource. */ @ConfigItem + @ConfigDocMapKey("label-name") Map labels; /** diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/Constants.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/Constants.java index a8590aaa6e50c..a445f1fdc79e8 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/Constants.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/Constants.java @@ -33,6 +33,8 @@ public final class Constants { public static final String ROUTE = "Route"; public static final String ROUTE_API_GROUP = "route.openshift.io/v1"; + static final String VERSION_LABEL = "app.kubernetes.io/version"; + static final String OPENSHIFT_APP_RUNTIME = "app.openshift.io/runtime"; static final String S2I = "s2i"; static final String DEFAULT_S2I_IMAGE_NAME = "s2i-java"; //refers to the Dekorate default image. diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/EnvVarsConfig.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/EnvVarsConfig.java index cdcf6e947a93a..95ff8ea865e4e 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/EnvVarsConfig.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/EnvVarsConfig.java @@ -4,6 +4,7 @@ import java.util.Map; import java.util.Optional; +import io.quarkus.runtime.annotations.ConfigDocMapKey; import io.quarkus.runtime.annotations.ConfigGroup; import io.quarkus.runtime.annotations.ConfigItem; @@ -28,6 +29,7 @@ public class EnvVarsConfig { * The map associating environment variable names to their associated field references they take their value from. */ @ConfigItem + @ConfigDocMapKey("environment-variable-name") Map fields; /** diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/IngressConfig.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/IngressConfig.java index 8cf029c7afc97..d1ea33b6c830a 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/IngressConfig.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/IngressConfig.java @@ -3,6 +3,7 @@ import java.util.Map; import java.util.Optional; +import io.quarkus.runtime.annotations.ConfigDocMapKey; import io.quarkus.runtime.annotations.ConfigGroup; import io.quarkus.runtime.annotations.ConfigItem; @@ -38,6 +39,7 @@ public class IngressConfig { * Custom annotations to add to exposition (route or ingress) resources */ @ConfigItem + @ConfigDocMapKey("annotation-name") Map annotations; /** diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KnativeConfig.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KnativeConfig.java index 4474a28da851e..5fc111bfe3b90 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KnativeConfig.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KnativeConfig.java @@ -8,6 +8,7 @@ import io.dekorate.kubernetes.annotation.ImagePullPolicy; import io.dekorate.kubernetes.annotation.ServiceType; import io.quarkus.kubernetes.spi.DeployStrategy; +import io.quarkus.runtime.annotations.ConfigDocMapKey; import io.quarkus.runtime.annotations.ConfigItem; import io.quarkus.runtime.annotations.ConfigRoot; @@ -49,12 +50,14 @@ public class KnativeConfig implements PlatformConfiguration { * Custom labels to add to all resources */ @ConfigItem + @ConfigDocMapKey("label-name") Map labels; /** * Custom annotations to add to all resources */ @ConfigItem + @ConfigDocMapKey("annotation-name") Map annotations; /** 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 f2075c21cd51e..f83be2afcc74f 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 @@ -1194,9 +1194,19 @@ private static List toPolicyRulesList(Map } private static String parseVCSUri(VCSUriConfig config, ScmInfo scm) { - if (config.enabled) { - return config.override.orElseGet(() -> scm != null ? Git.sanitizeRemoteUrl(scm.getRemote().get("origin")) : null); + if (!config.enabled) { + return null; } - return null; + if (config.override.isPresent()) { + return config.override.get(); + } + if (scm == null) { + return null; + } + String originRemote = scm.getRemote().get("origin"); + if (originRemote == null || originRemote.isBlank()) { + return null; + } + return Git.sanitizeRemoteUrl(originRemote); } } diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KubernetesConfig.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KubernetesConfig.java index f5aa8bf024442..33feeca5700e6 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KubernetesConfig.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KubernetesConfig.java @@ -14,6 +14,7 @@ import io.quarkus.deployment.Capabilities; import io.quarkus.deployment.Capability; import io.quarkus.kubernetes.spi.DeployStrategy; +import io.quarkus.runtime.annotations.ConfigDocMapKey; import io.quarkus.runtime.annotations.ConfigItem; import io.quarkus.runtime.annotations.ConfigRoot; @@ -61,12 +62,14 @@ public class KubernetesConfig implements PlatformConfiguration { * Custom labels to add to all resources */ @ConfigItem + @ConfigDocMapKey("label-name") Map labels; /** * Custom annotations to add to all resources */ @ConfigItem + @ConfigDocMapKey("annotation-name") Map annotations; /** diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KubernetesDeployer.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KubernetesDeployer.java index 6c348b20180f1..4c1230552f255 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KubernetesDeployer.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KubernetesDeployer.java @@ -5,6 +5,7 @@ import static io.quarkus.kubernetes.deployment.Constants.KNATIVE; import static io.quarkus.kubernetes.deployment.Constants.KUBERNETES; import static io.quarkus.kubernetes.deployment.Constants.MINIKUBE; +import static io.quarkus.kubernetes.deployment.Constants.VERSION_LABEL; import java.io.File; import java.io.FileInputStream; @@ -17,17 +18,22 @@ import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.Predicate; import java.util.stream.Collectors; import org.jboss.logging.Logger; import io.dekorate.utils.Serialization; +import io.fabric8.kubernetes.api.builder.Visitor; import io.fabric8.kubernetes.api.model.APIResourceList; import io.fabric8.kubernetes.api.model.GenericKubernetesResource; import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.api.model.KubernetesList; +import io.fabric8.kubernetes.api.model.KubernetesListBuilder; +import io.fabric8.kubernetes.api.model.LabelSelectorFluent; import io.fabric8.kubernetes.api.model.Service; +import io.fabric8.kubernetes.api.model.apps.Deployment; import io.fabric8.kubernetes.api.model.batch.v1.Job; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.KubernetesClientException; @@ -212,6 +218,11 @@ private DeploymentResultBuildItem deploy(DeploymentTargetEntry deploymentTarget, throw new IllegalStateException(messsage); } + list.getItems().stream().filter(distinctByResourceKey()).forEach(i -> { + Optional existing = Optional.ofNullable(client.resource(i).get()); + checkLabelSelectorVersions(deploymentTarget, i, existing); + }); + list.getItems().stream().filter(distinctByResourceKey()).forEach(i -> { deployResource(deploymentTarget, client, i, optionalResourceDefinitions); log.info("Applied: " + i.getKind() + " " + i.getMetadata().getName() + "."); @@ -239,6 +250,7 @@ private DeploymentResultBuildItem deploy(DeploymentTargetEntry deploymentTarget, private void deployResource(DeploymentTargetEntry deploymentTarget, KubernetesClient client, HasMetadata metadata, List optionalResourceDefinitions) { var r = findResource(client, metadata); + Optional existing = Optional.ofNullable(client.resource(metadata).get()); if (shouldDeleteExisting(deploymentTarget, metadata)) { deleteResource(metadata, r); } @@ -385,7 +397,6 @@ private static boolean shouldDeleteExisting(DeploymentTargetEntry deploymentTarg if (deploymentTarget.getDeployStrategy() != DeployStrategy.CreateOrUpdate) { return false; } - return KNATIVE.equalsIgnoreCase(deploymentTarget.getName()) || resource instanceof Service || (Objects.equals("v1", resource.getApiVersion()) && Objects.equals("Service", resource.getKind())) @@ -398,4 +409,44 @@ private static Predicate distinctByResourceKey() { return t -> seen.putIfAbsent(t.getApiVersion() + "/" + t.getKind() + ":" + t.getMetadata().getName(), Boolean.TRUE) == null; } + + private static void checkLabelSelectorVersions(DeploymentTargetEntry deploymnetTarget, HasMetadata resource, + Optional existing) { + if (!existing.isPresent()) { + return; + } + + if (resource instanceof Deployment) { + Optional version = getLabelSelectorVersion(resource); + Optional existingVersion = getLabelSelectorVersion(existing.get()); + if (version.isPresent() && existingVersion.isPresent()) { + if (!version.get().equals(existingVersion.get())) { + throw new IllegalStateException(String.format( + "A previous Deployment with a conflicting label %s=%s was found in the label selector (current is %s=%s). As the label selector is immutable, you need to either align versions or manually delete previous deployment.", + VERSION_LABEL, existingVersion.get(), VERSION_LABEL, version.get())); + } + } else if (version.isPresent()) { + throw new IllegalStateException(String.format( + "A Deployment with a conflicting label %s=%s was in the label selector was requested (previous had no such label). As the label selector is immutable, you need to either manually delete previous deployment, or remove the label (consider using quarkus.%s.add-version-to-label-selectors=false).", + VERSION_LABEL, version.get(), deploymnetTarget.getName().toLowerCase())); + } else if (existingVersion.isPresent()) { + throw new IllegalStateException(String.format( + "A Deployment with no label in the label selector was requested (previous includes %s=%s). As the label selector is immutable, you need to either manually delete previous deployment, or ensure the %s label is present (consider using quarkus.%s.add-version-to-label-selectors=true).", + VERSION_LABEL, existingVersion.get(), VERSION_LABEL, deploymnetTarget.getName().toLowerCase())); + } + } + } + + private static Optional getLabelSelectorVersion(HasMetadata resource) { + AtomicReference version = new AtomicReference<>(); + KubernetesList list = new KubernetesListBuilder().addToItems(resource).accept(new Visitor>() { + @Override + public void visit(LabelSelectorFluent item) { + if (item.getMatchLabels() != null) { + version.set(item.getMatchLabels().get(VERSION_LABEL)); + } + } + }).build(); + return Optional.ofNullable(version.get()); + } } diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/OpenshiftConfig.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/OpenshiftConfig.java index 89c6fb56ddc1f..1ac35b16e3b0d 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/OpenshiftConfig.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/OpenshiftConfig.java @@ -17,6 +17,7 @@ import io.quarkus.deployment.Capabilities; import io.quarkus.deployment.Capability; import io.quarkus.kubernetes.spi.DeployStrategy; +import io.quarkus.runtime.annotations.ConfigDocMapKey; import io.quarkus.runtime.annotations.ConfigItem; import io.quarkus.runtime.annotations.ConfigRoot; @@ -80,12 +81,14 @@ public static enum OpenshiftFlavor { * Custom labels to add to all resources */ @ConfigItem + @ConfigDocMapKey("label-name") Map labels; /** * Custom annotations to add to all resources */ @ConfigItem + @ConfigDocMapKey("annotation-name") Map annotations; /** diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/RoleBindingConfig.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/RoleBindingConfig.java index e390ea2d649e9..5acd683c01477 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/RoleBindingConfig.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/RoleBindingConfig.java @@ -3,6 +3,7 @@ import java.util.Map; import java.util.Optional; +import io.quarkus.runtime.annotations.ConfigDocMapKey; import io.quarkus.runtime.annotations.ConfigGroup; import io.quarkus.runtime.annotations.ConfigItem; @@ -20,6 +21,7 @@ public class RoleBindingConfig { * Labels to add into the RoleBinding resource. */ @ConfigItem + @ConfigDocMapKey("label-name") public Map labels; /** diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/RoleConfig.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/RoleConfig.java index 5edb212b6a816..7717555cfcea9 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/RoleConfig.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/RoleConfig.java @@ -3,6 +3,7 @@ import java.util.Map; import java.util.Optional; +import io.quarkus.runtime.annotations.ConfigDocMapKey; import io.quarkus.runtime.annotations.ConfigGroup; import io.quarkus.runtime.annotations.ConfigItem; @@ -25,6 +26,7 @@ public class RoleConfig { * Labels to add into the Role resource. */ @ConfigItem + @ConfigDocMapKey("label-name") Map labels; /** diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/RouteConfig.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/RouteConfig.java index 4e175e29724ca..dd8b2f4de974f 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/RouteConfig.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/RouteConfig.java @@ -3,6 +3,7 @@ import java.util.Map; import java.util.Optional; +import io.quarkus.runtime.annotations.ConfigDocMapKey; import io.quarkus.runtime.annotations.ConfigGroup; import io.quarkus.runtime.annotations.ConfigItem; @@ -32,6 +33,7 @@ public class RouteConfig { * Custom annotations to add to exposition (route or ingress) resources */ @ConfigItem + @ConfigDocMapKey("annotation-name") Map annotations; /** diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/SecurityContextConfig.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/SecurityContextConfig.java index a91e0a8014c2f..038f8b84d7a4e 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/SecurityContextConfig.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/SecurityContextConfig.java @@ -4,6 +4,7 @@ import java.util.Map; import java.util.Optional; +import io.quarkus.runtime.annotations.ConfigDocMapKey; import io.quarkus.runtime.annotations.ConfigGroup; import io.quarkus.runtime.annotations.ConfigItem; @@ -55,6 +56,7 @@ public class SecurityContextConfig { * Sysctls hold a list of namespaced sysctls used for the pod. */ @ConfigItem + @ConfigDocMapKey("sysctl-name") Optional> sysctls; /** diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/ServiceAccountConfig.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/ServiceAccountConfig.java index af96a4d6e3680..11a8c1828185f 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/ServiceAccountConfig.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/ServiceAccountConfig.java @@ -3,6 +3,7 @@ import java.util.Map; import java.util.Optional; +import io.quarkus.runtime.annotations.ConfigDocMapKey; import io.quarkus.runtime.annotations.ConfigGroup; import io.quarkus.runtime.annotations.ConfigItem; @@ -25,6 +26,7 @@ public class ServiceAccountConfig { * Labels of the service account. */ @ConfigItem + @ConfigDocMapKey("label-name") Map labels; /** diff --git a/extensions/liquibase/deployment/src/main/java/io/quarkus/liquibase/deployment/LiquibaseProcessor.java b/extensions/liquibase/deployment/src/main/java/io/quarkus/liquibase/deployment/LiquibaseProcessor.java index 818e6529ed24e..66e45af582ff9 100644 --- a/extensions/liquibase/deployment/src/main/java/io/quarkus/liquibase/deployment/LiquibaseProcessor.java +++ b/extensions/liquibase/deployment/src/main/java/io/quarkus/liquibase/deployment/LiquibaseProcessor.java @@ -16,6 +16,7 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.function.BiConsumer; +import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -36,6 +37,7 @@ import io.quarkus.arc.deployment.SyntheticBeanBuildItem; import io.quarkus.arc.processor.DotNames; import io.quarkus.datasource.common.runtime.DataSourceUtil; +import io.quarkus.deployment.Capabilities; import io.quarkus.deployment.Feature; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; @@ -101,6 +103,7 @@ void nativeImageConfiguration( LiquibaseBuildTimeConfig liquibaseBuildConfig, List jdbcDataSourceBuildItems, CombinedIndexBuildItem combinedIndex, + Capabilities capabilities, BuildProducer reflective, BuildProducer resource, BuildProducer services, @@ -212,7 +215,7 @@ void nativeImageConfiguration( // CommandStep implementations are needed consumeService(liquibase.command.CommandStep.class, (serviceClass, implementations) -> { var filteredImpls = implementations.stream() - .filter(not("liquibase.command.core.StartH2CommandStep"::equals)) + .filter(commandStepPredicate(capabilities)) .toArray(String[]::new); services.produce(new ServiceProviderBuildItem(serviceClass.getName(), filteredImpls)); reflective.produce(ReflectiveClassBuildItem.builder(filteredImpls).constructors().build()); @@ -250,6 +253,14 @@ void nativeImageConfiguration( resourceBundle.produce(new NativeImageResourceBundleBuildItem("liquibase/i18n/liquibase-core")); } + private static Predicate commandStepPredicate(Capabilities capabilities) { + if (capabilities.isPresent("io.quarkus.jdbc.h2")) { + return (s) -> true; + } else { + return not("liquibase.command.core.StartH2CommandStep"::equals); + } + } + private void consumeService(Class serviceClass, BiConsumer, Collection> consumer) { try { String service = "META-INF/services/" + serviceClass.getName(); diff --git a/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/LiquibaseFactory.java b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/LiquibaseFactory.java index db1c460fa1b64..1616bdb3262af 100644 --- a/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/LiquibaseFactory.java +++ b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/LiquibaseFactory.java @@ -10,6 +10,7 @@ import io.agroal.api.AgroalDataSource; import io.quarkus.liquibase.runtime.LiquibaseConfig; +import io.quarkus.runtime.ResettableSystemProperties; import io.quarkus.runtime.util.StringUtil; import liquibase.Contexts; import liquibase.LabelExpression; @@ -150,4 +151,12 @@ public Contexts createContexts() { public String getDataSourceName() { return dataSourceName; } + + public ResettableSystemProperties createResettableSystemProperties() { + if (config.allowDuplicatedChangesetIdentifiers.isEmpty()) { + return ResettableSystemProperties.empty(); + } + return ResettableSystemProperties.of("liquibase.allowDuplicatedChangesetIdentifiers", + config.allowDuplicatedChangesetIdentifiers.get().toString()); + } } diff --git a/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/runtime/LiquibaseConfig.java b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/runtime/LiquibaseConfig.java index de1f2e47a1f36..55d036b556281 100644 --- a/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/runtime/LiquibaseConfig.java +++ b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/runtime/LiquibaseConfig.java @@ -8,6 +8,8 @@ import java.util.Map; import java.util.Optional; +import io.quarkus.runtime.annotations.ConfigDocMapKey; + /** * The liquibase configuration */ @@ -48,6 +50,7 @@ public class LiquibaseConfig { */ public List labels = null; + @ConfigDocMapKey("parameter-name") public Map changeLogParameters = null; /** @@ -97,4 +100,9 @@ public class LiquibaseConfig { */ public Optional password = Optional.empty(); + /** + * Allows duplicated changeset identifiers without failing Liquibase execution. + */ + public Optional allowDuplicatedChangesetIdentifiers; + } diff --git a/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/runtime/LiquibaseCreator.java b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/runtime/LiquibaseCreator.java index a6c06d69fb47a..5dfae13603e4f 100644 --- a/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/runtime/LiquibaseCreator.java +++ b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/runtime/LiquibaseCreator.java @@ -43,6 +43,7 @@ public LiquibaseFactory createLiquibaseFactory(DataSource dataSource, String dat config.migrateAtStart = liquibaseRuntimeConfig.migrateAtStart; config.cleanAtStart = liquibaseRuntimeConfig.cleanAtStart; config.validateOnMigrate = liquibaseRuntimeConfig.validateOnMigrate; + config.allowDuplicatedChangesetIdentifiers = liquibaseRuntimeConfig.allowDuplicatedChangesetIdentifiers; return new LiquibaseFactory(config, dataSource, dataSourceName); } } diff --git a/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/runtime/LiquibaseDataSourceRuntimeConfig.java b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/runtime/LiquibaseDataSourceRuntimeConfig.java index c8cb0d1cf3e56..6dcc0f87f97d0 100644 --- a/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/runtime/LiquibaseDataSourceRuntimeConfig.java +++ b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/runtime/LiquibaseDataSourceRuntimeConfig.java @@ -5,6 +5,7 @@ import java.util.Map; import java.util.Optional; +import io.quarkus.runtime.annotations.ConfigDocMapKey; import io.quarkus.runtime.annotations.ConfigGroup; import io.quarkus.runtime.annotations.ConfigItem; @@ -74,6 +75,7 @@ public static final LiquibaseDataSourceRuntimeConfig defaultConfig() { * Map of parameters that can be used inside Liquibase changeLog files. */ @ConfigItem + @ConfigDocMapKey("parameter-name") public Map changeLogParameters = new HashMap<>(); /** @@ -133,4 +135,10 @@ public static final LiquibaseDataSourceRuntimeConfig defaultConfig() { @ConfigItem public Optional liquibaseTablespaceName = Optional.empty(); + /** + * Allows duplicated changeset identifiers without failing Liquibase execution. + */ + @ConfigItem + public Optional allowDuplicatedChangesetIdentifiers = Optional.empty(); + } diff --git a/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/runtime/LiquibaseRecorder.java b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/runtime/LiquibaseRecorder.java index f368806383203..01bde1120a577 100644 --- a/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/runtime/LiquibaseRecorder.java +++ b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/runtime/LiquibaseRecorder.java @@ -14,6 +14,7 @@ import io.quarkus.arc.SyntheticCreationalContext; import io.quarkus.datasource.common.runtime.DataSourceUtil; import io.quarkus.liquibase.LiquibaseFactory; +import io.quarkus.runtime.ResettableSystemProperties; import io.quarkus.runtime.RuntimeValue; import io.quarkus.runtime.annotations.Recorder; import liquibase.Liquibase; @@ -66,7 +67,9 @@ public void doStartActions(String dataSourceName) { if (!config.cleanAtStart && !config.migrateAtStart) { return; } - try (Liquibase liquibase = liquibaseFactory.createLiquibase()) { + try (Liquibase liquibase = liquibaseFactory.createLiquibase(); + ResettableSystemProperties resettableSystemProperties = liquibaseFactory + .createResettableSystemProperties()) { if (config.cleanAtStart) { liquibase.dropAll(); } diff --git a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/config/runtime/PrometheusRuntimeConfig.java b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/config/runtime/PrometheusRuntimeConfig.java index db40c700ecef8..c4ce90f86b6d1 100644 --- a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/config/runtime/PrometheusRuntimeConfig.java +++ b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/config/runtime/PrometheusRuntimeConfig.java @@ -2,6 +2,7 @@ import java.util.Map; +import io.quarkus.runtime.annotations.ConfigDocMapKey; import io.quarkus.runtime.annotations.ConfigItem; import io.quarkus.runtime.annotations.ConfigPhase; import io.quarkus.runtime.annotations.ConfigRoot; @@ -23,5 +24,6 @@ public class PrometheusRuntimeConfig { */ // @formatter:on @ConfigItem(name = ConfigItem.PARENT) + @ConfigDocMapKey("configuration-property-name") public Map prometheus; } diff --git a/extensions/mongodb-client/deployment/src/main/java/io/quarkus/mongodb/deployment/DevServicesBuildTimeConfig.java b/extensions/mongodb-client/deployment/src/main/java/io/quarkus/mongodb/deployment/DevServicesBuildTimeConfig.java index e89851d26c981..51da8b9b889e5 100644 --- a/extensions/mongodb-client/deployment/src/main/java/io/quarkus/mongodb/deployment/DevServicesBuildTimeConfig.java +++ b/extensions/mongodb-client/deployment/src/main/java/io/quarkus/mongodb/deployment/DevServicesBuildTimeConfig.java @@ -3,6 +3,7 @@ import java.util.Map; import java.util.Optional; +import io.quarkus.runtime.annotations.ConfigDocMapKey; import io.quarkus.runtime.annotations.ConfigGroup; import io.quarkus.runtime.annotations.ConfigItem; @@ -37,12 +38,14 @@ public class DevServicesBuildTimeConfig { * Generic properties that are added to the connection URL. */ @ConfigItem + @ConfigDocMapKey("property-key") public Map properties; /** * Environment variables that are passed to the container. */ @ConfigItem + @ConfigDocMapKey("environment-variable-name") public Map containerEnv; } diff --git a/extensions/mongodb-client/runtime/src/main/java/io/quarkus/mongodb/runtime/CredentialConfig.java b/extensions/mongodb-client/runtime/src/main/java/io/quarkus/mongodb/runtime/CredentialConfig.java index 8c38177b13952..004ae28900d33 100644 --- a/extensions/mongodb-client/runtime/src/main/java/io/quarkus/mongodb/runtime/CredentialConfig.java +++ b/extensions/mongodb-client/runtime/src/main/java/io/quarkus/mongodb/runtime/CredentialConfig.java @@ -3,6 +3,7 @@ import java.util.Map; import java.util.Optional; +import io.quarkus.runtime.annotations.ConfigDocMapKey; import io.quarkus.runtime.annotations.ConfigGroup; import io.quarkus.runtime.annotations.ConfigItem; import io.quarkus.runtime.annotations.ConvertWith; @@ -50,6 +51,7 @@ public class CredentialConfig { * Allows passing authentication mechanism properties. */ @ConfigItem + @ConfigDocMapKey("property-key") public Map authMechanismProperties; /** diff --git a/extensions/narayana-jta/deployment/src/main/java/io/quarkus/narayana/jta/deployment/NarayanaJtaProcessor.java b/extensions/narayana-jta/deployment/src/main/java/io/quarkus/narayana/jta/deployment/NarayanaJtaProcessor.java index 396279af870c0..c661f5ea866b6 100644 --- a/extensions/narayana-jta/deployment/src/main/java/io/quarkus/narayana/jta/deployment/NarayanaJtaProcessor.java +++ b/extensions/narayana-jta/deployment/src/main/java/io/quarkus/narayana/jta/deployment/NarayanaJtaProcessor.java @@ -47,6 +47,8 @@ import io.quarkus.arc.deployment.SyntheticBeansRuntimeInitBuildItem; import io.quarkus.arc.deployment.UnremovableBeanBuildItem; import io.quarkus.datasource.common.runtime.DataSourceUtil; +import io.quarkus.deployment.Capabilities; +import io.quarkus.deployment.Capability; import io.quarkus.deployment.Feature; import io.quarkus.deployment.IsTest; import io.quarkus.deployment.annotations.BuildProducer; @@ -56,16 +58,21 @@ import io.quarkus.deployment.annotations.Record; import io.quarkus.deployment.builditem.CombinedIndexBuildItem; import io.quarkus.deployment.builditem.FeatureBuildItem; +import io.quarkus.deployment.builditem.NativeImageFeatureBuildItem; import io.quarkus.deployment.builditem.ShutdownContextBuildItem; import io.quarkus.deployment.builditem.nativeimage.NativeImageSystemPropertyBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; import io.quarkus.deployment.builditem.nativeimage.RuntimeInitializedClassBuildItem; import io.quarkus.deployment.logging.LogCleanupFilterBuildItem; +import io.quarkus.deployment.pkg.steps.NativeOrNativeSourcesBuild; import io.quarkus.gizmo.ClassCreator; import io.quarkus.narayana.jta.runtime.NarayanaJtaProducers; import io.quarkus.narayana.jta.runtime.NarayanaJtaRecorder; +import io.quarkus.narayana.jta.runtime.TransactionManagerBuildTimeConfig; +import io.quarkus.narayana.jta.runtime.TransactionManagerBuildTimeConfig.UnsafeMultipleLastResourcesMode; import io.quarkus.narayana.jta.runtime.TransactionManagerConfiguration; import io.quarkus.narayana.jta.runtime.context.TransactionContext; +import io.quarkus.narayana.jta.runtime.graal.DisableLoggingFeature; import io.quarkus.narayana.jta.runtime.interceptor.TestTransactionInterceptor; import io.quarkus.narayana.jta.runtime.interceptor.TransactionalInterceptorMandatory; import io.quarkus.narayana.jta.runtime.interceptor.TransactionalInterceptorNever; @@ -93,7 +100,11 @@ public void build(NarayanaJtaRecorder recorder, BuildProducer reflectiveClass, BuildProducer runtimeInit, BuildProducer feature, - TransactionManagerConfiguration transactions, ShutdownContextBuildItem shutdownContextBuildItem) { + BuildProducer logCleanupFilters, + BuildProducer nativeImageFeatures, + TransactionManagerConfiguration transactions, TransactionManagerBuildTimeConfig transactionManagerBuildTimeConfig, + ShutdownContextBuildItem shutdownContextBuildItem, + Capabilities capabilities) { recorder.handleShutdown(shutdownContextBuildItem, transactions); feature.produce(new FeatureBuildItem(Feature.NARAYANA_JTA)); additionalBeans.produce(new AdditionalBeanBuildItem(NarayanaJtaProducers.class)); @@ -137,6 +148,12 @@ public void build(NarayanaJtaRecorder recorder, builder.addBeanClass(TransactionalInterceptorNotSupported.class); additionalBeans.produce(builder.build()); + transactionManagerBuildTimeConfig.unsafeMultipleLastResources.ifPresent(mode -> { + if (!mode.equals(UnsafeMultipleLastResourcesMode.FAIL)) { + recorder.logUnsafeMultipleLastResourcesOnStartup(mode); + } + }); + //we want to force Arjuna to init at static init time Properties defaultProperties = PropertiesFactory.getDefaultProperties(); //we don't want to store the system properties here @@ -144,14 +161,28 @@ public void build(NarayanaJtaRecorder recorder, for (Object i : System.getProperties().keySet()) { defaultProperties.remove(i); } + recorder.setDefaultProperties(defaultProperties); // This must be done before setNodeName as the code in setNodeName will create a TSM based on the value of this property recorder.disableTransactionStatusManager(); + allowUnsafeMultipleLastResources(recorder, transactionManagerBuildTimeConfig, capabilities, logCleanupFilters, + nativeImageFeatures); recorder.setNodeName(transactions); recorder.setDefaultTimeout(transactions); recorder.setConfig(transactions); } + @BuildStep(onlyIf = NativeOrNativeSourcesBuild.class) + public void nativeImageFeature(TransactionManagerBuildTimeConfig transactionManagerBuildTimeConfig, + BuildProducer nativeImageFeatures) { + switch (transactionManagerBuildTimeConfig.unsafeMultipleLastResources + .orElse(UnsafeMultipleLastResourcesMode.DEFAULT)) { + case ALLOW, WARN_FIRST, WARN_EACH -> { + nativeImageFeatures.produce(new NativeImageFeatureBuildItem(DisableLoggingFeature.class)); + } + } + } + @BuildStep @Record(RUNTIME_INIT) @Consume(NarayanaInitBuildItem.class) @@ -211,4 +242,35 @@ void unremovableBean(BuildProducer unremovableBeans) { void logCleanupFilters(BuildProducer logCleanupFilters) { logCleanupFilters.produce(new LogCleanupFilterBuildItem("com.arjuna.ats.jbossatx", "ARJUNA032010:", "ARJUNA032013:")); } + + private void allowUnsafeMultipleLastResources(NarayanaJtaRecorder recorder, + TransactionManagerBuildTimeConfig transactionManagerBuildTimeConfig, + Capabilities capabilities, BuildProducer logCleanupFilters, + BuildProducer nativeImageFeatures) { + switch (transactionManagerBuildTimeConfig.unsafeMultipleLastResources + .orElse(UnsafeMultipleLastResourcesMode.DEFAULT)) { + case ALLOW -> { + recorder.allowUnsafeMultipleLastResources(capabilities.isPresent(Capability.AGROAL), true); + // we will handle the warnings ourselves at runtime init when the option is set explicitly + logCleanupFilters.produce( + new LogCleanupFilterBuildItem("com.arjuna.ats.arjuna", "ARJUNA012139", "ARJUNA012141", "ARJUNA012142")); + } + case WARN_FIRST -> { + recorder.allowUnsafeMultipleLastResources(capabilities.isPresent(Capability.AGROAL), true); + // we will handle the warnings ourselves at runtime init when the option is set explicitly + // but we still want Narayana to produce a warning on the first offending transaction + logCleanupFilters.produce( + new LogCleanupFilterBuildItem("com.arjuna.ats.arjuna", "ARJUNA012139", "ARJUNA012142")); + } + case WARN_EACH -> { + recorder.allowUnsafeMultipleLastResources(capabilities.isPresent(Capability.AGROAL), false); + // we will handle the warnings ourselves at runtime init when the option is set explicitly + // but we still want Narayana to produce one warning per offending transaction + logCleanupFilters.produce( + new LogCleanupFilterBuildItem("com.arjuna.ats.arjuna", "ARJUNA012139", "ARJUNA012142")); + } + case FAIL -> { // No need to do anything, this is the default behavior of Narayana + } + } + } } diff --git a/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/NarayanaJtaRecorder.java b/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/NarayanaJtaRecorder.java index 83d9f1b865cfa..156dbd6d1c865 100644 --- a/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/NarayanaJtaRecorder.java +++ b/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/NarayanaJtaRecorder.java @@ -29,6 +29,7 @@ import io.quarkus.runtime.ShutdownContext; import io.quarkus.runtime.annotations.Recorder; import io.quarkus.runtime.configuration.ConfigurationException; +import io.quarkus.runtime.util.StringUtil; @Recorder public class NarayanaJtaRecorder { @@ -110,6 +111,30 @@ public void setConfig(final TransactionManagerConfiguration transactions) { .setXaResourceOrphanFilterClassNames(transactions.xaResourceOrphanFilters); } + /** + * This should be removed in the future. + */ + @Deprecated(forRemoval = true) + public void allowUnsafeMultipleLastResources(boolean agroalPresent, boolean disableMultipleLastResourcesWarning) { + arjPropertyManager.getCoreEnvironmentBean().setAllowMultipleLastResources(true); + arjPropertyManager.getCoreEnvironmentBean().setDisableMultipleLastResourcesWarning(disableMultipleLastResourcesWarning); + if (agroalPresent) { + jtaPropertyManager.getJTAEnvironmentBean() + .setLastResourceOptimisationInterfaceClassName("io.agroal.narayana.LocalXAResource"); + } + } + + /** + * This should be removed in the future. + */ + @Deprecated(forRemoval = true) + public void logUnsafeMultipleLastResourcesOnStartup( + TransactionManagerBuildTimeConfig.UnsafeMultipleLastResourcesMode mode) { + log.warnf( + "Setting quarkus.transaction-manager.unsafe-multiple-last-resources to '%s' makes adding multiple resources to the same transaction unsafe.", + StringUtil.hyphenate(mode.name()).replace('_', '-')); + } + private void setObjectStoreDir(String name, TransactionManagerConfiguration config) { BeanPopulator.getNamedInstance(ObjectStoreEnvironmentBean.class, name).setObjectStoreDir(config.objectStore.directory); } diff --git a/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/TransactionManagerBuildTimeConfig.java b/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/TransactionManagerBuildTimeConfig.java new file mode 100644 index 0000000000000..91cbeccf433d6 --- /dev/null +++ b/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/TransactionManagerBuildTimeConfig.java @@ -0,0 +1,66 @@ +package io.quarkus.narayana.jta.runtime; + +import java.util.Optional; + +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.annotations.ConfigRoot; + +@ConfigRoot(phase = ConfigPhase.BUILD_TIME) +public final class TransactionManagerBuildTimeConfig { + /** + * Define the behavior when using multiple XA unaware resources in the same transactional demarcation. + *

+ * Defaults to {@code fail}. + * {@code warn-each}, {@code warn-first}, and {@code allow} are UNSAFE and should only be used for compatibility. + * Either use XA for all resources if you want consistency, or split the code into separate + * methods with separate transactions. + *

+ * Note that using a single XA unaware resource together with XA aware resources, known as + * the Last Resource Commit Optimization (LRCO), is different from using multiple XA unaware + * resources. Although LRCO allows most transactions to complete normally, some errors can + * cause an inconsistent transaction outcome. Using multiple XA unaware resources is not + * recommended since the probability of inconsistent outcomes is significantly higher and + * much harder to recover from than LRCO. For this reason, use LRCO as a last resort. + *

+ * We do not recommend using this configuration property, and we plan to remove it in the future, + * so you should plan fixing your application accordingly. + * If you think your use case of this feature is valid and this option should be kept around, + * open an issue in our tracker explaining why. + * + * @deprecated This property is planned for removal in a future version. + */ + @Deprecated(forRemoval = true) + @ConfigItem(defaultValueDocumentation = "fail") + public Optional unsafeMultipleLastResources; + + public enum UnsafeMultipleLastResourcesMode { + /** + * Allow using multiple XA unaware resources in the same transactional demarcation. + *

+ * This will log a warning once on application startup, + * but not on each use of multiple XA unaware resources in the same transactional demarcation. + */ + ALLOW, + /** + * Allow using multiple XA unaware resources in the same transactional demarcation, + * but log a warning on the first occurrence. + */ + WARN_FIRST, + /** + * Allow using multiple XA unaware resources in the same transactional demarcation, + * but log a warning on each occurrence. + */ + WARN_EACH, + /** + * Allow using multiple XA unaware resources in the same transactional demarcation, + * but log a warning on each occurrence. + */ + FAIL; + + // The default is WARN_FIRST in Quarkus 3.8, FAIL in Quarkus 3.9+ + // Make sure to update defaultValueDocumentation on unsafeMultipleLastResources when changing this. + public static final UnsafeMultipleLastResourcesMode DEFAULT = FAIL; + } + +} diff --git a/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/graal/DisableLoggingFeature.java b/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/graal/DisableLoggingFeature.java new file mode 100644 index 0000000000000..1e32c4ec2a9d7 --- /dev/null +++ b/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/graal/DisableLoggingFeature.java @@ -0,0 +1,43 @@ +package io.quarkus.narayana.jta.runtime.graal; + +import java.util.HashMap; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.graalvm.nativeimage.hosted.Feature; + +/** + * Disables logging during the analysis phase + */ +public class DisableLoggingFeature implements Feature { + + private static final String[] CATEGORIES = { + "com.arjuna.ats.arjuna" + }; + + private final Map categoryMap = new HashMap<>(CATEGORIES.length); + + @Override + public void beforeAnalysis(BeforeAnalysisAccess access) { + for (String category : CATEGORIES) { + Logger logger = Logger.getLogger(category); + categoryMap.put(category, logger.getLevel()); + logger.setLevel(Level.SEVERE); + } + } + + @Override + public void afterAnalysis(AfterAnalysisAccess access) { + for (String category : CATEGORIES) { + Level level = categoryMap.remove(category); + Logger logger = Logger.getLogger(category); + logger.setLevel(level); + } + } + + @Override + public String getDescription() { + return "Disables INFO and WARN logging during the analysis phase"; + } +} diff --git a/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/OidcClientConfig.java b/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/OidcClientConfig.java index ee44ed0479be4..5b40ddb43d9c6 100644 --- a/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/OidcClientConfig.java +++ b/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/OidcClientConfig.java @@ -7,6 +7,7 @@ import io.quarkus.oidc.common.runtime.OidcCommonConfig; import io.quarkus.oidc.common.runtime.OidcConstants; +import io.quarkus.runtime.annotations.ConfigDocMapKey; import io.quarkus.runtime.annotations.ConfigGroup; import io.quarkus.runtime.annotations.ConfigItem; @@ -184,6 +185,7 @@ public void setRefreshExpiresInProperty(String refreshExpiresInProperty) { * Grant options */ @ConfigItem + @ConfigDocMapKey("grant-name") public Map> grantOptions; /** diff --git a/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcCommonConfig.java b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcCommonConfig.java index c16645774ba65..fd3e3e918f835 100644 --- a/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcCommonConfig.java +++ b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcCommonConfig.java @@ -7,6 +7,7 @@ import java.util.Optional; import java.util.OptionalInt; +import io.quarkus.runtime.annotations.ConfigDocMapKey; import io.quarkus.runtime.annotations.ConfigGroup; import io.quarkus.runtime.annotations.ConfigItem; @@ -342,6 +343,7 @@ public static enum Source { * Additional claims. */ @ConfigItem + @ConfigDocMapKey("claim-name") public Map claims = new HashMap<>(); /** diff --git a/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcCommonUtils.java b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcCommonUtils.java index fa855e47ec827..46acf9fbf6c5f 100644 --- a/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcCommonUtils.java +++ b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcCommonUtils.java @@ -546,7 +546,6 @@ public static List getMatchingOidcRequestFilters(Map> sendRequest(io.vertx.core.Vertx vertx, HttpRequest request, diff --git a/extensions/oidc-db-token-state-manager/deployment/pom.xml b/extensions/oidc-db-token-state-manager/deployment/pom.xml index a01b824efefb8..9ba49b5810968 100644 --- a/extensions/oidc-db-token-state-manager/deployment/pom.xml +++ b/extensions/oidc-db-token-state-manager/deployment/pom.xml @@ -44,7 +44,7 @@ test - net.sourceforge.htmlunit + org.htmlunit htmlunit test diff --git a/extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/AbstractDbTokenStateManagerTest.java b/extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/AbstractDbTokenStateManagerTest.java index e506b2f63d4fc..ddf3ddef5fb0d 100644 --- a/extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/AbstractDbTokenStateManagerTest.java +++ b/extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/AbstractDbTokenStateManagerTest.java @@ -10,17 +10,16 @@ import java.util.function.Consumer; import org.hamcrest.Matchers; +import org.htmlunit.SilentCssErrorHandler; +import org.htmlunit.TextPage; +import org.htmlunit.WebClient; +import org.htmlunit.WebRequest; +import org.htmlunit.WebResponse; +import org.htmlunit.html.HtmlForm; +import org.htmlunit.html.HtmlPage; import org.jboss.shrinkwrap.api.spec.JavaArchive; import org.junit.jupiter.api.Test; -import com.gargoylesoftware.htmlunit.SilentCssErrorHandler; -import com.gargoylesoftware.htmlunit.TextPage; -import com.gargoylesoftware.htmlunit.WebClient; -import com.gargoylesoftware.htmlunit.WebRequest; -import com.gargoylesoftware.htmlunit.WebResponse; -import com.gargoylesoftware.htmlunit.html.HtmlForm; -import com.gargoylesoftware.htmlunit.html.HtmlPage; - import io.quarkus.builder.Version; import io.quarkus.maven.dependency.Dependency; import io.quarkus.test.QuarkusUnitTest; diff --git a/extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/HibernateOrmPgDbTokenStateManagerTest.java b/extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/HibernateOrmPgDbTokenStateManagerTest.java index eaa9390090ae4..1f2189ecb871d 100644 --- a/extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/HibernateOrmPgDbTokenStateManagerTest.java +++ b/extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/HibernateOrmPgDbTokenStateManagerTest.java @@ -10,15 +10,14 @@ import java.util.List; import org.awaitility.Awaitility; +import org.htmlunit.WebClient; +import org.htmlunit.WebRequest; +import org.htmlunit.WebResponse; +import org.htmlunit.html.HtmlForm; +import org.htmlunit.html.HtmlPage; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; -import com.gargoylesoftware.htmlunit.WebClient; -import com.gargoylesoftware.htmlunit.WebRequest; -import com.gargoylesoftware.htmlunit.WebResponse; -import com.gargoylesoftware.htmlunit.html.HtmlForm; -import com.gargoylesoftware.htmlunit.html.HtmlPage; - import io.quarkus.builder.Version; import io.quarkus.maven.dependency.Dependency; import io.quarkus.test.QuarkusUnitTest; diff --git a/extensions/oidc-db-token-state-manager/deployment/src/test/resources/application.properties b/extensions/oidc-db-token-state-manager/deployment/src/test/resources/application.properties index 3d1deb0eee114..bd512fd1f5e46 100644 --- a/extensions/oidc-db-token-state-manager/deployment/src/test/resources/application.properties +++ b/extensions/oidc-db-token-state-manager/deployment/src/test/resources/application.properties @@ -1,7 +1,7 @@ quarkus.oidc.client-id=quarkus-web-app quarkus.oidc.application-type=web-app quarkus.oidc.logout.path=/protected/logout -quarkus.log.category."com.gargoylesoftware.htmlunit.javascript.host.css.CSSStyleSheet".level=FATAL -quarkus.log.category."com.gargoylesoftware.htmlunit.css".level=FATAL +quarkus.log.category."org.htmlunit.javascript.host.css.CSSStyleSheet".level=FATAL +quarkus.log.category."org.htmlunit.css".level=FATAL quarkus.hibernate-orm.enabled=false quarkus.datasource.jdbc=false diff --git a/extensions/oidc-db-token-state-manager/deployment/src/test/resources/hibernate-orm-application.properties b/extensions/oidc-db-token-state-manager/deployment/src/test/resources/hibernate-orm-application.properties index 5e07052d84a6c..720a1136664e2 100644 --- a/extensions/oidc-db-token-state-manager/deployment/src/test/resources/hibernate-orm-application.properties +++ b/extensions/oidc-db-token-state-manager/deployment/src/test/resources/hibernate-orm-application.properties @@ -1,7 +1,7 @@ quarkus.oidc.client-id=quarkus-web-app quarkus.oidc.application-type=web-app quarkus.oidc.logout.path=/protected/logout -quarkus.log.category."com.gargoylesoftware.htmlunit.javascript.host.css.CSSStyleSheet".level=FATAL -quarkus.log.category."com.gargoylesoftware.htmlunit.css".level=FATAL +quarkus.log.category."org.htmlunit.javascript.host.css.CSSStyleSheet".level=FATAL +quarkus.log.category."org.htmlunit.css".level=FATAL quarkus.oidc.db-token-state-manager.delete-expired-delay=3 quarkus.oidc.db-token-state-manager.create-database-table-if-not-exists=false diff --git a/extensions/oidc-token-propagation-reactive/deployment/pom.xml b/extensions/oidc-token-propagation-reactive/deployment/pom.xml index 9b5a44eb314b3..d30a567c6dd65 100644 --- a/extensions/oidc-token-propagation-reactive/deployment/pom.xml +++ b/extensions/oidc-token-propagation-reactive/deployment/pom.xml @@ -57,7 +57,7 @@ test - net.sourceforge.htmlunit + org.htmlunit htmlunit test diff --git a/extensions/oidc-token-propagation-reactive/deployment/src/test/java/io/quarkus/oidc/token/propagation/reactive/OidcTokenPropagationWithSecurityIdentityAugmentorTest.java b/extensions/oidc-token-propagation-reactive/deployment/src/test/java/io/quarkus/oidc/token/propagation/reactive/OidcTokenPropagationWithSecurityIdentityAugmentorTest.java index 64a3bc1d641cc..61743be804e2e 100644 --- a/extensions/oidc-token-propagation-reactive/deployment/src/test/java/io/quarkus/oidc/token/propagation/reactive/OidcTokenPropagationWithSecurityIdentityAugmentorTest.java +++ b/extensions/oidc-token-propagation-reactive/deployment/src/test/java/io/quarkus/oidc/token/propagation/reactive/OidcTokenPropagationWithSecurityIdentityAugmentorTest.java @@ -7,16 +7,15 @@ import java.io.IOException; import java.util.Set; +import org.htmlunit.SilentCssErrorHandler; +import org.htmlunit.TextPage; +import org.htmlunit.WebClient; +import org.htmlunit.html.HtmlForm; +import org.htmlunit.html.HtmlPage; import org.jboss.shrinkwrap.api.asset.StringAsset; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; -import com.gargoylesoftware.htmlunit.SilentCssErrorHandler; -import com.gargoylesoftware.htmlunit.TextPage; -import com.gargoylesoftware.htmlunit.WebClient; -import com.gargoylesoftware.htmlunit.html.HtmlForm; -import com.gargoylesoftware.htmlunit.html.HtmlPage; - import io.quarkus.test.QuarkusUnitTest; import io.quarkus.test.common.QuarkusTestResource; import io.quarkus.test.oidc.server.OidcWiremockTestResource; diff --git a/extensions/oidc/deployment/pom.xml b/extensions/oidc/deployment/pom.xml index 8abab01676220..34490f5d691d1 100644 --- a/extensions/oidc/deployment/pom.xml +++ b/extensions/oidc/deployment/pom.xml @@ -91,7 +91,7 @@ - net.sourceforge.htmlunit + org.htmlunit htmlunit test diff --git a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/DevUiConfig.java b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/DevUiConfig.java index 2c5bd13435d02..b16c3aebaa324 100644 --- a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/DevUiConfig.java +++ b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/DevUiConfig.java @@ -4,6 +4,7 @@ import java.util.Map; import java.util.Optional; +import io.quarkus.runtime.annotations.ConfigDocMapKey; import io.quarkus.runtime.annotations.ConfigGroup; import io.quarkus.runtime.annotations.ConfigItem; @@ -64,6 +65,7 @@ public String getGrantType() { * Grant options */ @ConfigItem + @ConfigDocMapKey("option-name") public Map> grantOptions; /** diff --git a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/OidcBuildStep.java b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/OidcBuildStep.java index a7e4e41dfbd2f..08dd65840379e 100644 --- a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/OidcBuildStep.java +++ b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/OidcBuildStep.java @@ -2,10 +2,12 @@ import static io.quarkus.arc.processor.BuiltinScope.APPLICATION; import static io.quarkus.arc.processor.DotNames.DEFAULT; +import static io.quarkus.arc.processor.DotNames.NAMED; import static io.quarkus.oidc.common.runtime.OidcConstants.BEARER_SCHEME; import static io.quarkus.oidc.common.runtime.OidcConstants.CODE_FLOW_CODE; import static io.quarkus.oidc.runtime.OidcUtils.DEFAULT_TENANT_ID; import static org.jboss.jandex.AnnotationTarget.Kind.CLASS; +import static org.jboss.jandex.AnnotationTarget.Kind.METHOD; import java.util.List; import java.util.Map; @@ -16,16 +18,23 @@ import org.eclipse.microprofile.jwt.Claim; import org.eclipse.microprofile.jwt.JsonWebToken; +import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.AnnotationTarget; +import org.jboss.jandex.AnnotationValue; import org.jboss.jandex.DotName; +import org.jboss.jandex.Type; import org.jboss.logging.Logger; import io.quarkus.arc.deployment.AdditionalBeanBuildItem; import io.quarkus.arc.deployment.BeanDiscoveryFinishedBuildItem; import io.quarkus.arc.deployment.BeanRegistrationPhaseBuildItem; +import io.quarkus.arc.deployment.InjectionPointTransformerBuildItem; import io.quarkus.arc.deployment.QualifierRegistrarBuildItem; import io.quarkus.arc.deployment.SyntheticBeanBuildItem; +import io.quarkus.arc.processor.Annotations; +import io.quarkus.arc.processor.DotNames; import io.quarkus.arc.processor.InjectionPointInfo; +import io.quarkus.arc.processor.InjectionPointsTransformer; import io.quarkus.arc.processor.QualifierRegistrar; import io.quarkus.deployment.Capabilities; import io.quarkus.deployment.Capability; @@ -151,6 +160,7 @@ ExtensionSslNativeSupportBuildItem enableSslInNative() { QualifierRegistrarBuildItem addQualifiers() { // this seems to be necessary; I think it's because sometimes we only access beans // annotated with @TenantFeature programmatically and no injection point is annotated with it + // TODO: drop @TenantFeature qualifier when 'TenantFeatureFinder' stop using this annotation as a qualifier return new QualifierRegistrarBuildItem(new QualifierRegistrar() { @Override public Map> getAdditionalQualifiers() { @@ -159,52 +169,94 @@ public Map> getAdditionalQualifiers() { }); } + @BuildStep + InjectionPointTransformerBuildItem makeTenantIdentityProviderInjectionPointsNamed() { + // @Tenant annotation cannot be a qualifier as it is used on resource methods and lead to illegal states + return new InjectionPointTransformerBuildItem(new InjectionPointsTransformer() { + @Override + public boolean appliesTo(Type requiredType) { + return requiredType.name().equals(TENANT_IDENTITY_PROVIDER_NAME); + } + + @Override + public void transform(TransformationContext ctx) { + if (ctx.getTarget().kind() == METHOD) { + ctx + .getAllAnnotations() + .stream() + .filter(a -> TENANT_NAME.equals(a.name())) + .forEach(a -> { + var annotationValue = new AnnotationValue[] { + AnnotationValue.createStringValue("value", a.value().asString()) }; + ctx + .transform() + .add(AnnotationInstance.create(NAMED, a.target(), annotationValue)) + .done(); + }); + } else { + // field + var tenantAnnotation = Annotations.find(ctx.getAllAnnotations(), TENANT_NAME); + if (tenantAnnotation != null && tenantAnnotation.value() != null) { + ctx + .transform() + .add(NAMED, AnnotationValue.createStringValue("value", tenantAnnotation.value().asString())) + .done(); + } + } + } + }); + } + /** - * Produce {@link OidcIdentityProvider} with already selected tenant for each {@link OidcIdentityProvider} - * injection point annotated with {@link TenantFeature} annotation. - * For example, we produce {@link OidcIdentityProvider} with pre-selected tenant 'my-tenant' for injection point: + * Produce {@link TenantIdentityProvider} with already selected tenant for each {@link TenantIdentityProvider} + * injection point annotated with {@link Tenant} annotation. + * For example, we produce {@link TenantIdentityProvider} with pre-selected tenant 'my-tenant' for injection point: * * * @Inject - * @TenantFeature("my-tenant") - * OidcIdentityProvider identityProvider; + * @Tenant("my-tenant") + * TenantIdentityProvider identityProvider; * */ @Record(ExecutionTime.STATIC_INIT) @BuildStep void produceTenantIdentityProviders(BuildProducer syntheticBeanProducer, OidcRecorder recorder, BeanDiscoveryFinishedBuildItem beans, CombinedIndexBuildItem combinedIndex) { - // create TenantIdentityProviders for tenants selected with @TenantFeature like: @TenantFeature("my-tenant") - if (!combinedIndex.getIndex().getAnnotations(TENANT_FEATURE_NAME).isEmpty()) { - // create TenantIdentityProviders for tenants selected with @TenantFeature like: @TenantFeature("my-tenant") + if (!combinedIndex.getIndex().getAnnotations(TENANT_NAME).isEmpty()) { + // create TenantIdentityProviders for tenants selected with @Tenant like: @Tenant("my-tenant") beans .getInjectionPoints() .stream() - .filter(ip -> ip.getRequiredQualifier(TENANT_FEATURE_NAME) != null) .filter(OidcBuildStep::isTenantIdentityProviderType) - .map(ip -> ip.getRequiredQualifier(TENANT_FEATURE_NAME).value().asString()) + .filter(ip -> ip.getRequiredQualifier(NAMED) != null) + .map(ip -> ip.getRequiredQualifier(NAMED).value().asString()) .distinct() .forEach(tenantName -> syntheticBeanProducer.produce( SyntheticBeanBuildItem .configure(TenantIdentityProvider.class) - .addQualifier().annotation(TENANT_FEATURE_NAME).addValue("value", tenantName).done() + .named(tenantName) .scope(APPLICATION.getInfo()) .supplier(recorder.createTenantIdentityProvider(tenantName)) .unremovable() .done())); } - // create TenantIdentityProvider for default tenant when tenant is not explicitly selected via @TenantFeature + // create TenantIdentityProvider for default tenant when tenant is not explicitly selected via @Tenant boolean createTenantIdentityProviderForDefaultTenant = beans .getInjectionPoints() .stream() - .filter(InjectionPointInfo::hasDefaultedQualifier) + .filter(ip -> ip.getRequiredQualifier(NAMED) == null) .anyMatch(OidcBuildStep::isTenantIdentityProviderType); if (createTenantIdentityProviderForDefaultTenant) { syntheticBeanProducer.produce( SyntheticBeanBuildItem .configure(TenantIdentityProvider.class) - .addQualifier(DEFAULT) .scope(APPLICATION.getInfo()) + .addQualifier(DEFAULT) + // named beans are implicitly default according to the specs + // when no other qualifiers are present other than @Named and @Any + // which means we need to handle ambiguous resolution + .alternative(true) + .priority(1) .supplier(recorder.createTenantIdentityProvider(DEFAULT_TENANT_ID)) .unremovable() .done()); @@ -243,8 +295,17 @@ public void registerTenantResolverInterceptor(Capabilities capabilities, OidcRec BuildProducer systemPropertyProducer) { if (!buildTimeConfig.auth.proactive && (capabilities.isPresent(Capability.RESTEASY_REACTIVE) || capabilities.isPresent(Capability.RESTEASY))) { - var annotationInstances = combinedIndexBuildItem.getIndex().getAnnotations(TENANT_NAME); - if (!annotationInstances.isEmpty()) { + boolean foundTenantResolver = combinedIndexBuildItem + .getIndex() + .getAnnotations(TENANT_NAME) + .stream() + .map(AnnotationInstance::target) + // ignored field injection points and injection setters + // as we don't want to count in the TenantIdentityProvider injection point + .filter(t -> t.kind() == METHOD) + .map(AnnotationTarget::asMethod) + .anyMatch(m -> !m.isConstructor() && !m.hasAnnotation(DotNames.INJECT)); + if (foundTenantResolver) { // register method interceptor that will be run before security checks bindingProducer.produce( new EagerSecurityInterceptorBindingBuildItem(recorder.tenantResolverInterceptorCreator(), TENANT_NAME)); diff --git a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/DevServicesConfig.java b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/DevServicesConfig.java index e7971fdf3014d..5a9ace88d43d5 100644 --- a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/DevServicesConfig.java +++ b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/DevServicesConfig.java @@ -7,6 +7,7 @@ import java.util.OptionalInt; import io.quarkus.oidc.deployment.DevUiConfig; +import io.quarkus.runtime.annotations.ConfigDocMapKey; import io.quarkus.runtime.annotations.ConfigGroup; import io.quarkus.runtime.annotations.ConfigItem; @@ -33,7 +34,7 @@ public class DevServicesConfig { * ends with `-legacy`. * Override with `quarkus.keycloak.devservices.keycloak-x-image`. */ - @ConfigItem(defaultValue = "quay.io/keycloak/keycloak:23.0.7") + @ConfigItem(defaultValue = "quay.io/keycloak/keycloak:24.0.4") public String imageName; /** @@ -95,6 +96,7 @@ public class DevServicesConfig { * Each map entry represents a mapping between an alias and a class or file system resource path. */ @ConfigItem + @ConfigDocMapKey("alias-name") public Map resourceAliases; /** * Additional class or file system resources that are used to initialize Keycloak. @@ -102,6 +104,7 @@ public class DevServicesConfig { * location. */ @ConfigItem + @ConfigDocMapKey("resource-name") public Map resourceMappings; /** @@ -161,6 +164,7 @@ public class DevServicesConfig { * This map is used for role creation when no realm file is found at the `realm-path`. */ @ConfigItem + @ConfigDocMapKey("role-name") public Map> roles; /** @@ -223,6 +227,7 @@ public String getGrantType() { * Environment variables to be passed to the container. */ @ConfigItem + @ConfigDocMapKey("environment-variable-name") public Map containerEnv; @Override diff --git a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakDevServicesProcessor.java b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakDevServicesProcessor.java index c2ef04ff3d074..f7f2e1cdff083 100644 --- a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakDevServicesProcessor.java +++ b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakDevServicesProcessor.java @@ -109,7 +109,8 @@ public class KeycloakDevServicesProcessor { private static final String KEYCLOAK_QUARKUS_HOSTNAME = "KC_HOSTNAME"; private static final String KEYCLOAK_QUARKUS_ADMIN_PROP = "KEYCLOAK_ADMIN"; private static final String KEYCLOAK_QUARKUS_ADMIN_PASSWORD_PROP = "KEYCLOAK_ADMIN_PASSWORD"; - private static final String KEYCLOAK_QUARKUS_START_CMD = "start --http-enabled=true --hostname-strict=false --hostname-strict-https=false"; + private static final String KEYCLOAK_QUARKUS_START_CMD = "start --http-enabled=true --hostname-strict=false --hostname-strict-https=false " + + "--spi-user-profile-declarative-user-profile-config-file=/opt/keycloak/upconfig.json"; private static final String JAVA_OPTS = "JAVA_OPTS"; private static final String OIDC_USERS = "oidc.users"; @@ -509,6 +510,7 @@ protected void configure() { addEnv(KEYCLOAK_QUARKUS_ADMIN_PASSWORD_PROP, KEYCLOAK_ADMIN_PASSWORD); withCommand(startCommand.orElse(KEYCLOAK_QUARKUS_START_CMD) + (useSharedNetwork ? " --hostname-port=" + fixedExposedPort.getAsInt() : "")); + addUpConfigResource(); } else { addEnv(KEYCLOAK_WILDFLY_USER_PROP, KEYCLOAK_ADMIN_USER); addEnv(KEYCLOAK_WILDFLY_PASSWORD_PROP, KEYCLOAK_ADMIN_PASSWORD); @@ -560,6 +562,13 @@ private void mapResource(String resourcePath, String mappedResource) { } } + private void addUpConfigResource() { + if (Thread.currentThread().getContextClassLoader().getResource("/dev-service/upconfig.json") != null) { + LOG.debug("Mapping the classpath /dev-service/upconfig.json resource to /opt/keycloak/upconfig.json"); + withClasspathResourceMapping("/dev-service/upconfig.json", "/opt/keycloak/upconfig.json", BindMode.READ_ONLY); + } + } + private Integer findRandomPort() { try (ServerSocket socket = new ServerSocket(0)) { return socket.getLocalPort(); diff --git a/extensions/oidc/deployment/src/main/resources/dev-service/upconfig.json b/extensions/oidc/deployment/src/main/resources/dev-service/upconfig.json new file mode 100644 index 0000000000000..8487089bc90fd --- /dev/null +++ b/extensions/oidc/deployment/src/main/resources/dev-service/upconfig.json @@ -0,0 +1,60 @@ +{ + "attributes": [ + { + "name": "username", + "displayName": "${username}", + "permissions": { + "view": ["admin", "user"], + "edit": ["admin", "user"] + }, + "validations": { + "length": { "min": 3, "max": 255 }, + "username-prohibited-characters": {}, + "up-username-not-idn-homograph": {} + } + }, + { + "name": "email", + "displayName": "${email}", + "permissions": { + "view": ["admin", "user"], + "edit": ["admin", "user"] + }, + "validations": { + "email" : {}, + "length": { "max": 255 } + } + }, + { + "name": "firstName", + "displayName": "${firstName}", + "permissions": { + "view": ["admin", "user"], + "edit": ["admin", "user"] + }, + "validations": { + "length": { "max": 255 }, + "person-name-prohibited-characters": {} + } + }, + { + "name": "lastName", + "displayName": "${lastName}", + "permissions": { + "view": ["admin", "user"], + "edit": ["admin", "user"] + }, + "validations": { + "length": { "max": 255 }, + "person-name-prohibited-characters": {} + } + } + ], + "groups": [ + { + "name": "user-metadata", + "displayHeader": "User metadata", + "displayDescription": "Attributes, which refer to user metadata" + } + ] +} \ No newline at end of file diff --git a/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/CodeFlowDevModeDefaultTenantTestCase.java b/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/CodeFlowDevModeDefaultTenantTestCase.java index 7262a3cd300fd..067351535035d 100644 --- a/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/CodeFlowDevModeDefaultTenantTestCase.java +++ b/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/CodeFlowDevModeDefaultTenantTestCase.java @@ -5,15 +5,14 @@ import java.io.IOException; +import org.htmlunit.FailingHttpStatusCodeException; +import org.htmlunit.SilentCssErrorHandler; +import org.htmlunit.WebClient; +import org.htmlunit.html.HtmlForm; +import org.htmlunit.html.HtmlPage; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; -import com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException; -import com.gargoylesoftware.htmlunit.SilentCssErrorHandler; -import com.gargoylesoftware.htmlunit.WebClient; -import com.gargoylesoftware.htmlunit.html.HtmlForm; -import com.gargoylesoftware.htmlunit.html.HtmlPage; - import io.quarkus.test.QuarkusDevModeTest; import io.quarkus.test.common.QuarkusTestResource; import io.quarkus.test.keycloak.server.KeycloakTestResourceLifecycleManager; diff --git a/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/CodeFlowDevModeTestCase.java b/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/CodeFlowDevModeTestCase.java index a3c4297b51700..d0343742c27b7 100644 --- a/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/CodeFlowDevModeTestCase.java +++ b/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/CodeFlowDevModeTestCase.java @@ -24,22 +24,21 @@ import org.awaitility.core.ThrowingRunnable; import org.eclipse.microprofile.config.spi.ConfigSource; +import org.htmlunit.CookieManager; +import org.htmlunit.FailingHttpStatusCodeException; +import org.htmlunit.SilentCssErrorHandler; +import org.htmlunit.WebClient; +import org.htmlunit.WebRequest; +import org.htmlunit.WebResponse; +import org.htmlunit.html.HtmlForm; +import org.htmlunit.html.HtmlPage; +import org.htmlunit.util.Cookie; import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestMethodOrder; import org.junit.jupiter.api.extension.RegisterExtension; -import com.gargoylesoftware.htmlunit.CookieManager; -import com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException; -import com.gargoylesoftware.htmlunit.SilentCssErrorHandler; -import com.gargoylesoftware.htmlunit.WebClient; -import com.gargoylesoftware.htmlunit.WebRequest; -import com.gargoylesoftware.htmlunit.WebResponse; -import com.gargoylesoftware.htmlunit.html.HtmlForm; -import com.gargoylesoftware.htmlunit.html.HtmlPage; -import com.gargoylesoftware.htmlunit.util.Cookie; - import io.quarkus.test.QuarkusDevModeTest; import io.quarkus.test.common.QuarkusTestResource; import io.quarkus.test.keycloak.server.KeycloakTestResourceLifecycleManager; diff --git a/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/CodeFlowVerifyAccessTokenDisabledTest.java b/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/CodeFlowVerifyAccessTokenDisabledTest.java index 81b8c996af8f2..3bd36d0d3ccb1 100644 --- a/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/CodeFlowVerifyAccessTokenDisabledTest.java +++ b/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/CodeFlowVerifyAccessTokenDisabledTest.java @@ -4,14 +4,13 @@ import java.io.IOException; +import org.htmlunit.SilentCssErrorHandler; +import org.htmlunit.WebClient; +import org.htmlunit.html.HtmlForm; +import org.htmlunit.html.HtmlPage; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; -import com.gargoylesoftware.htmlunit.SilentCssErrorHandler; -import com.gargoylesoftware.htmlunit.WebClient; -import com.gargoylesoftware.htmlunit.html.HtmlForm; -import com.gargoylesoftware.htmlunit.html.HtmlPage; - import io.quarkus.test.QuarkusUnitTest; import io.quarkus.test.common.QuarkusTestResource; import io.quarkus.test.keycloak.server.KeycloakTestResourceLifecycleManager; diff --git a/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/CodeFlowVerifyInjectedAccessTokenDisabledTest.java b/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/CodeFlowVerifyInjectedAccessTokenDisabledTest.java index 6d1aaf041f503..dd9d217415214 100644 --- a/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/CodeFlowVerifyInjectedAccessTokenDisabledTest.java +++ b/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/CodeFlowVerifyInjectedAccessTokenDisabledTest.java @@ -4,14 +4,13 @@ import java.io.IOException; +import org.htmlunit.SilentCssErrorHandler; +import org.htmlunit.WebClient; +import org.htmlunit.html.HtmlForm; +import org.htmlunit.html.HtmlPage; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; -import com.gargoylesoftware.htmlunit.SilentCssErrorHandler; -import com.gargoylesoftware.htmlunit.WebClient; -import com.gargoylesoftware.htmlunit.html.HtmlForm; -import com.gargoylesoftware.htmlunit.html.HtmlPage; - import io.quarkus.test.QuarkusUnitTest; import io.quarkus.test.common.QuarkusTestResource; import io.quarkus.test.keycloak.server.KeycloakTestResourceLifecycleManager; diff --git a/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/CodeTenantReauthenticateTestCase.java b/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/CodeTenantReauthenticateTestCase.java index 98a9bdd4aa52b..51bc73fa55cd6 100644 --- a/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/CodeTenantReauthenticateTestCase.java +++ b/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/CodeTenantReauthenticateTestCase.java @@ -4,18 +4,17 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; +import org.htmlunit.SilentCssErrorHandler; +import org.htmlunit.TextPage; +import org.htmlunit.WebClient; +import org.htmlunit.html.HtmlForm; +import org.htmlunit.html.HtmlPage; +import org.htmlunit.util.Cookie; import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestMethodOrder; import org.junit.jupiter.api.extension.RegisterExtension; -import com.gargoylesoftware.htmlunit.SilentCssErrorHandler; -import com.gargoylesoftware.htmlunit.TextPage; -import com.gargoylesoftware.htmlunit.WebClient; -import com.gargoylesoftware.htmlunit.html.HtmlForm; -import com.gargoylesoftware.htmlunit.html.HtmlPage; -import com.gargoylesoftware.htmlunit.util.Cookie; - import io.quarkus.test.QuarkusUnitTest; import io.quarkus.test.common.QuarkusTestResource; import io.quarkus.test.keycloak.server.KeycloakTestResourceLifecycleManager; diff --git a/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/CustomIdentityProviderTestCase.java b/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/CustomIdentityProviderTestCase.java index 7ad82b090c541..7f7c1cfa1e41e 100644 --- a/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/CustomIdentityProviderTestCase.java +++ b/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/CustomIdentityProviderTestCase.java @@ -5,15 +5,14 @@ import java.io.IOException; +import org.htmlunit.FailingHttpStatusCodeException; +import org.htmlunit.SilentCssErrorHandler; +import org.htmlunit.WebClient; +import org.htmlunit.html.HtmlForm; +import org.htmlunit.html.HtmlPage; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; -import com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException; -import com.gargoylesoftware.htmlunit.SilentCssErrorHandler; -import com.gargoylesoftware.htmlunit.WebClient; -import com.gargoylesoftware.htmlunit.html.HtmlForm; -import com.gargoylesoftware.htmlunit.html.HtmlPage; - import io.quarkus.test.QuarkusDevModeTest; import io.quarkus.test.common.QuarkusTestResource; import io.quarkus.test.keycloak.server.KeycloakTestResourceLifecycleManager; diff --git a/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/ImplicitBasicAuthAndCodeFlowAuthCombinationTest.java b/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/ImplicitBasicAuthAndCodeFlowAuthCombinationTest.java index 917cfe5b57dc1..8abd7dad9ab2e 100644 --- a/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/ImplicitBasicAuthAndCodeFlowAuthCombinationTest.java +++ b/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/ImplicitBasicAuthAndCodeFlowAuthCombinationTest.java @@ -10,16 +10,15 @@ import jakarta.ws.rs.Path; import org.eclipse.microprofile.jwt.JsonWebToken; +import org.htmlunit.FailingHttpStatusCodeException; +import org.htmlunit.SilentCssErrorHandler; +import org.htmlunit.WebClient; +import org.htmlunit.html.HtmlForm; +import org.htmlunit.html.HtmlPage; import org.jboss.shrinkwrap.api.asset.StringAsset; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; -import com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException; -import com.gargoylesoftware.htmlunit.SilentCssErrorHandler; -import com.gargoylesoftware.htmlunit.WebClient; -import com.gargoylesoftware.htmlunit.html.HtmlForm; -import com.gargoylesoftware.htmlunit.html.HtmlPage; - import io.quarkus.oidc.IdToken; import io.quarkus.test.QuarkusDevModeTest; import io.quarkus.test.common.QuarkusTestResource; diff --git a/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/SecurityDisabledTestCase.java b/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/SecurityDisabledTestCase.java index 4227e587a8a08..da3bf1ae5b0e7 100644 --- a/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/SecurityDisabledTestCase.java +++ b/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/SecurityDisabledTestCase.java @@ -4,13 +4,12 @@ import java.io.IOException; +import org.htmlunit.SilentCssErrorHandler; +import org.htmlunit.WebClient; +import org.htmlunit.html.HtmlPage; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; -import com.gargoylesoftware.htmlunit.SilentCssErrorHandler; -import com.gargoylesoftware.htmlunit.WebClient; -import com.gargoylesoftware.htmlunit.html.HtmlPage; - import io.quarkus.test.QuarkusUnitTest; public class SecurityDisabledTestCase { diff --git a/extensions/oidc/deployment/src/test/resources/application-dev-mode-default-tenant.properties b/extensions/oidc/deployment/src/test/resources/application-dev-mode-default-tenant.properties index 2853d435e676f..170123599d9bb 100644 --- a/extensions/oidc/deployment/src/test/resources/application-dev-mode-default-tenant.properties +++ b/extensions/oidc/deployment/src/test/resources/application-dev-mode-default-tenant.properties @@ -2,5 +2,5 @@ quarkus.oidc.client-id=quarkus-web-app quarkus.oidc.credentials.secret=secret #quarkus.oidc.application-type=web-app -quarkus.log.category."com.gargoylesoftware.htmlunit.javascript.host.css.CSSStyleSheet".level=FATAL +quarkus.log.category."org.htmlunit.javascript.host.css.CSSStyleSheet".level=FATAL diff --git a/extensions/oidc/deployment/src/test/resources/application-dev-mode.properties b/extensions/oidc/deployment/src/test/resources/application-dev-mode.properties index 4aaaec7a7eb44..faf2273824d87 100644 --- a/extensions/oidc/deployment/src/test/resources/application-dev-mode.properties +++ b/extensions/oidc/deployment/src/test/resources/application-dev-mode.properties @@ -7,9 +7,9 @@ quarkus.oidc.credentials.client-secret.provider.key=secret-from-vault-typo quarkus.oidc.application-type=web-app quarkus.oidc.logout.path=/protected/logout quarkus.oidc.authentication.pkce-required=true -quarkus.log.category."com.gargoylesoftware.htmlunit.javascript.host.css.CSSStyleSheet".level=FATAL +quarkus.log.category."org.htmlunit.javascript.host.css.CSSStyleSheet".level=FATAL quarkus.log.category."io.quarkus.oidc.runtime.TenantConfigContext".level=DEBUG quarkus.log.file.enable=true # use blocking DNS lookup so that we have it tested somewhere -quarkus.oidc.use-blocking-dns-lookup=true \ No newline at end of file +quarkus.oidc.use-blocking-dns-lookup=true diff --git a/extensions/oidc/deployment/src/test/resources/application-tenant-reauthenticate.properties b/extensions/oidc/deployment/src/test/resources/application-tenant-reauthenticate.properties index 3e5539ef71da8..5dfc6ecddbaa0 100644 --- a/extensions/oidc/deployment/src/test/resources/application-tenant-reauthenticate.properties +++ b/extensions/oidc/deployment/src/test/resources/application-tenant-reauthenticate.properties @@ -8,4 +8,4 @@ quarkus.oidc.tenant-resolver.client-id=quarkus-web-app quarkus.oidc.tenant-resolver.credentials.secret=secret quarkus.oidc.tenant-resolver.application-type=web-app -quarkus.log.category."com.gargoylesoftware.htmlunit".level=ERROR +quarkus.log.category."org.htmlunit".level=ERROR diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcRedirectFilter.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcRedirectFilter.java new file mode 100644 index 0000000000000..7927e69450287 --- /dev/null +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcRedirectFilter.java @@ -0,0 +1,30 @@ +package io.quarkus.oidc; + +import io.vertx.core.MultiMap; +import io.vertx.ext.web.RoutingContext; + +/** + * OIDC redirect filter which can be used to customize redirect requests to OIDC authorization and logout endpoints + * as well as local redirects to OIDC tenant error, session expired and other pages. + */ +public interface OidcRedirectFilter { + + /** + * OIDC redirect context which provides access to the routing context, current OIDC tenant configuration, redirect uri + * and additional query parameters. + * The additional query parameters are visible to all OIDC redirect filters. They are URL-encoded and added to + * the redirect URI after all the filters have run. + */ + record OidcRedirectContext(RoutingContext routingContext, OidcTenantConfig oidcTenantConfig, + String redirectUri, MultiMap additionalQueryParams) { + } + + /** + * Filter OIDC redirect. + * + * @param redirectContext the redirect context which provides access to the routing context, current OIDC tenant + * configuration, redirect uri and additional query parameters. + * + */ + void filter(OidcRedirectContext redirectContext); +} diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfig.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfig.java index 6dc4236c561a5..ace3645ff8dd3 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfig.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfig.java @@ -357,6 +357,7 @@ public static class Logout { * Additional properties which is added as the query parameters to the logout redirect URI. */ @ConfigItem + @ConfigDocMapKey("query-parameter-name") public Map extraParams; /** @@ -1054,6 +1055,7 @@ public enum ResponseMode { * Additional properties added as query parameters to the authentication redirect URI. */ @ConfigItem + @ConfigDocMapKey("parameter-name") public Map extraParams = new HashMap<>(); /** @@ -1501,12 +1503,14 @@ public static class CodeGrant { * which must be included to complete the authorization code grant request. */ @ConfigItem + @ConfigDocMapKey("parameter-name") public Map extraParams = new HashMap<>(); /** * Custom HTTP headers which must be sent to complete the authorization code grant request. */ @ConfigItem + @ConfigDocMapKey("header-name") public Map headers = new HashMap<>(); public Map getExtraParams() { diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/Redirect.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/Redirect.java new file mode 100644 index 0000000000000..2739d124574aa --- /dev/null +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/Redirect.java @@ -0,0 +1,52 @@ +package io.quarkus.oidc; + +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * Annotation that can be used to restrict {@link OidcRedirectFilter} to specific redirect locations + */ +@Target({ TYPE }) +@Retention(RUNTIME) +public @interface Redirect { + + enum Location { + ALL, + + /** + * Applies to OIDC authorization endpoint + */ + OIDC_AUTHORIZATION, + + /** + * Applies to OIDC logout endpoint + */ + OIDC_LOGOUT, + + /** + * Applies to the local redirect to a custom error page resource when an authorization code flow + * redirect from OIDC provider to Quarkus returns an error instead of an authorization code + */ + ERROR_PAGE, + + /** + * Applies to the local redirect to a custom session expired page resource when + * the current user's session has expired and no longer can be refreshed. + */ + SESSION_EXPIRED_PAGE, + + /** + * Applies to the local redirect to the callback resource which is done after successful authorization + * code flow completion in order to drop the code and state parameters from the callback URL. + */ + LOCAL_ENDPOINT_CALLBACK + } + + /** + * Identifies one or more redirect locations. + */ + Location[] value() default Location.ALL; +} diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/Tenant.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/Tenant.java index e882dfc299864..97cbab1568177 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/Tenant.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/Tenant.java @@ -1,6 +1,8 @@ package io.quarkus.oidc; +import static java.lang.annotation.ElementType.FIELD; import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; import static java.lang.annotation.ElementType.TYPE; import java.lang.annotation.Retention; @@ -9,8 +11,10 @@ /** * Annotation which can be used to associate OIDC tenant configurations with Jakarta REST resources and resource methods. + * When placed on injection points, this annotation can be used to select a tenant associated + * with the {@link TenantIdentityProvider}. */ -@Target({ TYPE, METHOD }) +@Target({ TYPE, METHOD, FIELD, PARAMETER }) @Retention(RetentionPolicy.RUNTIME) public @interface Tenant { /** diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/TenantIdentityProvider.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/TenantIdentityProvider.java index fd37e50e8c4a8..377a4a7185564 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/TenantIdentityProvider.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/TenantIdentityProvider.java @@ -5,7 +5,7 @@ /** * Tenant-specific {@link SecurityIdentity} provider. Associated tenant configuration needs to be selected - * with the {@link TenantFeature} qualifier. When injection point is not annotated with the {@link TenantFeature} + * with the {@link Tenant} qualifier. When injection point is not annotated with the {@link Tenant} * qualifier, default tenant is selected. */ public interface TenantIdentityProvider { diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java index f6cf3d717aa11..3ce27c54c8ccc 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java @@ -28,9 +28,12 @@ import io.quarkus.oidc.AuthorizationCodeTokens; import io.quarkus.oidc.IdTokenCredential; import io.quarkus.oidc.JavaScriptRequestChecker; +import io.quarkus.oidc.OidcRedirectFilter; +import io.quarkus.oidc.OidcRedirectFilter.OidcRedirectContext; import io.quarkus.oidc.OidcTenantConfig; import io.quarkus.oidc.OidcTenantConfig.Authentication; import io.quarkus.oidc.OidcTenantConfig.Authentication.ResponseMode; +import io.quarkus.oidc.Redirect; import io.quarkus.oidc.SecurityEvent; import io.quarkus.oidc.UserInfo; import io.quarkus.oidc.common.runtime.OidcCommonUtils; @@ -52,7 +55,6 @@ import io.vertx.core.http.Cookie; import io.vertx.core.http.CookieSameSite; import io.vertx.core.http.HttpHeaders; -import io.vertx.core.http.impl.CookieImpl; import io.vertx.core.http.impl.ServerCookie; import io.vertx.core.json.JsonObject; import io.vertx.ext.web.RoutingContext; @@ -61,6 +63,7 @@ public class CodeAuthenticationMechanism extends AbstractOidcAuthenticationMecha public static final String SESSION_MAX_AGE_PARAM = "session-max-age"; static final String AMP = "&"; + static final String QUESTION_MARK = "?"; static final String EQ = "="; static final String COOKIE_DELIM = "|"; static final Pattern COOKIE_PATTERN = Pattern.compile("\\" + COOKIE_DELIM); @@ -227,8 +230,10 @@ public Uni apply(TenantConfigContext tenantContext) { String finalErrorUri = errorUri.toString(); LOG.debugf("Error URI: %s", finalErrorUri); - return Uni.createFrom().failure(new AuthenticationRedirectException(finalErrorUri)); + return Uni.createFrom().failure(new AuthenticationRedirectException( + filterRedirect(context, tenantContext, finalErrorUri, Redirect.Location.ERROR_PAGE))); } + }); } else { LOG.error( @@ -242,6 +247,25 @@ public Uni apply(TenantConfigContext tenantContext) { } + private static String filterRedirect(RoutingContext context, + TenantConfigContext tenantContext, String redirectUri, Redirect.Location location) { + List redirectFilters = tenantContext.getOidcRedirectFilters(location); + if (!redirectFilters.isEmpty()) { + OidcRedirectContext redirectContext = new OidcRedirectContext(context, tenantContext.getOidcTenantConfig(), + redirectUri, MultiMap.caseInsensitiveMultiMap()); + for (OidcRedirectFilter filter : redirectFilters) { + filter.filter(redirectContext); + } + MultiMap queries = redirectContext.additionalQueryParams(); + if (!queries.isEmpty()) { + String encoded = OidcCommonUtils.encodeForm(new io.vertx.mutiny.core.MultiMap(queries)).toString(); + String sep = redirectUri.lastIndexOf("?") > 0 ? AMP : QUESTION_MARK; + redirectUri += (sep + encoded); + } + } + return redirectUri; + } + private Uni stateParamIsMissing(OidcTenantConfig oidcTenantConfig, RoutingContext context, Map cookies, boolean multipleStateQueryParams) { if (multipleStateQueryParams) { @@ -432,7 +456,8 @@ private Uni redirectToSessionExpiredPage(RoutingContext contex String sessionExpiredUri = sessionExpired.toString(); LOG.debugf("Session Expired URI: %s", sessionExpiredUri); return removeSessionCookie(context, configContext.oidcConfig) - .chain(() -> Uni.createFrom().failure(new AuthenticationRedirectException(sessionExpiredUri))); + .chain(() -> Uni.createFrom().failure(new AuthenticationRedirectException( + filterRedirect(context, configContext, sessionExpiredUri, Redirect.Location.SESSION_EXPIRED_PAGE)))); } private static String decryptIdTokenIfEncryptedByProvider(TenantConfigContext resolvedContext, String token) { @@ -692,6 +717,8 @@ && isRedirectFromProvider(context, configContext)) { String authorizationURL = configContext.provider.getMetadata().getAuthorizationUri() + "?" + codeFlowParams.toString(); + authorizationURL = filterRedirect(context, configContext, authorizationURL, + Redirect.Location.OIDC_AUTHORIZATION); LOG.debugf("Code flow redirect to: %s", authorizationURL); return Uni.createFrom().item(new ChallengeData(HttpResponseStatus.FOUND.code(), HttpHeaders.LOCATION, @@ -848,7 +875,9 @@ public SecurityIdentity apply(SecurityIdentity identity) { String finalRedirectUri = finalUriWithoutQuery.toString(); LOG.debugf("Removing code flow redirect parameters, final redirect URI: %s", finalRedirectUri); - throw new AuthenticationRedirectException(finalRedirectUri); + throw new AuthenticationRedirectException( + filterRedirect(context, configContext, finalRedirectUri, + Redirect.Location.LOCAL_ENDPOINT_CALLBACK)); } else { return identity; } @@ -872,10 +901,9 @@ public Throwable apply(Throwable tInner) { private static void logAuthenticationError(RoutingContext context, Throwable t) { final String errorMessage = errorMessage(t); - final boolean accessTokenFailure = context.get(OidcConstants.ACCESS_TOKEN_VALUE) != null - && context.get(OidcUtils.CODE_ACCESS_TOKEN_RESULT) == null; + final boolean accessTokenFailure = context.get(OidcUtils.CODE_ACCESS_TOKEN_FAILURE) != null; if (accessTokenFailure) { - LOG.errorf("Access token verification has failed: %s. ID token has not been verified yet", errorMessage); + LOG.errorf("Access token verification has failed: %s.", errorMessage); } else { LOG.errorf("ID token verification has failed: %s", errorMessage); } @@ -1151,18 +1179,9 @@ static ServerCookie createCookie(RoutingContext context, OidcTenantConfig oidcCo static ServerCookie createCookie(RoutingContext context, OidcTenantConfig oidcConfig, String name, String value, long maxAge, boolean sessionCookie) { - ServerCookie cookie = new CookieImpl(name, value); - cookie.setHttpOnly(true); - cookie.setSecure(oidcConfig.authentication.cookieForceSecure || context.request().isSSL()); - cookie.setMaxAge(maxAge); - LOG.debugf(name + " cookie 'max-age' parameter is set to %d", maxAge); - Authentication auth = oidcConfig.getAuthentication(); - OidcUtils.setCookiePath(context, auth, cookie); - if (auth.cookieDomain.isPresent()) { - cookie.setDomain(auth.getCookieDomain().get()); - } + ServerCookie cookie = OidcUtils.createCookie(context, oidcConfig, name, value, maxAge); if (sessionCookie) { - cookie.setSameSite(CookieSameSite.valueOf(auth.cookieSameSite.name())); + cookie.setSameSite(CookieSameSite.valueOf(oidcConfig.authentication.cookieSameSite.name())); } context.response().addCookie(cookie); return cookie; @@ -1369,7 +1388,8 @@ private Uni buildLogoutRedirectUriUni(RoutingContext context, TenantConfig public Void apply(Void t) { String logoutUri = buildLogoutRedirectUri(configContext, idToken, context); LOG.debugf("Logout uri: %s", logoutUri); - throw new AuthenticationRedirectException(logoutUri); + throw new AuthenticationRedirectException( + filterRedirect(context, configContext, logoutUri, Redirect.Location.OIDC_LOGOUT)); } }); } diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcIdentityProvider.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcIdentityProvider.java index e903255d343e7..1797e8f2812ce 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcIdentityProvider.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcIdentityProvider.java @@ -166,6 +166,7 @@ private Uni validateTokenWithUserInfoAndCreateIdentity(Map apply(TokenVerificationResult codeAccessToken, Throwable t) { if (t != null) { + requestData.put(OidcUtils.CODE_ACCESS_TOKEN_FAILURE, t); return Uni.createFrom().failure(new AuthenticationFailedException(t)); } @@ -217,6 +218,7 @@ public Uni apply(TokenVerificationResult result, Throwable t) public Uni apply(TokenVerificationResult codeAccessTokenResult, Throwable t) { if (t != null) { + requestData.put(OidcUtils.CODE_ACCESS_TOKEN_FAILURE, t); return Uni.createFrom().failure(t instanceof AuthenticationFailedException ? t : new AuthenticationFailedException(t)); } diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcUtils.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcUtils.java index d5c5d730a745e..03ddbacc4ea7b 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcUtils.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcUtils.java @@ -65,6 +65,7 @@ import io.vertx.core.http.HttpHeaders; import io.vertx.core.http.HttpMethod; import io.vertx.core.http.HttpServerRequest; +import io.vertx.core.http.impl.CookieImpl; import io.vertx.core.http.impl.ServerCookie; import io.vertx.core.json.JsonArray; import io.vertx.core.json.JsonObject; @@ -100,6 +101,7 @@ public final class OidcUtils { public static final String ANNOTATION_BASED_TENANT_RESOLUTION_ENABLED = "io.quarkus.oidc.runtime.select-tenants-with-annotation"; static final String UNDERSCORE = "_"; static final String CODE_ACCESS_TOKEN_RESULT = "code_flow_access_token_result"; + static final String CODE_ACCESS_TOKEN_FAILURE = "code_flow_access_token_failure"; static final String COMMA = ","; static final Uni VOID_UNI = Uni.createFrom().voidItem(); static final BlockingTaskRunner deleteTokensRequestContext = new BlockingTaskRunner(); @@ -491,7 +493,7 @@ static Uni removeSessionCookie(RoutingContext context, OidcTenantConfig oi } } - static String removeCookie(RoutingContext context, OidcTenantConfig oidcConfig, String cookieName) { + public static String removeCookie(RoutingContext context, OidcTenantConfig oidcConfig, String cookieName) { ServerCookie cookie = (ServerCookie) context.cookieMap().get(cookieName); String cookieValue = null; if (cookie != null) { @@ -786,4 +788,20 @@ public static boolean cacheUserInfoInIdToken(DefaultTenantConfigResolver resolve return resolver.getTokenStateManager() instanceof DefaultTokenStateManager && oidcConfig.tokenStateManager.encryptionRequired; } + + public static ServerCookie createCookie(RoutingContext context, OidcTenantConfig oidcConfig, + String name, String value, long maxAge) { + ServerCookie cookie = new CookieImpl(name, value); + cookie.setHttpOnly(true); + cookie.setSecure(oidcConfig.authentication.cookieForceSecure || context.request().isSSL()); + cookie.setMaxAge(maxAge); + LOG.debugf(name + " cookie 'max-age' parameter is set to %d", maxAge); + Authentication auth = oidcConfig.getAuthentication(); + OidcUtils.setCookiePath(context, oidcConfig.getAuthentication(), cookie); + if (auth.cookieDomain.isPresent()) { + cookie.setDomain(auth.getCookieDomain().get()); + } + context.response().addCookie(cookie); + return cookie; + } } diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/TenantConfigContext.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/TenantConfigContext.java index ce1c9b64eca99..da7ac79a6a364 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/TenantConfigContext.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/TenantConfigContext.java @@ -1,6 +1,10 @@ package io.quarkus.oidc.runtime; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import javax.crypto.KeyGenerator; import javax.crypto.SecretKey; @@ -8,9 +12,12 @@ import org.jboss.logging.Logger; +import io.quarkus.arc.ClientProxy; import io.quarkus.oidc.OIDCException; import io.quarkus.oidc.OidcConfigurationMetadata; +import io.quarkus.oidc.OidcRedirectFilter; import io.quarkus.oidc.OidcTenantConfig; +import io.quarkus.oidc.Redirect; import io.quarkus.oidc.common.runtime.OidcCommonUtils; import io.quarkus.runtime.configuration.ConfigurationException; @@ -27,6 +34,8 @@ public class TenantConfigContext { */ final OidcTenantConfig oidcConfig; + final Map> redirectFilters; + /** * PKCE Secret Key */ @@ -46,6 +55,7 @@ public TenantConfigContext(OidcProvider client, OidcTenantConfig config) { public TenantConfigContext(OidcProvider client, OidcTenantConfig config, boolean ready) { this.provider = client; this.oidcConfig = config; + this.redirectFilters = getRedirectFiltersMap(TenantFeatureFinder.find(config, OidcRedirectFilter.class)); this.ready = ready; boolean isService = OidcUtils.isServiceApp(config); @@ -174,4 +184,37 @@ public SecretKey getStateEncryptionKey() { public SecretKey getTokenEncSecretKey() { return tokenEncSecretKey; } + + private static Map> getRedirectFiltersMap(List filters) { + Map> map = new HashMap<>(); + for (OidcRedirectFilter filter : filters) { + Redirect redirect = ClientProxy.unwrap(filter).getClass().getAnnotation(Redirect.class); + if (redirect != null) { + for (Redirect.Location loc : redirect.value()) { + map.computeIfAbsent(loc, k -> new ArrayList()).add(filter); + } + } else { + map.computeIfAbsent(Redirect.Location.ALL, k -> new ArrayList()).add(filter); + } + } + return map; + } + + List getOidcRedirectFilters(Redirect.Location loc) { + List typeSpecific = redirectFilters.get(loc); + List all = redirectFilters.get(Redirect.Location.ALL); + if (typeSpecific == null && all == null) { + return List.of(); + } + if (typeSpecific != null && all == null) { + return typeSpecific; + } else if (typeSpecific == null && all != null) { + return all; + } else { + List combined = new ArrayList<>(typeSpecific.size() + all.size()); + combined.addAll(typeSpecific); + combined.addAll(all); + return combined; + } + } } 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 14f09b0a37753..4f67ed23eacdc 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,6 +1,9 @@ package io.quarkus.opentelemetry.deployment.tracing; import static io.quarkus.opentelemetry.runtime.config.build.OTelBuildConfig.SecurityEvents.SecurityEventType.ALL; +import static io.quarkus.opentelemetry.runtime.config.build.OTelBuildConfig.SecurityEvents.SecurityEventType.AUTHENTICATION_SUCCESS; +import static io.quarkus.opentelemetry.runtime.config.build.OTelBuildConfig.SecurityEvents.SecurityEventType.AUTHORIZATION_FAILURE; +import static io.quarkus.opentelemetry.runtime.config.build.OTelBuildConfig.SecurityEvents.SecurityEventType.AUTHORIZATION_SUCCESS; import java.net.URL; import java.util.ArrayList; @@ -49,6 +52,7 @@ import io.quarkus.opentelemetry.runtime.config.build.OTelBuildConfig.SecurityEvents.SecurityEventType; import io.quarkus.opentelemetry.runtime.tracing.TracerRecorder; import io.quarkus.opentelemetry.runtime.tracing.cdi.TracerProducer; +import io.quarkus.opentelemetry.runtime.tracing.security.EndUserSpanProcessor; import io.quarkus.opentelemetry.runtime.tracing.security.SecurityEventUtil; import io.quarkus.vertx.http.deployment.spi.FrameworkEndpointsBuildItem; import io.quarkus.vertx.http.deployment.spi.StaticResourcesBuildItem; @@ -198,6 +202,28 @@ void registerSecurityEventObserver(Capabilities capabilities, OTelBuildConfig bu } } + @BuildStep(onlyIf = EndUserAttributesEnabled.class) + void addEndUserAttributesSpanProcessor(BuildProducer additionalBeanProducer, + Capabilities capabilities) { + if (capabilities.isPresent(Capability.SECURITY)) { + additionalBeanProducer.produce(AdditionalBeanBuildItem.unremovableOf(EndUserSpanProcessor.class)); + } + } + + @BuildStep(onlyIf = EndUserAttributesEnabled.class) + void registerEndUserAttributesEventObserver(Capabilities capabilities, + ObserverRegistrationPhaseBuildItem observerRegistrationPhase, + BuildProducer observerProducer) { + if (capabilities.isPresent(Capability.SECURITY)) { + observerProducer + .produce(createEventObserver(observerRegistrationPhase, AUTHENTICATION_SUCCESS, "addEndUserAttributes")); + observerProducer + .produce(createEventObserver(observerRegistrationPhase, AUTHORIZATION_SUCCESS, "updateEndUserAttributes")); + observerProducer + .produce(createEventObserver(observerRegistrationPhase, AUTHORIZATION_FAILURE, "updateEndUserAttributes")); + } + } + private static ObserverConfiguratorBuildItem createEventObserver( ObserverRegistrationPhaseBuildItem observerRegistrationPhase, SecurityEventType eventType, String utilMethodName) { return new ObserverConfiguratorBuildItem(observerRegistrationPhase.getContext() @@ -232,4 +258,18 @@ public boolean getAsBoolean() { return enabled; } } + + static final class EndUserAttributesEnabled implements BooleanSupplier { + + private final boolean enabled; + + EndUserAttributesEnabled(OTelBuildConfig config) { + this.enabled = config.traces().addEndUserAttributes(); + } + + @Override + public boolean getAsBoolean() { + return enabled; + } + } } diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/build/TracesBuildConfig.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/build/TracesBuildConfig.java index 1c53611a14f1a..d387253747d62 100644 --- a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/build/TracesBuildConfig.java +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/build/TracesBuildConfig.java @@ -7,6 +7,7 @@ import io.quarkus.runtime.annotations.ConfigGroup; import io.smallrye.config.WithDefault; +import io.smallrye.config.WithName; /** * Tracing build time configuration @@ -51,4 +52,15 @@ public interface TracesBuildConfig { */ @WithDefault(SamplerType.Constants.PARENT_BASED_ALWAYS_ON) String sampler(); + + /** + * If OpenTelemetry End User attributes should be added as Span attributes on a best-efforts basis. + * + * @see OpenTelemetry End User + * attributes + */ + @WithName("eusp.enabled") + @WithDefault("false") + boolean addEndUserAttributes(); + } diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/security/EndUserSpanProcessor.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/security/EndUserSpanProcessor.java new file mode 100644 index 0000000000000..4660eced55417 --- /dev/null +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/security/EndUserSpanProcessor.java @@ -0,0 +1,35 @@ +package io.quarkus.opentelemetry.runtime.tracing.security; + +import jakarta.enterprise.context.Dependent; + +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.trace.ReadWriteSpan; +import io.opentelemetry.sdk.trace.ReadableSpan; +import io.opentelemetry.sdk.trace.SpanProcessor; + +/** + * Main purpose of this processor is to cover adding of the End User attributes to user-created Spans. + */ +@Dependent +public class EndUserSpanProcessor implements SpanProcessor { + + @Override + public void onStart(Context context, ReadWriteSpan span) { + SecurityEventUtil.addEndUserAttributes(span); + } + + @Override + public boolean isStartRequired() { + return true; + } + + @Override + public void onEnd(ReadableSpan readableSpan) { + + } + + @Override + public boolean isEndRequired() { + return false; + } +} diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/security/SecurityEventUtil.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/security/SecurityEventUtil.java index 674ded182b212..e7cb22987e320 100644 --- a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/security/SecurityEventUtil.java +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/security/SecurityEventUtil.java @@ -9,6 +9,8 @@ import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.common.AttributesBuilder; import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.semconv.SemanticAttributes; import io.quarkus.arc.Arc; import io.quarkus.security.identity.SecurityIdentity; import io.quarkus.security.spi.runtime.AuthenticationFailureEvent; @@ -16,10 +18,13 @@ import io.quarkus.security.spi.runtime.AuthorizationFailureEvent; import io.quarkus.security.spi.runtime.AuthorizationSuccessEvent; import io.quarkus.security.spi.runtime.SecurityEvent; +import io.quarkus.vertx.http.runtime.CurrentVertxRequest; +import io.quarkus.vertx.http.runtime.security.QuarkusHttpUser; +import io.vertx.ext.web.RoutingContext; /** * Synthetic CDI observers for various {@link SecurityEvent} types configured during the build time use this util class - * to export the events as the OpenTelemetry Span events. + * to export the events as the OpenTelemetry Span events, or authenticated user Span attributes. */ public final class SecurityEventUtil { public static final String QUARKUS_SECURITY_NAMESPACE = "quarkus.security."; @@ -38,8 +43,58 @@ private SecurityEventUtil() { // UTIL CLASS } + /** + * Adds Span attributes describing authenticated user if the user is authenticated and CDI request context is active. + * This will be true for example inside JAX-RS resources when the CDI request context is already setup and user code + * creates a new Span. + * + * @param span valid and recording Span; must not be null + */ + static void addEndUserAttributes(Span span) { + if (Arc.container().requestContext().isActive()) { + var currentVertxRequest = Arc.container().instance(CurrentVertxRequest.class).get(); + if (currentVertxRequest.getCurrent() != null) { + addEndUserAttribute(currentVertxRequest.getCurrent(), span); + } + } + } + + /** + * Updates authenticated user Span attributes if the {@link SecurityIdentity} got augmented during authorization. + * + * WARNING: This method is called from synthetic method observer. Any renaming must be reflected in the TracerProcessor. + * + * @param event {@link AuthorizationFailureEvent} + */ + public static void updateEndUserAttributes(AuthorizationFailureEvent event) { + addEndUserAttribute(event.getSecurityIdentity(), getSpan()); + } + + /** + * Updates authenticated user Span attributes if the {@link SecurityIdentity} got augmented during authorization. + * + * WARNING: This method is called from synthetic method observer. Any renaming must be reflected in the TracerProcessor. + * + * @param event {@link AuthorizationSuccessEvent} + */ + public static void updateEndUserAttributes(AuthorizationSuccessEvent event) { + addEndUserAttribute(event.getSecurityIdentity(), getSpan()); + } + + /** + * If there is already valid recording {@link Span}, attributes describing authenticated user are added to it. + * + * WARNING: This method is called from synthetic method observer. Any renaming must be reflected in the TracerProcessor. + * + * @param event {@link AuthenticationSuccessEvent} + */ + public static void addEndUserAttributes(AuthenticationSuccessEvent event) { + addEndUserAttribute(event.getSecurityIdentity(), getSpan()); + } + /** * Adds {@link SecurityEvent} as Span event. + * * WARNING: This method is called from synthetic method observer. Any renaming must be reflected in the TracerProcessor. */ public static void addAllEvents(SecurityEvent event) { @@ -57,6 +112,8 @@ public static void addAllEvents(SecurityEvent event) { } /** + * Adds {@link AuthenticationSuccessEvent} as Span event. + * * WARNING: This method is called from synthetic method observer. Any renaming must be reflected in the TracerProcessor. */ public static void addEvent(AuthenticationSuccessEvent event) { @@ -64,6 +121,8 @@ public static void addEvent(AuthenticationSuccessEvent event) { } /** + * Adds {@link AuthenticationFailureEvent} as Span event. + * * WARNING: This method is called from synthetic method observer. Any renaming must be reflected in the TracerProcessor. */ public static void addEvent(AuthenticationFailureEvent event) { @@ -71,6 +130,8 @@ public static void addEvent(AuthenticationFailureEvent event) { } /** + * Adds {@link AuthorizationSuccessEvent} as Span event. + * * WARNING: This method is called from synthetic method observer. Any renaming must be reflected in the TracerProcessor. */ public static void addEvent(AuthorizationSuccessEvent event) { @@ -79,6 +140,8 @@ public static void addEvent(AuthorizationSuccessEvent event) { } /** + * Adds {@link AuthorizationFailureEvent} as Span event. + * * WARNING: This method is called from synthetic method observer. Any renaming must be reflected in the TracerProcessor. */ public static void addEvent(AuthorizationFailureEvent event) { @@ -88,6 +151,7 @@ public static void addEvent(AuthorizationFailureEvent event) { /** * Adds {@link SecurityEvent} as Span event that is not authN/authZ success/failure. + * * WARNING: This method is called from synthetic method observer. Any renaming must be reflected in the TracerProcessor. */ public static void addEvent(SecurityEvent event) { @@ -112,15 +176,14 @@ public void accept(String key, Object value) { } private static void addEvent(String eventName, Attributes attributes) { - Span span = Arc.container().select(Span.class).get(); - if (span.getSpanContext().isValid() && span.isRecording()) { + Span span = getSpan(); + if (spanIsValidAndRecording(span)) { span.addEvent(eventName, attributes, Instant.now()); } } private static AttributesBuilder attributesBuilder(SecurityEvent event, String failureKey) { - Throwable failure = (Throwable) event.getEventProperties().get(failureKey); - if (failure != null) { + if (event.getEventProperties().get(failureKey) instanceof Throwable failure) { return attributesBuilder(event).put(FAILURE_NAME, failure.getClass().getName()); } return attributesBuilder(event); @@ -146,4 +209,55 @@ private static Attributes withAuthorizationContext(SecurityEvent event, Attribut } return builder.build(); } + + /** + * Adds Span attributes describing the authenticated user. + * + * @param event {@link RoutingContext}; must not be null + * @param span valid recording Span; must not be null + */ + private static void addEndUserAttribute(RoutingContext event, Span span) { + if (event.user() instanceof QuarkusHttpUser user) { + addEndUserAttribute(user.getSecurityIdentity(), span); + } + } + + /** + * Adds End User attributes to the {@code span}. Only authenticated user is added to the {@link Span}. + * Anonymous identity is ignored as it does not represent authenticated user. + * Passed {@code securityIdentity} is attached to the {@link Context} so that we recognize when identity changes. + * + * @param securityIdentity SecurityIdentity + * @param span Span + */ + private static void addEndUserAttribute(SecurityIdentity securityIdentity, Span span) { + if (securityIdentity != null && !securityIdentity.isAnonymous() && spanIsValidAndRecording(span)) { + span.setAllAttributes(Attributes.of( + SemanticAttributes.ENDUSER_ID, + securityIdentity.getPrincipal().getName(), + SemanticAttributes.ENDUSER_ROLE, + getRoles(securityIdentity))); + } + } + + private static String getRoles(SecurityIdentity securityIdentity) { + try { + return securityIdentity.getRoles().toString(); + } catch (UnsupportedOperationException e) { + // getting roles is not supported when the identity is enhanced by custom jakarta.ws.rs.core.SecurityContext + return ""; + } + } + + private static Span getSpan() { + if (Arc.container().requestContext().isActive()) { + return Arc.container().select(Span.class).get(); + } else { + return Span.current(); + } + } + + private static boolean spanIsValidAndRecording(Span span) { + return span.isRecording() && span.getSpanContext().isValid(); + } } 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 b764e7df152b0..9dee2dd5172f3 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 @@ -106,7 +106,12 @@ void replaceFieldAccesses(CombinedIndexBuildItem index, PanacheJpaEntityAccessorsEnhancer entityAccessorsEnhancer = new PanacheJpaEntityAccessorsEnhancer(index.getIndex(), modelInfo); for (String entityClassName : entitiesWithExternallyAccessibleFields) { - transformers.produce(new BytecodeTransformerBuildItem(entityClassName, entityAccessorsEnhancer)); + final BytecodeTransformerBuildItem transformation = new BytecodeTransformerBuildItem.Builder() + .setClassToTransform(entityClassName) + .setCacheable(true) + .setVisitorFunction(entityAccessorsEnhancer) + .build(); + transformers.produce(transformation); } // Replace field access in application code with calls to accessors @@ -125,8 +130,17 @@ void replaceFieldAccesses(CombinedIndexBuildItem index, continue; } produced.add(cn); - transformers.produce( - new BytecodeTransformerBuildItem(cn, panacheFieldAccessEnhancer, entityClassNamesInternal)); + //The following build item is not marked as CacheAble intentionally: see also https://github.com/quarkusio/quarkus/pull/40192#discussion_r1590605375. + //It shouldn't be too hard to improve on this by checking the related entities haven't been changed + //via LiveReloadBuildItem (#isLiveReload() && #getChangeInformation()) but I'm not comfortable in making this + //change without having solid integration tests. + final BytecodeTransformerBuildItem transformation = new BytecodeTransformerBuildItem.Builder() + .setClassToTransform(cn) + .setCacheable(false)//TODO this would be nice to improve on: see note above. + .setVisitorFunction(panacheFieldAccessEnhancer) + .setRequireConstPoolEntry(entityClassNamesInternal) + .build(); + transformers.produce(transformation); } } } diff --git a/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzExtensionPointConfig.java b/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzExtensionPointConfig.java index d0df8c85d4c6a..84d31f6d4b5f3 100644 --- a/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzExtensionPointConfig.java +++ b/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzExtensionPointConfig.java @@ -18,6 +18,6 @@ public class QuartzExtensionPointConfig { * The properties passed to the class. */ @ConfigItem - @ConfigDocMapKey("property-name") + @ConfigDocMapKey("property-key") public Map properties; } diff --git a/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/QuteConfig.java b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/QuteConfig.java index ad59c9a174406..0c1888fb1f738 100644 --- a/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/QuteConfig.java +++ b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/QuteConfig.java @@ -6,6 +6,7 @@ import java.util.Optional; import java.util.regex.Pattern; +import io.quarkus.runtime.annotations.ConfigDocMapKey; import io.quarkus.runtime.annotations.ConfigItem; import io.quarkus.runtime.annotations.ConfigPhase; import io.quarkus.runtime.annotations.ConfigRoot; @@ -28,6 +29,7 @@ public class QuteConfig { * {@link java.net.URLConnection#getFileNameMap()} is used to determine the content type of a template file. */ @ConfigItem + @ConfigDocMapKey("file-suffix") public Map contentTypes; /** diff --git a/extensions/reactive-datasource/runtime/src/main/java/io/quarkus/reactive/datasource/runtime/DataSourceReactiveRuntimeConfig.java b/extensions/reactive-datasource/runtime/src/main/java/io/quarkus/reactive/datasource/runtime/DataSourceReactiveRuntimeConfig.java index 555ecb3b82b77..2cecb4f2ac9ab 100644 --- a/extensions/reactive-datasource/runtime/src/main/java/io/quarkus/reactive/datasource/runtime/DataSourceReactiveRuntimeConfig.java +++ b/extensions/reactive-datasource/runtime/src/main/java/io/quarkus/reactive/datasource/runtime/DataSourceReactiveRuntimeConfig.java @@ -7,6 +7,7 @@ import java.util.OptionalInt; import io.quarkus.runtime.annotations.ConfigDocDefault; +import io.quarkus.runtime.annotations.ConfigDocMapKey; import io.quarkus.runtime.annotations.ConfigGroup; import io.quarkus.vertx.core.runtime.config.JksConfiguration; import io.quarkus.vertx.core.runtime.config.PemKeyCertConfiguration; @@ -147,5 +148,6 @@ public interface DataSourceReactiveRuntimeConfig { * Other unspecified properties to be passed through the Reactive SQL Client directly to the database when new connections * are initiated. */ + @ConfigDocMapKey("property-key") Map additionalProperties(); } diff --git a/extensions/redis-client/deployment/src/main/java/io/quarkus/redis/deployment/client/DevServicesConfig.java b/extensions/redis-client/deployment/src/main/java/io/quarkus/redis/deployment/client/DevServicesConfig.java index 308723ee1ba66..48623c9b696e0 100644 --- a/extensions/redis-client/deployment/src/main/java/io/quarkus/redis/deployment/client/DevServicesConfig.java +++ b/extensions/redis-client/deployment/src/main/java/io/quarkus/redis/deployment/client/DevServicesConfig.java @@ -4,6 +4,7 @@ import java.util.Optional; import java.util.OptionalInt; +import io.quarkus.runtime.annotations.ConfigDocMapKey; import io.quarkus.runtime.annotations.ConfigGroup; import io.smallrye.config.WithDefault; @@ -64,5 +65,6 @@ public interface DevServicesConfig { /** * Environment variables that are passed to the container. */ + @ConfigDocMapKey("environment-variable-name") Map containerEnv(); } diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/ReactiveJsonCommandsImpl.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/ReactiveJsonCommandsImpl.java index 166cae16936e3..d171793b2bf26 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/ReactiveJsonCommandsImpl.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/ReactiveJsonCommandsImpl.java @@ -111,7 +111,7 @@ static JsonObject getJsonObject(Response r) { } // With Redis 7.2 the response is a BULK (String) but using a nested array. Buffer buffer = r.toBuffer(); - if (buffer.toJson() instanceof JsonArray) { + if (buffer.toJsonValue() instanceof JsonArray) { var array = buffer.toJsonArray(); if (array.size() == 0) { return null; @@ -127,7 +127,7 @@ static JsonArray getJsonArrayFromJsonGet(Response r) { } // With Redis 7.2 the response is a BULK (String) but using a nested array. Buffer buffer = r.toBuffer(); - if (buffer.toJson() instanceof JsonArray) { + if (buffer.toJsonValue() instanceof JsonArray) { var array = buffer.toJsonArray(); if (array.size() == 0) { return new JsonArray(); diff --git a/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientConfig.java b/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientConfig.java index ff96a36023317..71c1ba1be8d2d 100644 --- a/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientConfig.java +++ b/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientConfig.java @@ -9,6 +9,7 @@ import org.eclipse.microprofile.rest.client.ext.QueryParamStyle; import io.quarkus.runtime.annotations.ConfigDocDefault; +import io.quarkus.runtime.annotations.ConfigDocMapKey; import io.quarkus.runtime.annotations.ConfigGroup; import io.quarkus.runtime.annotations.ConfigItem; import io.quarkus.runtime.configuration.MemorySize; @@ -233,6 +234,7 @@ public class RestClientConfig { * This property is not applicable to the RESTEasy Client. */ @ConfigItem + @ConfigDocMapKey("header-name") public Map headers; /** diff --git a/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientsConfig.java b/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientsConfig.java index d70ba974572de..b02e27baa1aa1 100644 --- a/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientsConfig.java +++ b/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientsConfig.java @@ -12,6 +12,7 @@ import io.quarkus.arc.Arc; import io.quarkus.arc.InstanceHandle; import io.quarkus.runtime.annotations.ConfigDocDefault; +import io.quarkus.runtime.annotations.ConfigDocMapKey; import io.quarkus.runtime.annotations.ConfigItem; import io.quarkus.runtime.annotations.ConfigPhase; import io.quarkus.runtime.annotations.ConfigRoot; @@ -134,6 +135,7 @@ public class RestClientsConfig { * The HTTP headers that should be applied to all requests of the rest client. */ @ConfigItem + @ConfigDocMapKey("header-name") public Map headers; /** diff --git a/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/UserFriendlyQuarkusRESTCapabilityCombinationTest.java b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/UserFriendlyQuarkusRESTCapabilityCombinationTest.java new file mode 100644 index 0000000000000..371aebc339999 --- /dev/null +++ b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/UserFriendlyQuarkusRESTCapabilityCombinationTest.java @@ -0,0 +1,31 @@ +package io.quarkus.resteasy.test; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.builder.Version; +import io.quarkus.deployment.Capability; +import io.quarkus.maven.dependency.Dependency; +import io.quarkus.test.QuarkusUnitTest; + +class UserFriendlyQuarkusRESTCapabilityCombinationTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setForcedDependencies(List.of(Dependency.of("io.quarkus", "quarkus-rest-deployment", Version.getVersion()))) + .assertException(t -> { + assertTrue(t.getMessage().contains("only one provider of the following capabilities"), t.getMessage()); + assertTrue(t.getMessage().contains("capability %s is provided by".formatted(Capability.REST)), t.getMessage()); + }); + + @Test + public void test() { + fail(); + } + +} diff --git a/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/AbstractSecurityEventTest.java b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/AbstractSecurityEventTest.java index 007fbb74b414e..87b13fd62d288 100644 --- a/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/AbstractSecurityEventTest.java +++ b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/AbstractSecurityEventTest.java @@ -8,6 +8,7 @@ import java.time.Duration; import java.util.List; +import java.util.Objects; import java.util.concurrent.CopyOnWriteArrayList; import jakarta.enterprise.event.Observes; @@ -22,6 +23,7 @@ import io.quarkus.security.AuthenticationFailedException; import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.security.runtime.interceptor.check.RolesAllowedCheck; import io.quarkus.security.spi.runtime.AuthenticationFailureEvent; import io.quarkus.security.spi.runtime.AuthenticationSuccessEvent; import io.quarkus.security.spi.runtime.AuthorizationFailureEvent; @@ -38,7 +40,7 @@ public abstract class AbstractSecurityEventTest { protected static final Class[] TEST_CLASSES = { RolesAllowedResource.class, TestIdentityProvider.class, TestIdentityController.class, UnsecuredResource.class, UnsecuredSubResource.class, EventObserver.class, UnsecuredResourceInterface.class, - UnsecuredParentResource.class + UnsecuredParentResource.class, RolesAllowedService.class, RolesAllowedServiceResource.class }; @Inject @@ -95,6 +97,99 @@ public void testRolesAllowed() { assertAsyncAuthZFailureObserved(2); } + @Test + public void testNestedRolesAllowed() { + // there are 2 different checks in place: user & admin on resource, admin on service + RestAssured.given().auth().preemptive().basic("admin", "admin").get("/roles-service/hello").then().statusCode(200) + .body(is(RolesAllowedService.SERVICE_HELLO)); + assertSyncObserved(3); + AuthenticationSuccessEvent successEvent = (AuthenticationSuccessEvent) observer.syncEvents.get(0); + SecurityIdentity identity = successEvent.getSecurityIdentity(); + assertNotNull(identity); + assertEquals("admin", identity.getPrincipal().getName()); + RoutingContext routingContext = (RoutingContext) successEvent.getEventProperties().get(RoutingContext.class.getName()); + assertNotNull(routingContext); + assertTrue(routingContext.request().path().endsWith("/roles-service/hello")); + // authorization success on endpoint + AuthorizationSuccessEvent authZSuccessEvent = (AuthorizationSuccessEvent) observer.syncEvents.get(1); + assertEquals(identity, authZSuccessEvent.getSecurityIdentity()); + identity = authZSuccessEvent.getSecurityIdentity(); + assertEquals(routingContext, authZSuccessEvent.getEventProperties().get(RoutingContext.class.getName())); + String securedMethod = (String) authZSuccessEvent.getEventProperties() + .get(AuthorizationSuccessEvent.SECURED_METHOD_KEY); + assertEquals("io.quarkus.resteasy.test.security.RolesAllowedServiceResource#getServiceHello", + securedMethod); + // authorization success on service level performed by CDI interceptor + authZSuccessEvent = (AuthorizationSuccessEvent) observer.syncEvents.get(2); + securedMethod = (String) authZSuccessEvent.getEventProperties().get(AuthorizationSuccessEvent.SECURED_METHOD_KEY); + assertEquals("io.quarkus.resteasy.test.security.RolesAllowedService#hello", securedMethod); + assertEquals(identity, authZSuccessEvent.getSecurityIdentity()); + assertNotNull(authZSuccessEvent.getEventProperties().get(RoutingContext.class.getName())); + assertAsyncAuthZFailureObserved(0); + RestAssured.given().auth().preemptive().basic("user", "user").get("/roles-service/hello").then().statusCode(403); + assertSyncObserved(6); + // "roles-service" Jakarta REST resource requires 'admin' or 'user' role, therefore check succeeds + successEvent = (AuthenticationSuccessEvent) observer.syncEvents.get(3); + identity = successEvent.getSecurityIdentity(); + assertNotNull(identity); + assertEquals("user", identity.getPrincipal().getName()); + routingContext = (RoutingContext) successEvent.getEventProperties().get(RoutingContext.class.getName()); + assertNotNull(routingContext); + assertTrue(routingContext.request().path().endsWith("/roles-service/hello")); + authZSuccessEvent = (AuthorizationSuccessEvent) observer.syncEvents.get(4); + assertEquals(identity, authZSuccessEvent.getSecurityIdentity()); + assertEquals(routingContext, authZSuccessEvent.getEventProperties().get(RoutingContext.class.getName())); + // RolesService requires 'admin' role, therefore user fails + assertAsyncAuthZFailureObserved(1); + AuthorizationFailureEvent authZFailureEvent = observer.asyncAuthZFailureEvents.get(0); + securedMethod = (String) authZFailureEvent.getEventProperties().get(AuthorizationFailureEvent.SECURED_METHOD_KEY); + assertEquals("io.quarkus.resteasy.test.security.RolesAllowedService#hello", securedMethod); + SecurityIdentity userIdentity = authZFailureEvent.getSecurityIdentity(); + assertNotNull(userIdentity); + assertTrue(userIdentity.hasRole("user")); + assertEquals("user", userIdentity.getPrincipal().getName()); + assertNotNull(authZFailureEvent.getEventProperties().get(RoutingContext.class.getName())); + assertEquals(RolesAllowedCheck.class.getName(), authZFailureEvent.getAuthorizationContext()); + } + + @Test + public void testNestedPermitAll() { + // @PermitAll is on CDI bean but resource is not secured + RestAssured.given().auth().preemptive().basic("admin", "admin").get("/roles-service/bye").then().statusCode(200) + .body(is(RolesAllowedService.SERVICE_BYE)); + final int expectedEventsCount; + if (isProactiveAuth()) { + // auth + @PermitAll + expectedEventsCount = 2; + } else { + // @PermitAll + expectedEventsCount = 1; + } + assertSyncObserved(expectedEventsCount, true); + + if (expectedEventsCount == 2) { + AuthenticationSuccessEvent successEvent = (AuthenticationSuccessEvent) observer.syncEvents.get(0); + SecurityIdentity identity = successEvent.getSecurityIdentity(); + assertNotNull(identity); + assertEquals("admin", identity.getPrincipal().getName()); + RoutingContext routingContext = (RoutingContext) successEvent.getEventProperties() + .get(RoutingContext.class.getName()); + assertNotNull(routingContext); + assertTrue(routingContext.request().path().endsWith("/roles-service/bye")); + } + // authorization success on service level performed by CDI interceptor + var authZSuccessEvent = (AuthorizationSuccessEvent) observer.syncEvents.get(expectedEventsCount - 1); + String securedMethod = (String) authZSuccessEvent.getEventProperties() + .get(AuthorizationSuccessEvent.SECURED_METHOD_KEY); + assertEquals("io.quarkus.resteasy.test.security.RolesAllowedService#bye", securedMethod); + assertNotNull(authZSuccessEvent.getEventProperties().get(RoutingContext.class.getName())); + if (isProactiveAuth()) { + assertNotNull(authZSuccessEvent.getSecurityIdentity()); + assertEquals("admin", authZSuccessEvent.getSecurityIdentity().getPrincipal().getName()); + } + assertAsyncAuthZFailureObserved(0); + } + @Test public void testAuthenticated() { RestAssured.given().auth().preemptive().basic("admin", "admin").get("/unsecured/authenticated").then().statusCode(200) @@ -115,6 +210,8 @@ public void testAuthenticated() { SecurityIdentity anonymousIdentity = authZFailure.getSecurityIdentity(); assertNotNull(anonymousIdentity); assertTrue(anonymousIdentity.isAnonymous()); + String securedMethod = (String) authZFailure.getEventProperties().get(AuthorizationFailureEvent.SECURED_METHOD_KEY); + assertEquals("io.quarkus.resteasy.test.security.UnsecuredResource#authenticated", securedMethod); } @Test @@ -152,8 +249,7 @@ public void testPermitAll() { assertNotNull(routingContext); assertTrue(routingContext.request().path().endsWith("/unsecured/permitAll")); AuthorizationSuccessEvent authZSuccessEvent = (AuthorizationSuccessEvent) observer.syncEvents.get(1); - // SecurityIdentity is not required for the permit all check - assertNull(authZSuccessEvent.getSecurityIdentity()); + assertNotNull(authZSuccessEvent.getSecurityIdentity()); } else { assertSyncObserved(1, true); AuthorizationSuccessEvent authZSuccessEvent = (AuthorizationSuccessEvent) observer.syncEvents.get(0); @@ -186,6 +282,9 @@ private void assertAsyncAuthZFailureObserved(int count) { .untilAsserted(() -> assertEquals(count, observer.asyncAuthZFailureEvents.size())); if (count > 0) { assertTrue(observer.asyncAuthZFailureEvents.stream().allMatch(e -> e.getSecurityIdentity() != null)); + assertTrue(observer.asyncAuthZFailureEvents.stream() + .map(e -> e.getEventProperties().get(RoutingContext.class.getName())) + .allMatch(Objects::nonNull)); } } diff --git a/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/RolesAllowedService.java b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/RolesAllowedService.java new file mode 100644 index 0000000000000..36931b20d44cd --- /dev/null +++ b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/RolesAllowedService.java @@ -0,0 +1,23 @@ +package io.quarkus.resteasy.test.security; + +import jakarta.annotation.security.PermitAll; +import jakarta.annotation.security.RolesAllowed; +import jakarta.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class RolesAllowedService { + + public static final String SERVICE_HELLO = "Hello from Service!"; + public static final String SERVICE_BYE = "Bye from Service!"; + + @RolesAllowed("admin") + public String hello() { + return SERVICE_HELLO; + } + + @PermitAll + public String bye() { + return SERVICE_BYE; + } + +} diff --git a/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/RolesAllowedServiceResource.java b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/RolesAllowedServiceResource.java new file mode 100644 index 0000000000000..f621b73b0da64 --- /dev/null +++ b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/RolesAllowedServiceResource.java @@ -0,0 +1,27 @@ +package io.quarkus.resteasy.test.security; + +import jakarta.annotation.security.RolesAllowed; +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +@Path("/roles-service") +public class RolesAllowedServiceResource { + + @Inject + RolesAllowedService rolesAllowedService; + + @Path("/hello") + @RolesAllowed({ "user", "admin" }) + @GET + public String getServiceHello() { + return rolesAllowedService.hello(); + } + + @Path("/bye") + @GET + public String getServiceBye() { + return rolesAllowedService.bye(); + } + +} diff --git a/extensions/resteasy-classic/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/EagerSecurityFilter.java b/extensions/resteasy-classic/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/EagerSecurityFilter.java index 950a3c355fe91..5453a44eedd65 100644 --- a/extensions/resteasy-classic/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/EagerSecurityFilter.java +++ b/extensions/resteasy-classic/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/EagerSecurityFilter.java @@ -22,7 +22,9 @@ import io.quarkus.security.spi.runtime.MethodDescription; import io.quarkus.security.spi.runtime.SecurityCheck; import io.quarkus.security.spi.runtime.SecurityCheckStorage; +import io.quarkus.vertx.http.runtime.CurrentVertxRequest; import io.quarkus.vertx.http.runtime.security.EagerSecurityInterceptorStorage; +import io.quarkus.vertx.http.runtime.security.QuarkusHttpUser; import io.vertx.ext.web.RoutingContext; @Priority(Priorities.AUTHENTICATION) @@ -35,7 +37,7 @@ public class EagerSecurityFilter implements ContainerRequestFilter { ResourceInfo resourceInfo; @Inject - RoutingContext routingContext; + CurrentVertxRequest currentVertxRequest; @Inject SecurityCheckStorage securityCheckStorage; @@ -71,19 +73,27 @@ public void filter(ContainerRequestContext requestContext) throws IOException { private void applySecurityChecks(MethodDescription description) { SecurityCheck check = securityCheckStorage.getSecurityCheck(description); if (check == null && securityCheckStorage.getDefaultSecurityCheck() != null - && routingContext.get(EagerSecurityFilter.class.getName()) == null - && routingContext.get(SKIP_DEFAULT_CHECK) == null) { + && routingContext().get(EagerSecurityFilter.class.getName()) == null + && routingContext().get(SKIP_DEFAULT_CHECK) == null) { check = securityCheckStorage.getDefaultSecurityCheck(); } if (check != null) { if (check.isPermitAll()) { - fireEventOnAuthZSuccess(check, null); + // add the identity only if authentication has already finished + final SecurityIdentity identity; + if (routingContext().user() instanceof QuarkusHttpUser user) { + identity = user.getSecurityIdentity(); + } else { + identity = null; + } + + fireEventOnAuthZSuccess(check, identity, description); } else { if (check.requiresMethodArguments()) { if (identityAssociation.getIdentity().isAnonymous()) { var exception = new UnauthorizedException(); if (jaxRsPermissionChecker.getEventHelper().fireEventOnFailure()) { - fireEventOnAuthZFailure(exception, check); + fireEventOnAuthZFailure(exception, check, description); } throw exception; } @@ -94,36 +104,43 @@ private void applySecurityChecks(MethodDescription description) { try { check.apply(identityAssociation.getIdentity(), description, null); } catch (Exception e) { - fireEventOnAuthZFailure(e, check); + fireEventOnAuthZFailure(e, check, description); throw e; } } else { check.apply(identityAssociation.getIdentity(), description, null); } - fireEventOnAuthZSuccess(check, identityAssociation.getIdentity()); + fireEventOnAuthZSuccess(check, identityAssociation.getIdentity(), description); } // prevent repeated security checks - routingContext.put(EagerSecurityFilter.class.getName(), resourceInfo.getResourceMethod()); + routingContext().put(EagerSecurityFilter.class.getName(), resourceInfo.getResourceMethod()); } } - private void fireEventOnAuthZFailure(Exception exception, SecurityCheck check) { + private void fireEventOnAuthZFailure(Exception exception, SecurityCheck check, MethodDescription description) { jaxRsPermissionChecker.getEventHelper().fireFailureEvent(new AuthorizationFailureEvent( identityAssociation.getIdentity(), exception, check.getClass().getName(), - Map.of(RoutingContext.class.getName(), routingContext))); + Map.of(RoutingContext.class.getName(), routingContext()), description)); } - private void fireEventOnAuthZSuccess(SecurityCheck check, SecurityIdentity securityIdentity) { + private void fireEventOnAuthZSuccess(SecurityCheck check, SecurityIdentity securityIdentity, + MethodDescription description) { if (jaxRsPermissionChecker.getEventHelper().fireEventOnSuccess()) { jaxRsPermissionChecker.getEventHelper().fireSuccessEvent(new AuthorizationSuccessEvent(securityIdentity, - check.getClass().getName(), Map.of(RoutingContext.class.getName(), routingContext))); + check.getClass().getName(), Map.of(RoutingContext.class.getName(), routingContext()), description)); } } + private RoutingContext routingContext() { + // use actual RoutingContext (not the bean) to async events are invoked with new CDI request context + // where the RoutingContext is not available + return currentVertxRequest.getCurrent(); + } + private void applyEagerSecurityInterceptors(MethodDescription description) { var interceptor = interceptorStorage.getInterceptor(description); if (interceptor != null) { - interceptor.accept(routingContext); + interceptor.accept(routingContext()); } } } diff --git a/extensions/resteasy-classic/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/StandardSecurityCheckInterceptor.java b/extensions/resteasy-classic/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/StandardSecurityCheckInterceptor.java index 657be48853e07..eb82be3eabf5d 100644 --- a/extensions/resteasy-classic/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/StandardSecurityCheckInterceptor.java +++ b/extensions/resteasy-classic/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/StandardSecurityCheckInterceptor.java @@ -58,7 +58,7 @@ public Object intercept(InvocationContext ic) throws Exception { */ @Interceptor @RolesAllowed("") - @Priority(Interceptor.Priority.PLATFORM_BEFORE) + @Priority(Interceptor.Priority.LIBRARY_BEFORE - 100) public static final class RolesAllowedInterceptor extends StandardSecurityCheckInterceptor { } @@ -68,7 +68,7 @@ public static final class RolesAllowedInterceptor extends StandardSecurityCheckI */ @Interceptor @PermissionsAllowed("") - @Priority(Interceptor.Priority.PLATFORM_BEFORE) + @Priority(Interceptor.Priority.LIBRARY_BEFORE - 100) public static final class PermissionsAllowedInterceptor extends StandardSecurityCheckInterceptor { } @@ -78,7 +78,7 @@ public static final class PermissionsAllowedInterceptor extends StandardSecurity */ @Interceptor @PermitAll - @Priority(Interceptor.Priority.PLATFORM_BEFORE) + @Priority(Interceptor.Priority.LIBRARY_BEFORE - 100) public static final class PermitAllInterceptor extends StandardSecurityCheckInterceptor { } @@ -88,7 +88,7 @@ public static final class PermitAllInterceptor extends StandardSecurityCheckInte */ @Interceptor @Authenticated - @Priority(Interceptor.Priority.PLATFORM_BEFORE) + @Priority(Interceptor.Priority.LIBRARY_BEFORE - 100) public static final class AuthenticatedInterceptor extends StandardSecurityCheckInterceptor { } diff --git a/extensions/resteasy-reactive/rest-client-jaxrs/deployment/src/main/java/io/quarkus/jaxrs/client/reactive/deployment/JaxrsClientReactiveProcessor.java b/extensions/resteasy-reactive/rest-client-jaxrs/deployment/src/main/java/io/quarkus/jaxrs/client/reactive/deployment/JaxrsClientReactiveProcessor.java index 9529ffff88426..73c7537d32921 100644 --- a/extensions/resteasy-reactive/rest-client-jaxrs/deployment/src/main/java/io/quarkus/jaxrs/client/reactive/deployment/JaxrsClientReactiveProcessor.java +++ b/extensions/resteasy-reactive/rest-client-jaxrs/deployment/src/main/java/io/quarkus/jaxrs/client/reactive/deployment/JaxrsClientReactiveProcessor.java @@ -1030,7 +1030,7 @@ A more full example of generated client (with sub-resource) can is at the bottom // NOTE: don't use type here, because we're not going through the collection converters and stuff Type parameterType = jandexMethod.parameterType(paramIdx); addFormParam(methodCreator, param.name, methodCreator.getMethodParam(paramIdx), - parameterType, param.declaredType, param.signature, index, + parameterType, param.signature, index, restClientInterface.getClassName(), methodCreator.getThis(), formParams, getGenericTypeFromArray(methodCreator, methodGenericParametersField, paramIdx), getAnnotationsFromArray(methodCreator, methodParamAnnotationsField, paramIdx), @@ -2538,7 +2538,7 @@ private void addSubBeanParamData(MethodInfo jandexMethod, int paramIndex, Byteco case FORM_PARAM: FormParamItem formParam = (FormParamItem) item; addFormParam(creator, formParam.getFormParamName(), formParam.extract(creator, param), - jandexMethod.parameterType(paramIndex), formParam.getParamType(), formParam.getParamSignature(), + formParam.getParamType(), formParam.getParamSignature(), index, restClientInterfaceClassName, client, formParams, @@ -2810,7 +2810,6 @@ private void addFormParam(BytecodeCreator methodCreator, String paramName, ResultHandle formParamHandle, Type parameterType, - String parameterTypeStr, String parameterSignature, IndexView index, String restClientInterfaceClassName, ResultHandle client, AssignableResultHandle formParams, @@ -2818,7 +2817,8 @@ private void addFormParam(BytecodeCreator methodCreator, ResultHandle parameterAnnotations, boolean multipart, String partType, String partFilename, String errorLocation) { if (multipart) { - handleMultipartField(paramName, partType, partFilename, parameterTypeStr, parameterSignature, formParamHandle, + handleMultipartField(paramName, partType, partFilename, parameterType.name().toString(), parameterSignature, + formParamHandle, formParams, methodCreator, client, restClientInterfaceClassName, parameterAnnotations, genericType, errorLocation); @@ -2846,7 +2846,8 @@ private void addFormParam(BytecodeCreator methodCreator, creator.invokeInterfaceMethod(MULTIVALUED_MAP_ADD_ALL, formParams, creator.load(paramName), convertedParamArray); } else { - ResultHandle convertedFormParam = convertParamToString(creator, client, formParamHandle, parameterTypeStr, + ResultHandle convertedFormParam = convertParamToString(creator, client, formParamHandle, + parameterType.name().toString(), genericType, parameterAnnotations); BytecodeCreator parameterIsStringBranch = checkStringParam(creator, convertedFormParam, restClientInterfaceClassName, errorLocation); diff --git a/extensions/resteasy-reactive/rest-client/deployment/src/main/java/io/quarkus/rest/client/reactive/deployment/RestClientReactiveProcessor.java b/extensions/resteasy-reactive/rest-client/deployment/src/main/java/io/quarkus/rest/client/reactive/deployment/RestClientReactiveProcessor.java index c673933a9cb1c..07d3e0cebed69 100644 --- a/extensions/resteasy-reactive/rest-client/deployment/src/main/java/io/quarkus/rest/client/reactive/deployment/RestClientReactiveProcessor.java +++ b/extensions/resteasy-reactive/rest-client/deployment/src/main/java/io/quarkus/rest/client/reactive/deployment/RestClientReactiveProcessor.java @@ -34,6 +34,7 @@ import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.function.Predicate; import java.util.stream.Collectors; import jakarta.enterprise.context.SessionScoped; @@ -90,6 +91,7 @@ import io.quarkus.deployment.builditem.GeneratedClassBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; import io.quarkus.deployment.builditem.nativeimage.ServiceProviderBuildItem; +import io.quarkus.deployment.execannotations.ExecutionModelAnnotationsAllowedBuildItem; import io.quarkus.gizmo.ClassCreator; import io.quarkus.gizmo.MethodCreator; import io.quarkus.gizmo.MethodDescriptor; @@ -251,19 +253,16 @@ public void registerProvidersInstances(CombinedIndexBuildItem indexBuildItem, *

  • registers all the provider implementations annotated with @Provider using * {@link AnnotationRegisteredProviders#addGlobalProvider(Class, int)}
  • * - * - * - * @param indexBuildItem index - * @param generatedBeans build producer for generated beans */ @BuildStep void registerProvidersFromAnnotations(CombinedIndexBuildItem indexBuildItem, List registerProviderAnnotationInstances, List annotationsToRegisterIntoClientContext, - BuildProducer generatedBeans, - BuildProducer generatedClasses, - BuildProducer unremovableBeans, - BuildProducer reflectiveClasses, + BuildProducer generatedBeansProducer, + BuildProducer generatedClassesProducer, + BuildProducer unremovableBeansProducer, + BuildProducer reflectiveClassesProducer, + BuildProducer executionModelAnnotationsAllowedProducer, RestClientReactiveConfig clientConfig) { String annotationRegisteredProvidersImpl = AnnotationRegisteredProviders.class.getName() + "Implementation"; IndexView index = indexBuildItem.getIndex(); @@ -276,7 +275,7 @@ void registerProvidersFromAnnotations(CombinedIndexBuildItem indexBuildItem, try (ClassCreator classCreator = ClassCreator.builder() .className(annotationRegisteredProvidersImpl) - .classOutput(new GeneratedBeanGizmoAdaptor(generatedBeans)) + .classOutput(new GeneratedBeanGizmoAdaptor(generatedBeansProducer)) .superClass(AnnotationRegisteredProviders.class) .build()) { @@ -316,12 +315,13 @@ void registerProvidersFromAnnotations(CombinedIndexBuildItem indexBuildItem, } MultivaluedMap generatedProviders = new QuarkusMultivaluedHashMap<>(); - populateClientExceptionMapperFromAnnotations(generatedClasses, reflectiveClasses, index) + populateClientExceptionMapperFromAnnotations(index, generatedClassesProducer, reflectiveClassesProducer, + executionModelAnnotationsAllowedProducer) .forEach(generatedProviders::add); - populateClientRedirectHandlerFromAnnotations(generatedClasses, reflectiveClasses, index) + populateClientRedirectHandlerFromAnnotations(generatedClassesProducer, reflectiveClassesProducer, index) .forEach(generatedProviders::add); for (AnnotationToRegisterIntoClientContextBuildItem annotation : annotationsToRegisterIntoClientContext) { - populateClientProviderFromAnnotations(annotation, generatedClasses, reflectiveClasses, index) + populateClientProviderFromAnnotations(annotation, generatedClassesProducer, reflectiveClassesProducer, index) .forEach(generatedProviders::add); } @@ -331,7 +331,7 @@ void registerProvidersFromAnnotations(CombinedIndexBuildItem indexBuildItem, constructor.returnValue(null); } - unremovableBeans.produce(UnremovableBeanBuildItem.beanClassNames(annotationRegisteredProvidersImpl)); + unremovableBeansProducer.produce(UnremovableBeanBuildItem.beanClassNames(annotationRegisteredProvidersImpl)); } @BuildStep @@ -418,8 +418,8 @@ void addRestClientBeans(Capabilities capabilities, Set registerRestClientAnnos = determineRegisterRestClientInstances(clientsBuildConfig, index); Map configKeys = new HashMap<>(); - var annotationsStore = new AnnotationStore(restClientAnnotationsTransformerBuildItem.stream() - .map(RestClientAnnotationsTransformerBuildItem::getAnnotationsTransformer).collect(toList())); + var annotationsStore = new AnnotationStore(index, restClientAnnotationsTransformerBuildItem.stream() + .map(RestClientAnnotationsTransformerBuildItem::getAnnotationTransformation).toList()); for (AnnotationInstance registerRestClient : registerRestClientAnnos) { ClassInfo jaxrsInterface = registerRestClient.target().asClass(); // for each interface annotated with @RegisterRestClient, generate a $$CDIWrapper CDI bean that can be injected @@ -629,12 +629,22 @@ private boolean skipAutoDiscoveredProvider(List providerInterfaceNames) } private Map populateClientExceptionMapperFromAnnotations( - BuildProducer generatedClasses, - BuildProducer reflectiveClasses, IndexView index) { + IndexView index, + BuildProducer generatedClassesProducer, + BuildProducer reflectiveClassesProducer, + BuildProducer executionModelAnnotationsAllowedProducer) { + + executionModelAnnotationsAllowedProducer.produce(new ExecutionModelAnnotationsAllowedBuildItem( + new Predicate<>() { + @Override + public boolean test(MethodInfo methodInfo) { + return methodInfo.hasDeclaredAnnotation(CLIENT_EXCEPTION_MAPPER); + } + })); var result = new HashMap(); ClientExceptionMapperHandler clientExceptionMapperHandler = new ClientExceptionMapperHandler( - new GeneratedClassGizmoAdaptor(generatedClasses, true)); + new GeneratedClassGizmoAdaptor(generatedClassesProducer, true)); for (AnnotationInstance instance : index.getAnnotations(CLIENT_EXCEPTION_MAPPER)) { GeneratedClassResult classResult = clientExceptionMapperHandler.generateResponseExceptionMapper(instance); if (classResult == null) { @@ -645,7 +655,7 @@ private Map populateClientExceptionMapperFromAnnot + "' is allowed per REST Client interface. Offending class is '" + classResult.interfaceName + "'"); } result.put(classResult.interfaceName, classResult); - reflectiveClasses.produce(ReflectiveClassBuildItem.builder(classResult.generatedClassName) + reflectiveClassesProducer.produce(ReflectiveClassBuildItem.builder(classResult.generatedClassName) .serialization(false).build()); } return result; diff --git a/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/FormListTest.java b/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/FormListTest.java index b05f4c58b8031..fe0bfd9543c30 100644 --- a/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/FormListTest.java +++ b/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/FormListTest.java @@ -5,6 +5,7 @@ import java.net.URI; import java.util.List; +import jakarta.ws.rs.BeanParam; import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; @@ -26,19 +27,21 @@ public class FormListTest { URI baseUri; @Test - void testHeadersWithSubresource() { + void test() { Client client = RestClientBuilder.newBuilder().baseUri(baseUri).build(Client.class); - assertThat(client.call(List.of("first", "second", "third"))).isEqualTo("first-second-third"); - assertThat(client.call(List.of("first"))).isEqualTo("first"); + Holder holder = new Holder(); + holder.input2 = List.of("1", "2"); + assertThat(client.call(List.of("first", "second", "third"), holder)).isEqualTo("first-second-third/1-2"); + assertThat(client.call(List.of("first"), holder)).isEqualTo("first/1-2"); } @Path("/test") public static class Resource { @POST - public String response(@RestForm List input) { - return String.join("-", input); + public String response(@RestForm List input, @RestForm List input2) { + return String.join("-", input) + "/" + String.join("-", input2); } } @@ -46,6 +49,11 @@ public String response(@RestForm List input) { public interface Client { @POST - String call(@RestForm List input); + String call(@RestForm List input, @BeanParam Holder holder); + } + + public static class Holder { + @RestForm + public List input2; } } diff --git a/extensions/resteasy-reactive/rest-client/spi-deployment/src/main/java/io/quarkus/rest/client/reactive/spi/RestClientAnnotationsTransformerBuildItem.java b/extensions/resteasy-reactive/rest-client/spi-deployment/src/main/java/io/quarkus/rest/client/reactive/spi/RestClientAnnotationsTransformerBuildItem.java index 6de1d8563953e..a3212d86b5bd9 100644 --- a/extensions/resteasy-reactive/rest-client/spi-deployment/src/main/java/io/quarkus/rest/client/reactive/spi/RestClientAnnotationsTransformerBuildItem.java +++ b/extensions/resteasy-reactive/rest-client/spi-deployment/src/main/java/io/quarkus/rest/client/reactive/spi/RestClientAnnotationsTransformerBuildItem.java @@ -1,6 +1,7 @@ package io.quarkus.rest.client.reactive.spi; import org.jboss.jandex.AnnotationTarget; +import org.jboss.jandex.AnnotationTransformation; import org.jboss.resteasy.reactive.common.processor.transformation.AnnotationStore; import org.jboss.resteasy.reactive.common.processor.transformation.AnnotationsTransformer; @@ -20,13 +21,32 @@ */ public final class RestClientAnnotationsTransformerBuildItem extends MultiBuildItem { - private final AnnotationsTransformer transformer; + private final AnnotationTransformation transformer; + /** + * @deprecated use {@link #RestClientAnnotationsTransformerBuildItem(AnnotationTransformation)} + */ + @Deprecated(forRemoval = true) public RestClientAnnotationsTransformerBuildItem(AnnotationsTransformer transformer) { this.transformer = transformer; } + public RestClientAnnotationsTransformerBuildItem(AnnotationTransformation transformation) { + this.transformer = transformation; + } + + /** + * @deprecated use {@link #getAnnotationTransformation()} + */ + @Deprecated(forRemoval = true) public AnnotationsTransformer getAnnotationsTransformer() { + if (transformer instanceof AnnotationsTransformer) { + return (AnnotationsTransformer) transformer; + } + throw new UnsupportedOperationException("AnnotationTransformation is not an AnnotationsTransformer: " + transformer); + } + + public AnnotationTransformation getAnnotationTransformation() { return transformer; } diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/main/java/io/quarkus/resteasy/reactive/jackson/deployment/processor/ResteasyReactiveJacksonProcessor.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/main/java/io/quarkus/resteasy/reactive/jackson/deployment/processor/ResteasyReactiveJacksonProcessor.java index 38f5ee6411377..b589e330ee903 100644 --- a/extensions/resteasy-reactive/rest-jackson/deployment/src/main/java/io/quarkus/resteasy/reactive/jackson/deployment/processor/ResteasyReactiveJacksonProcessor.java +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/main/java/io/quarkus/resteasy/reactive/jackson/deployment/processor/ResteasyReactiveJacksonProcessor.java @@ -490,16 +490,19 @@ private static boolean hasSecureFields(IndexView indexView, ClassInfo currentCla final boolean hasSecureFields; if (currentClassInfo.isInterface()) { - // check interface implementors as anyone of them can be returned - hasSecureFields = indexView.getAllKnownImplementors(currentClassInfo.name()).stream() - .anyMatch(ci -> hasSecureFields(indexView, ci, typeToHasSecureField, needToDeleteCache)); + if (isExcludedFromSecureFieldLookup(currentClassInfo.name())) { + hasSecureFields = false; + } else { + // check interface implementors as anyone of them can be returned + hasSecureFields = indexView.getAllKnownImplementors(currentClassInfo.name()).stream() + .anyMatch(ci -> hasSecureFields(indexView, ci, typeToHasSecureField, needToDeleteCache)); + } } else { // figure if any field or parent / subclass field is secured if (hasSecureFields(currentClassInfo)) { hasSecureFields = true; } else { - Predicate ignoredTypesPredicate = QuarkusResteasyReactiveDotNames.IGNORE_TYPE_FOR_REFLECTION_PREDICATE; - if (ignoredTypesPredicate.test(currentClassInfo.name())) { + if (isExcludedFromSecureFieldLookup(currentClassInfo.name())) { hasSecureFields = false; } else { hasSecureFields = anyFieldHasSecureFields(indexView, currentClassInfo, typeToHasSecureField, @@ -514,6 +517,10 @@ private static boolean hasSecureFields(IndexView indexView, ClassInfo currentCla return hasSecureFields; } + private static boolean isExcludedFromSecureFieldLookup(DotName name) { + return ((Predicate) QuarkusResteasyReactiveDotNames.IGNORE_TYPE_FOR_REFLECTION_PREDICATE).test(name); + } + private static boolean hasSecureFields(ClassInfo classInfo) { return classInfo.annotationsMap().containsKey(SECURE_FIELD); } @@ -548,7 +555,7 @@ private static boolean fieldTypeHasSecureFields(Type fieldType, IndexView indexV Map typeToHasSecureField, AtomicBoolean needToDeleteCache) { // this is the best effort and does not cover every possibility (e.g. type variables, wildcards) if (fieldType.kind() == Type.Kind.CLASS) { - if (fieldType.name().packagePrefix() != null && fieldType.name().packagePrefix().startsWith("java.")) { + if (isExcludedFromSecureFieldLookup(fieldType.name())) { return false; } final ClassInfo fieldClass = indexView.getClassByName(fieldType.name()); diff --git a/extensions/resteasy-reactive/rest/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java b/extensions/resteasy-reactive/rest/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java index 620889f1d6134..f5f4d4d7026e8 100644 --- a/extensions/resteasy-reactive/rest/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java +++ b/extensions/resteasy-reactive/rest/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java @@ -57,6 +57,7 @@ import org.eclipse.microprofile.config.ConfigProvider; import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.AnnotationTarget; +import org.jboss.jandex.AnnotationTransformation; import org.jboss.jandex.AnnotationValue; import org.jboss.jandex.ClassInfo; import org.jboss.jandex.DotName; @@ -647,11 +648,12 @@ public Supplier apply(ClassInfo classInfo) { } if (!annotationTransformerBuildItems.isEmpty()) { - List annotationsTransformers = new ArrayList<>(annotationTransformerBuildItems.size()); + List annotationTransformations = new ArrayList<>( + annotationTransformerBuildItems.size()); for (AnnotationsTransformerBuildItem bi : annotationTransformerBuildItems) { - annotationsTransformers.add(bi.getAnnotationsTransformer()); + annotationTransformations.add(bi.getAnnotationTransformation()); } - serverEndpointIndexerBuilder.setAnnotationsTransformers(annotationsTransformers); + serverEndpointIndexerBuilder.setAnnotationTransformations(annotationTransformations); } serverEndpointIndexerBuilder.setMultipartReturnTypeIndexerExtension(new QuarkusMultipartReturnTypeHandler( diff --git a/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/AbstractSecurityEventTest.java b/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/AbstractSecurityEventTest.java index 1d3366fd2220a..b88c862e5f85b 100644 --- a/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/AbstractSecurityEventTest.java +++ b/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/AbstractSecurityEventTest.java @@ -130,10 +130,16 @@ public void testNestedRolesAllowed() { assertEquals(identity, authZSuccessEvent.getSecurityIdentity()); identity = authZSuccessEvent.getSecurityIdentity(); assertEquals(routingContext, authZSuccessEvent.getEventProperties().get(RoutingContext.class.getName())); + String securedMethod = (String) authZSuccessEvent.getEventProperties() + .get(AuthorizationSuccessEvent.SECURED_METHOD_KEY); + assertEquals("io.quarkus.resteasy.reactive.server.test.security.RolesAllowedServiceResource#getServiceHello", + securedMethod); // authorization success on service level performed by CDI interceptor authZSuccessEvent = (AuthorizationSuccessEvent) observer.syncEvents.get(2); + securedMethod = (String) authZSuccessEvent.getEventProperties().get(AuthorizationSuccessEvent.SECURED_METHOD_KEY); + assertEquals("io.quarkus.resteasy.reactive.server.test.security.RolesAllowedService#hello", securedMethod); assertEquals(identity, authZSuccessEvent.getSecurityIdentity()); - assertNull(authZSuccessEvent.getEventProperties().get(RoutingContext.class.getName())); + assertNotNull(authZSuccessEvent.getEventProperties().get(RoutingContext.class.getName())); assertAsyncAuthZFailureObserved(0); RestAssured.given().auth().preemptive().basic("user", "user").get("/roles-service/hello").then().statusCode(403); assertSyncObserved(6, false, false); @@ -149,18 +155,56 @@ public void testNestedRolesAllowed() { assertEquals(identity, authZSuccessEvent.getSecurityIdentity()); assertEquals(routingContext, authZSuccessEvent.getEventProperties().get(RoutingContext.class.getName())); // RolesService requires 'admin' role, therefore user fails - // here security check is performed on CDI bean by security interceptor, therefore no RoutingContext is added - assertAsyncAuthZFailureObserved(1, false); + assertAsyncAuthZFailureObserved(1); AuthorizationFailureEvent authZFailureEvent = observer.asyncAuthZFailureEvents.get(0); + securedMethod = (String) authZFailureEvent.getEventProperties().get(AuthorizationFailureEvent.SECURED_METHOD_KEY); + assertEquals("io.quarkus.resteasy.reactive.server.test.security.RolesAllowedService#hello", securedMethod); SecurityIdentity userIdentity = authZFailureEvent.getSecurityIdentity(); assertNotNull(userIdentity); assertTrue(userIdentity.hasRole("user")); assertEquals("user", userIdentity.getPrincipal().getName()); - // there is no RoutingContext as the check is performed by security interceptor - assertNull(authZFailureEvent.getEventProperties().get(RoutingContext.class.getName())); + assertNotNull(authZFailureEvent.getEventProperties().get(RoutingContext.class.getName())); assertEquals(RolesAllowedCheck.class.getName(), authZFailureEvent.getAuthorizationContext()); } + @Test + public void testNestedPermitAll() { + // @PermitAll is on CDI bean but resource is not secured + RestAssured.given().auth().preemptive().basic("admin", "admin").get("/roles-service/bye").then().statusCode(200) + .body(is(RolesAllowedService.SERVICE_BYE)); + final int expectedEventsCount; + if (isProactiveAuth()) { + // auth + @PermitAll + expectedEventsCount = 2; + } else { + // @PermitAll + expectedEventsCount = 1; + } + assertSyncObserved(expectedEventsCount, true, true); + + if (expectedEventsCount == 2) { + AuthenticationSuccessEvent successEvent = (AuthenticationSuccessEvent) observer.syncEvents.get(0); + SecurityIdentity identity = successEvent.getSecurityIdentity(); + assertNotNull(identity); + assertEquals("admin", identity.getPrincipal().getName()); + RoutingContext routingContext = (RoutingContext) successEvent.getEventProperties() + .get(RoutingContext.class.getName()); + assertNotNull(routingContext); + assertTrue(routingContext.request().path().endsWith("/roles-service/bye")); + } + // authorization success on service level performed by CDI interceptor + var authZSuccessEvent = (AuthorizationSuccessEvent) observer.syncEvents.get(expectedEventsCount - 1); + String securedMethod = (String) authZSuccessEvent.getEventProperties() + .get(AuthorizationSuccessEvent.SECURED_METHOD_KEY); + assertEquals("io.quarkus.resteasy.reactive.server.test.security.RolesAllowedService#bye", securedMethod); + assertNotNull(authZSuccessEvent.getEventProperties().get(RoutingContext.class.getName())); + if (isProactiveAuth()) { + assertNotNull(authZSuccessEvent.getSecurityIdentity()); + assertEquals("admin", authZSuccessEvent.getSecurityIdentity().getPrincipal().getName()); + } + assertAsyncAuthZFailureObserved(0); + } + @Test public void testAuthenticated() { RestAssured.given().auth().preemptive().basic("admin", "admin").get("/unsecured/authenticated").then().statusCode(200) @@ -186,6 +230,8 @@ public void testAuthenticated() { routingContext = (RoutingContext) authZFailure.getEventProperties().get(RoutingContext.class.getName()); assertNotNull(routingContext); assertTrue(routingContext.request().path().endsWith("/unsecured/authenticated")); + String securedMethod = (String) authZFailure.getEventProperties().get(AuthorizationFailureEvent.SECURED_METHOD_KEY); + assertEquals("io.quarkus.resteasy.reactive.server.test.security.UnsecuredResource#authenticated", securedMethod); } @Test @@ -227,8 +273,7 @@ public void testPermitAll() { assertNotNull(routingContext); assertTrue(routingContext.request().path().endsWith("/unsecured/permitAll")); AuthorizationSuccessEvent authZSuccessEvent = (AuthorizationSuccessEvent) observer.syncEvents.get(1); - // SecurityIdentity is not required for the permit all check - assertNull(authZSuccessEvent.getSecurityIdentity()); + assertNotNull(authZSuccessEvent.getSecurityIdentity()); assertEquals(routingContext, authZSuccessEvent.getEventProperties().get(RoutingContext.class.getName())); } else { assertSyncObserved(1, true, true); @@ -263,19 +308,13 @@ private void assertSyncObserved(int count, boolean expectRoutingContext, boolean } private void assertAsyncAuthZFailureObserved(int count) { - assertAsyncAuthZFailureObserved(count, true); - } - - private void assertAsyncAuthZFailureObserved(int count, boolean expectRoutingContext) { Awaitility.await().atMost(Duration.ofSeconds(2)) .untilAsserted(() -> assertEquals(count, observer.asyncAuthZFailureEvents.size())); if (count > 0) { assertTrue(observer.asyncAuthZFailureEvents.stream().allMatch(e -> e.getSecurityIdentity() != null)); - if (expectRoutingContext) { - assertTrue(observer.asyncAuthZFailureEvents.stream() - .map(e -> e.getEventProperties().get(RoutingContext.class.getName())) - .allMatch(Objects::nonNull)); - } + assertTrue(observer.asyncAuthZFailureEvents.stream() + .map(e -> e.getEventProperties().get(RoutingContext.class.getName())) + .allMatch(Objects::nonNull)); } } diff --git a/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/RolesAllowedJaxRsTestCase.java b/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/RolesAllowedJaxRsTestCase.java index 6df18df664b07..67cc925f5e5f6 100644 --- a/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/RolesAllowedJaxRsTestCase.java +++ b/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/RolesAllowedJaxRsTestCase.java @@ -1,5 +1,6 @@ package io.quarkus.resteasy.reactive.server.test.security; +import static io.quarkus.resteasy.reactive.server.test.security.RolesAllowedService.EVENT_BUS_MESSAGES; import static org.hamcrest.Matchers.is; import java.io.IOException; @@ -14,6 +15,7 @@ import jakarta.ws.rs.ext.MessageBodyReader; import jakarta.ws.rs.ext.Provider; +import org.awaitility.Awaitility; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -90,6 +92,20 @@ public void testSecurityRunsBeforeValidation() { Assertions.assertFalse(read); } + @Test + public void testSecurityInterceptorsAfterHttpRequestCompleted() { + RestAssured + .given() + .auth().preemptive().basic("user", "user") + .body("message one") + .post("/roles-service/secured-event-bus") + .then() + .statusCode(204); + Awaitility.await().until(() -> !EVENT_BUS_MESSAGES.isEmpty()); + Assertions.assertEquals(1, EVENT_BUS_MESSAGES.size(), EVENT_BUS_MESSAGES.toString()); + Assertions.assertEquals("permit all message one", EVENT_BUS_MESSAGES.get(0)); + } + static volatile boolean read = false; @Provider diff --git a/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/RolesAllowedService.java b/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/RolesAllowedService.java index 23c1203c114e8..083b15faddb1f 100644 --- a/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/RolesAllowedService.java +++ b/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/RolesAllowedService.java @@ -1,16 +1,39 @@ package io.quarkus.resteasy.reactive.server.test.security; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +import jakarta.annotation.security.PermitAll; import jakarta.annotation.security.RolesAllowed; import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.context.control.ActivateRequestContext; @ApplicationScoped public class RolesAllowedService { public static final String SERVICE_HELLO = "Hello from Service!"; + public static final String SERVICE_BYE = "Bye from Service!"; + public static final List EVENT_BUS_MESSAGES = new CopyOnWriteArrayList<>(); @RolesAllowed("admin") public String hello() { return SERVICE_HELLO; } + @PermitAll + public String bye() { + return SERVICE_BYE; + } + + @PermitAll + @ActivateRequestContext + void receivePermitAllMessage(String m) { + EVENT_BUS_MESSAGES.add("permit all " + m); + } + + @RolesAllowed("admin") + @ActivateRequestContext + void receiveRolesAllowedMessage(String m) { + EVENT_BUS_MESSAGES.add("roles allowed " + m); + } } diff --git a/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/RolesAllowedServiceResource.java b/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/RolesAllowedServiceResource.java index 6ded9428be1b0..12ecf9c9752a4 100644 --- a/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/RolesAllowedServiceResource.java +++ b/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/RolesAllowedServiceResource.java @@ -1,16 +1,30 @@ package io.quarkus.resteasy.reactive.server.test.security; import jakarta.annotation.security.RolesAllowed; +import jakarta.enterprise.event.Observes; import jakarta.inject.Inject; import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; +import io.quarkus.runtime.ShutdownEvent; +import io.quarkus.runtime.StartupEvent; +import io.vertx.core.Vertx; +import io.vertx.mutiny.core.eventbus.EventBus; +import io.vertx.mutiny.core.eventbus.MessageConsumer; + @Path("/roles-service") public class RolesAllowedServiceResource { + private MessageConsumer permitAllConsumer; + private MessageConsumer rolesAllowedConsumer; + @Inject RolesAllowedService rolesAllowedService; + @Inject + EventBus bus; + @Path("/hello") @RolesAllowed({ "user", "admin" }) @GET @@ -18,4 +32,40 @@ public String getServiceHello() { return rolesAllowedService.hello(); } + @Path("/bye") + @GET + public String getServiceBye() { + return rolesAllowedService.bye(); + } + + @Path("/secured-event-bus") + @POST + public void sendMessage(String message) { + bus.send("roles-allowed-message", message); + bus.send("permit-all-message", message); + } + + void observeStartup(@Observes StartupEvent startupEvent, EventBus eventBus, Vertx vertx) { + permitAllConsumer = eventBus + . consumer("permit-all-message") + .handler(msg -> rolesAllowedService.receivePermitAllMessage(msg.body())); + + // this must always fail because the authorization is happening in a blank CDI request context + rolesAllowedConsumer = eventBus + . consumer("roles-allowed-message") + .handler(msg -> vertx.executeBlocking(() -> { + // make sure authentication is attempted on a worker thread to prevent blocking event loop + rolesAllowedService.receiveRolesAllowedMessage(msg.body()); + return null; + })); + } + + void observerShutdown(@Observes ShutdownEvent shutdownEvent) { + if (permitAllConsumer != null) { + permitAllConsumer.unregister().await().indefinitely(); + } + if (rolesAllowedConsumer != null) { + rolesAllowedConsumer.unregister().await().indefinitely(); + } + } } diff --git a/extensions/resteasy-reactive/rest/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/StandardSecurityCheckInterceptor.java b/extensions/resteasy-reactive/rest/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/StandardSecurityCheckInterceptor.java index 9f86466ac25d7..a58c53c83ad62 100644 --- a/extensions/resteasy-reactive/rest/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/StandardSecurityCheckInterceptor.java +++ b/extensions/resteasy-reactive/rest/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/StandardSecurityCheckInterceptor.java @@ -54,7 +54,7 @@ private boolean alreadyDoneByEagerSecurityHandler(Object methodWithFinishedCheck */ @Interceptor @RolesAllowed("") - @Priority(Interceptor.Priority.PLATFORM_BEFORE) + @Priority(Interceptor.Priority.LIBRARY_BEFORE - 100) public static final class RolesAllowedInterceptor extends StandardSecurityCheckInterceptor { } @@ -64,7 +64,7 @@ public static final class RolesAllowedInterceptor extends StandardSecurityCheckI */ @Interceptor @PermissionsAllowed("") - @Priority(Interceptor.Priority.PLATFORM_BEFORE) + @Priority(Interceptor.Priority.LIBRARY_BEFORE - 100) public static final class PermissionsAllowedInterceptor extends StandardSecurityCheckInterceptor { } @@ -74,7 +74,7 @@ public static final class PermissionsAllowedInterceptor extends StandardSecurity */ @Interceptor @PermitAll - @Priority(Interceptor.Priority.PLATFORM_BEFORE) + @Priority(Interceptor.Priority.LIBRARY_BEFORE - 100) public static final class PermitAllInterceptor extends StandardSecurityCheckInterceptor { } @@ -84,7 +84,7 @@ public static final class PermitAllInterceptor extends StandardSecurityCheckInte */ @Interceptor @Authenticated - @Priority(Interceptor.Priority.PLATFORM_BEFORE) + @Priority(Interceptor.Priority.LIBRARY_BEFORE - 100) public static final class AuthenticatedInterceptor extends StandardSecurityCheckInterceptor { } diff --git a/extensions/resteasy-reactive/rest/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/security/EagerSecurityHandler.java b/extensions/resteasy-reactive/rest/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/security/EagerSecurityHandler.java index 59b74d611bcb5..27da712905fe3 100644 --- a/extensions/resteasy-reactive/rest/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/security/EagerSecurityHandler.java +++ b/extensions/resteasy-reactive/rest/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/security/EagerSecurityHandler.java @@ -23,6 +23,7 @@ import io.quarkus.security.spi.runtime.AuthorizationSuccessEvent; import io.quarkus.security.spi.runtime.MethodDescription; import io.quarkus.security.spi.runtime.SecurityCheck; +import io.quarkus.vertx.http.runtime.security.QuarkusHttpUser; import io.smallrye.mutiny.Uni; import io.smallrye.mutiny.subscription.UniSubscriber; import io.smallrye.mutiny.subscription.UniSubscription; @@ -132,8 +133,18 @@ private Function> getSecurityCheck(ResteasyReactiveRequ preventRepeatedSecurityChecks(requestContext, methodDescription); if (EagerSecurityContext.instance.eventHelper.fireEventOnSuccess()) { requestContext.requireCDIRequestScope(); - EagerSecurityContext.instance.eventHelper.fireSuccessEvent(new AuthorizationSuccessEvent(null, - check.getClass().getName(), createEventPropsWithRoutingCtx(requestContext))); + + // add the identity only if authentication has already finished + final SecurityIdentity identity; + var event = requestContext.unwrap(RoutingContext.class); + if (event != null && event.user() instanceof QuarkusHttpUser user) { + identity = user.getSecurityIdentity(); + } else { + identity = null; + } + + EagerSecurityContext.instance.eventHelper.fireSuccessEvent(new AuthorizationSuccessEvent(identity, + check.getClass().getName(), createEventPropsWithRoutingCtx(requestContext), methodDescription)); } return null; } else { @@ -156,7 +167,8 @@ public Uni apply(SecurityIdentity securityIdentity) { if (EagerSecurityContext.instance.eventHelper.fireEventOnFailure()) { EagerSecurityContext.instance.eventHelper .fireFailureEvent(new AuthorizationFailureEvent(securityIdentity, unauthorizedException, - theCheck.getClass().getName(), createEventPropsWithRoutingCtx(requestContext))); + theCheck.getClass().getName(), createEventPropsWithRoutingCtx(requestContext), + methodDescription)); } throw unauthorizedException; } @@ -175,7 +187,7 @@ public void accept(Throwable throwable) { EagerSecurityContext.instance.eventHelper .fireFailureEvent(new AuthorizationFailureEvent( securityIdentity, throwable, theCheck.getClass().getName(), - createEventPropsWithRoutingCtx(requestContext))); + createEventPropsWithRoutingCtx(requestContext), methodDescription)); } }); } @@ -187,7 +199,7 @@ public void run() { EagerSecurityContext.instance.eventHelper.fireSuccessEvent( new AuthorizationSuccessEvent(securityIdentity, theCheck.getClass().getName(), - createEventPropsWithRoutingCtx(requestContext))); + createEventPropsWithRoutingCtx(requestContext), methodDescription)); } }); } diff --git a/extensions/resteasy-reactive/rest/spi-deployment/src/main/java/io/quarkus/resteasy/reactive/server/spi/AnnotationsTransformerBuildItem.java b/extensions/resteasy-reactive/rest/spi-deployment/src/main/java/io/quarkus/resteasy/reactive/server/spi/AnnotationsTransformerBuildItem.java index df3650abcf872..d676e59c96d23 100644 --- a/extensions/resteasy-reactive/rest/spi-deployment/src/main/java/io/quarkus/resteasy/reactive/server/spi/AnnotationsTransformerBuildItem.java +++ b/extensions/resteasy-reactive/rest/spi-deployment/src/main/java/io/quarkus/resteasy/reactive/server/spi/AnnotationsTransformerBuildItem.java @@ -1,6 +1,7 @@ package io.quarkus.resteasy.reactive.server.spi; import org.jboss.jandex.AnnotationTarget; +import org.jboss.jandex.AnnotationTransformation; import org.jboss.resteasy.reactive.common.processor.transformation.AnnotationStore; import org.jboss.resteasy.reactive.common.processor.transformation.AnnotationsTransformer; @@ -20,13 +21,32 @@ */ public final class AnnotationsTransformerBuildItem extends MultiBuildItem { - private final AnnotationsTransformer transformer; + private final AnnotationTransformation transformer; + /** + * @deprecated use {@link #AnnotationsTransformerBuildItem(AnnotationTransformation)} + */ + @Deprecated(forRemoval = true) public AnnotationsTransformerBuildItem(AnnotationsTransformer transformer) { this.transformer = transformer; } + public AnnotationsTransformerBuildItem(AnnotationTransformation transformation) { + this.transformer = transformation; + } + + /** + * @deprecated use {@link #getAnnotationTransformation()} + */ + @Deprecated(forRemoval = true) public AnnotationsTransformer getAnnotationsTransformer() { + if (transformer instanceof AnnotationsTransformer) { + return (AnnotationsTransformer) transformer; + } + throw new UnsupportedOperationException("AnnotationTransformation is not an AnnotationsTransformer: " + transformer); + } + + public AnnotationTransformation getAnnotationTransformation() { return transformer; } diff --git a/extensions/schema-registry/confluent/pom.xml b/extensions/schema-registry/confluent/pom.xml index 7f8cec8d484b1..516620a70eded 100644 --- a/extensions/schema-registry/confluent/pom.xml +++ b/extensions/schema-registry/confluent/pom.xml @@ -25,7 +25,7 @@ org.jetbrains.kotlin kotlin-scripting-compiler-embeddable - 1.9.23 + 2.0.0 org.json diff --git a/extensions/schema-registry/devservice/deployment/src/main/java/io/quarkus/apicurio/registry/devservice/ApicurioRegistryDevServicesBuildTimeConfig.java b/extensions/schema-registry/devservice/deployment/src/main/java/io/quarkus/apicurio/registry/devservice/ApicurioRegistryDevServicesBuildTimeConfig.java index c11a2aa85fbc6..acad0830a63eb 100644 --- a/extensions/schema-registry/devservice/deployment/src/main/java/io/quarkus/apicurio/registry/devservice/ApicurioRegistryDevServicesBuildTimeConfig.java +++ b/extensions/schema-registry/devservice/deployment/src/main/java/io/quarkus/apicurio/registry/devservice/ApicurioRegistryDevServicesBuildTimeConfig.java @@ -3,6 +3,7 @@ import java.util.Map; import java.util.Optional; +import io.quarkus.runtime.annotations.ConfigDocMapKey; import io.quarkus.runtime.annotations.ConfigItem; import io.quarkus.runtime.annotations.ConfigPhase; import io.quarkus.runtime.annotations.ConfigRoot; @@ -66,6 +67,7 @@ public class ApicurioRegistryDevServicesBuildTimeConfig { * Environment variables that are passed to the container. */ @ConfigItem + @ConfigDocMapKey("environment-variable-name") public Map containerEnv; } diff --git a/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/SecurityConfig.java b/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/SecurityConfig.java index 4821e78172252..76ee08a2f9140 100644 --- a/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/SecurityConfig.java +++ b/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/SecurityConfig.java @@ -4,6 +4,7 @@ import java.util.Optional; import java.util.Set; +import io.quarkus.runtime.annotations.ConfigDocMapKey; import io.quarkus.runtime.annotations.ConfigRoot; import io.smallrye.config.ConfigMapping; import io.smallrye.config.WithDefault; @@ -31,5 +32,6 @@ public interface SecurityConfig { /** * Security provider configuration */ + @ConfigDocMapKey("provider-name") Map securityProviderConfig(); } diff --git a/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/SecurityProcessor.java b/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/SecurityProcessor.java index ce61a24f2eeae..31dac288e8595 100644 --- a/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/SecurityProcessor.java +++ b/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/SecurityProcessor.java @@ -34,6 +34,7 @@ import java.util.function.Predicate; import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Singleton; import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.AnnotationTarget; @@ -55,6 +56,7 @@ import io.quarkus.arc.processor.BuildExtension; import io.quarkus.arc.processor.ObserverInfo; import io.quarkus.builder.item.MultiBuildItem; +import io.quarkus.deployment.Capabilities; import io.quarkus.deployment.Feature; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; @@ -68,6 +70,7 @@ import io.quarkus.deployment.builditem.NativeImageFeatureBuildItem; import io.quarkus.deployment.builditem.RunTimeConfigBuilderBuildItem; import io.quarkus.deployment.builditem.RuntimeConfigSetupCompleteBuildItem; +import io.quarkus.deployment.builditem.ShutdownContextBuildItem; import io.quarkus.deployment.builditem.nativeimage.JPMSExportBuildItem; import io.quarkus.deployment.builditem.nativeimage.NativeImageSecurityProviderBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; @@ -105,6 +108,7 @@ import io.quarkus.security.runtime.interceptor.SecurityHandler; import io.quarkus.security.spi.AdditionalSecuredClassesBuildItem; import io.quarkus.security.spi.AdditionalSecuredMethodsBuildItem; +import io.quarkus.security.spi.AdditionalSecurityConstrainerEventPropsBuildItem; import io.quarkus.security.spi.DefaultSecurityCheckBuildItem; import io.quarkus.security.spi.RolesAllowedConfigExpResolverBuildItem; import io.quarkus.security.spi.runtime.AuthorizationController; @@ -458,14 +462,38 @@ private static List registerProvider(String providerName, return providerClasses; } + @Consume(RuntimeConfigSetupCompleteBuildItem.class) + @Record(ExecutionTime.RUNTIME_INIT) + @BuildStep + void recordRuntimeConfigReady(SecurityCheckRecorder recorder, ShutdownContextBuildItem shutdownContextBuildItem, + LaunchModeBuildItem launchModeBuildItem) { + recorder.setRuntimeConfigReady(); + if (launchModeBuildItem.getLaunchMode() == LaunchMode.DEVELOPMENT) { + recorder.unsetRuntimeConfigReady(shutdownContextBuildItem); + } + } + + @Record(ExecutionTime.STATIC_INIT) @BuildStep void registerSecurityInterceptors(BuildProducer registrars, - BuildProducer beans) { + BuildProducer beans, + BuildProducer syntheticBeanProducer, SecurityCheckRecorder recorder, + Optional additionalSecurityConstrainerEventsItem) { registrars.produce(new InterceptorBindingRegistrarBuildItem(new SecurityAnnotationsRegistrar())); Class[] interceptors = { AuthenticatedInterceptor.class, DenyAllInterceptor.class, PermitAllInterceptor.class, RolesAllowedInterceptor.class, PermissionsAllowedInterceptor.class }; beans.produce(new AdditionalBeanBuildItem(interceptors)); - beans.produce(new AdditionalBeanBuildItem(SecurityHandler.class, SecurityConstrainer.class)); + beans.produce(new AdditionalBeanBuildItem(SecurityHandler.class)); + + var additionalEventsSupplier = additionalSecurityConstrainerEventsItem + .map(AdditionalSecurityConstrainerEventPropsBuildItem::getAdditionalEventPropsSupplier) + .orElse(null); + syntheticBeanProducer.produce(SyntheticBeanBuildItem + .configure(SecurityConstrainer.class) + .unremovable() + .scope(Singleton.class) + .supplier(recorder.createSecurityConstrainer(additionalEventsSupplier)) + .done()); } /** @@ -519,6 +547,7 @@ void transformSecurityAnnotations(BuildProducer } } + @Consume(Capabilities.class) // make sure extension combinations are validated before default security check @BuildStep @Record(ExecutionTime.STATIC_INIT) void gatherSecurityChecks(BuildProducer syntheticBeans, @@ -529,7 +558,7 @@ void gatherSecurityChecks(BuildProducer syntheticBeans, BuildProducer configBuilderProducer, List additionalSecuredMethods, SecurityCheckRecorder recorder, - Optional defaultSecurityCheckBuildItem, + List defaultSecurityCheckBuildItem, BuildProducer reflectiveClassBuildItemBuildProducer, List additionalSecurityChecks, SecurityBuildTimeConfig config) { classPredicate.produce(new ApplicationClassPredicateBuildItem(new SecurityCheckStorageAppPredicate())); @@ -563,8 +592,14 @@ void gatherSecurityChecks(BuildProducer syntheticBeans, methodEntry.getValue()); } - if (defaultSecurityCheckBuildItem.isPresent()) { - var roles = defaultSecurityCheckBuildItem.get().getRolesAllowed(); + if (!defaultSecurityCheckBuildItem.isEmpty()) { + if (defaultSecurityCheckBuildItem.size() > 1) { + int itemCount = defaultSecurityCheckBuildItem.size(); + throw new IllegalStateException("Found %d DefaultSecurityCheckBuildItem items, ".formatted(itemCount) + + "please make sure the item is produced exactly once"); + } + + var roles = defaultSecurityCheckBuildItem.get(0).getRolesAllowed(); if (roles == null) { recorder.registerDefaultSecurityCheck(builder, recorder.denyAll()); } else { diff --git a/extensions/security/runtime-spi/src/main/java/io/quarkus/security/spi/runtime/AbstractSecurityEvent.java b/extensions/security/runtime-spi/src/main/java/io/quarkus/security/spi/runtime/AbstractSecurityEvent.java index b4bfa041c50bb..fcfa902bc6093 100644 --- a/extensions/security/runtime-spi/src/main/java/io/quarkus/security/spi/runtime/AbstractSecurityEvent.java +++ b/extensions/security/runtime-spi/src/main/java/io/quarkus/security/spi/runtime/AbstractSecurityEvent.java @@ -2,6 +2,7 @@ import java.util.HashMap; import java.util.Map; +import java.util.Objects; import io.quarkus.security.identity.SecurityIdentity; @@ -25,15 +26,28 @@ public Map getEventProperties() { return eventProperties; } + protected static String toString(MethodDescription methodDescription) { + Objects.requireNonNull(methodDescription); + return methodDescription.getClassName() + "#" + methodDescription.getMethodName(); + } + protected static Map withProperties(String propertyKey, Object propertyValue, Map additionalProperties) { - final Map result = new HashMap<>(); + + final HashMap result; + if (additionalProperties instanceof HashMap additionalPropertiesHashMap) { + // do not recreate map when multiple props are added + result = additionalPropertiesHashMap; + } else { + result = new HashMap<>(); + if (additionalProperties != null && !additionalProperties.isEmpty()) { + result.putAll(additionalProperties); + } + } + if (propertyValue != null) { result.put(propertyKey, propertyValue); } - if (additionalProperties != null && !additionalProperties.isEmpty()) { - result.putAll(additionalProperties); - } return result; } } diff --git a/extensions/security/runtime-spi/src/main/java/io/quarkus/security/spi/runtime/AuthorizationFailureEvent.java b/extensions/security/runtime-spi/src/main/java/io/quarkus/security/spi/runtime/AuthorizationFailureEvent.java index 30f9286cba7ce..caf5cea6e716e 100644 --- a/extensions/security/runtime-spi/src/main/java/io/quarkus/security/spi/runtime/AuthorizationFailureEvent.java +++ b/extensions/security/runtime-spi/src/main/java/io/quarkus/security/spi/runtime/AuthorizationFailureEvent.java @@ -12,6 +12,7 @@ public final class AuthorizationFailureEvent extends AbstractSecurityEvent { public static final String AUTHORIZATION_FAILURE_KEY = AuthorizationFailureEvent.class.getName() + ".FAILURE"; public static final String AUTHORIZATION_CONTEXT_KEY = AuthorizationFailureEvent.class.getName() + ".CONTEXT"; + public static final String SECURED_METHOD_KEY = AuthorizationFailureEvent.class.getName() + ".SECURED_METHOD"; public AuthorizationFailureEvent(SecurityIdentity securityIdentity, Throwable authorizationFailure, String authorizationContext) { @@ -23,6 +24,12 @@ public AuthorizationFailureEvent(SecurityIdentity securityIdentity, Throwable au super(securityIdentity, withProperties(authorizationFailure, authorizationContext, eventProperties)); } + public AuthorizationFailureEvent(SecurityIdentity securityIdentity, Throwable authorizationFailure, + String authorizationContext, Map eventProperties, MethodDescription securedMethod) { + this(securityIdentity, authorizationFailure, authorizationContext, + withProperties(SECURED_METHOD_KEY, toString(securedMethod), eventProperties)); + } + public Throwable getAuthorizationFailure() { return (Throwable) eventProperties.get(AUTHORIZATION_FAILURE_KEY); } diff --git a/extensions/security/runtime-spi/src/main/java/io/quarkus/security/spi/runtime/AuthorizationSuccessEvent.java b/extensions/security/runtime-spi/src/main/java/io/quarkus/security/spi/runtime/AuthorizationSuccessEvent.java index 2160d74b87724..69fd6cbb9b002 100644 --- a/extensions/security/runtime-spi/src/main/java/io/quarkus/security/spi/runtime/AuthorizationSuccessEvent.java +++ b/extensions/security/runtime-spi/src/main/java/io/quarkus/security/spi/runtime/AuthorizationSuccessEvent.java @@ -10,6 +10,7 @@ */ public final class AuthorizationSuccessEvent extends AbstractSecurityEvent { public static final String AUTHORIZATION_CONTEXT = AuthorizationSuccessEvent.class.getName() + ".CONTEXT"; + public static final String SECURED_METHOD_KEY = AuthorizationSuccessEvent.class.getName() + ".SECURED_METHOD"; public AuthorizationSuccessEvent(SecurityIdentity securityIdentity, Map eventProperties) { super(securityIdentity, eventProperties); @@ -19,4 +20,10 @@ public AuthorizationSuccessEvent(SecurityIdentity securityIdentity, String autho Map eventProperties) { super(securityIdentity, withProperties(AUTHORIZATION_CONTEXT, authorizationContext, eventProperties)); } + + public AuthorizationSuccessEvent(SecurityIdentity securityIdentity, String authorizationContext, + Map eventProperties, MethodDescription securedMethod) { + this(securityIdentity, authorizationContext, withProperties(SECURED_METHOD_KEY, toString(securedMethod), + eventProperties)); + } } diff --git a/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/SecurityCheckRecorder.java b/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/SecurityCheckRecorder.java index 545b3249cd9b0..732bda8773e80 100644 --- a/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/SecurityCheckRecorder.java +++ b/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/SecurityCheckRecorder.java @@ -7,6 +7,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; @@ -17,16 +18,21 @@ import org.eclipse.microprofile.config.Config; import org.eclipse.microprofile.config.spi.ConfigProviderResolver; +import io.quarkus.arc.Arc; import io.quarkus.runtime.RuntimeValue; +import io.quarkus.runtime.ShutdownContext; import io.quarkus.runtime.annotations.Recorder; import io.quarkus.security.StringPermission; import io.quarkus.security.runtime.interceptor.SecurityCheckStorageBuilder; +import io.quarkus.security.runtime.interceptor.SecurityConstrainer; import io.quarkus.security.runtime.interceptor.check.AuthenticatedCheck; import io.quarkus.security.runtime.interceptor.check.DenyAllCheck; import io.quarkus.security.runtime.interceptor.check.PermissionSecurityCheck; import io.quarkus.security.runtime.interceptor.check.PermitAllCheck; import io.quarkus.security.runtime.interceptor.check.RolesAllowedCheck; import io.quarkus.security.runtime.interceptor.check.SupplierRolesAllowedCheck; +import io.quarkus.security.spi.runtime.AuthorizationFailureEvent; +import io.quarkus.security.spi.runtime.AuthorizationSuccessEvent; import io.quarkus.security.spi.runtime.SecurityCheck; import io.quarkus.security.spi.runtime.SecurityCheckStorage; import io.smallrye.config.Expressions; @@ -37,6 +43,7 @@ public class SecurityCheckRecorder { private static volatile SecurityCheckStorage storage; private static final Set configExpRolesAllowedChecks = ConcurrentHashMap.newKeySet(); + private static volatile boolean runtimeConfigReady = false; public static SecurityCheckStorage getStorage() { return storage; @@ -355,4 +362,37 @@ private Class loadClass(String className) { public void registerDefaultSecurityCheck(RuntimeValue builder, SecurityCheck securityCheck) { builder.getValue().registerDefaultSecurityCheck(securityCheck); } + + public Supplier createSecurityConstrainer(Supplier> additionalEventPropsSupplier) { + return new Supplier() { + @Override + public SecurityConstrainer get() { + var container = Arc.container(); + var beanManager = container.beanManager(); + var eventPropsSupplier = additionalEventPropsSupplier == null ? new Supplier>() { + @Override + public Map get() { + return Map.of(); + } + } : additionalEventPropsSupplier; + return new SecurityConstrainer(container.instance(SecurityCheckStorage.class).get(), + beanManager, beanManager.getEvent().select(AuthorizationFailureEvent.class), + beanManager.getEvent().select(AuthorizationSuccessEvent.class), runtimeConfigReady, + container.select(SecurityIdentityAssociation.class), eventPropsSupplier); + } + }; + } + + public void setRuntimeConfigReady() { + runtimeConfigReady = true; + } + + public void unsetRuntimeConfigReady(ShutdownContext shutdownContext) { + shutdownContext.addShutdownTask(new Runnable() { + @Override + public void run() { + runtimeConfigReady = false; + } + }); + } } diff --git a/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/SecurityProviderRecorder.java b/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/SecurityProviderRecorder.java index 01eb8e2c6bc88..1426988511198 100644 --- a/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/SecurityProviderRecorder.java +++ b/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/SecurityProviderRecorder.java @@ -6,16 +6,27 @@ import static io.quarkus.security.runtime.SecurityProviderUtils.loadProvider; import static io.quarkus.security.runtime.SecurityProviderUtils.loadProviderWithParams; +import java.security.NoSuchAlgorithmException; import java.security.Provider; +import java.security.SecureRandom; +import java.security.Security; + +import org.jboss.logging.Logger; import io.quarkus.runtime.annotations.Recorder; @Recorder public class SecurityProviderRecorder { + + private static final Logger LOG = Logger.getLogger(SecurityProviderRecorder.class); + public void addBouncyCastleProvider(boolean inFipsMode) { final String providerName = inFipsMode ? SecurityProviderUtils.BOUNCYCASTLE_FIPS_PROVIDER_CLASS_NAME : SecurityProviderUtils.BOUNCYCASTLE_PROVIDER_CLASS_NAME; addProvider(loadProvider(providerName)); + if (inFipsMode) { + setSecureRandomStrongAlgorithmIfNecessary(); + } } public void addBouncyCastleJsseProvider() { @@ -33,5 +44,23 @@ public void addBouncyCastleFipsJsseProvider() { Provider bcJsse = loadProviderWithParams(SecurityProviderUtils.BOUNCYCASTLE_JSSE_PROVIDER_CLASS_NAME, new Class[] { boolean.class, Provider.class }, new Object[] { true, bc }); insertProvider(bcJsse, sunIndex + 1); + setSecureRandomStrongAlgorithmIfNecessary(); + } + + private void setSecureRandomStrongAlgorithmIfNecessary() { + try { + // workaround for the issue on OpenJDK 17 & RHEL8 & FIPS + // see https://github.com/bcgit/bc-java/issues/1285#issuecomment-2068958587 + // we can remove this when OpenJDK 17 support is dropped or if it starts working on newer versions of RHEL8+ + SecureRandom.getInstanceStrong(); + } catch (NoSuchAlgorithmException e) { + SecureRandom secRandom = new SecureRandom(); + String origStrongAlgorithms = Security.getProperty("securerandom.strongAlgorithms"); + String usedAlgorithm = secRandom.getAlgorithm() + ":" + secRandom.getProvider().getName(); + String strongAlgorithms = origStrongAlgorithms == null ? usedAlgorithm : usedAlgorithm + "," + origStrongAlgorithms; + LOG.debugf("Strong SecureRandom algorithm '%s' is not available. " + + "Using fallback algorithm '%s'.", origStrongAlgorithms, usedAlgorithm); + Security.setProperty("securerandom.strongAlgorithms", strongAlgorithms); + } } } diff --git a/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/interceptor/SecurityConstrainer.java b/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/interceptor/SecurityConstrainer.java index a8d68b3c23e48..3d63ca29d80e4 100644 --- a/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/interceptor/SecurityConstrainer.java +++ b/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/interceptor/SecurityConstrainer.java @@ -4,19 +4,24 @@ import static io.quarkus.security.spi.runtime.SecurityEventHelper.AUTHORIZATION_SUCCESS; import java.lang.reflect.Method; +import java.util.Map; import java.util.function.Consumer; import java.util.function.Function; +import java.util.function.Supplier; import jakarta.enterprise.event.Event; +import jakarta.enterprise.inject.Instance; import jakarta.enterprise.inject.spi.BeanManager; -import jakarta.inject.Inject; import jakarta.inject.Singleton; +import org.eclipse.microprofile.config.ConfigProvider; + import io.quarkus.runtime.BlockingOperationNotAllowedException; import io.quarkus.security.identity.SecurityIdentity; import io.quarkus.security.runtime.SecurityIdentityAssociation; import io.quarkus.security.spi.runtime.AuthorizationFailureEvent; import io.quarkus.security.spi.runtime.AuthorizationSuccessEvent; +import io.quarkus.security.spi.runtime.MethodDescription; import io.quarkus.security.spi.runtime.SecurityCheck; import io.quarkus.security.spi.runtime.SecurityCheckStorage; import io.quarkus.security.spi.runtime.SecurityEventHelper; @@ -31,16 +36,26 @@ public class SecurityConstrainer { public static final Object CHECK_OK = new Object(); private final SecurityCheckStorage storage; private final SecurityEventHelper securityEventHelper; + private final Instance securityIdentityAssociation; + private final Supplier> additionalEventPropsSupplier; - @Inject - SecurityIdentityAssociation identityAssociation; - - SecurityConstrainer(SecurityCheckStorage storage, BeanManager beanManager, - Event authZFailureEvent, Event authZSuccessEvent) { + public SecurityConstrainer(SecurityCheckStorage storage, BeanManager beanManager, + Event authZFailureEvent, Event authZSuccessEvent, + boolean runtimeConfigReady, Instance securityIdentityAssociation, + Supplier> additionalEventPropsSupplier) { + this.securityIdentityAssociation = securityIdentityAssociation; + this.additionalEventPropsSupplier = additionalEventPropsSupplier; this.storage = storage; - // static interceptors are initialized during the static init, therefore we need to initialize the helper lazily - this.securityEventHelper = SecurityEventHelper.lazilyOf(authZSuccessEvent, authZFailureEvent, - AUTHORIZATION_SUCCESS, AUTHORIZATION_FAILURE, beanManager); + if (runtimeConfigReady) { + boolean securityEventsEnabled = ConfigProvider.getConfig().getValue("quarkus.security.events.enabled", + Boolean.class); + this.securityEventHelper = new SecurityEventHelper<>(authZSuccessEvent, authZFailureEvent, AUTHORIZATION_SUCCESS, + AUTHORIZATION_FAILURE, beanManager, securityEventsEnabled); + } else { + // static interceptors are initialized during the static init, therefore we need to initialize the helper lazily + this.securityEventHelper = SecurityEventHelper.lazilyOf(authZSuccessEvent, authZFailureEvent, + AUTHORIZATION_SUCCESS, AUTHORIZATION_FAILURE, beanManager); + } } public void check(Method method, Object[] parameters) { @@ -48,7 +63,7 @@ public void check(Method method, Object[] parameters) { SecurityIdentity identity = null; if (securityCheck != null && !securityCheck.isPermitAll()) { try { - identity = identityAssociation.getIdentity(); + identity = securityIdentityAssociation.get().getIdentity(); } catch (BlockingOperationNotAllowedException blockingException) { throw new BlockingOperationNotAllowedException( "Blocking security check attempted in code running on the event loop. " + @@ -61,7 +76,7 @@ public void check(Method method, Object[] parameters) { try { securityCheck.apply(identity, method, parameters); } catch (Exception exception) { - fireAuthZFailureEvent(identity, exception, securityCheck); + fireAuthZFailureEvent(identity, exception, securityCheck, method); throw exception; } } else { @@ -69,7 +84,7 @@ public void check(Method method, Object[] parameters) { } } if (securityEventHelper.fireEventOnSuccess()) { - fireAuthZSuccessEvent(securityCheck, identity); + fireAuthZSuccessEvent(securityCheck, identity, method); } } @@ -77,7 +92,7 @@ public Uni nonBlockingCheck(Method method, Object[] parameters) { SecurityCheck securityCheck = storage.getSecurityCheck(method); if (securityCheck != null) { if (!securityCheck.isPermitAll()) { - return identityAssociation.getDeferredIdentity() + return securityIdentityAssociation.get().getDeferredIdentity() .onItem() .transformToUni(new Function>() { @Override @@ -87,7 +102,7 @@ public Uni apply(SecurityIdentity securityIdentity) { checkResult = checkResult.onFailure().invoke(new Consumer() { @Override public void accept(Throwable throwable) { - fireAuthZFailureEvent(securityIdentity, throwable, securityCheck); + fireAuthZFailureEvent(securityIdentity, throwable, securityCheck, method); } }); } @@ -95,7 +110,7 @@ public void accept(Throwable throwable) { checkResult = checkResult.invoke(new Runnable() { @Override public void run() { - fireAuthZSuccessEvent(securityCheck, securityIdentity); + fireAuthZSuccessEvent(securityCheck, securityIdentity, method); } }); } @@ -103,19 +118,28 @@ public void run() { } }); } else if (securityEventHelper.fireEventOnSuccess()) { - fireAuthZSuccessEvent(securityCheck, null); + fireAuthZSuccessEvent(securityCheck, null, method); } } return Uni.createFrom().item(CHECK_OK); } - private void fireAuthZSuccessEvent(SecurityCheck securityCheck, SecurityIdentity identity) { + private void fireAuthZSuccessEvent(SecurityCheck securityCheck, SecurityIdentity identity, Method method) { var securityCheckName = securityCheck == null ? null : securityCheck.getClass().getName(); - securityEventHelper.fireSuccessEvent(new AuthorizationSuccessEvent(identity, securityCheckName, null)); + var additionalEventProps = additionalEventPropsSupplier.get(); + if (identity == null) { + // get identity from event if auth already finished + identity = (SecurityIdentity) additionalEventProps.get(SecurityIdentity.class.getName()); + } + securityEventHelper.fireSuccessEvent( + new AuthorizationSuccessEvent(identity, securityCheckName, additionalEventPropsSupplier.get(), + MethodDescription.ofMethod(method))); } - private void fireAuthZFailureEvent(SecurityIdentity identity, Throwable failure, SecurityCheck securityCheck) { + private void fireAuthZFailureEvent(SecurityIdentity identity, Throwable failure, SecurityCheck securityCheck, + Method method) { securityEventHelper - .fireFailureEvent(new AuthorizationFailureEvent(identity, failure, securityCheck.getClass().getName())); + .fireFailureEvent(new AuthorizationFailureEvent(identity, failure, securityCheck.getClass().getName(), + additionalEventPropsSupplier.get(), MethodDescription.ofMethod(method))); } } diff --git a/extensions/security/spi/src/main/java/io/quarkus/security/spi/AdditionalSecurityConstrainerEventPropsBuildItem.java b/extensions/security/spi/src/main/java/io/quarkus/security/spi/AdditionalSecurityConstrainerEventPropsBuildItem.java new file mode 100644 index 0000000000000..29617d9a0c3af --- /dev/null +++ b/extensions/security/spi/src/main/java/io/quarkus/security/spi/AdditionalSecurityConstrainerEventPropsBuildItem.java @@ -0,0 +1,24 @@ +package io.quarkus.security.spi; + +import java.util.Map; +import java.util.function.Supplier; + +import io.quarkus.builder.item.SimpleBuildItem; + +/** + * This item allows to enhance properties of security events produced by SecurityConstrainer. + * The SecurityConstrainer is usually invoked when CDI request context is already fully setup, and the additional + * properties can be added based on the active context. + */ +public final class AdditionalSecurityConstrainerEventPropsBuildItem extends SimpleBuildItem { + + private final Supplier> additionalEventPropsSupplier; + + public AdditionalSecurityConstrainerEventPropsBuildItem(Supplier> additionalEventPropsSupplier) { + this.additionalEventPropsSupplier = additionalEventPropsSupplier; + } + + public Supplier> getAdditionalEventPropsSupplier() { + return additionalEventPropsSupplier; + } +} diff --git a/extensions/security/spi/src/main/java/io/quarkus/security/spi/DefaultSecurityCheckBuildItem.java b/extensions/security/spi/src/main/java/io/quarkus/security/spi/DefaultSecurityCheckBuildItem.java index ed3dafe18de0d..67765b5728cf1 100644 --- a/extensions/security/spi/src/main/java/io/quarkus/security/spi/DefaultSecurityCheckBuildItem.java +++ b/extensions/security/spi/src/main/java/io/quarkus/security/spi/DefaultSecurityCheckBuildItem.java @@ -3,9 +3,16 @@ import java.util.List; import java.util.Objects; -import io.quarkus.builder.item.SimpleBuildItem; - -public final class DefaultSecurityCheckBuildItem extends SimpleBuildItem { +import io.quarkus.builder.item.MultiBuildItem; + +/** + * Registers default SecurityCheck with the SecurityCheckStorage. + * Please make sure this build item is produced exactly once or validation will fail and exception will be thrown. + */ +public final class DefaultSecurityCheckBuildItem + // we make this Multi to run CapabilityAggregationStep#aggregateCapabilities first + // so that user-friendly error message is logged when Quarkus REST and RESTEasy are used together + extends MultiBuildItem { public final List rolesAllowed; diff --git a/extensions/smallrye-graphql-client/deployment/src/main/java/io/quarkus/smallrye/graphql/client/deployment/SmallRyeGraphQLClientFinalIndexBuildItem.java b/extensions/smallrye-graphql-client/deployment/src/main/java/io/quarkus/smallrye/graphql/client/deployment/SmallRyeGraphQLClientFinalIndexBuildItem.java deleted file mode 100644 index bef82c88f1752..0000000000000 --- a/extensions/smallrye-graphql-client/deployment/src/main/java/io/quarkus/smallrye/graphql/client/deployment/SmallRyeGraphQLClientFinalIndexBuildItem.java +++ /dev/null @@ -1,18 +0,0 @@ -package io.quarkus.smallrye.graphql.client.deployment; - -import org.jboss.jandex.IndexView; - -import io.quarkus.builder.item.SimpleBuildItem; - -final class SmallRyeGraphQLClientFinalIndexBuildItem extends SimpleBuildItem { - - private final IndexView index; - - public SmallRyeGraphQLClientFinalIndexBuildItem(IndexView index) { - this.index = index; - } - - public IndexView getFinalIndex() { - return index; - } -} diff --git a/extensions/smallrye-graphql-client/deployment/src/test/java/io/quarkus/smallrye/graphql/client/deployment/TypesafeGraphQLRecursionTest.java b/extensions/smallrye-graphql-client/deployment/src/test/java/io/quarkus/smallrye/graphql/client/deployment/TypesafeGraphQLRecursionTest.java index 252abcaeed093..92acd76404ebd 100644 --- a/extensions/smallrye-graphql-client/deployment/src/test/java/io/quarkus/smallrye/graphql/client/deployment/TypesafeGraphQLRecursionTest.java +++ b/extensions/smallrye-graphql-client/deployment/src/test/java/io/quarkus/smallrye/graphql/client/deployment/TypesafeGraphQLRecursionTest.java @@ -13,7 +13,7 @@ import io.smallrye.graphql.client.typesafe.api.GraphQLClientApi; public class TypesafeGraphQLRecursionTest { - private static String EXPECTED_THROWN_MESSAGE = "SRGQLDC035008: Field recursion found"; + private final static String EXPECTED_THROWN_MESSAGE = "field recursion found"; @RegisterExtension static QuarkusUnitTest test = new QuarkusUnitTest() diff --git a/extensions/smallrye-graphql-client/runtime/src/main/java/io/quarkus/smallrye/graphql/client/runtime/GraphQLClientConfig.java b/extensions/smallrye-graphql-client/runtime/src/main/java/io/quarkus/smallrye/graphql/client/runtime/GraphQLClientConfig.java index dfb406ab47537..dcbe959117b0a 100644 --- a/extensions/smallrye-graphql-client/runtime/src/main/java/io/quarkus/smallrye/graphql/client/runtime/GraphQLClientConfig.java +++ b/extensions/smallrye-graphql-client/runtime/src/main/java/io/quarkus/smallrye/graphql/client/runtime/GraphQLClientConfig.java @@ -5,6 +5,7 @@ import java.util.Optional; import java.util.OptionalInt; +import io.quarkus.runtime.annotations.ConfigDocMapKey; import io.quarkus.runtime.annotations.ConfigGroup; import io.quarkus.runtime.annotations.ConfigItem; @@ -21,6 +22,7 @@ public class GraphQLClientConfig { * HTTP headers to add when communicating with the target GraphQL service. */ @ConfigItem(name = "header") + @ConfigDocMapKey("header-name") public Map headers; /** @@ -118,6 +120,7 @@ public class GraphQLClientConfig { * Additional payload sent on websocket initialization. */ @ConfigItem(name = "init-payload") + @ConfigDocMapKey("property-name") public Map initPayload; /** diff --git a/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeHealthRuntimeConfig.java b/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeHealthRuntimeConfig.java index 4b33d6c66312d..43c8adeb0bcc0 100644 --- a/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeHealthRuntimeConfig.java +++ b/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeHealthRuntimeConfig.java @@ -2,6 +2,7 @@ import java.util.Map; +import io.quarkus.runtime.annotations.ConfigDocMapKey; import io.quarkus.runtime.annotations.ConfigGroup; import io.quarkus.runtime.annotations.ConfigItem; import io.quarkus.runtime.annotations.ConfigPhase; @@ -20,12 +21,14 @@ public class SmallRyeHealthRuntimeConfig { * Additional top-level properties to be included in the resulting JSON object. */ @ConfigItem(name = "additional.property") + @ConfigDocMapKey("property-name") Map additionalProperties; /** * Specifications of checks that can be disabled. */ @ConfigItem + @ConfigDocMapKey("check-name") Map check; @ConfigGroup diff --git a/extensions/smallrye-openapi-common/deployment/src/main/java/io/quarkus/smallrye/openapi/common/deployment/SmallRyeOpenApiConfig.java b/extensions/smallrye-openapi-common/deployment/src/main/java/io/quarkus/smallrye/openapi/common/deployment/SmallRyeOpenApiConfig.java index 087b9dc7cf1cf..7e47a5188ae54 100644 --- a/extensions/smallrye-openapi-common/deployment/src/main/java/io/quarkus/smallrye/openapi/common/deployment/SmallRyeOpenApiConfig.java +++ b/extensions/smallrye-openapi-common/deployment/src/main/java/io/quarkus/smallrye/openapi/common/deployment/SmallRyeOpenApiConfig.java @@ -7,6 +7,7 @@ import java.util.Optional; import java.util.stream.Collectors; +import io.quarkus.runtime.annotations.ConfigDocMapKey; import io.quarkus.runtime.annotations.ConfigItem; import io.quarkus.runtime.annotations.ConfigRoot; @@ -73,6 +74,7 @@ public final class SmallRyeOpenApiConfig { * Add one or more extensions to the security scheme */ @ConfigItem + @ConfigDocMapKey("extension-name") public Map securitySchemeExtensions = Collections.emptyMap(); /** diff --git a/extensions/smallrye-reactive-messaging-amqp/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/amqp/deployment/AmqpDevServicesBuildTimeConfig.java b/extensions/smallrye-reactive-messaging-amqp/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/amqp/deployment/AmqpDevServicesBuildTimeConfig.java index 2f350c93139ee..26997157ad810 100644 --- a/extensions/smallrye-reactive-messaging-amqp/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/amqp/deployment/AmqpDevServicesBuildTimeConfig.java +++ b/extensions/smallrye-reactive-messaging-amqp/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/amqp/deployment/AmqpDevServicesBuildTimeConfig.java @@ -3,6 +3,7 @@ import java.util.Map; import java.util.Optional; +import io.quarkus.runtime.annotations.ConfigDocMapKey; import io.quarkus.runtime.annotations.ConfigGroup; import io.quarkus.runtime.annotations.ConfigItem; @@ -77,6 +78,7 @@ public class AmqpDevServicesBuildTimeConfig { * Environment variables that are passed to the container. */ @ConfigItem + @ConfigDocMapKey("environment-variable-name") public Map containerEnv; } diff --git a/extensions/smallrye-reactive-messaging-mqtt/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/mqtt/deployment/MqttDevServicesBuildTimeConfig.java b/extensions/smallrye-reactive-messaging-mqtt/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/mqtt/deployment/MqttDevServicesBuildTimeConfig.java index c657e38421663..b7e9be259235f 100644 --- a/extensions/smallrye-reactive-messaging-mqtt/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/mqtt/deployment/MqttDevServicesBuildTimeConfig.java +++ b/extensions/smallrye-reactive-messaging-mqtt/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/mqtt/deployment/MqttDevServicesBuildTimeConfig.java @@ -4,6 +4,7 @@ import java.util.Optional; import java.util.OptionalInt; +import io.quarkus.runtime.annotations.ConfigDocMapKey; import io.quarkus.runtime.annotations.ConfigGroup; import io.quarkus.runtime.annotations.ConfigItem; @@ -66,5 +67,6 @@ public class MqttDevServicesBuildTimeConfig { * Environment variables that are passed to the container. */ @ConfigItem + @ConfigDocMapKey("environment-variable-name") public Map containerEnv; } diff --git a/extensions/smallrye-reactive-messaging-pulsar/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/pulsar/deployment/PulsarDevServicesBuildTimeConfig.java b/extensions/smallrye-reactive-messaging-pulsar/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/pulsar/deployment/PulsarDevServicesBuildTimeConfig.java index 8b7d724005748..b6c59449488b4 100644 --- a/extensions/smallrye-reactive-messaging-pulsar/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/pulsar/deployment/PulsarDevServicesBuildTimeConfig.java +++ b/extensions/smallrye-reactive-messaging-pulsar/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/pulsar/deployment/PulsarDevServicesBuildTimeConfig.java @@ -3,6 +3,7 @@ import java.util.Map; import java.util.Optional; +import io.quarkus.runtime.annotations.ConfigDocMapKey; import io.quarkus.runtime.annotations.ConfigGroup; import io.quarkus.runtime.annotations.ConfigItem; @@ -67,6 +68,7 @@ public class PulsarDevServicesBuildTimeConfig { * Broker config to set on the Pulsar instance */ @ConfigItem + @ConfigDocMapKey("environment-variable-name") public Map brokerConfig; } diff --git a/extensions/smallrye-reactive-messaging-rabbitmq/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/rabbitmq/deployment/RabbitMQDevServicesBuildTimeConfig.java b/extensions/smallrye-reactive-messaging-rabbitmq/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/rabbitmq/deployment/RabbitMQDevServicesBuildTimeConfig.java index b127dcb0d54d8..cf5f3f26b589e 100644 --- a/extensions/smallrye-reactive-messaging-rabbitmq/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/rabbitmq/deployment/RabbitMQDevServicesBuildTimeConfig.java +++ b/extensions/smallrye-reactive-messaging-rabbitmq/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/rabbitmq/deployment/RabbitMQDevServicesBuildTimeConfig.java @@ -4,6 +4,7 @@ import java.util.Optional; import java.util.OptionalInt; +import io.quarkus.runtime.annotations.ConfigDocMapKey; import io.quarkus.runtime.annotations.ConfigGroup; import io.quarkus.runtime.annotations.ConfigItem; @@ -35,6 +36,7 @@ public static class Exchange { * Extra arguments for the exchange definition. */ @ConfigItem + @ConfigDocMapKey("argument-name") public Map arguments; } @@ -57,6 +59,7 @@ public static class Queue { * Extra arguments for the queue definition. */ @ConfigItem + @ConfigDocMapKey("argument-name") public Map arguments; } @@ -91,6 +94,7 @@ public static class Binding { * Extra arguments for the binding definition. */ @ConfigItem + @ConfigDocMapKey("argument-name") public Map arguments; } @@ -159,23 +163,27 @@ public static class Binding { * Exchanges that should be predefined after starting the RabbitMQ broker. */ @ConfigItem + @ConfigDocMapKey("exchange-name") public Map exchanges; /** * Queues that should be predefined after starting the RabbitMQ broker. */ @ConfigItem + @ConfigDocMapKey("queue-name") public Map queues; /** * Bindings that should be predefined after starting the RabbitMQ broker. */ @ConfigItem + @ConfigDocMapKey("binding-name") public Map bindings; /** * Environment variables that are passed to the container. */ @ConfigItem + @ConfigDocMapKey("environment-variable-name") public Map containerEnv; } diff --git a/extensions/smallrye-reactive-messaging/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/deployment/ReactiveMessagingDotNames.java b/extensions/smallrye-reactive-messaging/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/deployment/ReactiveMessagingDotNames.java index 1871c2a0575c4..e583f28d3c0c7 100644 --- a/extensions/smallrye-reactive-messaging/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/deployment/ReactiveMessagingDotNames.java +++ b/extensions/smallrye-reactive-messaging/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/deployment/ReactiveMessagingDotNames.java @@ -32,6 +32,7 @@ import io.smallrye.reactive.messaging.keyed.KeyValueExtractor; import io.smallrye.reactive.messaging.keyed.Keyed; import io.smallrye.reactive.messaging.keyed.KeyedMulti; +import io.smallrye.reactive.messaging.providers.extension.HealthCenter; public final class ReactiveMessagingDotNames { @@ -101,6 +102,8 @@ public final class ReactiveMessagingDotNames { static final DotName UNI = DotName.createSimple(Uni.class.getName()); static final DotName MULTI = DotName.createSimple(Multi.class.getName()); + static final DotName HEALTH_CENTER = DotName.createSimple(HealthCenter.class.getName()); + private ReactiveMessagingDotNames() { } diff --git a/extensions/smallrye-reactive-messaging/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/deployment/SmallRyeReactiveMessagingProcessor.java b/extensions/smallrye-reactive-messaging/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/deployment/SmallRyeReactiveMessagingProcessor.java index 1f5a629d49fe5..abd7dc0c5a2fd 100644 --- a/extensions/smallrye-reactive-messaging/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/deployment/SmallRyeReactiveMessagingProcessor.java +++ b/extensions/smallrye-reactive-messaging/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/deployment/SmallRyeReactiveMessagingProcessor.java @@ -72,6 +72,8 @@ import io.quarkus.smallrye.reactivemessaging.deployment.items.MediatorBuildItem; import io.quarkus.smallrye.reactivemessaging.runtime.DuplicatedContextConnectorFactory; import io.quarkus.smallrye.reactivemessaging.runtime.DuplicatedContextConnectorFactoryInterceptor; +import io.quarkus.smallrye.reactivemessaging.runtime.HealthCenterFilter; +import io.quarkus.smallrye.reactivemessaging.runtime.HealthCenterInterceptor; import io.quarkus.smallrye.reactivemessaging.runtime.QuarkusMediatorConfiguration; import io.quarkus.smallrye.reactivemessaging.runtime.QuarkusWorkerPoolRegistry; import io.quarkus.smallrye.reactivemessaging.runtime.ReactiveMessagingConfiguration; @@ -216,7 +218,8 @@ public void disableObservation(BuildProducer producer) { + BuildProducer producer, BuildProducer beans, + BuildProducer transformations) { producer.produce( new HealthBuildItem(SmallRyeReactiveMessagingLivenessCheck.class.getName(), buildTimeConfig.healthEnabled)); @@ -226,6 +229,24 @@ public void enableHealth(ReactiveMessagingBuildTimeConfig buildTimeConfig, producer.produce( new HealthBuildItem(SmallRyeReactiveMessagingStartupCheck.class.getName(), buildTimeConfig.healthEnabled)); + if (buildTimeConfig.healthEnabled) { + beans.produce(new AdditionalBeanBuildItem(HealthCenterFilter.class, HealthCenterInterceptor.class)); + + transformations.produce(new AnnotationsTransformerBuildItem(new AnnotationsTransformer() { + @Override + public boolean appliesTo(AnnotationTarget.Kind kind) { + return kind == AnnotationTarget.Kind.CLASS; + } + + @Override + public void transform(TransformationContext ctx) { + ClassInfo clazz = ctx.getTarget().asClass(); + if (clazz.name().equals(ReactiveMessagingDotNames.HEALTH_CENTER)) { + ctx.transform().add(HealthCenterFilter.class).done(); + } + } + })); + } } @BuildStep diff --git a/extensions/smallrye-reactive-messaging/runtime/src/main/java/io/quarkus/smallrye/reactivemessaging/runtime/HealthCenterFilter.java b/extensions/smallrye-reactive-messaging/runtime/src/main/java/io/quarkus/smallrye/reactivemessaging/runtime/HealthCenterFilter.java new file mode 100644 index 0000000000000..f3a4ced0b014e --- /dev/null +++ b/extensions/smallrye-reactive-messaging/runtime/src/main/java/io/quarkus/smallrye/reactivemessaging/runtime/HealthCenterFilter.java @@ -0,0 +1,15 @@ +package io.quarkus.smallrye.reactivemessaging.runtime; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import jakarta.interceptor.InterceptorBinding; + +@InterceptorBinding +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface HealthCenterFilter { + +} diff --git a/extensions/smallrye-reactive-messaging/runtime/src/main/java/io/quarkus/smallrye/reactivemessaging/runtime/HealthCenterFilterConfig.java b/extensions/smallrye-reactive-messaging/runtime/src/main/java/io/quarkus/smallrye/reactivemessaging/runtime/HealthCenterFilterConfig.java new file mode 100644 index 0000000000000..03866a1624e71 --- /dev/null +++ b/extensions/smallrye-reactive-messaging/runtime/src/main/java/io/quarkus/smallrye/reactivemessaging/runtime/HealthCenterFilterConfig.java @@ -0,0 +1,56 @@ +package io.quarkus.smallrye.reactivemessaging.runtime; + +import java.util.Map; + +import io.quarkus.runtime.annotations.ConfigDocMapKey; +import io.quarkus.runtime.annotations.ConfigDocSection; +import io.quarkus.runtime.annotations.ConfigGroup; +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.annotations.ConfigRoot; +import io.smallrye.config.ConfigMapping; +import io.smallrye.config.WithDefault; +import io.smallrye.config.WithName; + +@ConfigRoot(phase = ConfigPhase.RUN_TIME) +@ConfigMapping(prefix = "quarkus.messaging") +public interface HealthCenterFilterConfig { + + /** + * Configuration for the health center filter. + */ + @ConfigDocMapKey("channel") + @ConfigDocSection + Map health(); + + @ConfigGroup + interface HealthCenterConfig { + + /** + * Whether all health check is enabled + */ + @WithDefault("true") + boolean enabled(); + + /** + * Whether the readiness health check is enabled. + */ + @WithDefault("true") + @WithName("readiness.enabled") + boolean readinessEnabled(); + + /** + * Whether the liveness health check is enabled. + */ + @WithDefault("true") + @WithName("liveness.enabled") + boolean livenessEnabled(); + + /** + * Whether the startup health check is enabled. + */ + @WithDefault("true") + @WithName("startup.enabled") + boolean startupEnabled(); + } + +} diff --git a/extensions/smallrye-reactive-messaging/runtime/src/main/java/io/quarkus/smallrye/reactivemessaging/runtime/HealthCenterInterceptor.java b/extensions/smallrye-reactive-messaging/runtime/src/main/java/io/quarkus/smallrye/reactivemessaging/runtime/HealthCenterInterceptor.java new file mode 100644 index 0000000000000..0e8f52c5b33e0 --- /dev/null +++ b/extensions/smallrye-reactive-messaging/runtime/src/main/java/io/quarkus/smallrye/reactivemessaging/runtime/HealthCenterInterceptor.java @@ -0,0 +1,61 @@ +package io.quarkus.smallrye.reactivemessaging.runtime; + +import java.util.function.Function; + +import jakarta.annotation.Priority; +import jakarta.inject.Inject; +import jakarta.interceptor.AroundInvoke; +import jakarta.interceptor.Interceptor; +import jakarta.interceptor.InvocationContext; + +import io.quarkus.smallrye.reactivemessaging.runtime.HealthCenterFilterConfig.HealthCenterConfig; +import io.smallrye.reactive.messaging.health.HealthReport; + +@Interceptor +@HealthCenterFilter +@Priority(Interceptor.Priority.PLATFORM_BEFORE + 5) +public class HealthCenterInterceptor { + + private final HealthCenterFilterConfig healthCenterFilterConfig; + + @Inject + public HealthCenterInterceptor(HealthCenterFilterConfig healthCenterFilterConfig) { + this.healthCenterFilterConfig = healthCenterFilterConfig; + } + + @AroundInvoke + public Object intercept(InvocationContext ctx) throws Exception { + if (ctx.getMethod().getName().equals("getReadiness")) { + HealthReport result = (HealthReport) ctx.proceed(); + return applyFilter(result, HealthCenterConfig::readinessEnabled); + } + if (ctx.getMethod().getName().equals("getLiveness")) { + HealthReport result = (HealthReport) ctx.proceed(); + return applyFilter(result, HealthCenterConfig::livenessEnabled); + } + if (ctx.getMethod().getName().equals("getStartup")) { + HealthReport result = (HealthReport) ctx.proceed(); + return applyFilter(result, HealthCenterConfig::startupEnabled); + } + + return ctx.proceed(); + } + + private HealthReport applyFilter(HealthReport result, Function filterType) { + if (healthCenterFilterConfig.health().isEmpty()) { + return result; + } + HealthReport.HealthReportBuilder builder = HealthReport.builder(); + for (HealthReport.ChannelInfo channel : result.getChannels()) { + HealthCenterConfig config = healthCenterFilterConfig.health().get(channel.getChannel()); + if (config != null) { + if (config.enabled() && filterType.apply(config)) { + builder.add(channel); + } + } else { + builder.add(channel); + } + } + return builder.build(); + } +} diff --git a/extensions/smallrye-reactive-messaging/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/smallrye-reactive-messaging/runtime/src/main/resources/META-INF/quarkus-extension.yaml index 0dabfc88cf139..4b62dddd9391b 100644 --- a/extensions/smallrye-reactive-messaging/runtime/src/main/resources/META-INF/quarkus-extension.yaml +++ b/extensions/smallrye-reactive-messaging/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -6,6 +6,7 @@ metadata: - "messaging" - "reactive-messaging" - "reactive" + guide: "https://quarkus.io/guides/messaging" categories: - "messaging" status: "stable" diff --git a/extensions/spring-cloud-config-client/runtime/src/main/java/io/quarkus/spring/cloud/config/client/runtime/SpringCloudConfigClientConfig.java b/extensions/spring-cloud-config-client/runtime/src/main/java/io/quarkus/spring/cloud/config/client/runtime/SpringCloudConfigClientConfig.java index 6dc7f665c392e..0a0f08d09946d 100644 --- a/extensions/spring-cloud-config-client/runtime/src/main/java/io/quarkus/spring/cloud/config/client/runtime/SpringCloudConfigClientConfig.java +++ b/extensions/spring-cloud-config-client/runtime/src/main/java/io/quarkus/spring/cloud/config/client/runtime/SpringCloudConfigClientConfig.java @@ -6,6 +6,7 @@ import java.util.Map; import java.util.Optional; +import io.quarkus.runtime.annotations.ConfigDocMapKey; import io.quarkus.runtime.annotations.ConfigPhase; import io.quarkus.runtime.annotations.ConfigRoot; import io.quarkus.runtime.configuration.DurationConverter; @@ -109,6 +110,7 @@ public interface SpringCloudConfigClientConfig { /** * Custom headers to pass the Spring Cloud Config Server when performing the HTTP request */ + @ConfigDocMapKey("header-name") Map headers(); /** diff --git a/extensions/spring-cloud-config-client/runtime/src/main/java/io/quarkus/spring/cloud/config/client/runtime/VertxSpringCloudConfigGateway.java b/extensions/spring-cloud-config-client/runtime/src/main/java/io/quarkus/spring/cloud/config/client/runtime/VertxSpringCloudConfigGateway.java index 5bd22c7833404..fb65b3a0d43d0 100644 --- a/extensions/spring-cloud-config-client/runtime/src/main/java/io/quarkus/spring/cloud/config/client/runtime/VertxSpringCloudConfigGateway.java +++ b/extensions/spring-cloud-config-client/runtime/src/main/java/io/quarkus/spring/cloud/config/client/runtime/VertxSpringCloudConfigGateway.java @@ -18,6 +18,7 @@ import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; +import io.quarkus.runtime.ResettableSystemProperties; import io.quarkus.runtime.util.ClassPathUtils; import io.smallrye.mutiny.Uni; import io.vertx.core.VertxOptions; @@ -62,20 +63,10 @@ private Vertx createVertxInstance() { // This is done using the DISABLE_DNS_RESOLVER_PROP_NAME system property. // The DNS resolver used by vert.x is configured during the (synchronous) initialization. // So, we just need to disable the async resolver around the Vert.x instance creation. - String originalValue = System.getProperty(DISABLE_DNS_RESOLVER_PROP_NAME); - Vertx vertx; - try { - System.setProperty(DISABLE_DNS_RESOLVER_PROP_NAME, "true"); - vertx = Vertx.vertx(new VertxOptions()); - } finally { - // Restore the original value - if (originalValue == null) { - System.clearProperty(DISABLE_DNS_RESOLVER_PROP_NAME); - } else { - System.setProperty(DISABLE_DNS_RESOLVER_PROP_NAME, originalValue); - } + try (var resettableSystemProperties = ResettableSystemProperties.of( + DISABLE_DNS_RESOLVER_PROP_NAME, "true")) { + return Vertx.vertx(new VertxOptions()); } - return vertx; } public static WebClient createHttpClient(Vertx vertx, SpringCloudConfigClientConfig config) { diff --git a/extensions/swagger-ui/deployment/src/main/java/io/quarkus/swaggerui/deployment/SwaggerUiConfig.java b/extensions/swagger-ui/deployment/src/main/java/io/quarkus/swaggerui/deployment/SwaggerUiConfig.java index 63443a10698df..af2ab9d5e29de 100644 --- a/extensions/swagger-ui/deployment/src/main/java/io/quarkus/swaggerui/deployment/SwaggerUiConfig.java +++ b/extensions/swagger-ui/deployment/src/main/java/io/quarkus/swaggerui/deployment/SwaggerUiConfig.java @@ -5,6 +5,7 @@ import java.util.Optional; import java.util.OptionalInt; +import io.quarkus.runtime.annotations.ConfigDocMapKey; import io.quarkus.runtime.annotations.ConfigItem; import io.quarkus.runtime.annotations.ConfigRoot; import io.smallrye.openapi.ui.DocExpansion; @@ -35,6 +36,7 @@ public class SwaggerUiConfig { * Here you can override that and supply multiple urls that will appear in the TopBar plugin. */ @ConfigItem + @ConfigDocMapKey("name") Map urls; /** diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/BuildTimeContentProcessor.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/BuildTimeContentProcessor.java index e8560c2c8518d..86c18954b1cf2 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/BuildTimeContentProcessor.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/BuildTimeContentProcessor.java @@ -108,11 +108,6 @@ InternalImportMapBuildItem createKnownInternalImportMap(NonApplicationRootPathBu internalImportMapBuildItem.add("qwc-server-log", contextRoot + "qwc/qwc-server-log.js"); internalImportMapBuildItem.add("qwc-extension-link", contextRoot + "qwc/qwc-extension-link.js"); // Quarkus UI - internalImportMapBuildItem.add("qui/", contextRoot + "qui/"); - internalImportMapBuildItem.add("qui-card", contextRoot + "qui/qui-card.js"); - - internalImportMapBuildItem.add("qui-badge", contextRoot + "qui/qui-badge.js"); - internalImportMapBuildItem.add("qui-alert", contextRoot + "qui/qui-alert.js"); internalImportMapBuildItem.add("qui-ide-link", contextRoot + "qui/qui-ide-link.js"); // Echarts @@ -144,6 +139,28 @@ InternalImportMapBuildItem createKnownInternalImportMap(NonApplicationRootPathBu return internalImportMapBuildItem; } + @BuildStep(onlyIf = IsDevelopment.class) + RelocationImportMapBuildItem createRelocationMap() { + + RelocationImportMapBuildItem relocationImportMapBuildItem = new RelocationImportMapBuildItem(); + + // Backward compatibility mappings + relocationImportMapBuildItem.add("@quarkus-webcomponents/codeblock/", "@qomponent/qui-code-block/"); + relocationImportMapBuildItem.add("@quarkus-webcomponents/codeblock", "@qomponent/qui-code-block"); + + relocationImportMapBuildItem.add("qui-badge", "@qomponent/qui-badge"); + relocationImportMapBuildItem.add("qui/qui-badge.js", "@qomponent/qui-badge"); + + relocationImportMapBuildItem.add("qui-alert", "@qomponent/qui-alert"); + relocationImportMapBuildItem.add("qui/qui-alert.js", "@qomponent/qui-alert"); + + relocationImportMapBuildItem.add("qui-card", "@qomponent/qui-card"); + relocationImportMapBuildItem.add("qui/qui-card.js", "@qomponent/qui-card"); + + return relocationImportMapBuildItem; + + } + /** * Here we map all the pages (as defined by the extensions) build time data * @@ -312,7 +329,8 @@ QuteTemplateBuildItem createIndexHtmlTemplate( MvnpmBuildItem mvnpmBuildItem, ThemeVarsBuildItem themeVarsBuildItem, NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem, - List internalImportMapBuildItems) { + List internalImportMapBuildItems, + RelocationImportMapBuildItem relocationImportMapBuildItem) { QuteTemplateBuildItem quteTemplateBuildItem = new QuteTemplateBuildItem( QuteTemplateBuildItem.DEV_UI); @@ -321,6 +339,22 @@ QuteTemplateBuildItem createIndexHtmlTemplate( Map importMap = importMapBuildItem.getImportMap(); aggregator.addMappings(importMap); } + + Map currentImportMap = aggregator.aggregate(nonApplicationRootPathBuildItem.getNonApplicationRootPath()) + .getImports(); + Map relocationMap = relocationImportMapBuildItem.getRelocationMap(); + for (Map.Entry relocation : relocationMap.entrySet()) { + String from = relocation.getKey(); + String to = relocation.getValue(); + + if (currentImportMap.containsKey(to)) { + String newTo = currentImportMap.get(to); + aggregator.addMapping(from, newTo); + } else { + log.warn("Could not relocate " + from + " as " + to + " does not exist in the importmap"); + } + } + String esModuleShimsVersion = extractEsModuleShimsVersion(mvnpmBuildItem.getMvnpmJars()); String importmap = aggregator.aggregateAsJson(nonApplicationRootPathBuildItem.getNonApplicationRootPath()); aggregator.reset(); diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/RelocationImportMapBuildItem.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/RelocationImportMapBuildItem.java new file mode 100644 index 0000000000000..c0eeec0505aad --- /dev/null +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/RelocationImportMapBuildItem.java @@ -0,0 +1,26 @@ +package io.quarkus.devui.deployment; + +import java.util.HashMap; +import java.util.Map; + +import io.quarkus.builder.item.SimpleBuildItem; + +/** + * Used internally to relocate namespaces for backward compatibility + */ +public final class RelocationImportMapBuildItem extends SimpleBuildItem { + + private final Map relocations = new HashMap<>(); + + public RelocationImportMapBuildItem() { + + } + + public void add(String from, String to) { + this.relocations.put(from, to); + } + + public Map getRelocationMap() { + return relocations; + } +} diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/HttpSecurityProcessor.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/HttpSecurityProcessor.java index 235aa3500c784..6c77581e02424 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/HttpSecurityProcessor.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/HttpSecurityProcessor.java @@ -53,6 +53,7 @@ import io.quarkus.runtime.RuntimeValue; import io.quarkus.runtime.configuration.ConfigurationException; import io.quarkus.security.spi.AdditionalSecuredMethodsBuildItem; +import io.quarkus.security.spi.AdditionalSecurityConstrainerEventPropsBuildItem; import io.quarkus.security.spi.SecurityTransformerUtils; import io.quarkus.security.spi.runtime.MethodDescription; import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig; @@ -391,6 +392,16 @@ void produceEagerSecurityInterceptorStorage(HttpSecurityRecorder recorder, } } + @BuildStep + @Record(ExecutionTime.STATIC_INIT) + void addRoutingCtxToSecurityEventsForCdiBeans(HttpSecurityRecorder recorder, Capabilities capabilities, + BuildProducer producer) { + if (capabilities.isPresent(Capability.SECURITY)) { + producer.produce( + new AdditionalSecurityConstrainerEventPropsBuildItem(recorder.createAdditionalSecEventPropsSupplier())); + } + } + private static void validateAuthMechanismAnnotationUsage(Capabilities capabilities, HttpBuildTimeConfig buildTimeConfig, DotName[] annotationNames) { if (buildTimeConfig.auth.proactive diff --git a/extensions/vertx-http/dev-ui-resources/pom.xml b/extensions/vertx-http/dev-ui-resources/pom.xml index e81d2a928adc0..3eb8c18a87fee 100644 --- a/extensions/vertx-http/dev-ui-resources/pom.xml +++ b/extensions/vertx-http/dev-ui-resources/pom.xml @@ -105,6 +105,13 @@ runtime + + + org.mvnpm.at.mvnpm + qomponent + runtime + + org.mvnpm diff --git a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qui/qui-alert.js b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qui/qui-alert.js deleted file mode 100644 index 6160fa96e5545..0000000000000 --- a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qui/qui-alert.js +++ /dev/null @@ -1,167 +0,0 @@ -import {LitElement, html, css} from 'lit'; -import '@vaadin/icon'; - -export class QuiAlert extends LitElement { - - static styles = css` - .alert { - padding: 1rem 1rem; - margin: 1rem; - border: 1px solid transparent; - border-radius: 0.375rem; - position: relative; - display: flex; - justify-content: space-between; - } - - .info { - background-color: var(--lumo-primary-color-10pct); - color: var(--lumo-primary-text-color); - } - .success { - background-color: var(--lumo-success-color-10pct); - color: var(--lumo-success-text-color); - } - .warning { - background-color: var(--lumo-warning-color-10pct); - color: var(--lumo-warning-text-color); - } - .error { - background-color: var(--lumo-error-color-10pct); - color: var(--lumo-error-text-color); - } - - .infoprimary { - background-color: var(--lumo-primary-color); - color: var(--lumo-primary-contrast-color); - } - .successprimary { - background-color: var(--lumo-success-color); - color: var(--lumo-success-contrast-color); - } - .warningprimary { - background-color: var(--lumo-warning-color); - color: var(--lumo-warning-contrast-color); - } - .errorprimary { - background-color: var(--lumo-error-color); - color: var(--lumo-error-contrast-color); - } - - .layout { - display: flex; - flex-direction: column; - width: 100%; - } - - .content { - display: flex; - gap: 10px; - align-items: center; - width: 100%; - } - - .center { - justify-content: center; - } - - .close { - cursor: pointer; - } - - .title { - font-size: 1.4em; - padding-bottom: 10px; - } - `; - - static properties = { - // Tag attributes - title: {type: String}, // Optional title - level: {type: String}, // Level (info, success, warning, error) - default info - icon: {type: String}, // Icon - size: {type: String}, // Font size - default large - showIcon: {type: Boolean}, // Use default icon if none is supplied - default false - permanent: {type: Boolean}, // disallow dismissing - default false - primary: {type: Boolean}, // Primary - default false - center: {type: Boolean}, // Center - default false - // Internal state - _dismissed: {type: Boolean, state: true} - }; - - constructor() { - super(); - this.title = null; - this.level = "info"; - this.icon = null; - this.size = "large"; - this.showIcon = false; - this.permanent = false; - this.primary = false; - this.center = false; - this._dismissed - false; - } - render() { - if (!this._dismissed) { - let theme = this.level; - if(this.primary){ - theme = theme + "primary"; - } - - let contentClass="content"; - if(this.center){ - contentClass = contentClass + " center"; - } - return html` - `; - } - } - - _renderIcon(){ - if(this.icon){ - // User provided icon - return html``; - }else if (this.showIcon){ - // Default icon - if(this.level === "info"){ - return html``; - }else if(this.level === "success"){ - return html``; - }else if(this.level === "warning"){ - return html``; - }else if(this.level === "error"){ - return html``; - } - } - } - - _renderTitle(){ - if(this.title){ - return html`
    ${this.title}
    `; - } - } - - _renderClose(){ - if (!this.permanent) { - return html``; - } - } - - _dismiss() { - this._dismissed = true; - } - - - -} - -customElements.define('qui-alert', QuiAlert); diff --git a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qui/qui-badge.js b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qui/qui-badge.js deleted file mode 100644 index 0e84448a5c485..0000000000000 --- a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qui/qui-badge.js +++ /dev/null @@ -1,156 +0,0 @@ -import { LitElement, html, css} from 'lit'; -import '@vaadin/icon'; - -/** - * Badge UI Component based on the vaadin theme one - * see https://vaadin.com/docs/latest/components/badge - */ -export class QuiBadge extends LitElement { - static styles = css` - [theme~="badge"] { - display: inline-flex; - align-items: center; - justify-content: center; - box-sizing: border-box; - padding: 0.4em calc(0.5em + var(--lumo-border-radius-s) / 4); - color: var(--lumo-primary-text-color); - background-color: var(--lumo-primary-color-10pct); - border-radius: var(--lumo-border-radius-s); - font-family: var(--lumo-font-family); - font-size: var(--lumo-font-size-s); - line-height: 1; - font-weight: 500; - text-transform: initial; - letter-spacing: initial; - min-width: calc(var (--lumo-line-height-xs) * 1em + 0.45em); - } - [theme~="success"] { - color: var(--lumo-success-text-color); - background-color: var(--lumo-success-color-10pct); - } - [theme~="error"] { - color: var(--lumo-error-text-color); - background-color: var(--lumo-error-color-10pct); - } - [theme~="warning"] { - color: var(--lumo-warning-text-color); - background-color: var(--lumo-warning-color-10pct); - } - [theme~="contrast"] { - color: var(--lumo-contrast-80pct); - background-color: var(--lumo-contrast-5pct); - } - [theme~="small"] { - font-size: var(--lumo-font-size-xxs); - line-height: 1; - } - [theme~="tiny"] { - font-size: var(--lumo-font-size-xxs); - line-height: 1; - padding: 0.2em calc(0.2em + var(--lumo-border-radius-s) / 4); - } - [theme~="primary"] { - color: var(--lumo-primary-contrast-color); - background-color: var(--lumo-primary-color); - } - [theme~="successprimary"] { - color: var(--lumo-success-contrast-color); - background-color: var(--lumo-success-color); - } - [theme~="warningprimary"] { - color: var(--lumo-warning-contrast-color); - background-color: var(--lumo-warning-color); - } - [theme~="errorprimary"] { - color: var(--lumo-error-contrast-color); - background-color: var(--lumo-error-color); - } - [theme~="contrastprimary"] { - color: var(--lumo-base-color); - background-color: var(--lumo-contrast); - } - [theme~="pill"] { - --lumo-border-radius-s: 1em; - } - `; - - static properties = { - background: {type: String}, - color: {type: String}, - icon: {type: String}, - level: {type: String}, - small: {type: Boolean}, - tiny: {type: Boolean}, - primary: {type: Boolean}, - pill: {type: Boolean}, - clickable: {type: Boolean}, - _theme: {attribute: false}, - _style: {attribute: false}, - }; - - constructor(){ - super(); - this.icon = null; - this.level = null; - this.background = null; - this.color = null; - this.small = false; - this.primary = false; - this.pill = false; - this.clickable = false; - - this._theme = "badge"; - this._style = ""; - } - - connectedCallback() { - super.connectedCallback() - - if(this.level){ - this._theme = this._theme + " " + this.level; - } - if(this.primary){ - if(this.level){ - this._theme = this._theme + "primary"; - }else{ - this._theme = this._theme + " primary"; - } - } - - if(this.small && !this.tiny){ - this._theme = this._theme + " small"; - } - if(this.tiny){ - this._theme = this._theme + " tiny"; - } - - if(this.pill){ - this._theme = this._theme + " pill"; - } - - if(this.background){ - this._style = this._style + "background: " + this.background + ";"; - } - if(this.color){ - this._style = this._style + "color: " + this.color + ";"; - } - if(this.clickable){ - this._style = this._style + "cursor: pointer"; - } - } - - render() { - return html` - ${this._renderIcon()} - - `; - } - - _renderIcon(){ - if(this.icon){ - return html``; - } - } - -} -customElements.define('qui-badge', QuiBadge); \ No newline at end of file diff --git a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qui/qui-card.js b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qui/qui-card.js deleted file mode 100644 index 2ea714978cdc1..0000000000000 --- a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qui/qui-card.js +++ /dev/null @@ -1,92 +0,0 @@ -import { LitElement, html, css} from 'lit'; - -/** - * Card UI Component - */ -export class QuiCard extends LitElement { - - static styles = css` - .card { - display: flex; - flex-direction: column; - justify-content: space-between; - border: 1px solid var(--lumo-contrast-10pct); - border-radius: 4px; - filter: brightness(90%); - - } - - .card-header { - font-size: var(--lumo-font-size-l); - line-height: 1; - height: 25px; - display: flex; - flex-direction: row; - justify-content: space-between; - align-items: center; - padding: 10px 10px; - background-color: var(--lumo-contrast-5pct); - border-bottom: 1px solid var(--lumo-contrast-10pct); - } - - .card-footer { - height: 20px; - padding: 10px 10px; - color: var(--lumo-contrast-50pct); - display: flex; - flex-direction: row; - justify-content: space-between; - visibility:hidden; - }`; - - static properties = { - title: {type: String}, - width: {state: true}, - _hasFooter: {state: true}, - }; - - constructor(){ - super(); - this.width = "100%"; - this._hasFooter = false; - } - - connectedCallback() { - super.connectedCallback() - } - - render() { - return html`
    - ${this._headerTemplate()} - - ${this._footerTemplate()} -
    `; - } - - firstUpdated(){ - const footerSlot = this.shadowRoot.querySelector("#footer"); - if (footerSlot && footerSlot.assignedNodes().length>0){ - console.log('No content is available') - this._hasFooter = true; - } - } - - _headerTemplate() { - return html`
    -
    ${this.title}
    -
    - `; - } - - _footerTemplate() { - if(this._hasFooter){ - return html` - - `; - } - } - -} -customElements.define('qui-card', QuiCard); \ No newline at end of file diff --git a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-configuration-editor.js b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-configuration-editor.js index 5915a124d2bc1..91bf8c8de454c 100644 --- a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-configuration-editor.js +++ b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-configuration-editor.js @@ -4,7 +4,7 @@ import { notifier } from 'notifier'; import { observeState } from 'lit-element-state'; import { devuiState } from 'devui-state'; import { themeState } from 'theme-state'; -import '@quarkus-webcomponents/codeblock'; +import '@qomponent/qui-code-block'; import '@vaadin/button'; import '@vaadin/icon'; import '@vaadin/progress-bar'; diff --git a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-configuration.js b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-configuration.js index 6aec4cc1aacd2..ee3729fd0beba 100644 --- a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-configuration.js +++ b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-configuration.js @@ -2,7 +2,6 @@ import { LitElement, html, css } from 'lit'; import { JsonRpc } from 'jsonrpc'; import { RouterController } from 'router-controller'; import '@vaadin/grid'; -import 'qui/qui-alert.js'; import { columnBodyRenderer } from '@vaadin/grid/lit.js'; import '@vaadin/grid/vaadin-grid-sort-column.js'; import '@vaadin/icon'; @@ -14,13 +13,13 @@ import '@vaadin/text-field'; import '@vaadin/select'; import '@vaadin/details'; import '@vaadin/combo-box'; +import '@qomponent/qui-badge'; import { notifier } from 'notifier'; import { unsafeHTML } from 'lit/directives/unsafe-html.js'; import { gridRowDetailsRenderer } from '@vaadin/grid/lit.js'; import { observeState } from 'lit-element-state'; import { connectionState } from 'connection-state'; import { devuiState } from 'devui-state'; -import 'qui-badge'; /** * This component allows users to change the configuration diff --git a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-continuous-testing.js b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-continuous-testing.js index 82c85eb85863b..7c82175e701c4 100644 --- a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-continuous-testing.js +++ b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-continuous-testing.js @@ -5,12 +5,14 @@ import '@vaadin/icon'; import '@vaadin/details'; import '@vaadin/grid'; import '@vaadin/grid/vaadin-grid-sort-column.js'; -import 'qui-badge'; -import 'qui-ide-link'; -import { columnBodyRenderer } from '@vaadin/grid/lit.js'; -import { gridRowDetailsRenderer } from '@vaadin/grid/lit.js'; import '@vaadin/progress-bar'; import '@vaadin/checkbox'; +import { columnBodyRenderer } from '@vaadin/grid/lit.js'; +import { gridRowDetailsRenderer } from '@vaadin/grid/lit.js'; +import '@qomponent/qui-badge'; +import 'qui-ide-link'; + + import 'echarts-horizontal-stacked-bar'; /** diff --git a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-data-raw-page.js b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-data-raw-page.js index e29f3e158f500..516604cb833fa 100644 --- a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-data-raw-page.js +++ b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-data-raw-page.js @@ -2,7 +2,7 @@ import { LitElement, html, css} from 'lit'; import { RouterController } from 'router-controller'; import { observeState } from 'lit-element-state'; import { themeState } from 'theme-state'; -import '@quarkus-webcomponents/codeblock'; +import '@qomponent/qui-code-block'; /** * This component renders build time data in raw json format diff --git a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-dev-services.js b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-dev-services.js index 45470905e47b4..0b59e6c8a1f58 100644 --- a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-dev-services.js +++ b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-dev-services.js @@ -3,8 +3,8 @@ import { devServices } from 'devui-data'; import { observeState } from 'lit-element-state'; import { themeState } from 'theme-state'; import '@vaadin/icon'; -import '@quarkus-webcomponents/codeblock'; -import 'qui-card'; +import '@qomponent/qui-code-block'; +import '@qomponent/qui-card'; import 'qwc-no-data'; /** @@ -70,7 +70,7 @@ export class QwcDevServices extends observeState(QwcHotReloadElement) { } _renderCard(devService){ - return html` + return html`
    ${this._renderContainerDetails(devService)} ${this._renderConfigDetails(devService)} @@ -140,4 +140,4 @@ export class QwcDevServices extends observeState(QwcHotReloadElement) { } -customElements.define('qwc-dev-services', QwcDevServices); \ No newline at end of file +customElements.define('qwc-dev-services', QwcDevServices); diff --git a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-extension-link.js b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-extension-link.js index c514dfd82b43e..8dcae610d8625 100644 --- a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-extension-link.js +++ b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-extension-link.js @@ -2,7 +2,7 @@ import { QwcHotReloadElement, html, css} from 'qwc-hot-reload-element'; import { unsafeHTML } from 'lit-html/directives/unsafe-html.js'; import { JsonRpc } from 'jsonrpc'; import '@vaadin/icon'; -import 'qui-badge'; +import '@qomponent/qui-badge'; /** * This component adds a custom link on the Extension card diff --git a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-extension.js b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-extension.js index bb97a368653df..5c76cb0bfe8bd 100644 --- a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-extension.js +++ b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-extension.js @@ -2,7 +2,7 @@ import { LitElement, html, css} from 'lit'; import '@vaadin/icon'; import '@vaadin/dialog'; import { dialogHeaderRenderer, dialogRenderer } from '@vaadin/dialog/lit.js'; -import 'qui-badge'; +import '@qomponent/qui-badge'; /** * This component represent one extension @@ -123,9 +123,9 @@ export class QwcExtension extends LitElement { >
    - ${this._headerTemplate()} - - ${this._footerTemplate()} + ${this._headerTemplate()} + + ${this._footerTemplate()}
    `; } diff --git a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-external-page.js b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-external-page.js index 3b67934b3185a..5bf4e2c3a1218 100644 --- a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-external-page.js +++ b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-external-page.js @@ -4,7 +4,7 @@ import { JsonRpc } from 'jsonrpc'; import { observeState } from 'lit-element-state'; import { themeState } from 'theme-state'; import '@vaadin/icon'; -import '@quarkus-webcomponents/codeblock'; +import '@qomponent/qui-code-block'; import '@vaadin/progress-bar'; /** diff --git a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-server-log.js b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-server-log.js index d9a63017d3525..838189ff4e6df 100644 --- a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-server-log.js +++ b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-server-log.js @@ -3,19 +3,19 @@ import { repeat } from 'lit/directives/repeat.js'; import { LogController } from 'log-controller'; import { JsonRpc } from 'jsonrpc'; import { unsafeHTML } from 'lit-html/directives/unsafe-html.js'; +import { loggerLevels } from 'devui-data'; import '@vaadin/icon'; import '@vaadin/dialog'; import '@vaadin/select'; import '@vaadin/checkbox'; import '@vaadin/checkbox-group'; import { dialogHeaderRenderer, dialogRenderer } from '@vaadin/dialog/lit.js'; -import 'qui-badge'; -import 'qui-ide-link'; import '@vaadin/grid'; import { columnBodyRenderer } from '@vaadin/grid/lit.js'; import '@vaadin/grid/vaadin-grid-sort-column.js'; import '@vaadin/vertical-layout'; -import { loggerLevels } from 'devui-data'; +import '@qomponent/qui-badge'; +import 'qui-ide-link'; /** * This component represent the Server Log diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/MvnpmHandler.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/MvnpmHandler.java index 01e1a5f364174..2a454da27af2d 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/MvnpmHandler.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/MvnpmHandler.java @@ -80,7 +80,7 @@ private String formatDate(TemporalAccessor t) { private String getContentType(String filename) { String f = filename.toLowerCase(); - if (f.endsWith(DOT_JS)) { + if (f.endsWith(DOT_JS) || f.endsWith(DOT_MJS)) { return CONTENT_TYPE_JAVASCRIPT; } else if (f.endsWith(DOT_JSON)) { return CONTENT_TYPE_JSON; @@ -103,7 +103,7 @@ private String getContentType(String filename) { // .woff Web Open Font Format (WOFF) font/woff // .woff2 Web Open Font Format (WOFF) font/woff2 - return CONTENT_TYPE_TEXT; // default + return CONTENT_TYPE_JAVASCRIPT; // default } @@ -111,6 +111,7 @@ private String getContentType(String filename) { private static final String BASE_DIR = "META-INF/resources"; private static final String DOT = "."; private static final String DOT_JS = ".js"; + private static final String DOT_MJS = ".mjs"; private static final String DOT_JSON = ".json"; private static final String DOT_HTML = ".html"; private static final String DOT_HTM = ".htm"; diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/build/BuildMetricsDevUIController.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/build/BuildMetricsDevUIController.java index 93b9db1160cbd..b94a13fac6461 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/build/BuildMetricsDevUIController.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/build/BuildMetricsDevUIController.java @@ -43,6 +43,8 @@ private BuildMetricsDevUIController() { void setBuildMetricsPath(Path buildMetricsPath) { this.buildMetricsPath = buildMetricsPath; + // Reread the data after reload + this.buildStepsMetrics = null; } Map getBuildStepsMetrics() { diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/AuthRuntimeConfig.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/AuthRuntimeConfig.java index 01ee26d04e784..99d632542b63b 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/AuthRuntimeConfig.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/AuthRuntimeConfig.java @@ -34,8 +34,8 @@ public class AuthRuntimeConfig { * use this property to map the `user` role to the `UserRole` role, and have `SecurityIdentity` to have * both `user` and `UserRole` roles. */ - @ConfigDocMapKey("role1") @ConfigItem + @ConfigDocMapKey("role-name") public Map> rolesMapping; /** diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/FilterConfig.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/FilterConfig.java index 285a581d7759f..e7cfbf4aa50cc 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/FilterConfig.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/FilterConfig.java @@ -5,6 +5,7 @@ import java.util.Optional; import java.util.OptionalInt; +import io.quarkus.runtime.annotations.ConfigDocMapKey; import io.quarkus.runtime.annotations.ConfigGroup; import io.quarkus.runtime.annotations.ConfigItem; @@ -21,6 +22,7 @@ public class FilterConfig { * Additional HTTP Headers always sent in the response */ @ConfigItem + @ConfigDocMapKey("header-name") public Map header; /** diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/PolicyConfig.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/PolicyConfig.java index be472e3ea47b7..301be4ec35410 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/PolicyConfig.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/PolicyConfig.java @@ -26,8 +26,8 @@ public class PolicyConfig { * For example, the Quarkus OIDC extension can map roles from the verified JWT access token, and you may want * to remap them to a deployment specific roles. */ - @ConfigDocMapKey("role1") @ConfigItem + @ConfigDocMapKey("role-name") public Map> roles; /** @@ -37,8 +37,8 @@ public class PolicyConfig { * `quarkus.http.auth.policy.role-policy1.permissions.admin=perm1:action1,perm1:action2` configuration property. * Granted permissions are used for authorization with the `@PermissionsAllowed` annotation. */ - @ConfigDocMapKey("role1") @ConfigItem + @ConfigDocMapKey("role-name") public Map> permissions; /** diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/management/ManagementRuntimeAuthConfig.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/management/ManagementRuntimeAuthConfig.java index 60504136f8708..5e6cd28d987b4 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/management/ManagementRuntimeAuthConfig.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/management/ManagementRuntimeAuthConfig.java @@ -34,7 +34,7 @@ public class ManagementRuntimeAuthConfig { * use this property to map the `user` role to the `UserRole` role, and have `SecurityIdentity` to have * both `user` and `UserRole` roles. */ - @ConfigDocMapKey("role1") @ConfigItem + @ConfigDocMapKey("role-name") public Map> rolesMapping; } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityRecorder.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityRecorder.java index dd1071ea40e27..512542b7f670b 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityRecorder.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityRecorder.java @@ -37,6 +37,7 @@ import io.quarkus.security.identity.SecurityIdentity; import io.quarkus.security.identity.request.AnonymousAuthenticationRequest; import io.quarkus.security.spi.runtime.MethodDescription; +import io.quarkus.vertx.http.runtime.CurrentVertxRequest; import io.quarkus.vertx.http.runtime.HttpConfiguration; import io.smallrye.mutiny.CompositeException; import io.smallrye.mutiny.Uni; @@ -143,6 +144,30 @@ public String name() { }); } + public Supplier> createAdditionalSecEventPropsSupplier() { + return new Supplier>() { + @Override + public Map get() { + if (Arc.container().requestContext().isActive()) { + + // if present, add RoutingContext from CDI request to the SecurityEvents produced in Security extension + // it's done this way as Security extension is not Vert.x based, but users find RoutingContext useful + var event = Arc.container().instance(CurrentVertxRequest.class).get().getCurrent(); + if (event != null) { + + if (event.user() instanceof QuarkusHttpUser user) { + return Map.of(RoutingContext.class.getName(), event, SecurityIdentity.class.getName(), + user.getSecurityIdentity()); + } + + return Map.of(RoutingContext.class.getName(), event); + } + } + return Map.of(); + } + }; + } + public static abstract class DefaultAuthFailureHandler implements BiConsumer { protected DefaultAuthFailureHandler() { diff --git a/extensions/web-dependency-locator/deployment/src/main/java/io/quarkus/webdependency/locator/deployment/WebDependencyLocatorConfig.java b/extensions/web-dependency-locator/deployment/src/main/java/io/quarkus/webdependency/locator/deployment/WebDependencyLocatorConfig.java index 377a4233a1203..6c3603da6b99e 100644 --- a/extensions/web-dependency-locator/deployment/src/main/java/io/quarkus/webdependency/locator/deployment/WebDependencyLocatorConfig.java +++ b/extensions/web-dependency-locator/deployment/src/main/java/io/quarkus/webdependency/locator/deployment/WebDependencyLocatorConfig.java @@ -2,6 +2,7 @@ import java.util.Map; +import io.quarkus.runtime.annotations.ConfigDocMapKey; import io.quarkus.runtime.annotations.ConfigItem; import io.quarkus.runtime.annotations.ConfigRoot; @@ -21,6 +22,7 @@ public class WebDependencyLocatorConfig { * User defined import mappings */ @ConfigItem + @ConfigDocMapKey("module-specifier") public Map importMappings; /** diff --git a/extensions/websockets-next/deployment/pom.xml b/extensions/websockets-next/deployment/pom.xml index 9c33791094f42..78e90a6a61959 100644 --- a/extensions/websockets-next/deployment/pom.xml +++ b/extensions/websockets-next/deployment/pom.xml @@ -36,6 +36,16 @@ quarkus-test-vertx test + + io.quarkus + quarkus-security-deployment + test + + + io.quarkus + quarkus-security-test-utils + test + io.quarkus quarkus-junit5-internal diff --git a/extensions/websockets-next/deployment/src/main/java/io/quarkus/websockets/next/deployment/WebSocketProcessor.java b/extensions/websockets-next/deployment/src/main/java/io/quarkus/websockets/next/deployment/WebSocketProcessor.java index 465873ae3cad0..c9c67b9029829 100644 --- a/extensions/websockets-next/deployment/src/main/java/io/quarkus/websockets/next/deployment/WebSocketProcessor.java +++ b/extensions/websockets-next/deployment/src/main/java/io/quarkus/websockets/next/deployment/WebSocketProcessor.java @@ -44,6 +44,8 @@ import io.quarkus.arc.processor.DotNames; import io.quarkus.arc.processor.InjectionPointInfo; import io.quarkus.arc.processor.Types; +import io.quarkus.deployment.Capabilities; +import io.quarkus.deployment.Capability; import io.quarkus.deployment.GeneratedClassGizmoAdaptor; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; @@ -65,6 +67,7 @@ import io.quarkus.vertx.http.deployment.HttpRootPathBuildItem; import io.quarkus.vertx.http.deployment.RouteBuildItem; import io.quarkus.vertx.http.runtime.HandlerType; +import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig; import io.quarkus.websockets.next.InboundProcessingMode; import io.quarkus.websockets.next.WebSocketClientConnection; import io.quarkus.websockets.next.WebSocketClientException; @@ -79,6 +82,7 @@ import io.quarkus.websockets.next.runtime.ConnectionManager; import io.quarkus.websockets.next.runtime.ContextSupport; import io.quarkus.websockets.next.runtime.JsonTextMessageCodec; +import io.quarkus.websockets.next.runtime.SecuritySupport; import io.quarkus.websockets.next.runtime.WebSocketClientRecorder; import io.quarkus.websockets.next.runtime.WebSocketClientRecorder.ClientEndpoint; import io.quarkus.websockets.next.runtime.WebSocketConnectionBase; @@ -400,12 +404,19 @@ public String apply(String name) { @Record(RUNTIME_INIT) @BuildStep public void registerRoutes(WebSocketServerRecorder recorder, HttpRootPathBuildItem httpRootPath, - List generatedEndpoints, + List generatedEndpoints, HttpBuildTimeConfig httpConfig, Capabilities capabilities, BuildProducer routes) { for (GeneratedEndpointBuildItem endpoint : generatedEndpoints.stream().filter(GeneratedEndpointBuildItem::isServer) .toList()) { - RouteBuildItem.Builder builder = RouteBuildItem.builder() - .route(httpRootPath.relativePath(endpoint.path)) + RouteBuildItem.Builder builder = RouteBuildItem.builder(); + String relativePath = httpRootPath.relativePath(endpoint.path); + if (capabilities.isPresent(Capability.SECURITY) && !httpConfig.auth.proactive) { + // Add a special handler so that it's possible to capture the SecurityIdentity before the HTTP upgrade + builder.routeFunction(relativePath, recorder.initializeSecurityHandler()); + } else { + builder.route(relativePath); + } + builder .displayOnNotFoundPage("WebSocket Endpoint") .handlerType(HandlerType.NORMAL) .handler(recorder.createEndpointHandler(endpoint.generatedClassName, endpoint.endpointId)); @@ -546,8 +557,8 @@ private void validateOnClose(Callback callback) { * } * * public Echo_WebSocketEndpoint(WebSocketConnection connection, Codecs codecs, - * WebSocketRuntimeConfig config, ContextSupport contextSupport) { - * super(connection, codecs, config, contextSupport); + * WebSocketRuntimeConfig config, ContextSupport contextSupport, SecuritySupport securitySupport) { + * super(connection, codecs, config, contextSupport, securitySupport); * } * * public Uni doOnTextMessage(String message) { @@ -617,12 +628,12 @@ static String generateEndpoint(WebSocketEndpointBuildItem endpoint, .build(); MethodCreator constructor = endpointCreator.getConstructorCreator(WebSocketConnectionBase.class, - Codecs.class, ContextSupport.class); + Codecs.class, ContextSupport.class, SecuritySupport.class); constructor.invokeSpecialMethod( MethodDescriptor.ofConstructor(WebSocketEndpointBase.class, WebSocketConnectionBase.class, - Codecs.class, ContextSupport.class), + Codecs.class, ContextSupport.class, SecuritySupport.class), constructor.getThis(), constructor.getMethodParam(0), constructor.getMethodParam(1), - constructor.getMethodParam(2)); + constructor.getMethodParam(2), constructor.getMethodParam(3)); constructor.returnNull(); MethodCreator inboundProcessingMode = endpointCreator.getMethodCreator("inboundProcessingMode", @@ -1044,7 +1055,7 @@ private static ResultHandle encodeMessage(ResultHandle endpointThis, BytecodeCre return uniOnFailureDoOnError(endpointThis, method, callback, uniChain, endpoint, globalErrorHandlers); } } else if (callback.isReturnTypeMulti()) { - // return multiText(multi, broadcast, m -> { + // return multiText(multi, m -> { // try { // String text = encodeText(m); // return sendText(buffer,broadcast); diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/EchoWebSocketTest.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/EchoWebSocketTest.java index dddd23741b5a4..b2f9a1b08246e 100644 --- a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/EchoWebSocketTest.java +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/EchoWebSocketTest.java @@ -8,6 +8,8 @@ import java.util.concurrent.TimeUnit; import java.util.function.BiConsumer; +import jakarta.inject.Inject; + import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -21,6 +23,9 @@ public class EchoWebSocketTest { + @Inject + Vertx vertx; + @TestHTTPResource("echo") URI echoUri; @@ -127,7 +132,6 @@ public void assertEcho(URI testUri, String payload) throws Exception { } public void assertEcho(URI testUri, String payload, BiConsumer> action) throws Exception { - Vertx vertx = Vertx.vertx(); WebSocketClient client = vertx.createWebSocketClient(); try { LinkedBlockingDeque message = new LinkedBlockingDeque<>(); diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/broadcast/BroadcastConnectionTest.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/broadcast/BroadcastConnectionTest.java index 15feb4d16ec52..740a0c9100965 100644 --- a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/broadcast/BroadcastConnectionTest.java +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/broadcast/BroadcastConnectionTest.java @@ -9,6 +9,8 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import jakarta.inject.Inject; + import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -20,21 +22,23 @@ public class BroadcastConnectionTest { - @TestHTTPResource("lo-connection") - URI loConnectionUri; - @RegisterExtension public static final QuarkusUnitTest test = new QuarkusUnitTest() .withApplicationRoot(root -> { root.addClasses(LoConnection.class); }); + @TestHTTPResource("lo-connection") + URI loConnectionUri; + + @Inject + Vertx vertx; + @Test public void testBroadcast() throws Exception { WebSocketClient client1 = null, client2 = null, client3 = null; try { List messages = new CopyOnWriteArrayList<>(); - Vertx vertx = Vertx.vertx(); client1 = connect(vertx, "C1", messages); client2 = connect(vertx, "C2", messages); client3 = connect(vertx, "C3", messages); diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/broadcast/BroadcastOnMessageTest.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/broadcast/BroadcastOnMessageTest.java index 89e918bb0ebb7..b2f18ba95d697 100644 --- a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/broadcast/BroadcastOnMessageTest.java +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/broadcast/BroadcastOnMessageTest.java @@ -10,6 +10,8 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; +import jakarta.inject.Inject; + import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -21,6 +23,12 @@ public class BroadcastOnMessageTest { + @RegisterExtension + public static final QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot(root -> { + root.addClasses(Up.class, UpBlocking.class, UpMultiBidi.class); + }); + @TestHTTPResource("up") URI upUri; @@ -30,11 +38,8 @@ public class BroadcastOnMessageTest { @TestHTTPResource("up-multi-bidi") URI upMultiBidiUri; - @RegisterExtension - public static final QuarkusUnitTest test = new QuarkusUnitTest() - .withApplicationRoot(root -> { - root.addClasses(Up.class, UpBlocking.class, UpMultiBidi.class); - }); + @Inject + Vertx vertx; @Test public void testUp() throws Exception { @@ -52,7 +57,6 @@ public void testUpMultiBidi() throws Exception { } public void assertBroadcast(URI testUri) throws Exception { - Vertx vertx = Vertx.vertx(); WebSocketClient client1 = vertx.createWebSocketClient(); WebSocketClient client2 = vertx.createWebSocketClient(); try { diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/broadcast/BroadcastOnOpenTest.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/broadcast/BroadcastOnOpenTest.java index 8c4cbf205df7f..3303ede6151a0 100644 --- a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/broadcast/BroadcastOnOpenTest.java +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/broadcast/BroadcastOnOpenTest.java @@ -9,6 +9,8 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import jakarta.inject.Inject; + import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -20,6 +22,12 @@ public class BroadcastOnOpenTest { + @RegisterExtension + public static final QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot(root -> { + root.addClasses(Lo.class, LoBlocking.class, LoMultiProduce.class); + }); + @TestHTTPResource("lo") URI loUri; @@ -29,11 +37,8 @@ public class BroadcastOnOpenTest { @TestHTTPResource("lo-multi-produce") URI loMultiProduceUri; - @RegisterExtension - public static final QuarkusUnitTest test = new QuarkusUnitTest() - .withApplicationRoot(root -> { - root.addClasses(Lo.class, LoBlocking.class, LoMultiProduce.class); - }); + @Inject + Vertx vertx; @Test public void testLo() throws Exception { @@ -51,7 +56,6 @@ public void testLoMultiBidi() throws Exception { } public void assertBroadcast(URI testUri) throws Exception { - Vertx vertx = Vertx.vertx(); WebSocketClient client1 = vertx.createWebSocketClient(); WebSocketClient client2 = vertx.createWebSocketClient(); try { diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/client/ClientMessageErrorEndpoint.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/client/ClientMessageErrorEndpoint.java new file mode 100644 index 0000000000000..8de5fa38add05 --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/client/ClientMessageErrorEndpoint.java @@ -0,0 +1,35 @@ +package io.quarkus.websockets.next.test.client; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.CountDownLatch; + +import io.quarkus.websockets.next.OnClose; +import io.quarkus.websockets.next.OnTextMessage; +import io.quarkus.websockets.next.WebSocketClient; + +@WebSocketClient(path = "/endpoint") +public class ClientMessageErrorEndpoint { + + static final CountDownLatch MESSAGE_LATCH = new CountDownLatch(1); + + static final List MESSAGES = new CopyOnWriteArrayList<>(); + + static final CountDownLatch CLOSED_LATCH = new CountDownLatch(1); + + @OnTextMessage + void message(String message) { + if ("foo".equals(message)) { + throw new IllegalStateException("I cannot do it!"); + } else { + MESSAGES.add(message); + } + MESSAGE_LATCH.countDown(); + } + + @OnClose + void close() { + CLOSED_LATCH.countDown(); + } + +} \ No newline at end of file diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/client/ClientOpenErrorEndpoint.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/client/ClientOpenErrorEndpoint.java new file mode 100644 index 0000000000000..990c85bed80c7 --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/client/ClientOpenErrorEndpoint.java @@ -0,0 +1,37 @@ +package io.quarkus.websockets.next.test.client; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.CountDownLatch; + +import io.quarkus.websockets.next.OnClose; +import io.quarkus.websockets.next.OnOpen; +import io.quarkus.websockets.next.OnTextMessage; +import io.quarkus.websockets.next.WebSocketClient; + +@WebSocketClient(path = "/endpoint") +public class ClientOpenErrorEndpoint { + + static final CountDownLatch MESSAGE_LATCH = new CountDownLatch(1); + + static final List MESSAGES = new CopyOnWriteArrayList<>(); + + static final CountDownLatch CLOSED_LATCH = new CountDownLatch(1); + + @OnOpen + void open() { + throw new IllegalStateException("I cannot do it!"); + } + + @OnTextMessage + void message(String message) { + MESSAGES.add(message); + MESSAGE_LATCH.countDown(); + } + + @OnClose + void close() { + CLOSED_LATCH.countDown(); + } + +} \ No newline at end of file diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/client/ServerEndpoint.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/client/ServerEndpoint.java new file mode 100644 index 0000000000000..b2fbcbc19cd53 --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/client/ServerEndpoint.java @@ -0,0 +1,24 @@ +package io.quarkus.websockets.next.test.client; + +import java.util.concurrent.CountDownLatch; + +import io.quarkus.websockets.next.OnClose; +import io.quarkus.websockets.next.OnTextMessage; +import io.quarkus.websockets.next.WebSocket; + +@WebSocket(path = "/endpoint") +public class ServerEndpoint { + + static final CountDownLatch CLOSED_LATCH = new CountDownLatch(1); + + @OnTextMessage + String echo(String message) { + return message; + } + + @OnClose + void close() { + CLOSED_LATCH.countDown(); + } + +} \ No newline at end of file diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/client/UnhandledMessageFailureDefaultStrategyTest.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/client/UnhandledMessageFailureDefaultStrategyTest.java new file mode 100644 index 0000000000000..a1d80c81a021f --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/client/UnhandledMessageFailureDefaultStrategyTest.java @@ -0,0 +1,47 @@ +package io.quarkus.websockets.next.test.client; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.net.URI; +import java.util.concurrent.TimeUnit; + +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.netty.handler.codec.http.websocketx.WebSocketCloseStatus; +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.test.common.http.TestHTTPResource; +import io.quarkus.websockets.next.WebSocketClientConnection; +import io.quarkus.websockets.next.WebSocketConnector; + +public class UnhandledMessageFailureDefaultStrategyTest { + + @RegisterExtension + public static final QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot(root -> { + root.addClasses(ServerEndpoint.class, ClientMessageErrorEndpoint.class); + }); + + @Inject + WebSocketConnector connector; + + @TestHTTPResource("/") + URI testUri; + + @Test + void testError() throws InterruptedException { + WebSocketClientConnection connection = connector + .baseUri(testUri) + .connectAndAwait(); + connection.sendTextAndAwait("foo"); + assertTrue(ServerEndpoint.CLOSED_LATCH.await(5, TimeUnit.SECONDS)); + assertTrue(ClientMessageErrorEndpoint.CLOSED_LATCH.await(5, TimeUnit.SECONDS)); + assertTrue(connection.isClosed()); + assertEquals(WebSocketCloseStatus.INTERNAL_SERVER_ERROR.code(), connection.closeReason().getCode()); + assertTrue(ClientMessageErrorEndpoint.MESSAGES.isEmpty()); + } + +} diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/client/UnhandledMessageFailureLogStrategyTest.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/client/UnhandledMessageFailureLogStrategyTest.java new file mode 100644 index 0000000000000..1b047d03e5bd7 --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/client/UnhandledMessageFailureLogStrategyTest.java @@ -0,0 +1,46 @@ +package io.quarkus.websockets.next.test.client; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.net.URI; +import java.util.concurrent.TimeUnit; + +import jakarta.inject.Inject; + +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; +import io.quarkus.websockets.next.WebSocketClientConnection; +import io.quarkus.websockets.next.WebSocketConnector; + +public class UnhandledMessageFailureLogStrategyTest { + + @RegisterExtension + public static final QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot(root -> { + root.addClasses(ServerEndpoint.class, ClientMessageErrorEndpoint.class); + }).overrideConfigKey("quarkus.websockets-next.client.unhandled-failure-strategy", "log"); + + @Inject + WebSocketConnector connector; + + @TestHTTPResource("/") + URI testUri; + + @Test + void testError() throws InterruptedException { + WebSocketClientConnection connection = connector + .baseUri(testUri) + .connectAndAwait(); + connection.sendTextAndAwait("foo"); + assertFalse(connection.isClosed()); + connection.sendText("bar"); + assertTrue(ClientMessageErrorEndpoint.MESSAGE_LATCH.await(5, TimeUnit.SECONDS)); + assertEquals("bar", ClientMessageErrorEndpoint.MESSAGES.get(0)); + } + +} diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/client/UnhandledOpenFailureDefaultStrategyTest.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/client/UnhandledOpenFailureDefaultStrategyTest.java new file mode 100644 index 0000000000000..decf21f2b1705 --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/client/UnhandledOpenFailureDefaultStrategyTest.java @@ -0,0 +1,46 @@ +package io.quarkus.websockets.next.test.client; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.net.URI; +import java.util.concurrent.TimeUnit; + +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.netty.handler.codec.http.websocketx.WebSocketCloseStatus; +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.test.common.http.TestHTTPResource; +import io.quarkus.websockets.next.WebSocketClientConnection; +import io.quarkus.websockets.next.WebSocketConnector; + +public class UnhandledOpenFailureDefaultStrategyTest { + + @RegisterExtension + public static final QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot(root -> { + root.addClasses(ServerEndpoint.class, ClientOpenErrorEndpoint.class); + }); + + @Inject + WebSocketConnector connector; + + @TestHTTPResource("/") + URI testUri; + + @Test + void testError() throws InterruptedException { + WebSocketClientConnection connection = connector + .baseUri(testUri) + .connectAndAwait(); + assertTrue(ServerEndpoint.CLOSED_LATCH.await(5, TimeUnit.SECONDS)); + assertTrue(ClientOpenErrorEndpoint.CLOSED_LATCH.await(5, TimeUnit.SECONDS)); + assertTrue(connection.isClosed()); + assertEquals(WebSocketCloseStatus.INTERNAL_SERVER_ERROR.code(), connection.closeReason().getCode()); + assertTrue(ClientOpenErrorEndpoint.MESSAGES.isEmpty()); + } + +} diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/client/UnhandledOpenFailureLogStrategyTest.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/client/UnhandledOpenFailureLogStrategyTest.java new file mode 100644 index 0000000000000..dc5f6d41504fa --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/client/UnhandledOpenFailureLogStrategyTest.java @@ -0,0 +1,47 @@ +package io.quarkus.websockets.next.test.client; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.net.URI; +import java.util.concurrent.TimeUnit; + +import jakarta.inject.Inject; + +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; +import io.quarkus.websockets.next.WebSocketClientConnection; +import io.quarkus.websockets.next.WebSocketConnector; + +public class UnhandledOpenFailureLogStrategyTest { + + @RegisterExtension + public static final QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot(root -> { + root.addClasses(ServerEndpoint.class, ClientOpenErrorEndpoint.class); + }).overrideConfigKey("quarkus.websockets-next.client.unhandled-failure-strategy", "log"); + + @Inject + WebSocketConnector connector; + + @TestHTTPResource("/") + URI testUri; + + @Test + void testError() throws InterruptedException { + WebSocketClientConnection connection = connector + .baseUri(testUri) + .connectAndAwait(); + connection.sendTextAndAwait("foo"); + assertFalse(connection.isClosed()); + assertNull(connection.closeReason()); + assertTrue(ClientOpenErrorEndpoint.MESSAGE_LATCH.await(5, TimeUnit.SECONDS)); + assertEquals("foo", ClientOpenErrorEndpoint.MESSAGES.get(0)); + } + +} diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/codec/BinaryCodecTest.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/codec/BinaryCodecTest.java index 67f834b7f2ea2..21d4f94ddf7d4 100644 --- a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/codec/BinaryCodecTest.java +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/codec/BinaryCodecTest.java @@ -6,6 +6,8 @@ import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.TimeUnit; +import jakarta.inject.Inject; + import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -20,9 +22,6 @@ public class BinaryCodecTest { - @TestHTTPResource("find-binary") - URI findBinaryUri; - @RegisterExtension public static final QuarkusUnitTest test = new QuarkusUnitTest() .withApplicationRoot(root -> { @@ -30,6 +29,12 @@ public class BinaryCodecTest { FindBinary.ListItemBinaryMessageCodec.class); }); + @TestHTTPResource("find-binary") + URI findBinaryUri; + + @Inject + Vertx vertx; + @Test public void testCodec() throws Exception { JsonArray items = new JsonArray(); @@ -41,7 +46,6 @@ public void testCodec() throws Exception { public void assertCodec(URI testUri, Buffer payload, Buffer expected) throws Exception { - Vertx vertx = Vertx.vertx(); WebSocketClient client = vertx.createWebSocketClient(); try { LinkedBlockingDeque message = new LinkedBlockingDeque<>(); diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/codec/CustomCodecTest.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/codec/CustomCodecTest.java index 8fccf5a957f6e..de349ec3a87ab 100644 --- a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/codec/CustomCodecTest.java +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/codec/CustomCodecTest.java @@ -6,6 +6,8 @@ import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.TimeUnit; +import jakarta.inject.Inject; + import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -19,15 +21,18 @@ public class CustomCodecTest { - @TestHTTPResource("find") - URI findUri; - @RegisterExtension public static final QuarkusUnitTest test = new QuarkusUnitTest() .withApplicationRoot(root -> { root.addClasses(Find.class, Item.class, AbstractFind.class, MyItemCodec.class); }); + @TestHTTPResource("find") + URI findUri; + + @Inject + Vertx vertx; + @Test public void testCodec() throws Exception { JsonArray items = new JsonArray(); @@ -39,7 +44,6 @@ public void testCodec() throws Exception { public void assertCodec(URI testUri, String payload, String expected) throws Exception { - Vertx vertx = Vertx.vertx(); WebSocketClient client = vertx.createWebSocketClient(); try { LinkedBlockingDeque message = new LinkedBlockingDeque<>(); diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/codec/DefaultTextCodecTest.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/codec/DefaultTextCodecTest.java index 866172e1bf296..0002e03b08652 100644 --- a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/codec/DefaultTextCodecTest.java +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/codec/DefaultTextCodecTest.java @@ -6,6 +6,8 @@ import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.TimeUnit; +import jakarta.inject.Inject; + import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -19,15 +21,18 @@ public class DefaultTextCodecTest { - @TestHTTPResource("find") - URI findUri; - @RegisterExtension public static final QuarkusUnitTest test = new QuarkusUnitTest() .withApplicationRoot(root -> { root.addClasses(Find.class, AbstractFind.class, Item.class); }); + @TestHTTPResource("find") + URI findUri; + + @Inject + Vertx vertx; + @Test public void testCodec() throws Exception { JsonArray items = new JsonArray(); @@ -39,7 +44,6 @@ public void testCodec() throws Exception { public void assertCodec(URI testUri, String payload, String expected) throws Exception { - Vertx vertx = Vertx.vertx(); WebSocketClient client = vertx.createWebSocketClient(); try { LinkedBlockingDeque message = new LinkedBlockingDeque<>(); diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/codec/TextInputCodecTest.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/codec/TextInputCodecTest.java index 572c777810a52..d1d53473a8374 100644 --- a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/codec/TextInputCodecTest.java +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/codec/TextInputCodecTest.java @@ -6,6 +6,8 @@ import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.TimeUnit; +import jakarta.inject.Inject; + import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -19,15 +21,18 @@ public class TextInputCodecTest { - @TestHTTPResource("find-input-codec") - URI itemCodecUri; - @RegisterExtension public static final QuarkusUnitTest test = new QuarkusUnitTest() .withApplicationRoot(root -> { root.addClasses(FindInputCodec.class, FindInputCodec.MyInputCodec.class, AbstractFind.class, Item.class); }); + @TestHTTPResource("find-input-codec") + URI itemCodecUri; + + @Inject + Vertx vertx; + @Test public void testCodec() throws Exception { JsonArray items = new JsonArray(); @@ -39,7 +44,6 @@ public void testCodec() throws Exception { public void assertCodec(URI testUri, String payload, String expected) throws Exception { - Vertx vertx = Vertx.vertx(); WebSocketClient client = vertx.createWebSocketClient(); try { LinkedBlockingDeque message = new LinkedBlockingDeque<>(); diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/codec/TextOutputCodecTest.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/codec/TextOutputCodecTest.java index 66b04e7c2b277..c2970ff8d24a8 100644 --- a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/codec/TextOutputCodecTest.java +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/codec/TextOutputCodecTest.java @@ -6,6 +6,8 @@ import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.TimeUnit; +import jakarta.inject.Inject; + import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -19,15 +21,18 @@ public class TextOutputCodecTest { - @TestHTTPResource("find-output-codec") - URI itemCodecUri; - @RegisterExtension public static final QuarkusUnitTest test = new QuarkusUnitTest() .withApplicationRoot(root -> { root.addClasses(FindOutputCodec.class, FindOutputCodec.MyOutputCodec.class, AbstractFind.class, Item.class); }); + @Inject + Vertx vertx; + + @TestHTTPResource("find-output-codec") + URI itemCodecUri; + @Test public void testCodec() throws Exception { JsonArray items = new JsonArray(); @@ -39,7 +44,6 @@ public void testCodec() throws Exception { public void assertCodec(URI testUri, String payload, String expected) throws Exception { - Vertx vertx = Vertx.vertx(); WebSocketClient client = vertx.createWebSocketClient(); try { LinkedBlockingDeque message = new LinkedBlockingDeque<>(); diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/errors/EchoMessageError.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/errors/EchoMessageError.java new file mode 100644 index 0000000000000..3d52df32d1473 --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/errors/EchoMessageError.java @@ -0,0 +1,23 @@ +package io.quarkus.websockets.next.test.errors; + +import java.util.concurrent.CountDownLatch; + +import io.quarkus.websockets.next.OnTextMessage; +import io.quarkus.websockets.next.WebSocket; + +@WebSocket(path = "/echo") +public class EchoMessageError { + + static final CountDownLatch MESSAGE_FAILURE_CALLED = new CountDownLatch(1); + + @OnTextMessage + String echo(String message) { + if ("foo".equals(message)) { + MESSAGE_FAILURE_CALLED.countDown(); + throw new IllegalStateException("I cannot do it!"); + } else { + return message; + } + } + +} \ No newline at end of file diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/errors/EchoOpenError.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/errors/EchoOpenError.java new file mode 100644 index 0000000000000..7a079a0eb45c2 --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/errors/EchoOpenError.java @@ -0,0 +1,25 @@ +package io.quarkus.websockets.next.test.errors; + +import java.util.concurrent.CountDownLatch; + +import io.quarkus.websockets.next.OnOpen; +import io.quarkus.websockets.next.OnTextMessage; +import io.quarkus.websockets.next.WebSocket; + +@WebSocket(path = "/echo") +public class EchoOpenError { + + static final CountDownLatch OPEN_CALLED = new CountDownLatch(1); + + @OnOpen + void open() { + OPEN_CALLED.countDown(); + throw new IllegalStateException("I cannot do it!"); + } + + @OnTextMessage + String echo(String message) { + return message; + } + +} \ No newline at end of file diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/errors/RuntimeErrorTest.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/errors/RuntimeErrorTest.java index a519c95ea9be3..420f0ba1515ef 100644 --- a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/errors/RuntimeErrorTest.java +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/errors/RuntimeErrorTest.java @@ -80,8 +80,8 @@ String decodingError(BinaryDecodeException e) { Uni runtimeProblem(RuntimeException e, WebSocketConnection connection) { assertTrue(Context.isOnEventLoopThread()); assertEquals(connection.id(), this.connection.id()); - // The request context from @OnBinaryMessage is reused - assertEquals("ok", requestBean.getState()); + // A new request context is used + assertEquals("nok", requestBean.getState()); return connection.sendText(e.getMessage()); } diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/errors/UnhandledMessageFailureDefaultStrategyTest.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/errors/UnhandledMessageFailureDefaultStrategyTest.java new file mode 100644 index 0000000000000..1207e6689277a --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/errors/UnhandledMessageFailureDefaultStrategyTest.java @@ -0,0 +1,46 @@ +package io.quarkus.websockets.next.test.errors; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.net.URI; +import java.time.Duration; +import java.util.concurrent.TimeUnit; + +import jakarta.inject.Inject; + +import org.awaitility.Awaitility; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.netty.handler.codec.http.websocketx.WebSocketCloseStatus; +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.test.common.http.TestHTTPResource; +import io.quarkus.websockets.next.test.utils.WSClient; +import io.vertx.core.Vertx; + +public class UnhandledMessageFailureDefaultStrategyTest { + + @RegisterExtension + public static final QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot(root -> { + root.addClasses(EchoMessageError.class, WSClient.class); + }); + + @Inject + Vertx vertx; + + @TestHTTPResource("echo") + URI testUri; + + @Test + void testError() throws InterruptedException { + try (WSClient client = WSClient.create(vertx).connect(testUri)) { + client.sendAndAwait("foo"); + assertTrue(EchoMessageError.MESSAGE_FAILURE_CALLED.await(5, TimeUnit.SECONDS)); + Awaitility.await().atMost(Duration.ofSeconds(5)).until(() -> client.isClosed()); + assertEquals(WebSocketCloseStatus.INTERNAL_SERVER_ERROR.code(), client.closeStatusCode()); + } + } + +} diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/errors/UnhandledMessageFailureLogStrategyTest.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/errors/UnhandledMessageFailureLogStrategyTest.java new file mode 100644 index 0000000000000..0061937345fcf --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/errors/UnhandledMessageFailureLogStrategyTest.java @@ -0,0 +1,44 @@ +package io.quarkus.websockets.next.test.errors; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.net.URI; +import java.util.concurrent.TimeUnit; + +import jakarta.inject.Inject; + +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; +import io.quarkus.websockets.next.test.utils.WSClient; +import io.vertx.core.Vertx; + +public class UnhandledMessageFailureLogStrategyTest { + + @RegisterExtension + public static final QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot(root -> { + root.addClasses(EchoMessageError.class, WSClient.class); + }).overrideConfigKey("quarkus.websockets-next.server.unhandled-failure-strategy", "log"); + + @Inject + Vertx vertx; + + @TestHTTPResource("echo") + URI testUri; + + @Test + void testErrorDoesNotCloseConnection() throws InterruptedException { + try (WSClient client = WSClient.create(vertx).connect(testUri)) { + client.sendAndAwait("foo"); + assertTrue(EchoMessageError.MESSAGE_FAILURE_CALLED.await(5, TimeUnit.SECONDS)); + client.sendAndAwait("bar"); + client.waitForMessages(1); + assertEquals("bar", client.getLastMessage().toString()); + } + } + +} diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/errors/UnhandledOpenFailureDefaultStrategyTest.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/errors/UnhandledOpenFailureDefaultStrategyTest.java new file mode 100644 index 0000000000000..61c712d005d86 --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/errors/UnhandledOpenFailureDefaultStrategyTest.java @@ -0,0 +1,45 @@ +package io.quarkus.websockets.next.test.errors; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.net.URI; +import java.time.Duration; +import java.util.concurrent.TimeUnit; + +import jakarta.inject.Inject; + +import org.awaitility.Awaitility; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.netty.handler.codec.http.websocketx.WebSocketCloseStatus; +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.test.common.http.TestHTTPResource; +import io.quarkus.websockets.next.test.utils.WSClient; +import io.vertx.core.Vertx; + +public class UnhandledOpenFailureDefaultStrategyTest { + + @RegisterExtension + public static final QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot(root -> { + root.addClasses(EchoOpenError.class, WSClient.class); + }); + + @Inject + Vertx vertx; + + @TestHTTPResource("echo") + URI testUri; + + @Test + void testError() throws InterruptedException { + try (WSClient client = WSClient.create(vertx).connect(testUri)) { + assertTrue(EchoOpenError.OPEN_CALLED.await(5, TimeUnit.SECONDS)); + Awaitility.await().atMost(Duration.ofSeconds(5)).until(() -> client.isClosed()); + assertEquals(WebSocketCloseStatus.INTERNAL_SERVER_ERROR.code(), client.closeStatusCode()); + } + } + +} diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/errors/UnhandledOpenFailureLogStrategyTest.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/errors/UnhandledOpenFailureLogStrategyTest.java new file mode 100644 index 0000000000000..b704e8c551cde --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/errors/UnhandledOpenFailureLogStrategyTest.java @@ -0,0 +1,43 @@ +package io.quarkus.websockets.next.test.errors; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.net.URI; +import java.util.concurrent.TimeUnit; + +import jakarta.inject.Inject; + +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; +import io.quarkus.websockets.next.test.utils.WSClient; +import io.vertx.core.Vertx; + +public class UnhandledOpenFailureLogStrategyTest { + + @RegisterExtension + public static final QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot(root -> { + root.addClasses(EchoOpenError.class, WSClient.class); + }).overrideConfigKey("quarkus.websockets-next.server.unhandled-failure-strategy", "log"); + + @Inject + Vertx vertx; + + @TestHTTPResource("echo") + URI testUri; + + @Test + void testErrorDoesNotCloseConnection() throws InterruptedException { + try (WSClient client = WSClient.create(vertx).connect(testUri)) { + assertTrue(EchoOpenError.OPEN_CALLED.await(5, TimeUnit.SECONDS)); + client.sendAndAwait("foo"); + client.waitForMessages(1); + assertEquals("foo", client.getLastMessage().toString()); + } + } + +} diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/errors/UniFailureErrorTest.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/errors/UniFailureErrorTest.java index 933f681c26fcc..17164eb98836c 100644 --- a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/errors/UniFailureErrorTest.java +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/errors/UniFailureErrorTest.java @@ -80,8 +80,8 @@ String decodingError(BinaryDecodeException e) { String runtimeProblem(RuntimeException e, WebSocketConnection connection) { assertTrue(Context.isOnWorkerThread()); assertEquals(connection.id(), this.connection.id()); - // The request context from @OnBinaryMessage is reused - assertEquals("ok", requestBean.getState()); + // A new request context is used + assertEquals("nok", requestBean.getState()); return e.getMessage(); } diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/security/AdminService.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/security/AdminService.java new file mode 100644 index 0000000000000..38905495f4e66 --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/security/AdminService.java @@ -0,0 +1,14 @@ +package io.quarkus.websockets.next.test.security; + +import jakarta.annotation.security.RolesAllowed; +import jakarta.enterprise.context.ApplicationScoped; + +@RolesAllowed("admin") +@ApplicationScoped +public class AdminService { + + public String ping() { + return "" + 24; + } + +} diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/security/EagerSecurityTest.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/security/EagerSecurityTest.java new file mode 100644 index 0000000000000..506c1a5a55cd2 --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/security/EagerSecurityTest.java @@ -0,0 +1,59 @@ +package io.quarkus.websockets.next.test.security; + +import jakarta.annotation.security.RolesAllowed; +import jakarta.inject.Inject; + +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.security.Authenticated; +import io.quarkus.security.ForbiddenException; +import io.quarkus.security.identity.CurrentIdentityAssociation; +import io.quarkus.security.test.utils.TestIdentityController; +import io.quarkus.security.test.utils.TestIdentityProvider; +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.websockets.next.OnError; +import io.quarkus.websockets.next.OnOpen; +import io.quarkus.websockets.next.OnTextMessage; +import io.quarkus.websockets.next.WebSocket; +import io.quarkus.websockets.next.test.utils.WSClient; + +public class EagerSecurityTest extends SecurityTestBase { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addAsResource(new StringAsset("quarkus.http.auth.proactive=true\n" + + "quarkus.http.auth.permission.secured.paths=/end\n" + + "quarkus.http.auth.permission.secured.policy=authenticated\n"), "application.properties") + .addClasses(Endpoint.class, WSClient.class, TestIdentityProvider.class, TestIdentityController.class)); + + @Authenticated + @WebSocket(path = "/end") + public static class Endpoint { + + @Inject + CurrentIdentityAssociation currentIdentity; + + @OnOpen + String open() { + return "ready"; + } + + @RolesAllowed("admin") + @OnTextMessage + String echo(String message) { + if (!currentIdentity.getIdentity().hasRole("admin")) { + throw new IllegalStateException(); + } + return message; + } + + @OnError + String error(ForbiddenException t) { + return "forbidden:" + currentIdentity.getIdentity().getPrincipal().getName(); + } + + } + +} diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/security/EagerSecurityUniTest.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/security/EagerSecurityUniTest.java new file mode 100644 index 0000000000000..809bacfdb0627 --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/security/EagerSecurityUniTest.java @@ -0,0 +1,60 @@ +package io.quarkus.websockets.next.test.security; + +import jakarta.annotation.security.RolesAllowed; +import jakarta.inject.Inject; + +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.security.Authenticated; +import io.quarkus.security.ForbiddenException; +import io.quarkus.security.identity.CurrentIdentityAssociation; +import io.quarkus.security.test.utils.TestIdentityController; +import io.quarkus.security.test.utils.TestIdentityProvider; +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.websockets.next.OnError; +import io.quarkus.websockets.next.OnOpen; +import io.quarkus.websockets.next.OnTextMessage; +import io.quarkus.websockets.next.WebSocket; +import io.quarkus.websockets.next.test.utils.WSClient; +import io.smallrye.mutiny.Uni; + +public class EagerSecurityUniTest extends SecurityTestBase { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addAsResource(new StringAsset("quarkus.http.auth.proactive=true\n" + + "quarkus.http.auth.permission.secured.paths=/end\n" + + "quarkus.http.auth.permission.secured.policy=authenticated\n"), "application.properties") + .addClasses(Endpoint.class, WSClient.class, TestIdentityProvider.class, TestIdentityController.class)); + + @Authenticated + @WebSocket(path = "/end") + public static class Endpoint { + + @Inject + CurrentIdentityAssociation currentIdentity; + + @OnOpen + String open() { + return "ready"; + } + + @RolesAllowed("admin") + @OnTextMessage + Uni echo(String message) { + if (!currentIdentity.getIdentity().hasRole("admin")) { + throw new IllegalStateException(); + } + return Uni.createFrom().item(message); + } + + @OnError + String error(ForbiddenException t) { + return "forbidden:" + currentIdentity.getIdentity().getPrincipal().getName(); + } + + } + +} diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/security/LazySecurityTest.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/security/LazySecurityTest.java new file mode 100644 index 0000000000000..7d21f28dbc2c5 --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/security/LazySecurityTest.java @@ -0,0 +1,60 @@ +package io.quarkus.websockets.next.test.security; + +import jakarta.annotation.security.RolesAllowed; +import jakarta.inject.Inject; + +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.security.Authenticated; +import io.quarkus.security.ForbiddenException; +import io.quarkus.security.identity.CurrentIdentityAssociation; +import io.quarkus.security.test.utils.TestIdentityController; +import io.quarkus.security.test.utils.TestIdentityProvider; +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.websockets.next.OnError; +import io.quarkus.websockets.next.OnOpen; +import io.quarkus.websockets.next.OnTextMessage; +import io.quarkus.websockets.next.WebSocket; +import io.quarkus.websockets.next.test.security.EagerSecurityTest.Endpoint; +import io.quarkus.websockets.next.test.utils.WSClient; + +public class LazySecurityTest extends SecurityTestBase { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addAsResource(new StringAsset("quarkus.http.auth.proactive=false\n" + + "quarkus.http.auth.permission.secured.paths=/end\n" + + "quarkus.http.auth.permission.secured.policy=authenticated\n"), "application.properties") + .addClasses(Endpoint.class, WSClient.class, TestIdentityProvider.class, TestIdentityController.class)); + + @Authenticated + @WebSocket(path = "/end") + public static class Endpoint { + + @Inject + CurrentIdentityAssociation currentIdentity; + + @OnOpen + String open() { + return "ready"; + } + + @RolesAllowed("admin") + @OnTextMessage + String echo(String message) { + if (!currentIdentity.getIdentity().hasRole("admin")) { + throw new IllegalStateException(); + } + return message; + } + + @OnError + String error(ForbiddenException t) { + return "forbidden:" + currentIdentity.getIdentity().getPrincipal().getName(); + } + + } + +} diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/security/LazySecurityUniTest.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/security/LazySecurityUniTest.java new file mode 100644 index 0000000000000..cb968d397f890 --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/security/LazySecurityUniTest.java @@ -0,0 +1,60 @@ +package io.quarkus.websockets.next.test.security; + +import jakarta.annotation.security.RolesAllowed; +import jakarta.inject.Inject; + +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.security.Authenticated; +import io.quarkus.security.ForbiddenException; +import io.quarkus.security.identity.CurrentIdentityAssociation; +import io.quarkus.security.test.utils.TestIdentityController; +import io.quarkus.security.test.utils.TestIdentityProvider; +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.websockets.next.OnError; +import io.quarkus.websockets.next.OnOpen; +import io.quarkus.websockets.next.OnTextMessage; +import io.quarkus.websockets.next.WebSocket; +import io.quarkus.websockets.next.test.utils.WSClient; +import io.smallrye.mutiny.Uni; + +public class LazySecurityUniTest extends SecurityTestBase { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addAsResource(new StringAsset("quarkus.http.auth.proactive=false\n" + + "quarkus.http.auth.permission.secured.paths=/end\n" + + "quarkus.http.auth.permission.secured.policy=authenticated\n"), "application.properties") + .addClasses(Endpoint.class, WSClient.class, TestIdentityProvider.class, TestIdentityController.class)); + + @Authenticated + @WebSocket(path = "/end") + public static class Endpoint { + + @Inject + CurrentIdentityAssociation currentIdentity; + + @OnOpen + String open() { + return "ready"; + } + + @RolesAllowed("admin") + @OnTextMessage + Uni echo(String message) { + if (!currentIdentity.getIdentity().hasRole("admin")) { + throw new IllegalStateException(); + } + return Uni.createFrom().item(message); + } + + @OnError + String error(ForbiddenException t) { + return "forbidden:" + currentIdentity.getIdentity().getPrincipal().getName(); + } + + } + +} diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/security/RbacServiceSecurityTest.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/security/RbacServiceSecurityTest.java new file mode 100644 index 0000000000000..0207d3f1b03fd --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/security/RbacServiceSecurityTest.java @@ -0,0 +1,84 @@ +package io.quarkus.websockets.next.test.security; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.net.URI; +import java.util.Set; + +import jakarta.inject.Inject; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.security.ForbiddenException; +import io.quarkus.security.test.utils.TestIdentityController; +import io.quarkus.security.test.utils.TestIdentityProvider; +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.test.common.http.TestHTTPResource; +import io.quarkus.websockets.next.OnError; +import io.quarkus.websockets.next.OnTextMessage; +import io.quarkus.websockets.next.WebSocket; +import io.quarkus.websockets.next.test.utils.WSClient; +import io.vertx.core.Vertx; + +public class RbacServiceSecurityTest extends SecurityTestBase { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot(root -> root.addClasses(Endpoint.class, AdminService.class, UserService.class, + TestIdentityProvider.class, TestIdentityController.class, WSClient.class)); + + @Inject + Vertx vertx; + + @TestHTTPResource("end") + URI endUri; + + @BeforeAll + public static void setupUsers() { + TestIdentityController.resetRoles() + .add("admin", "admin", "admin") + .add("user", "user", "user"); + } + + @Test + public void testEndpoint() { + try (WSClient client = new WSClient(vertx)) { + client.connect(basicAuth("admin", "admin"), endUri); + client.sendAndAwait("hello"); // admin service + client.sendAndAwait("hi"); // forbidden + client.waitForMessages(2); + assertEquals(Set.of("24", "forbidden"), Set.copyOf(client.getMessages().stream().map(Object::toString).toList())); + } + try (WSClient client = new WSClient(vertx)) { + client.connect(basicAuth("user", "user"), endUri); + client.sendAndAwait("hello"); // forbidden + client.sendAndAwait("hi"); // user service + client.waitForMessages(2); + assertEquals(Set.of("42", "forbidden"), Set.copyOf(client.getMessages().stream().map(Object::toString).toList())); + } + } + + @WebSocket(path = "/end") + public static class Endpoint { + + @Inject + UserService userService; + + @Inject + AdminService adminService; + + @OnTextMessage + String echo(String message) { + return message.equals("hello") ? adminService.ping() : userService.ping(); + } + + @OnError + String error(ForbiddenException t) { + return "forbidden"; + } + + } + +} diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/security/SecurityTestBase.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/security/SecurityTestBase.java new file mode 100644 index 0000000000000..a9c94143ae59b --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/security/SecurityTestBase.java @@ -0,0 +1,71 @@ +package io.quarkus.websockets.next.test.security; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.net.URI; +import java.util.concurrent.CompletionException; + +import jakarta.inject.Inject; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import io.quarkus.runtime.util.ExceptionUtil; +import io.quarkus.security.test.utils.TestIdentityController; +import io.quarkus.test.common.http.TestHTTPResource; +import io.quarkus.websockets.next.test.utils.WSClient; +import io.vertx.core.Vertx; +import io.vertx.core.http.HttpHeaders; +import io.vertx.core.http.UpgradeRejectedException; +import io.vertx.core.http.WebSocketConnectOptions; +import io.vertx.ext.auth.authentication.UsernamePasswordCredentials; + +public abstract class SecurityTestBase { + + @Inject + Vertx vertx; + + @TestHTTPResource("end") + URI endUri; + + @BeforeAll + public static void setupUsers() { + TestIdentityController.resetRoles() + .add("admin", "admin", "admin") + .add("user", "user", "user"); + } + + @Test + public void testEndpoint() { + try (WSClient client = new WSClient(vertx)) { + CompletionException ce = assertThrows(CompletionException.class, () -> client.connect(endUri)); + Throwable root = ExceptionUtil.getRootCause(ce); + assertTrue(root instanceof UpgradeRejectedException); + assertTrue(root.getMessage().contains("401")); + } + try (WSClient client = new WSClient(vertx)) { + client.connect(basicAuth("admin", "admin"), endUri); + client.waitForMessages(1); + assertEquals("ready", client.getMessages().get(0).toString()); + client.sendAndAwait("hello"); + client.waitForMessages(2); + assertEquals("hello", client.getMessages().get(1).toString()); + } + try (WSClient client = new WSClient(vertx)) { + client.connect(basicAuth("user", "user"), endUri); + client.waitForMessages(1); + assertEquals("ready", client.getMessages().get(0).toString()); + client.sendAndAwait("hello"); + client.waitForMessages(2); + assertEquals("forbidden:user", client.getMessages().get(1).toString()); + } + } + + static WebSocketConnectOptions basicAuth(String username, String password) { + return new WebSocketConnectOptions().addHeader(HttpHeaders.AUTHORIZATION.toString(), + new UsernamePasswordCredentials(username, password).applyHttpChallenge(null).toHttpAuthorization()); + } + +} diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/security/UserService.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/security/UserService.java new file mode 100644 index 0000000000000..b8e8045314511 --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/security/UserService.java @@ -0,0 +1,14 @@ +package io.quarkus.websockets.next.test.security; + +import jakarta.annotation.security.RolesAllowed; +import jakarta.enterprise.context.ApplicationScoped; + +@RolesAllowed("user") +@ApplicationScoped +public class UserService { + + public String ping() { + return "" + 42; + } + +} diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/subsocket/SubWebSocketTest.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/subsocket/SubWebSocketTest.java index e2663b3813c09..39b9cb85f0d80 100644 --- a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/subsocket/SubWebSocketTest.java +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/subsocket/SubWebSocketTest.java @@ -6,6 +6,8 @@ import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.TimeUnit; +import jakarta.inject.Inject; + import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -19,6 +21,9 @@ public class SubWebSocketTest { + @Inject + Vertx vertx; + @TestHTTPResource("sub") URI echoUri; @@ -44,7 +49,6 @@ public void testSubSubSub() throws Exception { } public void assertEcho(URI testUri, String path, String payload, String expected) throws Exception { - Vertx vertx = Vertx.vertx(); WebSocketClient client = vertx.createWebSocketClient(); try { LinkedBlockingDeque message = new LinkedBlockingDeque<>(); diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/utils/WSClient.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/utils/WSClient.java index 773b9ab8d134f..955eb9c1b315c 100644 --- a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/utils/WSClient.java +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/utils/WSClient.java @@ -126,6 +126,10 @@ public boolean isClosed() { return socket.get().isClosed(); } + public int closeStatusCode() { + return socket.get().closeStatusCode(); + } + @Override public void close() { disconnect(); diff --git a/extensions/websockets-next/runtime/pom.xml b/extensions/websockets-next/runtime/pom.xml index 76f218d21b125..d913689652388 100644 --- a/extensions/websockets-next/runtime/pom.xml +++ b/extensions/websockets-next/runtime/pom.xml @@ -26,6 +26,11 @@ io.quarkus quarkus-jackson + + + io.quarkus.security + quarkus-security + org.junit.jupiter diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/CloseReason.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/CloseReason.java index 55e100a9b9e7d..108c2d150b55b 100644 --- a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/CloseReason.java +++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/CloseReason.java @@ -15,6 +15,8 @@ public class CloseReason { public static final CloseReason NORMAL = new CloseReason(WebSocketCloseStatus.NORMAL_CLOSURE.code()); + public static final CloseReason INTERNAL_SERVER_ERROR = new CloseReason(WebSocketCloseStatus.INTERNAL_SERVER_ERROR.code()); + private final int code; private final String message; diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/HandshakeRequest.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/HandshakeRequest.java index 052dda407a11b..447dc7d0ae098 100644 --- a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/HandshakeRequest.java +++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/HandshakeRequest.java @@ -13,6 +13,11 @@ public interface HandshakeRequest { * * @param name * @return the first header value for the given header name, or {@code null} + * @see HandshakeRequest#SEC_WEBSOCKET_KEY + * @see HandshakeRequest#SEC_WEBSOCKET_ACCEPT + * @see HandshakeRequest#SEC_WEBSOCKET_EXTENSIONS + * @see HandshakeRequest#SEC_WEBSOCKET_PROTOCOL + * @see HandshakeRequest#SEC_WEBSOCKET_VERSION */ String header(String name); @@ -21,6 +26,11 @@ public interface HandshakeRequest { * * @param name * @return an immutable list of header values for the given header name, never {@code null} + * @see HandshakeRequest#SEC_WEBSOCKET_KEY + * @see HandshakeRequest#SEC_WEBSOCKET_ACCEPT + * @see HandshakeRequest#SEC_WEBSOCKET_EXTENSIONS + * @see HandshakeRequest#SEC_WEBSOCKET_PROTOCOL + * @see HandshakeRequest#SEC_WEBSOCKET_VERSION */ List headers(String name); @@ -62,28 +72,28 @@ public interface HandshakeRequest { String query(); /** - * See The WebSocket Protocol. + * See Sec-WebSocket-Key. */ - public static final String SEC_WEBSOCKET_KEY = "Sec-WebSocket-Key"; + String SEC_WEBSOCKET_KEY = "Sec-WebSocket-Key"; /** - * See The WebSocket Protocol. + * See Sec-WebSocket-Extensions. */ - public static final String SEC_WEBSOCKET_EXTENSIONS = "Sec-WebSocket-Extensions"; + String SEC_WEBSOCKET_EXTENSIONS = "Sec-WebSocket-Extensions"; /** - * See The WebSocket Protocol. + * See Sec-WebSocket-Accept. */ - public static final String SEC_WEBSOCKET_ACCEPT = "Sec-WebSocket-Accept"; + String SEC_WEBSOCKET_ACCEPT = "Sec-WebSocket-Accept"; /** - * See The WebSocket Protocol. + * See Sec-WebSocket-Protocol. */ - public static final String SEC_WEBSOCKET_PROTOCOL = "Sec-WebSocket-Protocol"; + String SEC_WEBSOCKET_PROTOCOL = "Sec-WebSocket-Protocol"; /** - * See The WebSocket Protocol. + * See Sec-WebSocket-Version. */ - public static final String SEC_WEBSOCKET_VERSION = "Sec-WebSocket-Version"; + String SEC_WEBSOCKET_VERSION = "Sec-WebSocket-Version"; } \ No newline at end of file diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/UnhandledFailureStrategy.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/UnhandledFailureStrategy.java new file mode 100644 index 0000000000000..bdfb1f17ad2be --- /dev/null +++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/UnhandledFailureStrategy.java @@ -0,0 +1,20 @@ +package io.quarkus.websockets.next; + +/** + * The strategy used when an error occurs but no error handler can handle the failure. + */ +public enum UnhandledFailureStrategy { + /** + * Close the connection. + */ + CLOSE, + /** + * Log an error message. + */ + LOG, + /** + * No operation. + */ + NOOP; + +} \ No newline at end of file diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketClientConnection.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketClientConnection.java index 5151349c559d8..e262a9839bd44 100644 --- a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketClientConnection.java +++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketClientConnection.java @@ -27,7 +27,7 @@ public interface WebSocketClientConnection extends Sender, BlockingSender { /** * * @param name - * @return the actual value of the path parameter or null + * @return the actual value of the path parameter or {@code null} * @see WebSocketClient#path() */ String pathParam(String name); @@ -42,6 +42,12 @@ public interface WebSocketClientConnection extends Sender, BlockingSender { */ boolean isClosed(); + /** + * + * @return the close reason or {@code null} if the connection is not closed + */ + CloseReason closeReason(); + /** * * @return {@code true} if the WebSocket is open diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketConnection.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketConnection.java index be8acb1a93539..d8e1a3cd98551 100644 --- a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketConnection.java +++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketConnection.java @@ -37,7 +37,7 @@ public interface WebSocketConnection extends Sender, BlockingSender { /** * * @param name - * @return the actual value of the path parameter or null + * @return the actual value of the path parameter or {@code null} * @see WebSocket#path() */ String pathParam(String name); @@ -67,6 +67,12 @@ public interface WebSocketConnection extends Sender, BlockingSender { */ boolean isClosed(); + /** + * + * @return the close reason or {@code null} if the connection is not closed + */ + CloseReason closeReason(); + /** * * @return {@code true} if the WebSocket is open diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketsClientRuntimeConfig.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketsClientRuntimeConfig.java index dff4780aa45c7..ecaf0bb169d0d 100644 --- a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketsClientRuntimeConfig.java +++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketsClientRuntimeConfig.java @@ -40,4 +40,12 @@ public interface WebSocketsClientRuntimeConfig { */ Optional autoPingInterval(); + /** + * The strategy used when an error occurs but no error handler can handle the failure. + *

    + * By default, the connection is closed when an unhandled failure occurs. + */ + @WithDefault("close") + UnhandledFailureStrategy unhandledFailureStrategy(); + } diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketsServerRuntimeConfig.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketsServerRuntimeConfig.java index 28e9d284c2fce..43beffda35600 100644 --- a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketsServerRuntimeConfig.java +++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketsServerRuntimeConfig.java @@ -46,4 +46,12 @@ public interface WebSocketsServerRuntimeConfig { */ Optional autoPingInterval(); + /** + * The strategy used when an error occurs but no error handler can handle the failure. + *

    + * By default, the connection is closed when an unhandled failure occurs. + */ + @WithDefault("close") + UnhandledFailureStrategy unhandledFailureStrategy(); + } diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/ConcurrencyLimiter.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/ConcurrencyLimiter.java index 8a690d793ce5d..2e05b10648247 100644 --- a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/ConcurrencyLimiter.java +++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/ConcurrencyLimiter.java @@ -25,7 +25,8 @@ class ConcurrencyLimiter { ConcurrencyLimiter(WebSocketConnectionBase connection) { this.connection = connection; this.uncompleted = new AtomicLong(); - this.queueCounter = new AtomicLong(); + // Counter is only used for debugging + this.queueCounter = LOG.isDebugEnabled() ? new AtomicLong() : null; this.queue = Queues.createMpscQueue(); } @@ -51,7 +52,7 @@ void run(Context context, Runnable action) { LOG.debugf("Run action: %s", connection); action.run(); } else { - long queueIndex = queueCounter.incrementAndGet(); + long queueIndex = queueCounter != null ? queueCounter.incrementAndGet() : 0l; LOG.debugf("Action queued as %s: %s", queueIndex, connection); queue.offer(new Action(queueIndex, action, context)); // We need to make sure that at least one completion is in flight @@ -76,18 +77,7 @@ void failure(Throwable t) { try { promise.fail(t); } finally { - if (uncompleted.decrementAndGet() == 0) { - return; - } - Action queuedAction = queue.poll(); - assert queuedAction != null; - LOG.debugf("Run action %s from queue: %s", queuedAction.queueIndex, connection); - queuedAction.context.runOnContext(new Handler() { - @Override - public void handle(Void event) { - queuedAction.runnable.run(); - } - }); + tryNext(); } } @@ -95,19 +85,23 @@ void complete() { try { promise.complete(); } finally { - if (uncompleted.decrementAndGet() == 0) { - return; - } - Action queuedAction = queue.poll(); - assert queuedAction != null; - LOG.debugf("Run action %s from queue: %s", queuedAction.queueIndex, connection); - queuedAction.context.runOnContext(new Handler() { - @Override - public void handle(Void event) { - queuedAction.runnable.run(); - } - }); + tryNext(); + } + } + + private void tryNext() { + if (uncompleted.decrementAndGet() == 0) { + return; } + Action queuedAction = queue.poll(); + assert queuedAction != null; + LOG.debugf("Run action %s from queue: %s", queuedAction.queueIndex, connection); + queuedAction.context.runOnContext(new Handler() { + @Override + public void handle(Void event) { + queuedAction.runnable.run(); + } + }); } } diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/ContextSupport.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/ContextSupport.java index 0b018b6fe2eaf..b36d4dc834b3e 100644 --- a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/ContextSupport.java +++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/ContextSupport.java @@ -36,7 +36,6 @@ void start() { void start(ContextState requestContextState) { LOG.debugf("Start contexts: %s", connection); startSession(); - // Activate a new request context requestContext.activate(requestContextState); } diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/Endpoints.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/Endpoints.java index 85ab430d8dd52..ce4d2c096628d 100644 --- a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/Endpoints.java +++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/Endpoints.java @@ -10,6 +10,11 @@ import io.quarkus.arc.ArcContainer; import io.quarkus.arc.InjectableContext; +import io.quarkus.security.AuthenticationFailedException; +import io.quarkus.security.ForbiddenException; +import io.quarkus.security.UnauthorizedException; +import io.quarkus.websockets.next.CloseReason; +import io.quarkus.websockets.next.UnhandledFailureStrategy; import io.quarkus.websockets.next.WebSocketException; import io.quarkus.websockets.next.runtime.WebSocketSessionContext.SessionContextState; import io.smallrye.mutiny.Multi; @@ -25,7 +30,8 @@ class Endpoints { private static final Logger LOG = Logger.getLogger(Endpoints.class); static void initialize(Vertx vertx, ArcContainer container, Codecs codecs, WebSocketConnectionBase connection, - WebSocketBase ws, String generatedEndpointClass, Optional autoPingInterval, Runnable onClose) { + WebSocketBase ws, String generatedEndpointClass, Optional autoPingInterval, + SecuritySupport securitySupport, UnhandledFailureStrategy unhandledFailureStrategy, Runnable onClose) { Context context = vertx.getOrCreateContext(); @@ -38,7 +44,8 @@ static void initialize(Vertx vertx, ArcContainer container, Codecs codecs, WebSo container.requestContext()); // Create an endpoint that delegates callbacks to the endpoint bean - WebSocketEndpoint endpoint = createEndpoint(generatedEndpointClass, context, connection, codecs, contextSupport); + WebSocketEndpoint endpoint = createEndpoint(generatedEndpointClass, context, connection, codecs, contextSupport, + securitySupport); // A broadcast processor is only needed if Multi is consumed by the callback BroadcastProcessor textBroadcastProcessor = endpoint.consumedTextMultiType() != null @@ -70,7 +77,7 @@ public void handle(Void event) { LOG.debugf("@OnTextMessage callback consuming Multi completed: %s", connection); } else { - logFailure(r.cause(), + handleFailure(unhandledFailureStrategy, r.cause(), "Unable to complete @OnTextMessage callback consuming Multi", connection); } @@ -88,7 +95,7 @@ public void handle(Void event) { LOG.debugf("@OnBinaryMessage callback consuming Multi completed: %s", connection); } else { - logFailure(r.cause(), + handleFailure(unhandledFailureStrategy, r.cause(), "Unable to complete @OnBinaryMessage callback consuming Multi", connection); } @@ -97,7 +104,7 @@ public void handle(Void event) { }); } } else { - logFailure(r.cause(), "Unable to complete @OnOpen callback", connection); + handleFailure(unhandledFailureStrategy, r.cause(), "Unable to complete @OnOpen callback", connection); } }); } @@ -110,7 +117,8 @@ public void handle(Void event) { if (r.succeeded()) { LOG.debugf("@OnTextMessage callback consumed text message: %s", connection); } else { - logFailure(r.cause(), "Unable to consume text message in @OnTextMessage callback", + handleFailure(unhandledFailureStrategy, r.cause(), + "Unable to consume text message in @OnTextMessage callback", connection); } }); @@ -118,13 +126,15 @@ public void handle(Void event) { } else { textMessageHandler(connection, endpoint, ws, onOpenContext, m -> { contextSupport.start(); + securitySupport.start(); try { textBroadcastProcessor.onNext(endpoint.decodeTextMultiItem(m)); LOG.debugf("Text message >> Multi: %s", connection); } catch (Throwable throwable) { endpoint.doOnError(throwable).subscribe().with( v -> LOG.debugf("Text message >> Multi: %s", connection), - t -> LOG.errorf(t, "Unable to send text message to Multi: %s", connection)); + t -> handleFailure(unhandledFailureStrategy, t, "Unable to send text message to Multi", + connection)); } finally { contextSupport.end(false); } @@ -138,7 +148,8 @@ public void handle(Void event) { if (r.succeeded()) { LOG.debugf("@OnBinaryMessage callback consumed binary message: %s", connection); } else { - logFailure(r.cause(), "Unable to consume binary message in @OnBinaryMessage callback", + handleFailure(unhandledFailureStrategy, r.cause(), + "Unable to consume binary message in @OnBinaryMessage callback", connection); } }); @@ -146,13 +157,15 @@ public void handle(Void event) { } else { binaryMessageHandler(connection, endpoint, ws, onOpenContext, m -> { contextSupport.start(); + securitySupport.start(); try { binaryBroadcastProcessor.onNext(endpoint.decodeBinaryMultiItem(m)); LOG.debugf("Binary message >> Multi: %s", connection); } catch (Throwable throwable) { endpoint.doOnError(throwable).subscribe().with( v -> LOG.debugf("Binary message >> Multi: %s", connection), - t -> LOG.errorf(t, "Unable to send binary message to Multi: %s", connection)); + t -> handleFailure(unhandledFailureStrategy, t, "Unable to send binary message to Multi", + connection)); } finally { contextSupport.end(false); } @@ -164,7 +177,8 @@ public void handle(Void event) { if (r.succeeded()) { LOG.debugf("@OnPongMessage callback consumed text message: %s", connection); } else { - logFailure(r.cause(), "Unable to consume text message in @OnPongMessage callback", connection); + handleFailure(unhandledFailureStrategy, r.cause(), + "Unable to consume text message in @OnPongMessage callback", connection); } }); }); @@ -191,7 +205,8 @@ public void handle(Void event) { if (r.succeeded()) { LOG.debugf("@OnClose callback completed: %s", connection); } else { - logFailure(r.cause(), "Unable to complete @OnClose callback", connection); + handleFailure(unhandledFailureStrategy, r.cause(), "Unable to complete @OnClose callback", + connection); } onClose.run(); if (timerId != null) { @@ -211,19 +226,38 @@ public void handle(Throwable t) { public void handle(Void event) { endpoint.doOnError(t).subscribe().with( v -> LOG.debugf("Error [%s] processed: %s", t.getClass(), connection), - t -> LOG.errorf(t, "Unhandled error occurred: %s", t.toString(), - connection)); + t -> handleFailure(unhandledFailureStrategy, t, "Unhandled error occurred", connection)); } }); } }); } + private static void handleFailure(UnhandledFailureStrategy strategy, Throwable cause, String message, + WebSocketConnectionBase connection) { + switch (strategy) { + case CLOSE -> closeConnection(cause, connection); + case LOG -> logFailure(cause, message, connection); + case NOOP -> LOG.tracef("Unhandled failure ignored: %s", connection); + default -> throw new IllegalArgumentException("Unexpected strategy: " + strategy); + } + } + + private static void closeConnection(Throwable cause, WebSocketConnectionBase connection) { + connection.close(CloseReason.INTERNAL_SERVER_ERROR).subscribe().with( + v -> LOG.debugf("Connection closed due to unhandled failure %s: %s", cause, connection), + t -> LOG.errorf("Unable to close connection [%s] due to unhandled failure [%s]: %s", connection.id(), cause, + t)); + } + private static void logFailure(Throwable throwable, String message, WebSocketConnectionBase connection) { if (isWebSocketIsClosedFailure(throwable, connection)) { LOG.debugf(throwable, message + ": %s", connection); + } else if (isSecurityFailure(throwable)) { + // Avoid excessive logging for security failures + LOG.errorf("Security failure: %s", throwable.toString()); } else { LOG.errorf(throwable, message + ": %s", @@ -231,6 +265,12 @@ private static void logFailure(Throwable throwable, String message, WebSocketCon } } + private static boolean isSecurityFailure(Throwable throwable) { + return throwable instanceof UnauthorizedException + || throwable instanceof AuthenticationFailedException + || throwable instanceof ForbiddenException; + } + private static boolean isWebSocketIsClosedFailure(Throwable throwable, WebSocketConnectionBase connection) { if (!connection.isClosed()) { return false; @@ -298,8 +338,7 @@ public void handle(Void event) { } private static WebSocketEndpoint createEndpoint(String endpointClassName, Context context, - WebSocketConnectionBase connection, - Codecs codecs, ContextSupport contextSupport) { + WebSocketConnectionBase connection, Codecs codecs, ContextSupport contextSupport, SecuritySupport securitySupport) { try { ClassLoader cl = Thread.currentThread().getContextClassLoader(); if (cl == null) { @@ -309,8 +348,9 @@ private static WebSocketEndpoint createEndpoint(String endpointClassName, Contex Class endpointClazz = (Class) cl .loadClass(endpointClassName); WebSocketEndpoint endpoint = (WebSocketEndpoint) endpointClazz - .getDeclaredConstructor(WebSocketConnectionBase.class, Codecs.class, ContextSupport.class) - .newInstance(connection, codecs, contextSupport); + .getDeclaredConstructor(WebSocketConnectionBase.class, Codecs.class, ContextSupport.class, + SecuritySupport.class) + .newInstance(connection, codecs, contextSupport, securitySupport); return endpoint; } catch (Exception e) { throw new WebSocketException("Unable to create endpoint instance: " + endpointClassName, e); diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/SecuritySupport.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/SecuritySupport.java new file mode 100644 index 0000000000000..8ec115e085e70 --- /dev/null +++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/SecuritySupport.java @@ -0,0 +1,32 @@ +package io.quarkus.websockets.next.runtime; + +import java.util.Objects; + +import jakarta.enterprise.inject.Instance; + +import io.quarkus.security.identity.CurrentIdentityAssociation; +import io.quarkus.security.identity.SecurityIdentity; + +public class SecuritySupport { + + static final SecuritySupport NOOP = new SecuritySupport(null, null); + + private final Instance currentIdentity; + private final SecurityIdentity identity; + + SecuritySupport(Instance currentIdentity, SecurityIdentity identity) { + this.currentIdentity = currentIdentity; + this.identity = currentIdentity != null ? Objects.requireNonNull(identity) : identity; + } + + /** + * This method is called before an endpoint callback is invoked. + */ + void start() { + if (currentIdentity != null) { + CurrentIdentityAssociation current = currentIdentity.get(); + current.setIdentity(identity); + } + } + +} diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketConnectionBase.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketConnectionBase.java index 0887228169baf..00ae0dc9e0d1f 100644 --- a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketConnectionBase.java +++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketConnectionBase.java @@ -93,6 +93,10 @@ public Uni close() { } public Uni close(CloseReason reason) { + if (isClosed()) { + LOG.warnf("Connection already closed: %s", this); + return Uni.createFrom().voidItem(); + } return UniHelper.toUni(webSocket().close((short) reason.getCode(), reason.getMessage())); } @@ -121,6 +125,6 @@ public CloseReason closeReason() { if (ws.isClosed()) { return new CloseReason(ws.closeStatusCode(), ws.closeReason()); } - throw new IllegalStateException("Connection is not closed"); + return null; } } diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketConnectorImpl.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketConnectorImpl.java index a4abe65f42162..8b8781ccac2ed 100644 --- a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketConnectorImpl.java +++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketConnectorImpl.java @@ -115,7 +115,8 @@ public Uni connect() { connectionManager.add(clientEndpoint.generatedEndpointClass, connection); Endpoints.initialize(vertx, Arc.container(), codecs, connection, ws, - clientEndpoint.generatedEndpointClass, config.autoPingInterval(), + clientEndpoint.generatedEndpointClass, config.autoPingInterval(), SecuritySupport.NOOP, + config.unhandledFailureStrategy(), () -> { connectionManager.remove(clientEndpoint.generatedEndpointClass, connection); client.close(); diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketEndpointBase.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketEndpointBase.java index ed453f59a97c9..03d39284e0170 100644 --- a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketEndpointBase.java +++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketEndpointBase.java @@ -13,7 +13,6 @@ import io.quarkus.arc.Arc; import io.quarkus.arc.ArcContainer; import io.quarkus.arc.InjectableBean; -import io.quarkus.arc.InjectableContext.ContextState; import io.quarkus.virtual.threads.VirtualThreadsRecorder; import io.quarkus.websockets.next.InboundProcessingMode; import io.quarkus.websockets.next.runtime.ConcurrencyLimiter.PromiseComplete; @@ -42,15 +41,20 @@ public abstract class WebSocketEndpointBase implements WebSocketEndpoint { private final ContextSupport contextSupport; + private final SecuritySupport securitySupport; + private final InjectableBean bean; + private final Object beanInstance; - public WebSocketEndpointBase(WebSocketConnectionBase connection, Codecs codecs, ContextSupport contextSupport) { + public WebSocketEndpointBase(WebSocketConnectionBase connection, Codecs codecs, ContextSupport contextSupport, + SecuritySupport securitySupport) { this.connection = connection; this.codecs = codecs; this.limiter = inboundProcessingMode() == InboundProcessingMode.SERIAL ? new ConcurrencyLimiter(connection) : null; this.container = Arc.container(); this.contextSupport = contextSupport; + this.securitySupport = securitySupport; InjectableBean bean = container.bean(beanIdentifier()); if (bean.getScope().equals(ApplicationScoped.class) || bean.getScope().equals(Singleton.class)) { @@ -105,18 +109,18 @@ private Future execute(M message, ExecutionModel executionModel, limiter.run(context, new Runnable() { @Override public void run() { - doExecute(context, promise, message, executionModel, action, terminateSession, complete::complete, + doExecute(context, message, executionModel, action, terminateSession, complete::complete, complete::failure); } }); } else { // No need to limit the concurrency - doExecute(context, promise, message, executionModel, action, terminateSession, promise::complete, promise::fail); + doExecute(context, message, executionModel, action, terminateSession, promise::complete, promise::fail); } return promise.future(); } - private void doExecute(Context context, Promise promise, M message, ExecutionModel executionModel, + private void doExecute(Context context, M message, ExecutionModel executionModel, Function> action, boolean terminateSession, Runnable onComplete, Consumer onFailure) { Handler contextSupportEnd = executionModel.isBlocking() ? new Handler() { @@ -133,6 +137,7 @@ public void handle(Void event) { public void run() { Context context = Vertx.currentContext(); contextSupport.start(); + securitySupport.start(); action.apply(message).subscribe().with( v -> { context.runOnContext(contextSupportEnd); @@ -150,6 +155,7 @@ public void run() { public Void call() { Context context = Vertx.currentContext(); contextSupport.start(); + securitySupport.start(); action.apply(message).subscribe().with( v -> { context.runOnContext(contextSupportEnd); @@ -165,6 +171,7 @@ public Void call() { } else { // Event loop contextSupport.start(); + securitySupport.start(); action.apply(message).subscribe().with( v -> { contextSupport.end(terminateSession); @@ -179,72 +186,76 @@ public Void call() { public Uni doErrorExecute(Throwable throwable, ExecutionModel executionModel, Function> action) { - // We need to capture the current request context state so that it can be activated - // when the error callback is executed - ContextState requestContextState = contextSupport.currentRequestContextState(); - Handler contextSupportEnd = new Handler() { - + Promise promise = Promise.promise(); + // Always exeute error handler on a new duplicated context + ContextSupport.createNewDuplicatedContext(Vertx.currentContext(), connection).runOnContext(new Handler() { @Override public void handle(Void event) { - contextSupport.end(false, false); - } - }; - contextSupportEnd.handle(null); - - Promise promise = Promise.promise(); - if (executionModel == ExecutionModel.VIRTUAL_THREAD) { - VirtualThreadsRecorder.getCurrent().execute(new Runnable() { - @Override - public void run() { - Context context = Vertx.currentContext(); - contextSupport.start(requestContextState); - action.apply(throwable).subscribe().with( - v -> { - context.runOnContext(contextSupportEnd); - promise.complete(); - }, - t -> { - context.runOnContext(contextSupportEnd); - promise.fail(t); - }); - } - }); - } else if (executionModel == ExecutionModel.WORKER_THREAD) { - Vertx.currentContext().executeBlocking(new Callable() { - @Override - public Void call() { - Context context = Vertx.currentContext(); - contextSupport.start(requestContextState); - action.apply(throwable).subscribe().with( - v -> { - context.runOnContext(contextSupportEnd); - promise.complete(); - }, - t -> { - context.runOnContext(contextSupportEnd); - promise.fail(t); - }); - return null; - } - }, false); - } else { - Vertx.currentContext().runOnContext(new Handler() { - @Override - public void handle(Void event) { - Context context = Vertx.currentContext(); - contextSupport.start(requestContextState); - action.apply(throwable).subscribe().with( - v -> { - context.runOnContext(contextSupportEnd); - promise.complete(); - }, - t -> { - context.runOnContext(contextSupportEnd); - promise.fail(t); - }); + Handler contextSupportEnd = new Handler() { + @Override + public void handle(Void event) { + contextSupport.end(false); + } + }; + + if (executionModel == ExecutionModel.VIRTUAL_THREAD) { + VirtualThreadsRecorder.getCurrent().execute(new Runnable() { + @Override + public void run() { + Context context = Vertx.currentContext(); + contextSupport.start(); + securitySupport.start(); + action.apply(throwable).subscribe().with( + v -> { + context.runOnContext(contextSupportEnd); + promise.complete(); + }, + t -> { + context.runOnContext(contextSupportEnd); + promise.fail(t); + }); + } + }); + } else if (executionModel == ExecutionModel.WORKER_THREAD) { + Vertx.currentContext().executeBlocking(new Callable() { + @Override + public Void call() { + Context context = Vertx.currentContext(); + contextSupport.start(); + securitySupport.start(); + action.apply(throwable).subscribe().with( + v -> { + context.runOnContext(contextSupportEnd); + promise.complete(); + }, + t -> { + context.runOnContext(contextSupportEnd); + promise.fail(t); + }); + return null; + } + }, false); + } else { + Vertx.currentContext().runOnContext(new Handler() { + @Override + public void handle(Void event) { + Context context = Vertx.currentContext(); + contextSupport.start(); + securitySupport.start(); + action.apply(throwable).subscribe().with( + v -> { + context.runOnContext(contextSupportEnd); + promise.complete(); + }, + t -> { + context.runOnContext(contextSupportEnd); + promise.fail(t); + }); + } + }); } - }); - } + } + }); return UniHelper.toUni(promise.future()); } diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketServerRecorder.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketServerRecorder.java index e580cf85791e7..35bdae2ca2206 100644 --- a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketServerRecorder.java +++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketServerRecorder.java @@ -1,21 +1,29 @@ package io.quarkus.websockets.next.runtime; +import java.util.function.Consumer; import java.util.function.Supplier; +import jakarta.enterprise.inject.Instance; + import org.jboss.logging.Logger; import io.quarkus.arc.Arc; import io.quarkus.arc.ArcContainer; import io.quarkus.runtime.annotations.Recorder; +import io.quarkus.security.identity.CurrentIdentityAssociation; +import io.quarkus.security.identity.SecurityIdentity; import io.quarkus.vertx.core.runtime.VertxCoreRecorder; +import io.quarkus.vertx.http.runtime.security.QuarkusHttpUser; import io.quarkus.websockets.next.WebSocketServerException; import io.quarkus.websockets.next.WebSocketsServerRuntimeConfig; import io.smallrye.common.vertx.VertxContext; +import io.smallrye.mutiny.Uni; import io.vertx.core.Context; import io.vertx.core.Future; import io.vertx.core.Handler; import io.vertx.core.Vertx; import io.vertx.core.http.ServerWebSocket; +import io.vertx.ext.web.Route; import io.vertx.ext.web.RoutingContext; @Recorder @@ -46,6 +54,34 @@ public Object get() { }; } + public Consumer initializeSecurityHandler() { + return new Consumer() { + + @Override + public void accept(Route route) { + // Force authentication so that it's possible to capture the SecurityIdentity before the HTTP upgrade + route.handler(new Handler() { + + @Override + public void handle(RoutingContext ctx) { + if (ctx.user() == null) { + Uni deferredIdentity = ctx + .> get(QuarkusHttpUser.DEFERRED_IDENTITY_KEY); + deferredIdentity.subscribe().with(i -> { + if (ctx.response().ended()) { + return; + } + ctx.next(); + }, ctx::fail); + } else { + ctx.next(); + } + } + }); + } + }; + } + public Handler createEndpointHandler(String generatedEndpointClass, String endpointId) { ArcContainer container = Arc.container(); ConnectionManager connectionManager = container.instance(ConnectionManager.class).get(); @@ -54,6 +90,8 @@ public Handler createEndpointHandler(String generatedEndpointCla @Override public void handle(RoutingContext ctx) { + SecuritySupport securitySupport = initializeSecuritySupport(container, ctx); + Future future = ctx.request().toWebSocket(); future.onSuccess(ws -> { Vertx vertx = VertxCoreRecorder.getVertx().get(); @@ -64,10 +102,24 @@ public void handle(RoutingContext ctx) { LOG.debugf("Connection created: %s", connection); Endpoints.initialize(vertx, container, codecs, connection, ws, generatedEndpointClass, - config.autoPingInterval(), () -> connectionManager.remove(generatedEndpointClass, connection)); + config.autoPingInterval(), securitySupport, config.unhandledFailureStrategy(), + () -> connectionManager.remove(generatedEndpointClass, connection)); }); } }; } + SecuritySupport initializeSecuritySupport(ArcContainer container, RoutingContext ctx) { + Instance currentIdentityAssociation = container.select(CurrentIdentityAssociation.class); + if (currentIdentityAssociation.isResolvable()) { + // Security extension is present + // Obtain the current security identity from the handshake request + QuarkusHttpUser user = (QuarkusHttpUser) ctx.user(); + if (user != null) { + return new SecuritySupport(currentIdentityAssociation, user.getSecurityIdentity()); + } + } + return SecuritySupport.NOOP; + } + } diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketSessionContext.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketSessionContext.java index f6e931a2c850f..3d6c488289c41 100644 --- a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketSessionContext.java +++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketSessionContext.java @@ -24,6 +24,7 @@ import io.quarkus.arc.ArcContainer; import io.quarkus.arc.ContextInstanceHandle; import io.quarkus.arc.CurrentContext; +import io.quarkus.arc.CurrentContextFactory; import io.quarkus.arc.InjectableBean; import io.quarkus.arc.ManagedContext; import io.quarkus.arc.impl.ComputingCacheContextInstances; @@ -35,19 +36,13 @@ public class WebSocketSessionContext implements ManagedContext { private static final Logger LOG = Logger.getLogger(WebSocketSessionContext.class); - private final LazyValue> currentContext; + private final CurrentContext currentContext; private final LazyValue> initializedEvent; private final LazyValue> beforeDestroyEvent; private final LazyValue> destroyEvent; - public WebSocketSessionContext() { - // Use lazy value because no-args constructor is needed - this.currentContext = new LazyValue<>(new Supplier>() { - @Override - public CurrentContext get() { - return Arc.container().getCurrentContextFactory().create(SessionScoped.class); - } - }); + public WebSocketSessionContext(CurrentContextFactory currentContextFactory) { + this.currentContext = currentContextFactory.create(SessionScoped.class); this.initializedEvent = newEvent(Initialized.Literal.SESSION, Any.Literal.INSTANCE); this.beforeDestroyEvent = newEvent(BeforeDestroyed.Literal.SESSION, Any.Literal.INSTANCE); this.destroyEvent = newEvent(Destroyed.Literal.SESSION, Any.Literal.INSTANCE); @@ -62,7 +57,6 @@ public Class getScope() { public ContextState getState() { SessionContextState state = currentState(); if (state == null) { - // Thread local not set - context is not active! throw notActive(); } return state; @@ -72,11 +66,11 @@ public ContextState getState() { public ContextState activate(ContextState initialState) { if (initialState == null) { SessionContextState state = initializeContextState(); - currentContext().set(state); + currentContext.set(state); return state; } else { if (initialState instanceof SessionContextState) { - currentContext().set((SessionContextState) initialState); + currentContext.set((SessionContextState) initialState); return initialState; } else { throw new IllegalArgumentException("Invalid initial state: " + initialState.getClass().getName()); @@ -86,7 +80,7 @@ public ContextState activate(ContextState initialState) { @Override public void deactivate() { - currentContext().remove(); + currentContext.remove(); } @SuppressWarnings("unchecked") @@ -176,12 +170,8 @@ SessionContextState initializeContextState() { return state; } - private CurrentContext currentContext() { - return currentContext.get(); - } - private SessionContextState currentState() { - return currentContext().get(); + return currentContext.get(); } private IllegalArgumentException invalidScope() { diff --git a/independent-projects/arc/pom.xml b/independent-projects/arc/pom.xml index 558816db7cc3f..eb1782e79ceec 100644 --- a/independent-projects/arc/pom.xml +++ b/independent-projects/arc/pom.xml @@ -45,16 +45,16 @@ 2.0.1 1.8.0 - 3.1.8 - 3.5.3.Final + 3.2.0 + 3.6.0.Final 2.6.0 1.6.Final 3.25.3 5.10.2 - 1.9.23 - 1.8.0 - 5.11.0 + 2.0.0 + 1.8.1 + 5.12.0 1.7.0.Final 2.0.1 diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/AnnotationStore.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/AnnotationStore.java index dc3557562f721..e0d0304882176 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/AnnotationStore.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/AnnotationStore.java @@ -1,26 +1,15 @@ package io.quarkus.arc.processor; -import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; -import java.util.Collections; -import java.util.EnumMap; -import java.util.List; -import java.util.Objects; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; -import java.util.stream.Collectors; +import java.util.HashSet; +import java.util.Set; import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.AnnotationOverlay; import org.jboss.jandex.AnnotationTarget; -import org.jboss.jandex.AnnotationTarget.Kind; +import org.jboss.jandex.AnnotationTransformation; import org.jboss.jandex.DotName; -import org.jboss.jandex.FieldInfo; -import org.jboss.jandex.MethodInfo; -import org.jboss.logging.Logger; - -import io.quarkus.arc.processor.AnnotationsTransformer.TransformationContext; -import io.quarkus.arc.processor.BuildExtension.BuildContext; +import org.jboss.jandex.IndexView; /** * Applies {@link AnnotationsTransformer}s and caches the results of transformations. @@ -30,24 +19,17 @@ */ public final class AnnotationStore { - private final ConcurrentMap> transformed; - - private final EnumMap> transformersMap; + private final AnnotationOverlay delegate; - private final BuildContext buildContext; + AnnotationStore(IndexView index, Collection transformations) { + this.delegate = AnnotationOverlay.builder(index, transformations) + .compatibleMode() + .runtimeAnnotationsOnly() + .build(); + } - AnnotationStore(Collection transformers, BuildContext buildContext) { - if (transformers == null || transformers.isEmpty()) { - this.transformed = null; - this.transformersMap = null; - } else { - this.transformed = new ConcurrentHashMap<>(); - this.transformersMap = new EnumMap<>(Kind.class); - this.transformersMap.put(Kind.CLASS, initTransformers(Kind.CLASS, transformers)); - this.transformersMap.put(Kind.METHOD, initTransformers(Kind.METHOD, transformers)); - this.transformersMap.put(Kind.FIELD, initTransformers(Kind.FIELD, transformers)); - } - this.buildContext = buildContext; + public AnnotationOverlay overlay() { + return delegate; } /** @@ -57,10 +39,7 @@ public final class AnnotationStore { * @return the annotation instance for the given target */ public Collection getAnnotations(AnnotationTarget target) { - if (transformed != null) { - return transformed.computeIfAbsent(new AnnotationTargetKey(target), this::transform); - } - return getOriginalAnnotations(target); + return delegate.annotations(target.asDeclaration()); } /** @@ -71,163 +50,33 @@ public Collection getAnnotations(AnnotationTarget target) { * @see #getAnnotations(AnnotationTarget) */ public AnnotationInstance getAnnotation(AnnotationTarget target, DotName name) { - return Annotations.find(getAnnotations(target), name); + return delegate.annotation(target.asDeclaration(), name); } /** * * @param target * @param name - * @return {@code true} if the specified target contains the specified annotation, @{code false} otherwise + * @return {@code true} if the specified target contains the specified annotation, {@code false} otherwise * @see #getAnnotations(AnnotationTarget) */ public boolean hasAnnotation(AnnotationTarget target, DotName name) { - return Annotations.contains(getAnnotations(target), name); + return delegate.hasAnnotation(target.asDeclaration(), name); } /** * * @param target * @param names - * @return {@code true} if the specified target contains any of the specified annotations, @{code false} otherwise + * @return {@code true} if the specified target contains any of the specified annotations, {@code false} otherwise * @see #getAnnotations(AnnotationTarget) */ public boolean hasAnyAnnotation(AnnotationTarget target, Iterable names) { - return Annotations.containsAny(getAnnotations(target), names); - } - - private Collection transform(AnnotationTargetKey key) { - AnnotationTarget target = key.target; - Collection annotations = getOriginalAnnotations(target); - List transformers = transformersMap.get(target.kind()); - if (transformers.isEmpty()) { - return annotations; - } - TransformationContextImpl transformationContext = new TransformationContextImpl(buildContext, target, annotations); - for (AnnotationsTransformer transformer : transformers) { - transformer.transform(transformationContext); - } - return transformationContext.getAnnotations(); - } - - private Collection getOriginalAnnotations(AnnotationTarget target) { - Collection annotations; - switch (target.kind()) { - case CLASS: - annotations = target.asClass().declaredAnnotations(); - break; - case METHOD: - // Note that the returning collection also contains method params annotations - annotations = target.asMethod().annotations(); - break; - case FIELD: - annotations = target.asField().annotations(); - break; - default: - throw new IllegalArgumentException("Unsupported annotation target"); - } - - return Annotations.onlyRuntimeVisible(annotations); - } - - private List initTransformers(Kind kind, Collection transformers) { - List found = new ArrayList<>(); - for (AnnotationsTransformer transformer : transformers) { - if (transformer.appliesTo(kind)) { - found.add(transformer); - } - } - if (found.isEmpty()) { - return Collections.emptyList(); - } - found.sort(BuildExtension::compare); - return found; - } - - static class TransformationContextImpl extends AnnotationsTransformationContext> - implements TransformationContext { - - private static final Logger LOG = Logger.getLogger(TransformationContextImpl.class); - - public TransformationContextImpl(BuildContext buildContext, AnnotationTarget target, - Collection annotations) { - super(buildContext, target, annotations); + Set set = new HashSet<>(); + for (DotName name : names) { + set.add(name); } - - @Override - public Transformation transform() { - if (LOG.isTraceEnabled()) { - String stack = Arrays.stream(Thread.currentThread().getStackTrace()) - .skip(2) - .limit(7) - .map(se -> "\n\t" + se.toString()) - .collect(Collectors.joining()); - LOG.tracef("Transforming annotations of %s %s\n\t...", target, stack); - } - return new Transformation(new ArrayList<>(getAnnotations()), getTarget(), this::setAnnotations); - } - - } - - /** - * We cannot use annotation target directly as a key in a Map. Only {@link MethodInfo} overrides equals/hashCode. - */ - static final class AnnotationTargetKey { - - final AnnotationTarget target; - - public AnnotationTargetKey(AnnotationTarget target) { - this.target = target; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null) { - return false; - } - if (getClass() != obj.getClass()) { - return false; - } - AnnotationTargetKey other = (AnnotationTargetKey) obj; - if (target.kind() != other.target.kind()) { - return false; - } - switch (target.kind()) { - case METHOD: - return target.asMethod().equals(other.target); - case FIELD: - FieldInfo field = target.asField(); - FieldInfo otherField = other.target.asField(); - return Objects.equals(field.name(), otherField.name()) - && Objects.equals(field.declaringClass().name(), otherField.declaringClass().name()); - case CLASS: - return target.asClass().name().equals(other.target.asClass().name()); - default: - throw unsupportedAnnotationTarget(target); - } - } - - @Override - public int hashCode() { - switch (target.kind()) { - case METHOD: - return target.asMethod().hashCode(); - case FIELD: - return Objects.hash(target.asField().name(), target.asField().declaringClass().name()); - case CLASS: - return target.asClass().name().hashCode(); - default: - throw unsupportedAnnotationTarget(target); - } - } - - } - - private static IllegalArgumentException unsupportedAnnotationTarget(AnnotationTarget target) { - return new IllegalArgumentException("Unsupported annotation target: " + target.kind()); + return delegate.hasAnyAnnotation(target.asDeclaration(), set); } } diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/AnnotationsTransformer.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/AnnotationsTransformer.java index 86da29f85e481..dbfb0b71d3600 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/AnnotationsTransformer.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/AnnotationsTransformer.java @@ -13,6 +13,7 @@ import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.AnnotationTarget; import org.jboss.jandex.AnnotationTarget.Kind; +import org.jboss.jandex.AnnotationTransformation; import org.jboss.jandex.ClassInfo; import org.jboss.jandex.DotName; import org.jboss.jandex.FieldInfo; @@ -26,7 +27,7 @@ * * @see Builder */ -public interface AnnotationsTransformer extends BuildExtension { +public interface AnnotationsTransformer extends AnnotationTransformation, BuildExtension { /** * By default, the transformation is applied to all kinds of targets. @@ -48,6 +49,56 @@ default boolean appliesTo(Kind kind) { */ void transform(TransformationContext transformationContext); + // --- + // implementation of `AnnotationTransformation` methods + + @Override + default int priority() { + return getPriority(); + } + + @Override + default boolean supports(Kind kind) { + return appliesTo(kind); + } + + @Override + default void apply(AnnotationTransformation.TransformationContext context) { + transform(new TransformationContext() { + @Override + public AnnotationTarget getTarget() { + return context.declaration(); + } + + @Override + public Collection getAnnotations() { + return context.annotations(); + } + + @Override + public Transformation transform() { + return new Transformation(context); + } + + @Override + public V get(Key key) { + throw new UnsupportedOperationException(); + } + + @Override + public V put(Key key, V value) { + throw new UnsupportedOperationException(); + } + }); + } + + @Override + default boolean requiresCompatibleMode() { + return true; + } + + // --- + /** * * @return a new builder instance diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanDeployment.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanDeployment.java index b58413e26ead9..59c34e9625f2b 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanDeployment.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanDeployment.java @@ -114,6 +114,7 @@ public class BeanDeployment { private final Set beansWithRuntimeDeferredUnproxyableError; + // scope -> list of funs that accept the method creator for ComponentsProvider#getComponents() private final Map>> customContexts; private final Map beanDefiningAnnotations; @@ -152,7 +153,10 @@ public class BeanDeployment { this.beanArchiveImmutableIndex = Objects.requireNonNull(builder.beanArchiveImmutableIndex); this.applicationIndex = builder.applicationIndex; this.applicationClassPredicate = builder.applicationClassPredicate; - this.annotationStore = new AnnotationStore(initAndSort(builder.annotationTransformers, buildContext), buildContext); + this.annotationStore = new AnnotationStore(builder.beanArchiveComputingIndex != null + ? builder.beanArchiveComputingIndex + : builder.beanArchiveImmutableIndex, + builder.annotationTransformers); buildContext.putInternal(Key.ANNOTATION_STORE, annotationStore); this.injectionPointTransformer = new InjectionPointModifier( @@ -214,7 +218,7 @@ public class BeanDeployment { additionalStereotypes.addAll(stereotypeRegistrar.getAdditionalStereotypes()); } - this.stereotypes = findStereotypes(interceptorBindings, customContexts, additionalStereotypes, + this.stereotypes = findStereotypes(interceptorBindings, customContexts.keySet(), additionalStereotypes, annotationStore); buildContext.putInternal(Key.STEREOTYPES, Collections.unmodifiableMap(stereotypes)); @@ -734,7 +738,7 @@ Map>> getCustomContexts() } ScopeInfo getScope(DotName scopeAnnotationName) { - return getScope(scopeAnnotationName, customContexts); + return getScope(scopeAnnotationName, customContexts.keySet()); } /** @@ -874,8 +878,7 @@ private static Set recursiveBuild(DotName name, } private Map findStereotypes(Map interceptorBindings, - Map>> customContexts, - Set additionalStereotypes, AnnotationStore annotationStore) { + Set customContextScopes, Set additionalStereotypes, AnnotationStore annotationStore) { Map stereotypes = new HashMap<>(); @@ -917,7 +920,7 @@ private Map findStereotypes(Map int } else if (DotNames.PRIORITY.equals(annotation.name())) { alternativePriority = annotation.value().asInt(); } else { - final ScopeInfo scope = getScope(annotation.name(), customContexts); + final ScopeInfo scope = getScope(annotation.name(), customContextScopes); if (scope != null) { scopes.add(scope); } @@ -933,13 +936,12 @@ private Map findStereotypes(Map int return stereotypes; } - private ScopeInfo getScope(DotName scopeAnnotationName, - Map>> customContexts) { + private ScopeInfo getScope(DotName scopeAnnotationName, Set customContextScopes) { BuiltinScope builtin = BuiltinScope.from(scopeAnnotationName); if (builtin != null) { return builtin.getInfo(); } - for (ScopeInfo customScope : customContexts.keySet()) { + for (ScopeInfo customScope : customContextScopes) { if (customScope.getDotName().equals(scopeAnnotationName)) { return customScope; } diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanProcessor.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanProcessor.java index 5cfb81c54e5f0..ef57b1a97f735 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanProcessor.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanProcessor.java @@ -25,6 +25,7 @@ import jakarta.annotation.Priority; +import org.jboss.jandex.AnnotationTransformation; import org.jboss.jandex.ClassInfo; import org.jboss.jandex.DotName; import org.jboss.jandex.IndexView; @@ -543,7 +544,8 @@ private Set findSingleContextNormalScopes() { // built-in contexts contextsForScope.put(BuiltinScope.REQUEST.getName(), 1); // custom contexts - for (Map.Entry>> entry : beanDeployment.getCustomContexts() + for (Map.Entry>> entry : beanDeployment + .getCustomContexts() .entrySet()) { if (entry.getKey().isNormal()) { contextsForScope.merge(entry.getKey().getDotName(), entry.getValue().size(), Integer::sum); @@ -568,7 +570,7 @@ public static class Builder { ReflectionRegistration reflectionRegistration; final List resourceAnnotations; - final List annotationTransformers; + final List annotationTransformers; final List injectionPointTransformers; final List observerTransformers; final List beanRegistrars; @@ -713,11 +715,20 @@ public Builder setReflectionRegistration(ReflectionRegistration reflectionRegist return this; } + /** + * @deprecated use {@link #addAnnotationTransformation(AnnotationTransformation)} + */ + @Deprecated(forRemoval = true) public Builder addAnnotationTransformer(AnnotationsTransformer transformer) { this.annotationTransformers.add(transformer); return this; } + public Builder addAnnotationTransformation(AnnotationTransformation transformation) { + this.annotationTransformers.add(transformation); + return this; + } + public Builder addInjectionPointTransformer(InjectionPointsTransformer transformer) { this.injectionPointTransformers.add(transformer); return this; diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/ComponentsProviderGenerator.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/ComponentsProviderGenerator.java index f60ee63e7907d..7a38309b826c3 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/ComponentsProviderGenerator.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/ComponentsProviderGenerator.java @@ -28,6 +28,7 @@ import io.quarkus.arc.Arc; import io.quarkus.arc.Components; import io.quarkus.arc.ComponentsProvider; +import io.quarkus.arc.CurrentContextFactory; import io.quarkus.arc.InjectableBean; import io.quarkus.arc.processor.ResourceOutput.Resource; import io.quarkus.gizmo.AssignableResultHandle; @@ -82,7 +83,8 @@ Collection generate(String name, BeanDeployment beanDeployment, Map> dependencyMap = initBeanDependencyMap(beanDeployment); @@ -100,9 +102,10 @@ Collection generate(String name, BeanDeployment beanDeployment, Map>> entry : beanDeployment.getCustomContexts() + for (Entry>> e : beanDeployment + .getCustomContexts() .entrySet()) { - for (Function func : entry.getValue()) { + for (Function func : e.getValue()) { ResultHandle contextHandle = func.apply(getComponents); getComponents.invokeInterfaceMethod(MethodDescriptors.LIST_ADD, contextsHandle, contextHandle); } diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/ContextConfigurator.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/ContextConfigurator.java index ab8f8959372f2..b7cdb0ee02074 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/ContextConfigurator.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/ContextConfigurator.java @@ -1,6 +1,8 @@ package io.quarkus.arc.processor; import java.lang.annotation.Annotation; +import java.lang.reflect.Constructor; +import java.lang.reflect.Modifier; import java.util.HashMap; import java.util.Map; import java.util.Map.Entry; @@ -12,6 +14,7 @@ import jakarta.enterprise.context.NormalScope; import io.quarkus.arc.ContextCreator; +import io.quarkus.arc.CurrentContextFactory; import io.quarkus.arc.InjectableContext; import io.quarkus.gizmo.MethodCreator; import io.quarkus.gizmo.MethodDescriptor; @@ -100,12 +103,53 @@ public ContextConfigurator normal(boolean value) { } public ContextConfigurator contextClass(Class contextClazz) { - return creator(mc -> mc.newInstance(MethodDescriptor.ofConstructor(contextClazz))); + if (!Modifier.isPublic(contextClazz.getModifiers()) + || Modifier.isAbstract(contextClazz.getModifiers()) + || contextClazz.isAnonymousClass() + || contextClazz.isLocalClass() + || (contextClazz.getEnclosingClass() != null && !Modifier.isStatic(contextClazz.getModifiers()))) { + throw new IllegalArgumentException( + "A context class must be a public non-abstract top-level or static nested class"); + } + Constructor constructor = getConstructor(contextClazz); + if (constructor == null) { + throw new IllegalArgumentException( + "A context class must either declare a no-args constructor or a constructor that accepts a single parameter of type io.quarkus.arc.CurrentContextFactory"); + } + return creator(new Function<>() { + @Override + public ResultHandle apply(MethodCreator mc) { + ResultHandle[] args; + if (constructor.getParameterCount() == 0) { + args = new ResultHandle[0]; + } else { + args = new ResultHandle[] { mc.getMethodParam(0) }; + } + return mc.newInstance(MethodDescriptor.ofConstructor(contextClazz, constructor.getParameterTypes()), args); + } + }); + } + + private Constructor getConstructor(Class contextClazz) { + Constructor constructor = null; + try { + constructor = contextClazz.getDeclaredConstructor(CurrentContextFactory.class); + } catch (NoSuchMethodException ignored) { + } + if (constructor == null) { + try { + constructor = contextClazz.getDeclaredConstructor(); + } catch (NoSuchMethodException ignored) { + } + } + return constructor; } public ContextConfigurator creator(Class creatorClazz) { return creator(mc -> { ResultHandle paramsHandle = mc.newInstance(MethodDescriptor.ofConstructor(HashMap.class)); + mc.invokeInterfaceMethod(MethodDescriptors.MAP_PUT, paramsHandle, + mc.load(ContextCreator.KEY_CURRENT_CONTEXT_FACTORY), mc.getMethodParam(0)); for (Entry entry : params.entrySet()) { ResultHandle valHandle = null; if (entry.getValue() instanceof String) { diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/Transformation.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/Transformation.java index cd8b099733d38..5af6614fc12d0 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/Transformation.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/Transformation.java @@ -1,10 +1,18 @@ package io.quarkus.arc.processor; +import java.lang.annotation.Annotation; +import java.util.Arrays; import java.util.Collection; -import java.util.function.Consumer; +import java.util.Collections; +import java.util.HashSet; +import java.util.function.Predicate; +import java.util.stream.Collectors; import org.jboss.jandex.AnnotationInstance; -import org.jboss.jandex.AnnotationTarget; +import org.jboss.jandex.AnnotationTransformation; +import org.jboss.jandex.AnnotationValue; +import org.jboss.jandex.DotName; +import org.jboss.logging.Logger; /** * Represents a transformation of an annotation target. @@ -13,16 +21,65 @@ * * @see AnnotationsTransformer */ -public final class Transformation extends AbstractAnnotationsTransformation> { +public final class Transformation implements AnnotationsTransformation { + private static final Logger LOG = Logger.getLogger(Transformation.class); - public Transformation(Collection annotations, AnnotationTarget target, - Consumer> transformationConsumer) { - super(annotations, target, transformationConsumer); + private final AnnotationTransformation.TransformationContext ctx; + private final Collection modifiedAnnotations; + + Transformation(AnnotationTransformation.TransformationContext ctx) { + this.ctx = ctx; + this.modifiedAnnotations = new HashSet<>(ctx.annotations()); + + if (LOG.isTraceEnabled()) { + String stack = Arrays.stream(Thread.currentThread().getStackTrace()) + .skip(2) + .limit(7) + .map(se -> "\n\t" + se.toString()) + .collect(Collectors.joining()); + LOG.tracef("Transforming annotations of %s %s\n\t...", ctx.declaration(), stack); + } + } + + public Transformation add(AnnotationInstance annotation) { + modifiedAnnotations.add(annotation); + return this; + } + + public Transformation addAll(Collection annotations) { + modifiedAnnotations.addAll(annotations); + return this; + } + + public Transformation addAll(AnnotationInstance... annotations) { + Collections.addAll(modifiedAnnotations, annotations); + return this; } - @Override - protected Transformation self() { + public Transformation add(Class annotationType, AnnotationValue... values) { + add(DotName.createSimple(annotationType.getName()), values); return this; } + public Transformation add(DotName name, AnnotationValue... values) { + add(AnnotationInstance.create(name, ctx.declaration(), values)); + return this; + } + + public Transformation remove(Predicate predicate) { + modifiedAnnotations.removeIf(predicate); + return this; + } + + public Transformation removeAll() { + modifiedAnnotations.clear(); + return this; + } + + public void done() { + LOG.tracef("Annotations of %s transformed: %s", ctx.declaration(), modifiedAnnotations); + ctx.removeAll(); + ctx.addAll(modifiedAnnotations); + } + } diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/AllAnnotationOverlays.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/AllAnnotationOverlays.java deleted file mode 100644 index 6f73acd842bc1..0000000000000 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/AllAnnotationOverlays.java +++ /dev/null @@ -1,22 +0,0 @@ -package io.quarkus.arc.processor.bcextensions; - -class AllAnnotationOverlays { - final AnnotationsOverlay.Classes classes; - final AnnotationsOverlay.Methods methods; - final AnnotationsOverlay.Parameters parameters; - final AnnotationsOverlay.Fields fields; - - AllAnnotationOverlays() { - classes = new AnnotationsOverlay.Classes(); - methods = new AnnotationsOverlay.Methods(); - parameters = new AnnotationsOverlay.Parameters(); - fields = new AnnotationsOverlay.Fields(); - } - - void invalidate() { - classes.invalidate(); - methods.invalidate(); - parameters.invalidate(); - fields.invalidate(); - } -} diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/AllAnnotationTransformations.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/AllAnnotationTransformations.java deleted file mode 100644 index f3511cbb5a7f9..0000000000000 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/AllAnnotationTransformations.java +++ /dev/null @@ -1,24 +0,0 @@ -package io.quarkus.arc.processor.bcextensions; - -class AllAnnotationTransformations { - final AllAnnotationOverlays annotationOverlays; - final AnnotationsTransformation.Classes classes; - final AnnotationsTransformation.Methods methods; - final AnnotationsTransformation.Parameters parameters; - final AnnotationsTransformation.Fields fields; - - AllAnnotationTransformations(org.jboss.jandex.IndexView jandexIndex, AllAnnotationOverlays annotationOverlays) { - this.annotationOverlays = annotationOverlays; - classes = new AnnotationsTransformation.Classes(jandexIndex, annotationOverlays); - methods = new AnnotationsTransformation.Methods(jandexIndex, annotationOverlays); - parameters = new AnnotationsTransformation.Parameters(jandexIndex, annotationOverlays); - fields = new AnnotationsTransformation.Fields(jandexIndex, annotationOverlays); - } - - void freeze() { - classes.freeze(); - methods.freeze(); - parameters.freeze(); - fields.freeze(); - } -} diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/AnnotationBuilderFactoryImpl.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/AnnotationBuilderFactoryImpl.java index 2f840d5ecf2fb..dda1dae25e048 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/AnnotationBuilderFactoryImpl.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/AnnotationBuilderFactoryImpl.java @@ -10,30 +10,31 @@ final class AnnotationBuilderFactoryImpl implements AnnotationBuilderFactory { private final org.jboss.jandex.IndexView beanArchiveIndex; - private final AllAnnotationOverlays annotationOverlays; + private final org.jboss.jandex.MutableAnnotationOverlay annotationOverlay; - AnnotationBuilderFactoryImpl(org.jboss.jandex.IndexView beanArchiveIndex, AllAnnotationOverlays annotationOverlays) { + AnnotationBuilderFactoryImpl(org.jboss.jandex.IndexView beanArchiveIndex, + org.jboss.jandex.MutableAnnotationOverlay annotationOverlay) { this.beanArchiveIndex = beanArchiveIndex; - this.annotationOverlays = annotationOverlays; + this.annotationOverlay = annotationOverlay; } @Override public AnnotationBuilder create(Class annotationType) { - if (beanArchiveIndex == null || annotationOverlays == null) { + if (beanArchiveIndex == null || annotationOverlay == null) { throw new IllegalStateException("Can't create AnnotationBuilder right now"); } DotName jandexAnnotationName = DotName.createSimple(annotationType.getName()); - return new AnnotationBuilderImpl(beanArchiveIndex, annotationOverlays, jandexAnnotationName); + return new AnnotationBuilderImpl(beanArchiveIndex, annotationOverlay, jandexAnnotationName); } @Override public AnnotationBuilder create(ClassInfo annotationType) { - if (beanArchiveIndex == null || annotationOverlays == null) { + if (beanArchiveIndex == null || annotationOverlay == null) { throw new IllegalStateException("Can't create AnnotationBuilder right now"); } DotName jandexAnnotationName = ((ClassInfoImpl) annotationType).jandexDeclaration.name(); - return new AnnotationBuilderImpl(beanArchiveIndex, annotationOverlays, jandexAnnotationName); + return new AnnotationBuilderImpl(beanArchiveIndex, annotationOverlay, jandexAnnotationName); } } diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/AnnotationBuilderImpl.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/AnnotationBuilderImpl.java index e45cf95bfeebc..816ca82f69c4e 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/AnnotationBuilderImpl.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/AnnotationBuilderImpl.java @@ -3,7 +3,6 @@ import java.lang.annotation.Annotation; import java.util.ArrayList; import java.util.List; -import java.util.stream.Collectors; import jakarta.enterprise.inject.build.compatible.spi.AnnotationBuilder; import jakarta.enterprise.lang.model.AnnotationInfo; @@ -19,15 +18,15 @@ class AnnotationBuilderImpl implements AnnotationBuilder { private final org.jboss.jandex.IndexView jandexIndex; - private final AllAnnotationOverlays annotationOverlays; + private final org.jboss.jandex.MutableAnnotationOverlay annotationOverlay; private final DotName jandexClassName; private final List jandexAnnotationMembers = new ArrayList<>(); - AnnotationBuilderImpl(org.jboss.jandex.IndexView jandexIndex, AllAnnotationOverlays annotationOverlays, + AnnotationBuilderImpl(org.jboss.jandex.IndexView jandexIndex, org.jboss.jandex.MutableAnnotationOverlay annotationOverlay, DotName jandexAnnotationName) { this.jandexIndex = jandexIndex; - this.annotationOverlays = annotationOverlays; + this.annotationOverlay = annotationOverlay; this.jandexClassName = jandexAnnotationName; } @@ -455,7 +454,7 @@ public AnnotationInfo build() { for (org.jboss.jandex.MethodInfo jandexAnnotationMember : jandexAnnotationClass.methods() .stream() .filter(MethodPredicates.IS_METHOD_JANDEX) - .collect(Collectors.toUnmodifiableList())) { + .toList()) { if (jandexAnnotationMember.defaultValue() != null) { continue; } @@ -471,6 +470,6 @@ public AnnotationInfo build() { org.jboss.jandex.AnnotationInstance jandexAnnotation = org.jboss.jandex.AnnotationInstance.create( jandexClassName, null, jandexAnnotationMembers); - return new AnnotationInfoImpl(jandexIndex, annotationOverlays, jandexAnnotation); + return new AnnotationInfoImpl(jandexIndex, annotationOverlay, jandexAnnotation); } } diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/AnnotationInfoImpl.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/AnnotationInfoImpl.java index c993d4745f40d..1a6274ad5e9e4 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/AnnotationInfoImpl.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/AnnotationInfoImpl.java @@ -13,13 +13,13 @@ class AnnotationInfoImpl implements AnnotationInfo { final org.jboss.jandex.IndexView jandexIndex; - final AllAnnotationOverlays annotationOverlays; + final org.jboss.jandex.MutableAnnotationOverlay annotationOverlay; final org.jboss.jandex.AnnotationInstance jandexAnnotation; - AnnotationInfoImpl(org.jboss.jandex.IndexView jandexIndex, AllAnnotationOverlays annotationOverlays, + AnnotationInfoImpl(org.jboss.jandex.IndexView jandexIndex, org.jboss.jandex.MutableAnnotationOverlay annotationOverlay, org.jboss.jandex.AnnotationInstance jandexAnnotation) { this.jandexIndex = jandexIndex; - this.annotationOverlays = annotationOverlays; + this.annotationOverlay = annotationOverlay; this.jandexAnnotation = jandexAnnotation; } @@ -30,7 +30,7 @@ public ClassInfo declaration() { if (annotationClass == null) { throw new IllegalStateException("Class " + annotationClassName + " not found in Jandex"); } - return new ClassInfoImpl(jandexIndex, annotationOverlays, annotationClass); + return new ClassInfoImpl(jandexIndex, annotationOverlay, annotationClass); } @Override @@ -40,7 +40,7 @@ public boolean hasMember(String name) { @Override public AnnotationMember member(String name) { - return new AnnotationMemberImpl(jandexIndex, annotationOverlays, + return new AnnotationMemberImpl(jandexIndex, annotationOverlay, jandexAnnotation.valueWithDefault(jandexIndex, name)); } @@ -49,7 +49,7 @@ public Map members() { Map result = new HashMap<>(); for (org.jboss.jandex.AnnotationValue jandexAnnotationMember : jandexAnnotation.valuesWithDefaults(jandexIndex)) { result.put(jandexAnnotationMember.name(), - new AnnotationMemberImpl(jandexIndex, annotationOverlays, jandexAnnotationMember)); + new AnnotationMemberImpl(jandexIndex, annotationOverlay, jandexAnnotationMember)); } return Collections.unmodifiableMap(result); } diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/AnnotationMemberImpl.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/AnnotationMemberImpl.java index 210b849b9a6b7..a11642626f5e6 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/AnnotationMemberImpl.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/AnnotationMemberImpl.java @@ -2,7 +2,6 @@ import java.util.List; import java.util.Objects; -import java.util.stream.Collectors; import jakarta.enterprise.lang.model.AnnotationInfo; import jakarta.enterprise.lang.model.AnnotationMember; @@ -11,50 +10,36 @@ class AnnotationMemberImpl implements AnnotationMember { final org.jboss.jandex.IndexView jandexIndex; - final AllAnnotationOverlays annotationOverlays; + final org.jboss.jandex.MutableAnnotationOverlay annotationOverlay; final Kind kind; final org.jboss.jandex.AnnotationValue jandexAnnotationMember; - AnnotationMemberImpl(org.jboss.jandex.IndexView jandexIndex, AllAnnotationOverlays annotationOverlays, + AnnotationMemberImpl(org.jboss.jandex.IndexView jandexIndex, org.jboss.jandex.MutableAnnotationOverlay annotationOverlay, org.jboss.jandex.AnnotationValue jandexAnnotationMember) { this.jandexIndex = jandexIndex; - this.annotationOverlays = annotationOverlays; + this.annotationOverlay = annotationOverlay; this.kind = determineKind(jandexAnnotationMember); this.jandexAnnotationMember = jandexAnnotationMember; } private static Kind determineKind(org.jboss.jandex.AnnotationValue value) { - switch (value.kind()) { - case BOOLEAN: - return Kind.BOOLEAN; - case BYTE: - return Kind.BYTE; - case SHORT: - return Kind.SHORT; - case INTEGER: - return Kind.INT; - case LONG: - return Kind.LONG; - case FLOAT: - return Kind.FLOAT; - case DOUBLE: - return Kind.DOUBLE; - case CHARACTER: - return Kind.CHAR; - case STRING: - return Kind.STRING; - case ENUM: - return Kind.ENUM; - case CLASS: - return Kind.CLASS; - case NESTED: - return Kind.NESTED_ANNOTATION; - case ARRAY: - return Kind.ARRAY; - default: - throw new IllegalArgumentException("Unknown annotation member " + value); - } + return switch (value.kind()) { + case BOOLEAN -> Kind.BOOLEAN; + case BYTE -> Kind.BYTE; + case SHORT -> Kind.SHORT; + case INTEGER -> Kind.INT; + case LONG -> Kind.LONG; + case FLOAT -> Kind.FLOAT; + case DOUBLE -> Kind.DOUBLE; + case CHARACTER -> Kind.CHAR; + case STRING -> Kind.STRING; + case ENUM -> Kind.ENUM; + case CLASS -> Kind.CLASS; + case NESTED -> Kind.NESTED_ANNOTATION; + case ARRAY -> Kind.ARRAY; + default -> throw new IllegalArgumentException("Unknown annotation member " + value); + }; } private void checkKind(Kind kind) { @@ -137,20 +122,20 @@ public String asEnumConstant() { @Override public ClassInfo asEnumClass() { checkKind(Kind.ENUM); - return new ClassInfoImpl(jandexIndex, annotationOverlays, + return new ClassInfoImpl(jandexIndex, annotationOverlay, jandexIndex.getClassByName(jandexAnnotationMember.asEnumType())); } @Override public Type asType() { checkKind(Kind.CLASS); - return TypeImpl.fromJandexType(jandexIndex, annotationOverlays, jandexAnnotationMember.asClass()); + return TypeImpl.fromJandexType(jandexIndex, annotationOverlay, jandexAnnotationMember.asClass()); } @Override public AnnotationInfo asNestedAnnotation() { checkKind(Kind.NESTED_ANNOTATION); - return new AnnotationInfoImpl(jandexIndex, annotationOverlays, jandexAnnotationMember.asNested()); + return new AnnotationInfoImpl(jandexIndex, annotationOverlay, jandexAnnotationMember.asNested()); } @Override @@ -158,8 +143,8 @@ public List asArray() { checkKind(Kind.ARRAY); return jandexAnnotationMember.asArrayList() .stream() - .map(it -> new AnnotationMemberImpl(jandexIndex, annotationOverlays, it)) - .collect(Collectors.toUnmodifiableList()); + .map(it -> (AnnotationMember) new AnnotationMemberImpl(jandexIndex, annotationOverlay, it)) + .toList(); } @Override diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/AnnotationSet.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/AnnotationSet.java deleted file mode 100644 index 1bf4c672f7ba6..0000000000000 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/AnnotationSet.java +++ /dev/null @@ -1,133 +0,0 @@ -package io.quarkus.arc.processor.bcextensions; - -import java.lang.annotation.Annotation; -import java.lang.annotation.Repeatable; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.function.Predicate; - -import org.jboss.jandex.DotName; - -class AnnotationSet { - private final Map data; - - // used only when this AnnotationSet represents annotations on a class declaration, - // because in such case, annotations may be inherited from superclasses - // - // for each annotation type, this map contains a distance from the original class - // to the class on which an annotation of that type is declared (0 means the annotation - // is declared directly on the original class, 1 means the annotation is declared - // directly on a direct superclass of the original class, etc.) - // - // if this AnnotationSet represents annotations on any other annotation target, - // this map contains 0 for all annotation types - private final Map inheritanceDistances; - - private static Map zeroDistances(Collection jandexAnnotations) { - Map distances = new ConcurrentHashMap<>(); - for (org.jboss.jandex.AnnotationInstance jandexAnnotation : jandexAnnotations) { - distances.put(jandexAnnotation.name(), 0); - } - return distances; - } - - AnnotationSet(Collection jandexAnnotations) { - this(jandexAnnotations, zeroDistances(jandexAnnotations)); - } - - AnnotationSet(Collection jandexAnnotations, - Map inheritanceDistances) { - Map data = new ConcurrentHashMap<>(); - for (org.jboss.jandex.AnnotationInstance jandexAnnotation : jandexAnnotations) { - data.put(jandexAnnotation.name(), jandexAnnotation); - } - this.data = data; - this.inheritanceDistances = inheritanceDistances; - } - - boolean hasAnnotation(Class annotationType) { - DotName name = DotName.createSimple(annotationType.getName()); - return hasAnnotation(name); - } - - boolean hasAnnotation(DotName annotationName) { - return data.containsKey(annotationName); - } - - org.jboss.jandex.AnnotationInstance annotation(Class annotationType) { - DotName name = DotName.createSimple(annotationType.getName()); - return data.get(name); - } - - Collection annotationsWithRepeatable(Class annotationType) { - Repeatable repeatable = annotationType.getAnnotation(Repeatable.class); - - DotName name = DotName.createSimple(annotationType.getName()); - DotName containerName = repeatable != null - ? DotName.createSimple(repeatable.value().getName()) - : DotName.OBJECT_NAME; // not an annotation name, so never present in the map - - if (data.containsKey(name) && data.containsKey(containerName)) { - int annDistance = inheritanceDistances.get(name); - int containerAnnDistance = inheritanceDistances.get(containerName); - if (annDistance < containerAnnDistance) { - return List.of(data.get(name)); - } else if (annDistance == containerAnnDistance) { - // equal inheritance distances may happen if a single annotation of a repeatable annotation type - // is declared, and an annotation of the containing annotation type is also (explicitly!) declared - // (on the same annotation target) - List result = new ArrayList<>(); - result.add(data.get(name)); - org.jboss.jandex.AnnotationInstance container = data.get(containerName); - org.jboss.jandex.AnnotationInstance[] values = container.value().asNestedArray(); - result.addAll(Arrays.asList(values)); - return result; - } else { - org.jboss.jandex.AnnotationInstance container = data.get(containerName); - org.jboss.jandex.AnnotationInstance[] values = container.value().asNestedArray(); - return List.of(values); - } - } else if (data.containsKey(name)) { - return List.of(data.get(name)); - } else if (data.containsKey(containerName)) { - org.jboss.jandex.AnnotationInstance container = data.get(containerName); - org.jboss.jandex.AnnotationInstance[] values = container.value().asNestedArray(); - return List.of(values); - } else { - return List.of(); - } - } - - Collection annotations() { - return Collections.unmodifiableCollection(data.values()); - } - - // --- - // modifications, can only be called from AnnotationsTransformation - - void add(org.jboss.jandex.AnnotationInstance jandexAnnotation) { - data.put(jandexAnnotation.name(), jandexAnnotation); - inheritanceDistances.put(jandexAnnotation.name(), 0); - } - - void removeIf(Predicate predicate) { - Set toRemove = new HashSet<>(); - for (org.jboss.jandex.AnnotationInstance jandexAnnotation : data.values()) { - if (predicate.test(jandexAnnotation)) { - toRemove.add(jandexAnnotation.name()); - } - } - - for (DotName name : toRemove) { - data.remove(name); - inheritanceDistances.remove(name); - } - } -} diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/AnnotationTargetImpl.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/AnnotationTargetImpl.java new file mode 100644 index 0000000000000..39af3dc7ef4dc --- /dev/null +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/AnnotationTargetImpl.java @@ -0,0 +1,24 @@ +package io.quarkus.arc.processor.bcextensions; + +abstract class AnnotationTargetImpl { + final org.jboss.jandex.IndexView jandexIndex; + final org.jboss.jandex.MutableAnnotationOverlay annotationOverlay; + private final org.jboss.jandex.EquivalenceKey key; // for equals/hashCode + + AnnotationTargetImpl(org.jboss.jandex.IndexView jandexIndex, org.jboss.jandex.MutableAnnotationOverlay annotationOverlay, + org.jboss.jandex.EquivalenceKey key) { + this.jandexIndex = jandexIndex; + this.annotationOverlay = annotationOverlay; + this.key = key; + } + + @Override + public boolean equals(Object obj) { + return obj instanceof AnnotationTargetImpl && key.equals(((AnnotationTargetImpl) obj).key); + } + + @Override + public int hashCode() { + return key.hashCode(); + } +} diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/AnnotationsOverlay.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/AnnotationsOverlay.java deleted file mode 100644 index 2ccc6a78a12ab..0000000000000 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/AnnotationsOverlay.java +++ /dev/null @@ -1,168 +0,0 @@ -package io.quarkus.arc.processor.bcextensions; - -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.stream.Collectors; - -import org.jboss.jandex.DotName; - -// JandexDeclaration must be a Jandex declaration for which Arc supports annotation transformations -// directly (classes, methods, fields) or indirectly (parameters); see also AnnotationsTransformation -abstract class AnnotationsOverlay { - private final Map overlay = new ConcurrentHashMap<>(); - private volatile boolean invalid = false; - - AnnotationSet getAnnotations(JandexDeclaration jandexDeclaration, org.jboss.jandex.IndexView jandexIndex) { - if (invalid) { - throw new IllegalStateException("Annotations overlay no longer valid"); - } - - org.jboss.jandex.EquivalenceKey key = org.jboss.jandex.EquivalenceKey.of(jandexDeclaration); - if (overlay.containsKey(key)) { - return overlay.get(key); - } - - AnnotationSet annotationSet = createAnnotationSet(jandexDeclaration, jandexIndex); - overlay.put(key, annotationSet); - return annotationSet; - } - - boolean hasAnnotation(JandexDeclaration jandexDeclaration, DotName annotationName, org.jboss.jandex.IndexView jandexIndex) { - if (invalid) { - throw new IllegalStateException("Annotations overlay no longer valid"); - } - - org.jboss.jandex.EquivalenceKey key = org.jboss.jandex.EquivalenceKey.of(jandexDeclaration); - boolean hasOverlay = overlay.containsKey(key); - - if (hasOverlay) { - return getAnnotations(jandexDeclaration, jandexIndex).hasAnnotation(annotationName); - } else { - return originalJandexAnnotationsContain(jandexDeclaration, annotationName, jandexIndex); - } - } - - void invalidate() { - overlay.clear(); - invalid = true; - } - - abstract AnnotationSet createAnnotationSet(JandexDeclaration jandexDeclaration, org.jboss.jandex.IndexView jandexIndex); - - // this is "just" an optimization to avoid creating and populating an `AnnotationSet` - // when the only thing we need to know is if an annotation is present - abstract boolean originalJandexAnnotationsContain(JandexDeclaration jandexDeclaration, DotName annotationName, - org.jboss.jandex.IndexView jandexIndex); - - static class Classes extends AnnotationsOverlay { - @Override - AnnotationSet createAnnotationSet(org.jboss.jandex.ClassInfo classInfo, - org.jboss.jandex.IndexView jandexIndex) { - // if an `@Inherited` annotation of some type is declared directly on class C, then annotations - // of the same type declared directly on any direct or indirect superclass are _not_ present on C - Set alreadySeen = new HashSet<>(); - - List jandexAnnotations = new ArrayList<>(); - Map inheritanceDistances = new ConcurrentHashMap<>(); - - int currentDistance = 0; - while (classInfo != null && !classInfo.name().equals(DotNames.OBJECT)) { - for (org.jboss.jandex.AnnotationInstance jandexAnnotation : classInfo.declaredAnnotations()) { - if (!jandexAnnotation.runtimeVisible()) { - continue; - } - - if (alreadySeen.contains(jandexAnnotation.name())) { - continue; - } - alreadySeen.add(jandexAnnotation.name()); - - jandexAnnotations.add(jandexAnnotation); - inheritanceDistances.put(jandexAnnotation.name(), currentDistance); - } - - DotName superClassName = classInfo.superName(); - classInfo = jandexIndex.getClassByName(superClassName); - currentDistance++; - } - - return new AnnotationSet(jandexAnnotations, inheritanceDistances); - } - - @Override - boolean originalJandexAnnotationsContain(org.jboss.jandex.ClassInfo classInfo, DotName annotationName, - org.jboss.jandex.IndexView jandexIndex) { - while (classInfo != null && !classInfo.name().equals(DotNames.OBJECT)) { - org.jboss.jandex.AnnotationInstance jandexAnnotation = classInfo.declaredAnnotation(annotationName); - if (jandexAnnotation != null && jandexAnnotation.runtimeVisible()) { - return true; - } - - DotName superClassName = classInfo.superName(); - classInfo = jandexIndex.getClassByName(superClassName); - } - return false; - } - } - - static class Methods extends AnnotationsOverlay { - @Override - AnnotationSet createAnnotationSet(org.jboss.jandex.MethodInfo methodInfo, - org.jboss.jandex.IndexView jandexIndex) { - List jandexAnnotations = methodInfo.declaredAnnotations() - .stream() - .filter(org.jboss.jandex.AnnotationInstance::runtimeVisible) - .collect(Collectors.toUnmodifiableList()); - return new AnnotationSet(jandexAnnotations); - } - - @Override - boolean originalJandexAnnotationsContain(org.jboss.jandex.MethodInfo methodInfo, DotName annotationName, - org.jboss.jandex.IndexView jandexIndex) { - org.jboss.jandex.AnnotationInstance jandexAnnotation = methodInfo.declaredAnnotation(annotationName); - return jandexAnnotation != null && jandexAnnotation.runtimeVisible(); - } - } - - static class Parameters extends AnnotationsOverlay { - @Override - AnnotationSet createAnnotationSet(org.jboss.jandex.MethodParameterInfo methodParameterInfo, - org.jboss.jandex.IndexView jandexIndex) { - List jandexAnnotations = methodParameterInfo.declaredAnnotations() - .stream() - .filter(org.jboss.jandex.AnnotationInstance::runtimeVisible) - .collect(Collectors.toUnmodifiableList()); - return new AnnotationSet(jandexAnnotations); - } - - @Override - boolean originalJandexAnnotationsContain(org.jboss.jandex.MethodParameterInfo methodParameterInfo, - DotName annotationName, org.jboss.jandex.IndexView jandexIndex) { - org.jboss.jandex.AnnotationInstance jandexAnnotation = methodParameterInfo.declaredAnnotation(annotationName); - return jandexAnnotation != null && jandexAnnotation.runtimeVisible(); - } - } - - static class Fields extends AnnotationsOverlay { - @Override - AnnotationSet createAnnotationSet(org.jboss.jandex.FieldInfo fieldInfo, - org.jboss.jandex.IndexView jandexIndex) { - List jandexAnnotations = fieldInfo.declaredAnnotations() - .stream() - .filter(org.jboss.jandex.AnnotationInstance::runtimeVisible) - .collect(Collectors.toUnmodifiableList()); - return new AnnotationSet(jandexAnnotations); - } - - @Override - boolean originalJandexAnnotationsContain(org.jboss.jandex.FieldInfo fieldInfo, DotName annotationName, - org.jboss.jandex.IndexView jandexIndex) { - org.jboss.jandex.AnnotationInstance jandexAnnotation = fieldInfo.declaredAnnotation(annotationName); - return jandexAnnotation != null && jandexAnnotation.runtimeVisible(); - } - } -} diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/AnnotationsTransformation.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/AnnotationsTransformation.java deleted file mode 100644 index 955204c767a99..0000000000000 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/AnnotationsTransformation.java +++ /dev/null @@ -1,215 +0,0 @@ -package io.quarkus.arc.processor.bcextensions; - -import java.lang.annotation.Annotation; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.function.Consumer; -import java.util.function.Predicate; - -import jakarta.enterprise.lang.model.AnnotationInfo; - -import org.jboss.jandex.DotName; - -import io.quarkus.arc.processor.Annotations; - -// this must be symmetric with AnnotationsOverlay -abstract class AnnotationsTransformation - implements io.quarkus.arc.processor.AnnotationsTransformer { - - final org.jboss.jandex.IndexView jandexIndex; - final AllAnnotationOverlays annotationOverlays; - - private final org.jboss.jandex.AnnotationTarget.Kind kind; - private final Map>> transformations = new ConcurrentHashMap<>(); - - private volatile boolean frozen = false; - - AnnotationsTransformation(org.jboss.jandex.IndexView jandexIndex, AllAnnotationOverlays annotationOverlays, - org.jboss.jandex.AnnotationTarget.Kind kind) { - this.jandexIndex = jandexIndex; - this.annotationOverlays = annotationOverlays; - this.kind = kind; - } - - private void addAnnotation(JandexDeclaration jandexDeclaration, org.jboss.jandex.AnnotationInstance jandexAnnotation) { - if (frozen) { - throw new IllegalStateException("Annotations transformation frozen"); - } - - org.jboss.jandex.EquivalenceKey key = org.jboss.jandex.EquivalenceKey.of(jandexDeclaration); - - org.jboss.jandex.AnnotationInstance jandexAnnotationWithTarget = org.jboss.jandex.AnnotationInstance.create( - jandexAnnotation.name(), jandexDeclaration, jandexAnnotation.values()); - - annotationsOverlay().getAnnotations(jandexDeclaration, jandexIndex).add(jandexAnnotationWithTarget); - - Consumer transformation = ctx -> { - ctx.transform().add(jandexAnnotationWithTarget).done(); - }; - transformations.computeIfAbsent(key, ignored -> new ArrayList<>()).add(transformation); - } - - void addAnnotation(JandexDeclaration jandexDeclaration, Class clazz) { - org.jboss.jandex.AnnotationInstance jandexAnnotation = org.jboss.jandex.AnnotationInstance.create( - DotName.createSimple(clazz.getName()), null, AnnotationValueArray.EMPTY); - - addAnnotation(jandexDeclaration, jandexAnnotation); - } - - void addAnnotation(JandexDeclaration jandexDeclaration, AnnotationInfo annotation) { - addAnnotation(jandexDeclaration, ((AnnotationInfoImpl) annotation).jandexAnnotation); - } - - void addAnnotation(JandexDeclaration jandexDeclaration, Annotation annotation) { - addAnnotation(jandexDeclaration, Annotations.jandexAnnotation(annotation)); - } - - private void removeMatchingAnnotations(JandexDeclaration declaration, - Predicate predicate) { - - if (frozen) { - throw new IllegalStateException("Annotations transformation frozen"); - } - - org.jboss.jandex.EquivalenceKey key = org.jboss.jandex.EquivalenceKey.of(declaration); - - annotationsOverlay().getAnnotations(declaration, jandexIndex).removeIf(predicate); - - Consumer transformation = ctx -> { - ctx.transform().remove(predicate).done(); - }; - transformations.computeIfAbsent(key, ignored -> new ArrayList<>()).add(transformation); - } - - void removeAnnotation(JandexDeclaration declaration, Predicate predicate) { - org.jboss.jandex.EquivalenceKey key = org.jboss.jandex.EquivalenceKey.of(declaration); - - removeMatchingAnnotations(declaration, new Predicate() { - @Override - public boolean test(org.jboss.jandex.AnnotationInstance jandexAnnotation) { - // we only verify the target here because ArC doesn't support annotation transformation - // on method parameters directly; instead, it must be implemented indirectly by transforming - // annotations on the _method_ - return key.equals(org.jboss.jandex.EquivalenceKey.of(jandexAnnotation.target())) - && predicate.test(new AnnotationInfoImpl(jandexIndex, annotationOverlays, jandexAnnotation)); - } - }); - } - - void removeAllAnnotations(JandexDeclaration declaration) { - removeAnnotation(declaration, ignored -> true); - } - - void freeze() { - frozen = true; - } - - // `appliesTo` and `transform` must be overridden for `Parameters`, because ArC doesn't - // support annotation transformation on method parameters directly; instead, it must be - // implemented indirectly by transforming annotations on the _method_ (and setting proper - // annotation target) - - @Override - public boolean appliesTo(org.jboss.jandex.AnnotationTarget.Kind kind) { - return this.kind == kind; - } - - @Override - public void transform(TransformationContext ctx) { - JandexDeclaration jandexDeclaration = targetJandexDeclaration(ctx); - org.jboss.jandex.EquivalenceKey key = org.jboss.jandex.EquivalenceKey.of(jandexDeclaration); - transformations.getOrDefault(key, Collections.emptyList()) - .forEach(it -> it.accept(ctx)); - } - - abstract JandexDeclaration targetJandexDeclaration(TransformationContext ctx); - - abstract AnnotationsOverlay annotationsOverlay(); - - static class Classes extends AnnotationsTransformation { - Classes(org.jboss.jandex.IndexView jandexIndex, AllAnnotationOverlays annotationOverlays) { - super(jandexIndex, annotationOverlays, org.jboss.jandex.AnnotationTarget.Kind.CLASS); - } - - @Override - protected org.jboss.jandex.ClassInfo targetJandexDeclaration( - io.quarkus.arc.processor.AnnotationsTransformer.TransformationContext ctx) { - return ctx.getTarget().asClass(); - } - - @Override - AnnotationsOverlay annotationsOverlay() { - return annotationOverlays.classes; - } - } - - static class Methods extends AnnotationsTransformation { - Methods(org.jboss.jandex.IndexView jandexIndex, AllAnnotationOverlays annotationOverlays) { - super(jandexIndex, annotationOverlays, org.jboss.jandex.AnnotationTarget.Kind.METHOD); - } - - @Override - protected org.jboss.jandex.MethodInfo targetJandexDeclaration( - io.quarkus.arc.processor.AnnotationsTransformer.TransformationContext ctx) { - return ctx.getTarget().asMethod(); - } - - @Override - AnnotationsOverlay annotationsOverlay() { - return annotationOverlays.methods; - } - } - - static class Parameters extends AnnotationsTransformation { - Parameters(org.jboss.jandex.IndexView jandexIndex, AllAnnotationOverlays annotationOverlays) { - super(jandexIndex, annotationOverlays, org.jboss.jandex.AnnotationTarget.Kind.METHOD_PARAMETER); - } - - @Override - protected org.jboss.jandex.MethodParameterInfo targetJandexDeclaration( - io.quarkus.arc.processor.AnnotationsTransformer.TransformationContext ctx) { - // `targetJandexDeclaration` is only called from `super.transform`, which we override here - throw new UnsupportedOperationException(); - } - - @Override - AnnotationsOverlay annotationsOverlay() { - return annotationOverlays.parameters; - } - - @Override - public boolean appliesTo(org.jboss.jandex.AnnotationTarget.Kind kind) { - return org.jboss.jandex.AnnotationTarget.Kind.METHOD == kind; - } - - @Override - public void transform(TransformationContext ctx) { - org.jboss.jandex.MethodInfo jandexMethod = ctx.getTarget().asMethod(); - for (org.jboss.jandex.MethodParameterInfo jandexDeclaration : jandexMethod.parameters()) { - org.jboss.jandex.EquivalenceKey key = org.jboss.jandex.EquivalenceKey.of(jandexDeclaration); - super.transformations.getOrDefault(key, Collections.emptyList()) - .forEach(it -> it.accept(ctx)); - } - } - } - - static class Fields extends AnnotationsTransformation { - Fields(org.jboss.jandex.IndexView jandexIndex, AllAnnotationOverlays annotationOverlays) { - super(jandexIndex, annotationOverlays, org.jboss.jandex.AnnotationTarget.Kind.FIELD); - } - - @Override - protected org.jboss.jandex.FieldInfo targetJandexDeclaration( - io.quarkus.arc.processor.AnnotationsTransformer.TransformationContext ctx) { - return ctx.getTarget().asField(); - } - - @Override - AnnotationsOverlay annotationsOverlay() { - return annotationOverlays.fields; - } - } -} diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ArrayTypeImpl.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ArrayTypeImpl.java index 88e20c2c9cfe6..f0e697834c724 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ArrayTypeImpl.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ArrayTypeImpl.java @@ -4,9 +4,9 @@ import jakarta.enterprise.lang.model.types.Type; class ArrayTypeImpl extends TypeImpl implements ArrayType { - ArrayTypeImpl(org.jboss.jandex.IndexView jandexIndex, AllAnnotationOverlays annotationOverlays, + ArrayTypeImpl(org.jboss.jandex.IndexView jandexIndex, org.jboss.jandex.MutableAnnotationOverlay annotationOverlay, org.jboss.jandex.ArrayType jandexType) { - super(jandexIndex, annotationOverlays, jandexType); + super(jandexIndex, annotationOverlay, jandexType); } @Override @@ -15,6 +15,6 @@ public Type componentType() { org.jboss.jandex.Type componentType = dimensions == 1 ? jandexType.constituent() : org.jboss.jandex.ArrayType.create(jandexType.constituent(), dimensions - 1); - return fromJandexType(jandexIndex, annotationOverlays, componentType); + return fromJandexType(jandexIndex, annotationOverlay, componentType); } } diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/BeanInfoImpl.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/BeanInfoImpl.java index 7be224753d5cc..430008708b6d1 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/BeanInfoImpl.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/BeanInfoImpl.java @@ -16,35 +16,36 @@ class BeanInfoImpl implements BeanInfo { final org.jboss.jandex.IndexView jandexIndex; - final AllAnnotationOverlays annotationOverlays; + final org.jboss.jandex.MutableAnnotationOverlay annotationOverlay; final io.quarkus.arc.processor.BeanInfo arcBeanInfo; - static BeanInfoImpl create(org.jboss.jandex.IndexView jandexIndex, AllAnnotationOverlays annotationOverlays, + static BeanInfoImpl create(org.jboss.jandex.IndexView jandexIndex, + org.jboss.jandex.MutableAnnotationOverlay annotationOverlay, io.quarkus.arc.processor.BeanInfo arcBeanInfo) { if (arcBeanInfo.isInterceptor()) { - return new InterceptorInfoImpl(jandexIndex, annotationOverlays, + return new InterceptorInfoImpl(jandexIndex, annotationOverlay, (io.quarkus.arc.processor.InterceptorInfo) arcBeanInfo); } - return new BeanInfoImpl(jandexIndex, annotationOverlays, arcBeanInfo); + return new BeanInfoImpl(jandexIndex, annotationOverlay, arcBeanInfo); } - BeanInfoImpl(org.jboss.jandex.IndexView jandexIndex, AllAnnotationOverlays annotationOverlays, + BeanInfoImpl(org.jboss.jandex.IndexView jandexIndex, org.jboss.jandex.MutableAnnotationOverlay annotationOverlay, io.quarkus.arc.processor.BeanInfo arcBeanInfo) { this.jandexIndex = jandexIndex; - this.annotationOverlays = annotationOverlays; + this.annotationOverlay = annotationOverlay; this.arcBeanInfo = arcBeanInfo; } @Override public ScopeInfo scope() { - return new ScopeInfoImpl(jandexIndex, annotationOverlays, arcBeanInfo.getScope()); + return new ScopeInfoImpl(jandexIndex, annotationOverlay, arcBeanInfo.getScope()); } @Override public Collection types() { return arcBeanInfo.getTypes() .stream() - .map(it -> TypeImpl.fromJandexType(jandexIndex, annotationOverlays, it)) + .map(it -> TypeImpl.fromJandexType(jandexIndex, annotationOverlay, it)) .collect(Collectors.toUnmodifiableSet()); } @@ -52,14 +53,14 @@ public Collection types() { public Collection qualifiers() { return arcBeanInfo.getQualifiers() .stream() - .map(it -> new AnnotationInfoImpl(jandexIndex, annotationOverlays, it)) - .collect(Collectors.toUnmodifiableList()); + .map(it -> (AnnotationInfo) new AnnotationInfoImpl(jandexIndex, annotationOverlay, it)) + .toList(); } @Override public ClassInfo declaringClass() { org.jboss.jandex.ClassInfo beanClass = jandexIndex.getClassByName(arcBeanInfo.getBeanClass()); - return new ClassInfoImpl(jandexIndex, annotationOverlays, beanClass); + return new ClassInfoImpl(jandexIndex, annotationOverlay, beanClass); } @Override @@ -85,7 +86,7 @@ public boolean isSynthetic() { @Override public MethodInfo producerMethod() { if (arcBeanInfo.isProducerMethod()) { - return new MethodInfoImpl(jandexIndex, annotationOverlays, arcBeanInfo.getTarget().get().asMethod()); + return new MethodInfoImpl(jandexIndex, annotationOverlay, arcBeanInfo.getTarget().get().asMethod()); } return null; } @@ -93,7 +94,7 @@ public MethodInfo producerMethod() { @Override public FieldInfo producerField() { if (arcBeanInfo.isProducerField()) { - return new FieldInfoImpl(jandexIndex, annotationOverlays, arcBeanInfo.getTarget().get().asField()); + return new FieldInfoImpl(jandexIndex, annotationOverlay, arcBeanInfo.getTarget().get().asField()); } return null; } @@ -116,23 +117,23 @@ public String name() { @Override public DisposerInfo disposer() { io.quarkus.arc.processor.DisposerInfo disposer = arcBeanInfo.getDisposer(); - return disposer != null ? new DisposerInfoImpl(jandexIndex, annotationOverlays, disposer) : null; + return disposer != null ? new DisposerInfoImpl(jandexIndex, annotationOverlay, disposer) : null; } @Override public Collection stereotypes() { return arcBeanInfo.getStereotypes() .stream() - .map(it -> new StereotypeInfoImpl(jandexIndex, annotationOverlays, it)) - .collect(Collectors.toUnmodifiableList()); + .map(it -> (StereotypeInfo) new StereotypeInfoImpl(jandexIndex, annotationOverlay, it)) + .toList(); } @Override public Collection injectionPoints() { return arcBeanInfo.getAllInjectionPoints() .stream() - .map(it -> new InjectionPointInfoImpl(jandexIndex, annotationOverlays, it)) - .collect(Collectors.toUnmodifiableList()); + .map(it -> (InjectionPointInfo) new InjectionPointInfoImpl(jandexIndex, annotationOverlay, it)) + .toList(); } @Override diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/BuildServicesImpl.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/BuildServicesImpl.java index 59eabf9063d23..ee8abcfeec094 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/BuildServicesImpl.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/BuildServicesImpl.java @@ -5,21 +5,21 @@ public class BuildServicesImpl implements BuildServices { private static volatile org.jboss.jandex.IndexView beanArchiveIndex; - private static volatile AllAnnotationOverlays annotationOverlays; + private static volatile org.jboss.jandex.MutableAnnotationOverlay annotationOverlay; - static void init(org.jboss.jandex.IndexView beanArchiveIndex, AllAnnotationOverlays annotationOverlays) { + static void init(org.jboss.jandex.IndexView beanArchiveIndex, org.jboss.jandex.MutableAnnotationOverlay annotationOverlay) { BuildServicesImpl.beanArchiveIndex = beanArchiveIndex; - BuildServicesImpl.annotationOverlays = annotationOverlays; + BuildServicesImpl.annotationOverlay = annotationOverlay; } static void reset() { BuildServicesImpl.beanArchiveIndex = null; - BuildServicesImpl.annotationOverlays = null; + BuildServicesImpl.annotationOverlay = null; } @Override public AnnotationBuilderFactory annotationBuilderFactory() { - return new AnnotationBuilderFactoryImpl(beanArchiveIndex, annotationOverlays); + return new AnnotationBuilderFactoryImpl(beanArchiveIndex, annotationOverlay); } @Override diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ClassConfigImpl.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ClassConfigImpl.java index a3e04d0cc1fb6..1a8f3975b5637 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ClassConfigImpl.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ClassConfigImpl.java @@ -13,21 +13,21 @@ import jakarta.enterprise.lang.model.declarations.MethodInfo; class ClassConfigImpl extends DeclarationConfigImpl implements ClassConfig { - ClassConfigImpl(org.jboss.jandex.IndexView jandexIndex, AllAnnotationTransformations allTransformations, + ClassConfigImpl(org.jboss.jandex.IndexView jandexIndex, org.jboss.jandex.MutableAnnotationOverlay annotationOverlay, org.jboss.jandex.ClassInfo jandexDeclaration) { - super(jandexIndex, allTransformations, allTransformations.classes, jandexDeclaration); + super(jandexIndex, annotationOverlay, jandexDeclaration); } @Override public ClassInfo info() { - return new ClassInfoImpl(jandexIndex, allTransformations.annotationOverlays, jandexDeclaration); + return new ClassInfoImpl(jandexIndex, annotationOverlay, jandexDeclaration); } @Override public Collection constructors() { List result = new ArrayList<>(); for (MethodInfo constructor : info().constructors()) { - result.add(new MethodConfigImpl(jandexIndex, allTransformations, ((MethodInfoImpl) constructor).jandexDeclaration)); + result.add(new MethodConfigImpl(jandexIndex, annotationOverlay, ((MethodInfoImpl) constructor).jandexDeclaration)); } return Collections.unmodifiableList(result); } @@ -36,7 +36,7 @@ public Collection constructors() { public Collection methods() { List result = new ArrayList<>(); for (MethodInfo method : info().methods()) { - result.add(new MethodConfigImpl(jandexIndex, allTransformations, ((MethodInfoImpl) method).jandexDeclaration)); + result.add(new MethodConfigImpl(jandexIndex, annotationOverlay, ((MethodInfoImpl) method).jandexDeclaration)); } return Collections.unmodifiableList(result); } @@ -45,7 +45,7 @@ public Collection methods() { public Collection fields() { List result = new ArrayList<>(); for (FieldInfo field : info().fields()) { - result.add(new FieldConfigImpl(jandexIndex, allTransformations, ((FieldInfoImpl) field).jandexDeclaration)); + result.add(new FieldConfigImpl(jandexIndex, annotationOverlay, ((FieldInfoImpl) field).jandexDeclaration)); } return Collections.unmodifiableList(result); } diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ClassInfoImpl.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ClassInfoImpl.java index d39cdd033e8e6..56f792b0bf236 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ClassInfoImpl.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ClassInfoImpl.java @@ -7,10 +7,8 @@ import java.util.Collections; import java.util.HashSet; import java.util.List; -import java.util.Objects; import java.util.Queue; import java.util.Set; -import java.util.stream.Collectors; import jakarta.enterprise.lang.model.declarations.ClassInfo; import jakarta.enterprise.lang.model.declarations.FieldInfo; @@ -23,13 +21,9 @@ import org.jboss.jandex.DotName; class ClassInfoImpl extends DeclarationInfoImpl implements ClassInfo { - // only for equals/hashCode - private final DotName name; - - ClassInfoImpl(org.jboss.jandex.IndexView jandexIndex, AllAnnotationOverlays annotationOverlays, + ClassInfoImpl(org.jboss.jandex.IndexView jandexIndex, org.jboss.jandex.MutableAnnotationOverlay annotationOverlay, org.jboss.jandex.ClassInfo jandexDeclaration) { - super(jandexIndex, annotationOverlays, jandexDeclaration); - this.name = jandexDeclaration.name(); + super(jandexIndex, annotationOverlay, jandexDeclaration); } @Override @@ -47,17 +41,17 @@ public PackageInfo packageInfo() { String packageName = jandexDeclaration.name().packagePrefix(); org.jboss.jandex.ClassInfo packageClass = jandexIndex.getClassByName( DotName.createSimple(packageName + ".package-info")); - return new PackageInfoImpl(jandexIndex, annotationOverlays, packageClass); + return new PackageInfoImpl(jandexIndex, annotationOverlay, packageClass); } @Override public List typeParameters() { return jandexDeclaration.typeParameters() .stream() - .map(it -> TypeImpl.fromJandexType(jandexIndex, annotationOverlays, it)) + .map(it -> TypeImpl.fromJandexType(jandexIndex, annotationOverlay, it)) .filter(Type::isTypeVariable) // not necessary, just as a precaution .map(Type::asTypeVariable) // not necessary, just as a precaution - .collect(Collectors.toUnmodifiableList()); + .toList(); } @Override @@ -66,7 +60,7 @@ public Type superClass() { if (jandexSuperType == null) { return null; } - return TypeImpl.fromJandexType(jandexIndex, annotationOverlays, jandexSuperType); + return TypeImpl.fromJandexType(jandexIndex, annotationOverlay, jandexSuperType); } @Override @@ -75,23 +69,23 @@ public ClassInfo superClassDeclaration() { if (jandexSuperType == null) { return null; } - return new ClassInfoImpl(jandexIndex, annotationOverlays, jandexIndex.getClassByName(jandexSuperType)); + return new ClassInfoImpl(jandexIndex, annotationOverlay, jandexIndex.getClassByName(jandexSuperType)); } @Override public List superInterfaces() { return jandexDeclaration.interfaceTypes() .stream() - .map(it -> TypeImpl.fromJandexType(jandexIndex, annotationOverlays, it)) - .collect(Collectors.toUnmodifiableList()); + .map(it -> TypeImpl.fromJandexType(jandexIndex, annotationOverlay, it)) + .toList(); } @Override public List superInterfacesDeclarations() { return jandexDeclaration.interfaceNames() .stream() - .map(it -> new ClassInfoImpl(jandexIndex, annotationOverlays, jandexIndex.getClassByName(it))) - .collect(Collectors.toUnmodifiableList()); + .map(it -> (ClassInfo) new ClassInfoImpl(jandexIndex, annotationOverlay, jandexIndex.getClassByName(it))) + .toList(); } @Override @@ -145,7 +139,7 @@ public Collection constructors() { continue; } if (MethodPredicates.IS_CONSTRUCTOR_JANDEX.test(jandexMethod)) { - result.add(new MethodInfoImpl(jandexIndex, annotationOverlays, jandexMethod)); + result.add(new MethodInfoImpl(jandexIndex, annotationOverlay, jandexMethod)); } } return Collections.unmodifiableList(result); @@ -187,7 +181,7 @@ public Collection methods() { continue; } if (MethodPredicates.IS_METHOD_JANDEX.test(jandexMethod)) { - result.add(new MethodInfoImpl(jandexIndex, annotationOverlays, jandexMethod)); + result.add(new MethodInfoImpl(jandexIndex, annotationOverlay, jandexMethod)); } } } @@ -202,7 +196,7 @@ public Collection fields() { if (jandexField.isSynthetic()) { continue; } - result.add(new FieldInfoImpl(jandexIndex, annotationOverlays, jandexField)); + result.add(new FieldInfoImpl(jandexIndex, annotationOverlay, jandexField)); } } return Collections.unmodifiableList(result); @@ -210,29 +204,10 @@ public Collection fields() { @Override public Collection recordComponents() { - return jandexDeclaration.recordComponents() - .stream() - .map(it -> new RecordComponentInfoImpl(jandexIndex, annotationOverlays, it)) - .collect(Collectors.toUnmodifiableList()); - } - - @Override - AnnotationsOverlay annotationsOverlay() { - return annotationOverlays.classes; - } - - @Override - public boolean equals(Object o) { - if (this == o) - return true; - if (o == null || getClass() != o.getClass()) - return false; - ClassInfoImpl classInfo = (ClassInfoImpl) o; - return Objects.equals(name, classInfo.name); - } - - @Override - public int hashCode() { - return Objects.hash(name); + List result = new ArrayList<>(); + for (org.jboss.jandex.RecordComponentInfo recordComponent : jandexDeclaration.recordComponents()) { + result.add(new RecordComponentInfoImpl(jandexIndex, annotationOverlay, recordComponent)); + } + return Collections.unmodifiableList(result); } } diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ClassTypeImpl.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ClassTypeImpl.java index faa789d1b89c1..e1e4e5b262c25 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ClassTypeImpl.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ClassTypeImpl.java @@ -6,14 +6,14 @@ import org.jboss.jandex.DotName; class ClassTypeImpl extends TypeImpl implements ClassType { - ClassTypeImpl(org.jboss.jandex.IndexView jandexIndex, AllAnnotationOverlays annotationOverlays, + ClassTypeImpl(org.jboss.jandex.IndexView jandexIndex, org.jboss.jandex.MutableAnnotationOverlay annotationOverlay, org.jboss.jandex.ClassType jandexType) { - super(jandexIndex, annotationOverlays, jandexType); + super(jandexIndex, annotationOverlay, jandexType); } @Override public ClassInfo declaration() { DotName name = jandexType.name(); - return new ClassInfoImpl(jandexIndex, annotationOverlays, jandexIndex.getClassByName(name)); + return new ClassInfoImpl(jandexIndex, annotationOverlay, jandexIndex.getClassByName(name)); } } diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/DeclarationConfigImpl.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/DeclarationConfigImpl.java index d88e14473d703..1ae02cca43cc6 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/DeclarationConfigImpl.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/DeclarationConfigImpl.java @@ -6,48 +6,51 @@ import jakarta.enterprise.inject.build.compatible.spi.DeclarationConfig; import jakarta.enterprise.lang.model.AnnotationInfo; -abstract class DeclarationConfigImpl> +abstract class DeclarationConfigImpl> implements DeclarationConfig { final org.jboss.jandex.IndexView jandexIndex; - final AllAnnotationTransformations allTransformations; - final AnnotationsTransformation transformations; + final org.jboss.jandex.MutableAnnotationOverlay annotationOverlay; final JandexDeclaration jandexDeclaration; - DeclarationConfigImpl(org.jboss.jandex.IndexView jandexIndex, AllAnnotationTransformations allTransformations, - AnnotationsTransformation transformations, JandexDeclaration jandexDeclaration) { + DeclarationConfigImpl(org.jboss.jandex.IndexView jandexIndex, org.jboss.jandex.MutableAnnotationOverlay annotationOverlay, + JandexDeclaration jandexDeclaration) { this.jandexIndex = jandexIndex; - this.allTransformations = allTransformations; - this.transformations = transformations; + this.annotationOverlay = annotationOverlay; this.jandexDeclaration = jandexDeclaration; } @Override public THIS addAnnotation(Class annotationType) { - transformations.addAnnotation(jandexDeclaration, annotationType); + annotationOverlay.addAnnotation(jandexDeclaration, org.jboss.jandex.AnnotationInstance.builder(annotationType).build()); return (THIS) this; } @Override public THIS addAnnotation(AnnotationInfo annotation) { - transformations.addAnnotation(jandexDeclaration, annotation); + annotationOverlay.addAnnotation(jandexDeclaration, ((AnnotationInfoImpl) annotation).jandexAnnotation); return (THIS) this; } @Override public THIS addAnnotation(Annotation annotation) { - transformations.addAnnotation(jandexDeclaration, annotation); + annotationOverlay.addAnnotation(jandexDeclaration, io.quarkus.arc.processor.Annotations.jandexAnnotation(annotation)); return (THIS) this; } @Override public THIS removeAnnotation(Predicate predicate) { - transformations.removeAnnotation(jandexDeclaration, predicate); + annotationOverlay.removeAnnotations(jandexDeclaration, new Predicate() { + @Override + public boolean test(org.jboss.jandex.AnnotationInstance annotationInstance) { + return predicate.test(new AnnotationInfoImpl(jandexIndex, annotationOverlay, annotationInstance)); + } + }); return (THIS) this; } @Override public THIS removeAllAnnotations() { - transformations.removeAllAnnotations(jandexDeclaration); + annotationOverlay.removeAnnotations(jandexDeclaration, ignored -> true); return (THIS) this; } } diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/DeclarationInfoImpl.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/DeclarationInfoImpl.java index 72da5f7d03bd3..e7571b769de70 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/DeclarationInfoImpl.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/DeclarationInfoImpl.java @@ -1,93 +1,80 @@ package io.quarkus.arc.processor.bcextensions; import java.lang.annotation.Annotation; +import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; +import java.util.List; import java.util.function.Predicate; -import java.util.stream.Collectors; import jakarta.enterprise.lang.model.AnnotationInfo; import jakarta.enterprise.lang.model.declarations.DeclarationInfo; -import org.jboss.jandex.DotName; - -abstract class DeclarationInfoImpl implements DeclarationInfo { - final org.jboss.jandex.IndexView jandexIndex; - final AllAnnotationOverlays annotationOverlays; +abstract class DeclarationInfoImpl extends AnnotationTargetImpl + implements DeclarationInfo { final JandexDeclaration jandexDeclaration; - DeclarationInfoImpl(org.jboss.jandex.IndexView jandexIndex, AllAnnotationOverlays annotationOverlays, + DeclarationInfoImpl(org.jboss.jandex.IndexView jandexIndex, org.jboss.jandex.MutableAnnotationOverlay annotationOverlay, JandexDeclaration jandexDeclaration) { - this.jandexIndex = jandexIndex; - this.annotationOverlays = annotationOverlays; + super(jandexIndex, annotationOverlay, org.jboss.jandex.EquivalenceKey.of(jandexDeclaration)); this.jandexDeclaration = jandexDeclaration; } - static DeclarationInfo fromJandexDeclaration(org.jboss.jandex.IndexView jandexIndex, - AllAnnotationOverlays annotationOverlays, - org.jboss.jandex.AnnotationTarget jandexDeclaration) { - switch (jandexDeclaration.kind()) { - case CLASS: - return new ClassInfoImpl(jandexIndex, annotationOverlays, jandexDeclaration.asClass()); - case METHOD: - return new MethodInfoImpl(jandexIndex, annotationOverlays, jandexDeclaration.asMethod()); - case METHOD_PARAMETER: - return new ParameterInfoImpl(jandexIndex, annotationOverlays, jandexDeclaration.asMethodParameter()); - case FIELD: - return new FieldInfoImpl(jandexIndex, annotationOverlays, jandexDeclaration.asField()); - default: - throw new IllegalStateException("Unknown declaration " + jandexDeclaration); - } - } - @Override public boolean hasAnnotation(Class annotationType) { - return annotationsOverlay().hasAnnotation(jandexDeclaration, DotName.createSimple(annotationType.getName()), - jandexIndex); + return annotationOverlay.hasAnnotation(jandexDeclaration, annotationType); } @Override public boolean hasAnnotation(Predicate predicate) { - return annotationsOverlay().getAnnotations(jandexDeclaration, jandexIndex).annotations() - .stream() - .anyMatch(it -> predicate.test(new AnnotationInfoImpl(jandexIndex, annotationOverlays, it))); + for (org.jboss.jandex.AnnotationInstance annotation : annotationOverlay.annotations(jandexDeclaration)) { + if (predicate.test(new AnnotationInfoImpl(jandexIndex, annotationOverlay, annotation))) { + return true; + } + } + return false; } @Override public AnnotationInfo annotation(Class annotationType) { - org.jboss.jandex.AnnotationInstance jandexAnnotation = annotationsOverlay().getAnnotations( - jandexDeclaration, jandexIndex).annotation(annotationType); - if (jandexAnnotation == null) { + org.jboss.jandex.AnnotationInstance annotation = annotationOverlay.annotation(jandexDeclaration, annotationType); + if (annotation == null) { return null; } - return new AnnotationInfoImpl(jandexIndex, annotationOverlays, jandexAnnotation); + return new AnnotationInfoImpl(jandexIndex, annotationOverlay, annotation); } @Override public Collection repeatableAnnotation(Class annotationType) { - return annotationsOverlay().getAnnotations(jandexDeclaration, jandexIndex) - .annotationsWithRepeatable(annotationType) - .stream() - .map(it -> new AnnotationInfoImpl(jandexIndex, annotationOverlays, it)) - .collect(Collectors.toUnmodifiableList()); + List result = new ArrayList<>(); + for (org.jboss.jandex.AnnotationInstance annotation : annotationOverlay.annotationsWithRepeatable(jandexDeclaration, + annotationType)) { + result.add(new AnnotationInfoImpl(jandexIndex, annotationOverlay, annotation)); + } + return Collections.unmodifiableList(result); } @Override public Collection annotations(Predicate predicate) { - return annotationsOverlay().getAnnotations(jandexDeclaration, jandexIndex) - .annotations() - .stream() - .map(it -> new AnnotationInfoImpl(jandexIndex, annotationOverlays, it)) - .filter(predicate) - .collect(Collectors.toUnmodifiableList()); + List result = new ArrayList<>(); + for (org.jboss.jandex.AnnotationInstance annotation : annotationOverlay.annotations(jandexDeclaration)) { + AnnotationInfo annotationInfo = new AnnotationInfoImpl(jandexIndex, annotationOverlay, annotation); + if (predicate.test(annotationInfo)) { + result.add(annotationInfo); + } + } + return Collections.unmodifiableList(result); } @Override public Collection annotations() { - return annotations(it -> true); + List result = new ArrayList<>(); + for (org.jboss.jandex.AnnotationInstance annotation : annotationOverlay.annotations(jandexDeclaration)) { + result.add(new AnnotationInfoImpl(jandexIndex, annotationOverlay, annotation)); + } + return Collections.unmodifiableList(result); } - abstract AnnotationsOverlay annotationsOverlay(); - @Override public String toString() { return jandexDeclaration.toString(); diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/DisposerInfoImpl.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/DisposerInfoImpl.java index 7dbe54ed0ea0b..4c14bed64d692 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/DisposerInfoImpl.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/DisposerInfoImpl.java @@ -6,25 +6,25 @@ class DisposerInfoImpl implements DisposerInfo { private final org.jboss.jandex.IndexView jandexIndex; - private final AllAnnotationOverlays annotationOverlays; + private final org.jboss.jandex.MutableAnnotationOverlay annotationOverlay; private final io.quarkus.arc.processor.DisposerInfo arcDisposerInfo; - DisposerInfoImpl(org.jboss.jandex.IndexView jandexIndex, AllAnnotationOverlays annotationOverlays, + DisposerInfoImpl(org.jboss.jandex.IndexView jandexIndex, org.jboss.jandex.MutableAnnotationOverlay annotationOverlay, io.quarkus.arc.processor.DisposerInfo arcDisposerInfo) { this.jandexIndex = jandexIndex; - this.annotationOverlays = annotationOverlays; + this.annotationOverlay = annotationOverlay; this.arcDisposerInfo = arcDisposerInfo; } @Override public MethodInfo disposerMethod() { org.jboss.jandex.MethodInfo jandexMethod = arcDisposerInfo.getDisposerMethod(); - return new MethodInfoImpl(jandexIndex, annotationOverlays, jandexMethod); + return new MethodInfoImpl(jandexIndex, annotationOverlay, jandexMethod); } @Override public ParameterInfo disposedParameter() { org.jboss.jandex.MethodParameterInfo jandexParameter = arcDisposerInfo.getDisposedParameter(); - return new ParameterInfoImpl(jandexIndex, annotationOverlays, jandexParameter); + return new ParameterInfoImpl(jandexIndex, annotationOverlay, jandexParameter); } } diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ExtensionInvoker.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ExtensionInvoker.java index bdfa4e8542ec7..72dc4ef9ca3a0 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ExtensionInvoker.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ExtensionInvoker.java @@ -10,13 +10,11 @@ import java.util.Map; import java.util.ServiceLoader; import java.util.concurrent.ConcurrentHashMap; -import java.util.stream.Collectors; import jakarta.enterprise.inject.build.compatible.spi.BuildCompatibleExtension; import jakarta.interceptor.Interceptor; import org.jboss.jandex.DotName; -import org.jboss.jandex.JandexReflection; // only this class uses reflection, everything else in this package is reflection-free class ExtensionInvoker { @@ -69,7 +67,7 @@ List findExtensionMethods(DotName annotation) { return p1 < p2 ? -1 : 1; }) .map(ExtensionMethod::new) - .collect(Collectors.toUnmodifiableList()); + .toList(); } private int getExtensionMethodPriority(org.jboss.jandex.MethodInfo method) { @@ -85,7 +83,7 @@ void callExtensionMethod(ExtensionMethod method, List arguments) Class[] parameterTypes = new Class[arguments.size()]; for (int i = 0; i < parameterTypes.length; i++) { - parameterTypes[i] = JandexReflection.loadRawType(method.parameterType(i)); + parameterTypes[i] = org.jboss.jandex.JandexReflection.loadRawType(method.parameterType(i)); } Class extensionClass = extensionClasses.get(method.extensionClass.name().toString()); diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ExtensionPhaseDiscovery.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ExtensionPhaseDiscovery.java index ca4a3cf79cc5c..3451406386f83 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ExtensionPhaseDiscovery.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ExtensionPhaseDiscovery.java @@ -11,19 +11,19 @@ class ExtensionPhaseDiscovery extends ExtensionPhaseBase { private final Set additionalClasses; - private final AllAnnotationTransformations annotationTransformations; + private final org.jboss.jandex.MutableAnnotationOverlay annotationOverlay; private final Map qualifiers; private final Map interceptorBindings; private final Map stereotypes; private final List contexts; ExtensionPhaseDiscovery(ExtensionInvoker invoker, org.jboss.jandex.IndexView applicationIndex, SharedErrors errors, - Set additionalClasses, AllAnnotationTransformations annotationTransformations, + Set additionalClasses, org.jboss.jandex.MutableAnnotationOverlay annotationOverlay, Map qualifiers, Map interceptorBindings, Map stereotypes, List contexts) { super(ExtensionPhase.DISCOVERY, invoker, applicationIndex, errors); this.additionalClasses = additionalClasses; - this.annotationTransformations = annotationTransformations; + this.annotationOverlay = annotationOverlay; this.qualifiers = qualifiers; this.interceptorBindings = interceptorBindings; this.stereotypes = stereotypes; @@ -32,15 +32,11 @@ class ExtensionPhaseDiscovery extends ExtensionPhaseBase { @Override Object argumentForExtensionMethod(ExtensionMethodParameter type, ExtensionMethod method) { - switch (type) { - case META_ANNOTATIONS: - return new MetaAnnotationsImpl(index, annotationTransformations, qualifiers, interceptorBindings, - stereotypes, contexts); - case SCANNED_CLASSES: - return new ScannedClassesImpl(additionalClasses); - - default: - return super.argumentForExtensionMethod(type, method); - } + return switch (type) { + case META_ANNOTATIONS -> new MetaAnnotationsImpl(index, annotationOverlay, qualifiers, interceptorBindings, + stereotypes, contexts); + case SCANNED_CLASSES -> new ScannedClassesImpl(additionalClasses); + default -> super.argumentForExtensionMethod(type, method); + }; } } diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ExtensionPhaseEnhancement.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ExtensionPhaseEnhancement.java index 87f36c5d07edb..05933271b4cef 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ExtensionPhaseEnhancement.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ExtensionPhaseEnhancement.java @@ -7,7 +7,6 @@ import java.util.HashSet; import java.util.List; import java.util.Set; -import java.util.stream.Collectors; import java.util.stream.Stream; import jakarta.enterprise.inject.spi.DefinitionException; @@ -15,14 +14,12 @@ import org.jboss.jandex.DotName; class ExtensionPhaseEnhancement extends ExtensionPhaseBase { - private final AllAnnotationOverlays annotationOverlays; - private final AllAnnotationTransformations annotationTransformations; + private final org.jboss.jandex.MutableAnnotationOverlay annotationOverlay; ExtensionPhaseEnhancement(ExtensionInvoker invoker, org.jboss.jandex.IndexView beanArchiveIndex, - SharedErrors errors, AllAnnotationTransformations annotationTransformations) { + SharedErrors errors, org.jboss.jandex.MutableAnnotationOverlay annotationOverlay) { super(ExtensionPhase.ENHANCEMENT, invoker, beanArchiveIndex, errors); - this.annotationOverlays = annotationTransformations.annotationOverlays; - this.annotationTransformations = annotationTransformations; + this.annotationOverlay = annotationOverlay; } @Override @@ -56,35 +53,35 @@ void runExtensionMethod(ExtensionMethod method) throws ReflectiveOperationExcept .get(); // guaranteed to be there List matchingClasses = matchingClasses(method.jandex); - List allValuesForQueryParameter; + List allValuesForQueryParameter; if (query == ExtensionMethodParameter.CLASS_INFO) { allValuesForQueryParameter = matchingClasses.stream() - .map(it -> new ClassInfoImpl(index, annotationOverlays, it)) - .collect(Collectors.toUnmodifiableList()); + .map(it -> new ClassInfoImpl(index, annotationOverlay, it)) + .toList(); } else if (query == ExtensionMethodParameter.METHOD_INFO) { allValuesForQueryParameter = matchingClasses.stream() - .map(it -> new ClassInfoImpl(index, annotationOverlays, it)) + .map(it -> new ClassInfoImpl(index, annotationOverlay, it)) .flatMap(it -> Stream.concat(it.constructors().stream(), it.methods().stream())) - .collect(Collectors.toUnmodifiableList()); + .toList(); } else if (query == ExtensionMethodParameter.FIELD_INFO) { allValuesForQueryParameter = matchingClasses.stream() - .map(it -> new ClassInfoImpl(index, annotationOverlays, it)) + .map(it -> new ClassInfoImpl(index, annotationOverlay, it)) .flatMap(it -> it.fields().stream()) - .collect(Collectors.toUnmodifiableList()); + .toList(); } else if (query == ExtensionMethodParameter.CLASS_CONFIG) { allValuesForQueryParameter = matchingClasses.stream() - .map(it -> new ClassConfigImpl(index, annotationTransformations, it)) - .collect(Collectors.toUnmodifiableList()); + .map(it -> new ClassConfigImpl(index, annotationOverlay, it)) + .toList(); } else if (query == ExtensionMethodParameter.METHOD_CONFIG) { allValuesForQueryParameter = matchingClasses.stream() - .map(it -> new ClassConfigImpl(index, annotationTransformations, it)) + .map(it -> new ClassConfigImpl(index, annotationOverlay, it)) .flatMap(it -> Stream.concat(it.constructors().stream(), it.methods().stream())) - .collect(Collectors.toUnmodifiableList()); + .toList(); } else if (query == ExtensionMethodParameter.FIELD_CONFIG) { allValuesForQueryParameter = matchingClasses.stream() - .map(it -> new ClassConfigImpl(index, annotationTransformations, it)) + .map(it -> new ClassConfigImpl(index, annotationOverlay, it)) .flatMap(it -> it.fields().stream()) - .collect(Collectors.toUnmodifiableList()); + .toList(); } else { throw new IllegalArgumentException("Unknown query parameter " + query); } @@ -168,7 +165,7 @@ && isAnyAnnotationPresent(annotationNames, annotationDeclaration, alreadyProcess @Override Object argumentForExtensionMethod(ExtensionMethodParameter type, ExtensionMethod method) { if (type == ExtensionMethodParameter.TYPES) { - return new TypesImpl(index, annotationOverlays); + return new TypesImpl(index, annotationOverlay); } return super.argumentForExtensionMethod(type, method); diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ExtensionPhaseRegistration.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ExtensionPhaseRegistration.java index 40cf58d2612fc..7f5e3667f84e2 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ExtensionPhaseRegistration.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ExtensionPhaseRegistration.java @@ -5,31 +5,28 @@ import java.util.Collections; import java.util.List; import java.util.Set; -import java.util.stream.Collectors; import java.util.stream.Stream; import jakarta.enterprise.inject.build.compatible.spi.BeanInfo; import jakarta.enterprise.inject.build.compatible.spi.ObserverInfo; import jakarta.enterprise.inject.spi.DefinitionException; -import org.jboss.jandex.IndexView; - import io.quarkus.arc.processor.InterceptorInfo; class ExtensionPhaseRegistration extends ExtensionPhaseBase { - private final AllAnnotationOverlays annotationOverlays; + private final org.jboss.jandex.MutableAnnotationOverlay annotationOverlay; private final Collection allBeans; private final Collection allInterceptors; private final Collection allObservers; private final io.quarkus.arc.processor.InvokerFactory invokerFactory; private final io.quarkus.arc.processor.AssignabilityCheck assignability; - ExtensionPhaseRegistration(ExtensionInvoker invoker, IndexView beanArchiveIndex, SharedErrors errors, - AllAnnotationOverlays annotationOverlays, Collection allBeans, + ExtensionPhaseRegistration(ExtensionInvoker invoker, org.jboss.jandex.IndexView beanArchiveIndex, SharedErrors errors, + org.jboss.jandex.MutableAnnotationOverlay annotationOverlay, Collection allBeans, Collection allInterceptors, Collection allObservers, io.quarkus.arc.processor.InvokerFactory invokerFactory) { super(ExtensionPhase.REGISTRATION, invoker, beanArchiveIndex, errors); - this.annotationOverlays = annotationOverlays; + this.annotationOverlay = annotationOverlay; this.allBeans = allBeans; this.allInterceptors = allInterceptors; this.allObservers = allObservers; @@ -106,8 +103,8 @@ private List matchingBeans(org.jboss.jandex.MethodInfo jandexMethod, b } return false; }) - .map(it -> BeanInfoImpl.create(index, annotationOverlays, it)) - .collect(Collectors.toUnmodifiableList()); + .map(it -> (BeanInfo) BeanInfoImpl.create(index, annotationOverlay, it)) + .toList(); } private List matchingObservers(org.jboss.jandex.MethodInfo jandexMethod) { @@ -122,18 +119,17 @@ private List matchingObservers(org.jboss.jandex.MethodInfo jandexM } return false; }) - .map(it -> new ObserverInfoImpl(index, annotationOverlays, it)) - .collect(Collectors.toUnmodifiableList()); + .map(it -> (ObserverInfo) new ObserverInfoImpl(index, annotationOverlay, it)) + .toList(); } @Override Object argumentForExtensionMethod(ExtensionMethodParameter type, ExtensionMethod method) { - if (type == ExtensionMethodParameter.INVOKER_FACTORY) { - return new InvokerFactoryImpl(invokerFactory); - } else if (type == ExtensionMethodParameter.TYPES) { - return new TypesImpl(index, annotationOverlays); - } + return switch (type) { + case INVOKER_FACTORY -> new InvokerFactoryImpl(invokerFactory); + case TYPES -> new TypesImpl(index, annotationOverlay); + default -> super.argumentForExtensionMethod(type, method); + }; - return super.argumentForExtensionMethod(type, method); } } diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ExtensionPhaseSynthesis.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ExtensionPhaseSynthesis.java index 40f4e18b2ff8f..cd4ec5135d0c1 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ExtensionPhaseSynthesis.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ExtensionPhaseSynthesis.java @@ -2,34 +2,28 @@ import java.util.List; -import org.jboss.jandex.DotName; - class ExtensionPhaseSynthesis extends ExtensionPhaseBase { - private final AllAnnotationOverlays annotationOverlays; + private final org.jboss.jandex.MutableAnnotationOverlay annotationOverlay; private final List> syntheticBeans; private final List> syntheticObservers; ExtensionPhaseSynthesis(ExtensionInvoker invoker, org.jboss.jandex.IndexView beanArchiveIndex, SharedErrors errors, - AllAnnotationOverlays annotationOverlays, List> syntheticBeans, + org.jboss.jandex.MutableAnnotationOverlay annotationOverlay, List> syntheticBeans, List> syntheticObservers) { super(ExtensionPhase.SYNTHESIS, invoker, beanArchiveIndex, errors); - this.annotationOverlays = annotationOverlays; + this.annotationOverlay = annotationOverlay; this.syntheticBeans = syntheticBeans; this.syntheticObservers = syntheticObservers; } @Override Object argumentForExtensionMethod(ExtensionMethodParameter type, ExtensionMethod method) { - switch (type) { - case SYNTHETIC_COMPONENTS: - DotName extensionClass = method.extensionClass.name(); - return new SyntheticComponentsImpl(syntheticBeans, syntheticObservers, extensionClass); - case TYPES: - return new TypesImpl(index, annotationOverlays); - - default: - return super.argumentForExtensionMethod(type, method); - } + return switch (type) { + case SYNTHETIC_COMPONENTS -> new SyntheticComponentsImpl(syntheticBeans, syntheticObservers, + method.extensionClass.name()); + case TYPES -> new TypesImpl(index, annotationOverlay); + default -> super.argumentForExtensionMethod(type, method); + }; } } diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ExtensionPhaseValidation.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ExtensionPhaseValidation.java index aaf22610e81c5..0e8d8852ecefe 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ExtensionPhaseValidation.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ExtensionPhaseValidation.java @@ -3,15 +3,15 @@ import java.util.Collection; class ExtensionPhaseValidation extends ExtensionPhaseBase { - private final AllAnnotationOverlays annotationOverlays; + private final org.jboss.jandex.MutableAnnotationOverlay annotationOverlay; private final Collection allBeans; private final Collection allObservers; ExtensionPhaseValidation(ExtensionInvoker invoker, org.jboss.jandex.IndexView beanArchiveIndex, SharedErrors errors, - AllAnnotationOverlays annotationOverlays, Collection allBeans, + org.jboss.jandex.MutableAnnotationOverlay annotationOverlay, Collection allBeans, Collection allObservers) { super(ExtensionPhase.VALIDATION, invoker, beanArchiveIndex, errors); - this.annotationOverlays = annotationOverlays; + this.annotationOverlay = annotationOverlay; this.allBeans = allBeans; this.allObservers = allObservers; } @@ -19,7 +19,7 @@ class ExtensionPhaseValidation extends ExtensionPhaseBase { @Override Object argumentForExtensionMethod(ExtensionMethodParameter type, ExtensionMethod method) { if (type == ExtensionMethodParameter.TYPES) { - return new TypesImpl(index, annotationOverlays); + return new TypesImpl(index, annotationOverlay); } return super.argumentForExtensionMethod(type, method); diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ExtensionsEntryPoint.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ExtensionsEntryPoint.java index f75bc6c805b4d..9bb597fd67d43 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ExtensionsEntryPoint.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ExtensionsEntryPoint.java @@ -29,7 +29,9 @@ import jakarta.enterprise.inject.spi.EventContext; import jakarta.enterprise.util.Nonbinding; +import org.jboss.jandex.AnnotationTransformation; import org.jboss.jandex.DotName; +import org.jboss.jandex.MutableAnnotationOverlay; import io.quarkus.arc.InjectableContext; import io.quarkus.arc.impl.CreationalContextImpl; @@ -73,15 +75,15 @@ */ public class ExtensionsEntryPoint { private final ExtensionInvoker invoker; - private final AllAnnotationOverlays annotationOverlays; private final SharedErrors errors; private final Map qualifiers; private final Map interceptorBindings; private final Map stereotypes; private final List contexts; + private final List preAnnotationTransformations; - private volatile AllAnnotationTransformations preAnnotationTransformations; + private volatile MutableAnnotationOverlay annotationOverlay; private final List> syntheticBeans; private final List> syntheticObservers; @@ -94,21 +96,21 @@ public ExtensionsEntryPoint() { public ExtensionsEntryPoint(List extensions) { invoker = new ExtensionInvoker(extensions); if (invoker.isEmpty()) { - annotationOverlays = null; errors = null; qualifiers = null; interceptorBindings = null; stereotypes = null; contexts = null; + preAnnotationTransformations = null; syntheticBeans = null; syntheticObservers = null; } else { - annotationOverlays = new AllAnnotationOverlays(); errors = new SharedErrors(); qualifiers = new ConcurrentHashMap<>(); interceptorBindings = new ConcurrentHashMap<>(); stereotypes = new ConcurrentHashMap<>(); contexts = Collections.synchronizedList(new ArrayList<>()); + preAnnotationTransformations = Collections.synchronizedList(new ArrayList<>()); syntheticBeans = Collections.synchronizedList(new ArrayList<>()); syntheticObservers = Collections.synchronizedList(new ArrayList<>()); } @@ -123,10 +125,14 @@ public void runDiscovery(org.jboss.jandex.IndexView applicationIndex, Set(), applicationIndex); new ExtensionPhaseDiscovery(invoker, computingApplicationIndex, errors, additionalClasses, - preAnnotationTransformations, qualifiers, interceptorBindings, stereotypes, contexts).run(); + overlay, qualifiers, interceptorBindings, stereotypes, contexts).run(); } finally { // noone should attempt annotation transformations on custom meta-annotations after `@Discovery` is finished - preAnnotationTransformations.freeze(); + preAnnotationTransformations.addAll(overlay.freeze()); BuildServicesImpl.reset(); } @@ -152,10 +158,17 @@ public void registerMetaAnnotations(BeanProcessor.Builder builder, CustomAlterab if (invoker.isEmpty()) { return; } - builder.addAnnotationTransformer(preAnnotationTransformations.classes); - builder.addAnnotationTransformer(preAnnotationTransformations.methods); - builder.addAnnotationTransformer(preAnnotationTransformations.parameters); - builder.addAnnotationTransformer(preAnnotationTransformations.fields); + + builder.addAnnotationTransformation(new AnnotationTransformation() { + @Override + public void apply(TransformationContext context) { + for (AnnotationTransformation preAnnotationTransformation : preAnnotationTransformations) { + if (preAnnotationTransformation.supports(context.declaration().kind())) { + preAnnotationTransformation.apply(context); + } + } + } + }); if (!qualifiers.isEmpty()) { builder.addQualifierRegistrar(new QualifierRegistrar() { @@ -196,7 +209,7 @@ public List getAdditionalBindings() { return InterceptorBinding.of(annotationName, nonbindingMembers); }) - .collect(Collectors.toUnmodifiableList()); + .toList(); } }); } @@ -247,20 +260,31 @@ public void runEnhancement(org.jboss.jandex.IndexView beanArchiveIndex, BeanProc if (invoker.isEmpty()) { return; } - AllAnnotationTransformations annotationTransformations = new AllAnnotationTransformations(beanArchiveIndex, - annotationOverlays); - builder.addAnnotationTransformer(annotationTransformations.classes); - builder.addAnnotationTransformer(annotationTransformations.methods); - builder.addAnnotationTransformer(annotationTransformations.parameters); - builder.addAnnotationTransformer(annotationTransformations.fields); - BuildServicesImpl.init(beanArchiveIndex, annotationOverlays); + annotationOverlay = MutableAnnotationOverlay.builder(beanArchiveIndex) + .compatibleMode() + .runtimeAnnotationsOnly() + .inheritedAnnotations() + .build(); + + BuildServicesImpl.init(beanArchiveIndex, annotationOverlay); try { - new ExtensionPhaseEnhancement(invoker, beanArchiveIndex, errors, annotationTransformations).run(); + new ExtensionPhaseEnhancement(invoker, beanArchiveIndex, errors, annotationOverlay).run(); } finally { // noone should attempt annotation transformations on application classes after `@Enhancement` is finished - annotationTransformations.freeze(); + List annotationTransformations = annotationOverlay.freeze(); + + builder.addAnnotationTransformation(new AnnotationTransformation() { + @Override + public void apply(TransformationContext context) { + for (AnnotationTransformation annotationTransformation : annotationTransformations) { + if (annotationTransformation.supports(context.declaration().kind())) { + annotationTransformation.apply(context); + } + } + } + }); BuildServicesImpl.reset(); } @@ -280,10 +304,10 @@ public void runRegistration(org.jboss.jandex.IndexView beanArchiveIndex, return; } - BuildServicesImpl.init(beanArchiveIndex, annotationOverlays); + BuildServicesImpl.init(beanArchiveIndex, annotationOverlay); try { - new ExtensionPhaseRegistration(invoker, beanArchiveIndex, errors, annotationOverlays, + new ExtensionPhaseRegistration(invoker, beanArchiveIndex, errors, annotationOverlay, allBeans, allInterceptors, allObservers, invokerFactory).run(); } finally { BuildServicesImpl.reset(); @@ -300,10 +324,10 @@ public void runSynthesis(org.jboss.jandex.IndexView beanArchiveIndex) { return; } - BuildServicesImpl.init(beanArchiveIndex, annotationOverlays); + BuildServicesImpl.init(beanArchiveIndex, annotationOverlay); try { - new ExtensionPhaseSynthesis(invoker, beanArchiveIndex, errors, annotationOverlays, + new ExtensionPhaseSynthesis(invoker, beanArchiveIndex, errors, annotationOverlay, syntheticBeans, syntheticObservers).run(); } finally { BuildServicesImpl.reset(); @@ -579,15 +603,15 @@ public void runRegistrationAgain(org.jboss.jandex.IndexView beanArchiveIndex, Collection syntheticBeans = allBeans.stream() .filter(BeanInfo::isSynthetic) - .collect(Collectors.toUnmodifiableList()); + .toList(); Collection syntheticObservers = allObservers.stream() .filter(ObserverInfo::isSynthetic) - .collect(Collectors.toUnmodifiableList()); + .toList(); - BuildServicesImpl.init(beanArchiveIndex, annotationOverlays); + BuildServicesImpl.init(beanArchiveIndex, annotationOverlay); try { - new ExtensionPhaseRegistration(invoker, beanArchiveIndex, errors, annotationOverlays, + new ExtensionPhaseRegistration(invoker, beanArchiveIndex, errors, annotationOverlay, syntheticBeans, Collections.emptyList(), syntheticObservers, invokerFactory).run(); } finally { BuildServicesImpl.reset(); @@ -606,10 +630,10 @@ public void runValidation(org.jboss.jandex.IndexView beanArchiveIndex, return; } - BuildServicesImpl.init(beanArchiveIndex, annotationOverlays); + BuildServicesImpl.init(beanArchiveIndex, annotationOverlay); try { - new ExtensionPhaseValidation(invoker, beanArchiveIndex, errors, annotationOverlays, + new ExtensionPhaseValidation(invoker, beanArchiveIndex, errors, annotationOverlay, allBeans, allObservers).run(); } finally { BuildServicesImpl.reset(); @@ -631,7 +655,5 @@ public void registerValidationErrors(BeanDeploymentValidator.ValidationContext c } invoker.invalidate(); - - annotationOverlays.invalidate(); } } diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/FieldConfigImpl.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/FieldConfigImpl.java index 0bfe085f125ad..5026fa263e434 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/FieldConfigImpl.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/FieldConfigImpl.java @@ -4,13 +4,13 @@ import jakarta.enterprise.lang.model.declarations.FieldInfo; class FieldConfigImpl extends DeclarationConfigImpl implements FieldConfig { - FieldConfigImpl(org.jboss.jandex.IndexView jandexIndex, AllAnnotationTransformations allTransformations, + FieldConfigImpl(org.jboss.jandex.IndexView jandexIndex, org.jboss.jandex.MutableAnnotationOverlay annotationOverlay, org.jboss.jandex.FieldInfo jandexDeclaration) { - super(jandexIndex, allTransformations, allTransformations.fields, jandexDeclaration); + super(jandexIndex, annotationOverlay, jandexDeclaration); } @Override public FieldInfo info() { - return new FieldInfoImpl(jandexIndex, allTransformations.annotationOverlays, jandexDeclaration); + return new FieldInfoImpl(jandexIndex, annotationOverlay, jandexDeclaration); } } diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/FieldInfoImpl.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/FieldInfoImpl.java index 7198a4133198b..cd310185c7b95 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/FieldInfoImpl.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/FieldInfoImpl.java @@ -1,24 +1,15 @@ package io.quarkus.arc.processor.bcextensions; import java.lang.reflect.Modifier; -import java.util.Objects; import jakarta.enterprise.lang.model.declarations.ClassInfo; import jakarta.enterprise.lang.model.declarations.FieldInfo; import jakarta.enterprise.lang.model.types.Type; -import org.jboss.jandex.DotName; - class FieldInfoImpl extends DeclarationInfoImpl implements FieldInfo { - // only for equals/hashCode - private final DotName className; - private final String name; - - FieldInfoImpl(org.jboss.jandex.IndexView jandexIndex, AllAnnotationOverlays annotationOverlays, + FieldInfoImpl(org.jboss.jandex.IndexView jandexIndex, org.jboss.jandex.MutableAnnotationOverlay annotationOverlay, org.jboss.jandex.FieldInfo jandexDeclaration) { - super(jandexIndex, annotationOverlays, jandexDeclaration); - this.className = jandexDeclaration.declaringClass().name(); - this.name = jandexDeclaration.name(); + super(jandexIndex, annotationOverlay, jandexDeclaration); } @Override @@ -28,7 +19,7 @@ public String name() { @Override public Type type() { - return TypeImpl.fromJandexType(jandexIndex, annotationOverlays, jandexDeclaration.type()); + return TypeImpl.fromJandexType(jandexIndex, annotationOverlay, jandexDeclaration.type()); } @Override @@ -48,27 +39,6 @@ public int modifiers() { @Override public ClassInfo declaringClass() { - return new ClassInfoImpl(jandexIndex, annotationOverlays, jandexDeclaration.declaringClass()); - } - - @Override - AnnotationsOverlay annotationsOverlay() { - return annotationOverlays.fields; - } - - @Override - public boolean equals(Object o) { - if (this == o) - return true; - if (o == null || getClass() != o.getClass()) - return false; - FieldInfoImpl fieldInfo = (FieldInfoImpl) o; - return Objects.equals(className, fieldInfo.className) - && Objects.equals(name, fieldInfo.name); - } - - @Override - public int hashCode() { - return Objects.hash(className, name); + return new ClassInfoImpl(jandexIndex, annotationOverlay, jandexDeclaration.declaringClass()); } } diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/InjectionPointInfoImpl.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/InjectionPointInfoImpl.java index 790a7a56a28c9..a4f99e34b50aa 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/InjectionPointInfoImpl.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/InjectionPointInfoImpl.java @@ -1,7 +1,6 @@ package io.quarkus.arc.processor.bcextensions; import java.util.Collection; -import java.util.stream.Collectors; import jakarta.enterprise.inject.build.compatible.spi.InjectionPointInfo; import jakarta.enterprise.lang.model.AnnotationInfo; @@ -10,40 +9,40 @@ class InjectionPointInfoImpl implements InjectionPointInfo { private final org.jboss.jandex.IndexView jandexIndex; - private final AllAnnotationOverlays annotationOverlays; + private final org.jboss.jandex.MutableAnnotationOverlay annotationOverlay; private final io.quarkus.arc.processor.InjectionPointInfo arcInjectionPointInfo; - InjectionPointInfoImpl(org.jboss.jandex.IndexView jandexIndex, AllAnnotationOverlays annotationOverlays, + InjectionPointInfoImpl(org.jboss.jandex.IndexView jandexIndex, org.jboss.jandex.MutableAnnotationOverlay annotationOverlay, io.quarkus.arc.processor.InjectionPointInfo arcInjectionPointInfo) { this.jandexIndex = jandexIndex; - this.annotationOverlays = annotationOverlays; + this.annotationOverlay = annotationOverlay; this.arcInjectionPointInfo = arcInjectionPointInfo; } @Override public Type type() { - return TypeImpl.fromJandexType(jandexIndex, annotationOverlays, arcInjectionPointInfo.getRequiredType()); + return TypeImpl.fromJandexType(jandexIndex, annotationOverlay, arcInjectionPointInfo.getRequiredType()); } @Override public Collection qualifiers() { return arcInjectionPointInfo.getRequiredQualifiers() .stream() - .map(it -> new AnnotationInfoImpl(jandexIndex, annotationOverlays, it)) - .collect(Collectors.toUnmodifiableList()); + .map(it -> (AnnotationInfo) new AnnotationInfoImpl(jandexIndex, annotationOverlay, it)) + .toList(); } @Override public DeclarationInfo declaration() { if (arcInjectionPointInfo.isField()) { org.jboss.jandex.FieldInfo jandexField = arcInjectionPointInfo.getTarget().asField(); - return new FieldInfoImpl(jandexIndex, annotationOverlays, jandexField); + return new FieldInfoImpl(jandexIndex, annotationOverlay, jandexField); } else if (arcInjectionPointInfo.isParam()) { org.jboss.jandex.MethodInfo jandexMethod = arcInjectionPointInfo.getTarget().asMethod(); int parameterPosition = arcInjectionPointInfo.getPosition(); org.jboss.jandex.MethodParameterInfo jandexParameter = org.jboss.jandex.MethodParameterInfo.create( jandexMethod, (short) parameterPosition); - return new ParameterInfoImpl(jandexIndex, annotationOverlays, jandexParameter); + return new ParameterInfoImpl(jandexIndex, annotationOverlay, jandexParameter); } else { throw new IllegalStateException("Unknown injection point: " + arcInjectionPointInfo); } diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/InterceptorInfoImpl.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/InterceptorInfoImpl.java index a447078cd2627..97333ddd58a3d 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/InterceptorInfoImpl.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/InterceptorInfoImpl.java @@ -10,9 +10,9 @@ class InterceptorInfoImpl extends BeanInfoImpl implements InterceptorInfo { private final io.quarkus.arc.processor.InterceptorInfo arcInterceptorInfo; - InterceptorInfoImpl(org.jboss.jandex.IndexView jandexIndex, AllAnnotationOverlays annotationOverlays, + InterceptorInfoImpl(org.jboss.jandex.IndexView jandexIndex, org.jboss.jandex.MutableAnnotationOverlay annotationOverlay, io.quarkus.arc.processor.InterceptorInfo arcInterceptorInfo) { - super(jandexIndex, annotationOverlays, arcInterceptorInfo); + super(jandexIndex, annotationOverlay, arcInterceptorInfo); this.arcInterceptorInfo = arcInterceptorInfo; } @@ -25,7 +25,7 @@ public Integer priority() { public Collection interceptorBindings() { return arcInterceptorInfo.getBindings() .stream() - .map(it -> new AnnotationInfoImpl(jandexIndex, annotationOverlays, it)) + .map(it -> new AnnotationInfoImpl(jandexIndex, annotationOverlay, it)) .collect(Collectors.toUnmodifiableSet()); } diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/MetaAnnotationsImpl.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/MetaAnnotationsImpl.java index 03ce0d1712842..c825d5eed3f38 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/MetaAnnotationsImpl.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/MetaAnnotationsImpl.java @@ -13,18 +13,19 @@ class MetaAnnotationsImpl implements MetaAnnotations { private final org.jboss.jandex.IndexView applicationIndex; - private final AllAnnotationTransformations annotationTransformations; + private final org.jboss.jandex.MutableAnnotationOverlay annotationOverlay; private final Map qualifiers; private final Map interceptorBindings; private final Map stereotypes; private final List contexts; - MetaAnnotationsImpl(org.jboss.jandex.IndexView applicationIndex, AllAnnotationTransformations annotationTransformations, + MetaAnnotationsImpl(org.jboss.jandex.IndexView applicationIndex, + org.jboss.jandex.MutableAnnotationOverlay annotationOverlay, Map qualifiers, Map interceptorBindings, Map stereotypes, List contexts) { this.applicationIndex = applicationIndex; - this.annotationTransformations = annotationTransformations; + this.annotationOverlay = annotationOverlay; this.qualifiers = qualifiers; this.interceptorBindings = interceptorBindings; this.stereotypes = stereotypes; @@ -49,7 +50,7 @@ public ClassConfig addStereotype(Class annotation) { private ClassConfig addMetaAnnotation(Class annotation, Map map) { DotName annotationName = DotName.createSimple(annotation.getName()); org.jboss.jandex.ClassInfo jandexClass = applicationIndex.getClassByName(annotationName); - ClassConfig classConfig = new ClassConfigImpl(applicationIndex, annotationTransformations, jandexClass); + ClassConfig classConfig = new ClassConfigImpl(applicationIndex, annotationOverlay, jandexClass); map.put(annotationName, classConfig); return classConfig; } diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/MethodConfigImpl.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/MethodConfigImpl.java index efed586c47b2b..6ccdc0ebad103 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/MethodConfigImpl.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/MethodConfigImpl.java @@ -9,21 +9,21 @@ import jakarta.enterprise.lang.model.declarations.MethodInfo; class MethodConfigImpl extends DeclarationConfigImpl implements MethodConfig { - MethodConfigImpl(org.jboss.jandex.IndexView jandexIndex, AllAnnotationTransformations allTransformations, + MethodConfigImpl(org.jboss.jandex.IndexView jandexIndex, org.jboss.jandex.MutableAnnotationOverlay annotationOverlay, org.jboss.jandex.MethodInfo jandexDeclaration) { - super(jandexIndex, allTransformations, allTransformations.methods, jandexDeclaration); + super(jandexIndex, annotationOverlay, jandexDeclaration); } @Override public MethodInfo info() { - return new MethodInfoImpl(jandexIndex, allTransformations.annotationOverlays, jandexDeclaration); + return new MethodInfoImpl(jandexIndex, annotationOverlay, jandexDeclaration); } @Override public List parameters() { List result = new ArrayList<>(jandexDeclaration.parametersCount()); for (org.jboss.jandex.MethodParameterInfo jandexParameter : jandexDeclaration.parameters()) { - result.add(new ParameterConfigImpl(jandexIndex, allTransformations, jandexParameter)); + result.add(new ParameterConfigImpl(jandexIndex, annotationOverlay, jandexParameter)); } return Collections.unmodifiableList(result); } diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/MethodInfoImpl.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/MethodInfoImpl.java index 9cda38188d698..992b393ff5920 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/MethodInfoImpl.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/MethodInfoImpl.java @@ -1,11 +1,14 @@ package io.quarkus.arc.processor.bcextensions; +import java.lang.annotation.Annotation; import java.lang.reflect.Modifier; import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; import java.util.List; -import java.util.Objects; -import java.util.stream.Collectors; +import java.util.function.Predicate; +import jakarta.enterprise.lang.model.AnnotationInfo; import jakarta.enterprise.lang.model.declarations.ClassInfo; import jakarta.enterprise.lang.model.declarations.MethodInfo; import jakarta.enterprise.lang.model.declarations.ParameterInfo; @@ -15,17 +18,9 @@ import org.jboss.jandex.DotName; class MethodInfoImpl extends DeclarationInfoImpl implements MethodInfo { - // only for equals/hashCode - private final DotName className; - private final String name; - private final List parameterTypes; - - MethodInfoImpl(org.jboss.jandex.IndexView jandexIndex, AllAnnotationOverlays annotationOverlays, + MethodInfoImpl(org.jboss.jandex.IndexView jandexIndex, org.jboss.jandex.MutableAnnotationOverlay annotationOverlay, org.jboss.jandex.MethodInfo jandexDeclaration) { - super(jandexIndex, annotationOverlays, jandexDeclaration); - this.className = jandexDeclaration.declaringClass().name(); - this.name = jandexDeclaration.name(); - this.parameterTypes = jandexDeclaration.parameterTypes(); + super(jandexIndex, annotationOverlay, jandexDeclaration); } @Override @@ -40,7 +35,7 @@ public String name() { public List parameters() { List result = new ArrayList<>(jandexDeclaration.parametersCount()); for (org.jboss.jandex.MethodParameterInfo jandexParameter : jandexDeclaration.parameters()) { - result.add(new ParameterInfoImpl(jandexIndex, annotationOverlays, jandexParameter)); + result.add(new ParameterInfoImpl(jandexIndex, annotationOverlay, jandexParameter)); } return result; } @@ -56,9 +51,9 @@ public Type returnType() { .toArray(new org.jboss.jandex.AnnotationInstance[0]); org.jboss.jandex.Type classType = org.jboss.jandex.Type.createWithAnnotations( jandexDeclaration.declaringClass().name(), org.jboss.jandex.Type.Kind.CLASS, typeAnnotations); - return TypeImpl.fromJandexType(jandexIndex, annotationOverlays, classType); + return TypeImpl.fromJandexType(jandexIndex, annotationOverlay, classType); } - return TypeImpl.fromJandexType(jandexIndex, annotationOverlays, jandexDeclaration.returnType()); + return TypeImpl.fromJandexType(jandexIndex, annotationOverlay, jandexDeclaration.returnType()); } @Override @@ -81,25 +76,25 @@ public Type receiverType() { } } - return TypeImpl.fromJandexType(jandexIndex, annotationOverlays, jandexDeclaration.receiverType()); + return TypeImpl.fromJandexType(jandexIndex, annotationOverlay, jandexDeclaration.receiverType()); } @Override public List throwsTypes() { return jandexDeclaration.exceptions() .stream() - .map(it -> TypeImpl.fromJandexType(jandexIndex, annotationOverlays, it)) - .collect(Collectors.toUnmodifiableList()); + .map(it -> TypeImpl.fromJandexType(jandexIndex, annotationOverlay, it)) + .toList(); } @Override public List typeParameters() { return jandexDeclaration.typeParameters() .stream() - .map(it -> TypeImpl.fromJandexType(jandexIndex, annotationOverlays, it)) + .map(it -> TypeImpl.fromJandexType(jandexIndex, annotationOverlay, it)) .filter(Type::isTypeVariable) // not necessary, just as a precaution .map(Type::asTypeVariable) // not necessary, just as a precaution - .collect(Collectors.toUnmodifiableList()); + .toList(); } @Override @@ -129,28 +124,82 @@ public int modifiers() { @Override public ClassInfo declaringClass() { - return new ClassInfoImpl(jandexIndex, annotationOverlays, jandexDeclaration.declaringClass()); + return new ClassInfoImpl(jandexIndex, annotationOverlay, jandexDeclaration.declaringClass()); + } + + @Override + public boolean hasAnnotation(Class annotationType) { + DotName annotationName = DotName.createSimple(annotationType); + for (org.jboss.jandex.AnnotationInstance annotation : annotationOverlay.annotations(jandexDeclaration)) { + if (annotation.name().equals(annotationName) + && annotation.target() != null + && annotation.target().kind() == org.jboss.jandex.AnnotationTarget.Kind.METHOD) { + return true; + } + } + return false; + } + + @Override + public boolean hasAnnotation(Predicate predicate) { + for (org.jboss.jandex.AnnotationInstance annotation : annotationOverlay.annotations(jandexDeclaration)) { + if (predicate.test(new AnnotationInfoImpl(jandexIndex, annotationOverlay, annotation)) + && annotation.target() != null + && annotation.target().kind() == org.jboss.jandex.AnnotationTarget.Kind.METHOD) { + return true; + } + } + return false; } @Override - AnnotationsOverlay annotationsOverlay() { - return annotationOverlays.methods; + public AnnotationInfo annotation(Class annotationType) { + org.jboss.jandex.AnnotationInstance jandexAnnotation = annotationOverlay.annotation(jandexDeclaration, annotationType); + if (jandexAnnotation == null + || jandexAnnotation.target() == null + || jandexAnnotation.target().kind() != org.jboss.jandex.AnnotationTarget.Kind.METHOD) { + return null; + } + return new AnnotationInfoImpl(jandexIndex, annotationOverlay, jandexAnnotation); } @Override - public boolean equals(Object o) { - if (this == o) - return true; - if (o == null || getClass() != o.getClass()) - return false; - MethodInfoImpl that = (MethodInfoImpl) o; - return Objects.equals(className, that.className) - && Objects.equals(name, that.name) - && Objects.equals(parameterTypes, that.parameterTypes); + public Collection repeatableAnnotation(Class annotationType) { + List result = new ArrayList<>(); + for (org.jboss.jandex.AnnotationInstance annotation : annotationOverlay.annotationsWithRepeatable(jandexDeclaration, + annotationType)) { + if (annotation.target() != null + && annotation.target().kind() == org.jboss.jandex.AnnotationTarget.Kind.METHOD) { + result.add(new AnnotationInfoImpl(jandexIndex, annotationOverlay, annotation)); + } + } + return Collections.unmodifiableList(result); } @Override - public int hashCode() { - return Objects.hash(className, name, parameterTypes); + public Collection annotations(Predicate predicate) { + List result = new ArrayList<>(); + for (org.jboss.jandex.AnnotationInstance annotation : annotationOverlay.annotations(jandexDeclaration)) { + if (annotation.target() != null + && annotation.target().kind() == org.jboss.jandex.AnnotationTarget.Kind.METHOD) { + AnnotationInfo annotationInfo = new AnnotationInfoImpl(jandexIndex, annotationOverlay, annotation); + if (predicate.test(annotationInfo)) { + result.add(annotationInfo); + } + } + } + return Collections.unmodifiableList(result); + } + + @Override + public Collection annotations() { + List result = new ArrayList<>(); + for (org.jboss.jandex.AnnotationInstance annotation : annotationOverlay.annotations(jandexDeclaration)) { + if (annotation.target() != null + && annotation.target().kind() == org.jboss.jandex.AnnotationTarget.Kind.METHOD) { + result.add(new AnnotationInfoImpl(jandexIndex, annotationOverlay, annotation)); + } + } + return Collections.unmodifiableList(result); } } diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ObserverInfoImpl.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ObserverInfoImpl.java index 9171adf46be44..e17d91ffd4344 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ObserverInfoImpl.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ObserverInfoImpl.java @@ -1,7 +1,6 @@ package io.quarkus.arc.processor.bcextensions; import java.util.Collection; -import java.util.stream.Collectors; import jakarta.enterprise.event.Reception; import jakarta.enterprise.event.TransactionPhase; @@ -15,33 +14,33 @@ class ObserverInfoImpl implements ObserverInfo { private final org.jboss.jandex.IndexView jandexIndex; - private final AllAnnotationOverlays annotationOverlays; + private final org.jboss.jandex.MutableAnnotationOverlay annotationOverlay; private final io.quarkus.arc.processor.ObserverInfo arcObserverInfo; - ObserverInfoImpl(org.jboss.jandex.IndexView jandexIndex, AllAnnotationOverlays annotationOverlays, + ObserverInfoImpl(org.jboss.jandex.IndexView jandexIndex, org.jboss.jandex.MutableAnnotationOverlay annotationOverlay, io.quarkus.arc.processor.ObserverInfo arcObserverInfo) { this.jandexIndex = jandexIndex; - this.annotationOverlays = annotationOverlays; + this.annotationOverlay = annotationOverlay; this.arcObserverInfo = arcObserverInfo; } @Override public Type eventType() { - return TypeImpl.fromJandexType(jandexIndex, annotationOverlays, arcObserverInfo.getObservedType()); + return TypeImpl.fromJandexType(jandexIndex, annotationOverlay, arcObserverInfo.getObservedType()); } @Override public Collection qualifiers() { return arcObserverInfo.getQualifiers() .stream() - .map(it -> new AnnotationInfoImpl(jandexIndex, annotationOverlays, it)) - .collect(Collectors.toUnmodifiableList()); + .map(it -> (AnnotationInfo) new AnnotationInfoImpl(jandexIndex, annotationOverlay, it)) + .toList(); } @Override public ClassInfo declaringClass() { org.jboss.jandex.ClassInfo jandexClass = jandexIndex.getClassByName(arcObserverInfo.getBeanClass()); - return new ClassInfoImpl(jandexIndex, annotationOverlays, jandexClass); + return new ClassInfoImpl(jandexIndex, annotationOverlay, jandexClass); } @Override @@ -49,7 +48,7 @@ public MethodInfo observerMethod() { if (arcObserverInfo.isSynthetic()) { return null; } - return new MethodInfoImpl(jandexIndex, annotationOverlays, arcObserverInfo.getObserverMethod()); + return new MethodInfoImpl(jandexIndex, annotationOverlay, arcObserverInfo.getObserverMethod()); } @Override @@ -58,7 +57,7 @@ public ParameterInfo eventParameter() { return null; } org.jboss.jandex.MethodParameterInfo jandexParameter = arcObserverInfo.getEventParameter(); - return new ParameterInfoImpl(jandexIndex, annotationOverlays, jandexParameter); + return new ParameterInfoImpl(jandexIndex, annotationOverlay, jandexParameter); } @Override @@ -66,7 +65,7 @@ public BeanInfo bean() { if (arcObserverInfo.isSynthetic()) { return null; } - return BeanInfoImpl.create(jandexIndex, annotationOverlays, arcObserverInfo.getDeclaringBean()); + return BeanInfoImpl.create(jandexIndex, annotationOverlay, arcObserverInfo.getDeclaringBean()); } @Override diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/PackageInfoImpl.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/PackageInfoImpl.java index 3b103594512e4..6c54401300e2a 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/PackageInfoImpl.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/PackageInfoImpl.java @@ -1,96 +1,15 @@ package io.quarkus.arc.processor.bcextensions; -import java.lang.annotation.Annotation; -import java.util.Collection; -import java.util.Objects; -import java.util.function.Predicate; -import java.util.stream.Collectors; - -import jakarta.enterprise.lang.model.AnnotationInfo; import jakarta.enterprise.lang.model.declarations.PackageInfo; -class PackageInfoImpl implements PackageInfo { - final org.jboss.jandex.IndexView jandexIndex; - final AllAnnotationOverlays annotationOverlays; - final org.jboss.jandex.ClassInfo jandexDeclaration; // package-info.class - - private AnnotationSet annotationSet; - - PackageInfoImpl(org.jboss.jandex.IndexView jandexIndex, AllAnnotationOverlays annotationOverlays, +class PackageInfoImpl extends DeclarationInfoImpl implements PackageInfo { + PackageInfoImpl(org.jboss.jandex.IndexView jandexIndex, org.jboss.jandex.MutableAnnotationOverlay annotationOverlay, org.jboss.jandex.ClassInfo jandexDeclaration) { - this.jandexIndex = jandexIndex; - this.annotationOverlays = annotationOverlays; - this.jandexDeclaration = jandexDeclaration; + super(jandexIndex, annotationOverlay, jandexDeclaration); } @Override public String name() { return jandexDeclaration.name().packagePrefix(); } - - private AnnotationSet annotationSet() { - if (annotationSet == null) { - annotationSet = new AnnotationSet(jandexDeclaration.declaredAnnotations()); - } - - return annotationSet; - } - - @Override - public boolean hasAnnotation(Class annotationType) { - return annotationSet().hasAnnotation(annotationType); - } - - @Override - public boolean hasAnnotation(Predicate predicate) { - return annotationSet().annotations() - .stream() - .anyMatch(it -> predicate.test(new AnnotationInfoImpl(jandexIndex, annotationOverlays, it))); - } - - @Override - public AnnotationInfo annotation(Class annotationType) { - org.jboss.jandex.AnnotationInstance jandexAnnotation = annotationSet().annotation(annotationType); - if (jandexAnnotation == null) { - return null; - } - return new AnnotationInfoImpl(jandexIndex, annotationOverlays, jandexAnnotation); - } - - @Override - public Collection repeatableAnnotation(Class annotationType) { - return annotationSet().annotationsWithRepeatable(annotationType) - .stream() - .map(it -> new AnnotationInfoImpl(jandexIndex, annotationOverlays, it)) - .collect(Collectors.toUnmodifiableList()); - } - - @Override - public Collection annotations(Predicate predicate) { - return annotationSet().annotations() - .stream() - .map(it -> new AnnotationInfoImpl(jandexIndex, annotationOverlays, it)) - .filter(predicate) - .collect(Collectors.toUnmodifiableList()); - } - - @Override - public Collection annotations() { - return annotations(it -> true); - } - - @Override - public boolean equals(Object o) { - if (this == o) - return true; - if (o == null || getClass() != o.getClass()) - return false; - PackageInfoImpl that = (PackageInfoImpl) o; - return Objects.equals(jandexDeclaration.name(), that.jandexDeclaration.name()); - } - - @Override - public int hashCode() { - return Objects.hash(jandexDeclaration.name()); - } } diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ParameterConfigImpl.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ParameterConfigImpl.java index 50c99e2452ef4..9c8435baeab55 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ParameterConfigImpl.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ParameterConfigImpl.java @@ -5,13 +5,13 @@ class ParameterConfigImpl extends DeclarationConfigImpl implements ParameterConfig { - ParameterConfigImpl(org.jboss.jandex.IndexView jandexIndex, AllAnnotationTransformations allTransformations, + ParameterConfigImpl(org.jboss.jandex.IndexView jandexIndex, org.jboss.jandex.MutableAnnotationOverlay annotationOverlay, org.jboss.jandex.MethodParameterInfo jandexDeclaration) { - super(jandexIndex, allTransformations, allTransformations.parameters, jandexDeclaration); + super(jandexIndex, annotationOverlay, jandexDeclaration); } @Override public ParameterInfo info() { - return new ParameterInfoImpl(jandexIndex, allTransformations.annotationOverlays, jandexDeclaration); + return new ParameterInfoImpl(jandexIndex, annotationOverlay, jandexDeclaration); } } diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ParameterInfoImpl.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ParameterInfoImpl.java index c1ce57570be72..8c047020fa1c1 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ParameterInfoImpl.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ParameterInfoImpl.java @@ -1,21 +1,23 @@ package io.quarkus.arc.processor.bcextensions; -import java.util.Objects; +import java.lang.annotation.Annotation; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.function.Predicate; +import jakarta.enterprise.lang.model.AnnotationInfo; import jakarta.enterprise.lang.model.declarations.MethodInfo; import jakarta.enterprise.lang.model.declarations.ParameterInfo; import jakarta.enterprise.lang.model.types.Type; -class ParameterInfoImpl extends DeclarationInfoImpl implements ParameterInfo { - // only for equals/hashCode - private final MethodInfoImpl method; - private final short position; +import org.jboss.jandex.DotName; - ParameterInfoImpl(org.jboss.jandex.IndexView jandexIndex, AllAnnotationOverlays annotationOverlays, +class ParameterInfoImpl extends DeclarationInfoImpl implements ParameterInfo { + ParameterInfoImpl(org.jboss.jandex.IndexView jandexIndex, org.jboss.jandex.MutableAnnotationOverlay annotationOverlay, org.jboss.jandex.MethodParameterInfo jandexDeclaration) { - super(jandexIndex, annotationOverlays, jandexDeclaration); - this.method = new MethodInfoImpl(jandexIndex, annotationOverlays, jandexDeclaration.method()); - this.position = jandexDeclaration.position(); + super(jandexIndex, annotationOverlay, jandexDeclaration); } @Override @@ -26,37 +28,100 @@ public String name() { @Override public Type type() { - return TypeImpl.fromJandexType(jandexIndex, annotationOverlays, jandexDeclaration.type()); + return TypeImpl.fromJandexType(jandexIndex, annotationOverlay, jandexDeclaration.type()); } @Override public MethodInfo declaringMethod() { - return new MethodInfoImpl(jandexIndex, annotationOverlays, jandexDeclaration.method()); + return new MethodInfoImpl(jandexIndex, annotationOverlay, jandexDeclaration.method()); } @Override - public String toString() { - return "parameter " + name() + " of method " + jandexDeclaration.method(); + public boolean hasAnnotation(Class annotationType) { + DotName annotationName = DotName.createSimple(annotationType); + for (org.jboss.jandex.AnnotationInstance annotation : annotationOverlay.annotations(jandexDeclaration.method())) { + if (annotation.name().equals(annotationName) + && annotation.target() != null + && annotation.target().kind() == org.jboss.jandex.AnnotationTarget.Kind.METHOD_PARAMETER + && annotation.target().asMethodParameter().position() == jandexDeclaration.position()) { + return true; + } + } + return false; } @Override - AnnotationsOverlay annotationsOverlay() { - return annotationOverlays.parameters; + public boolean hasAnnotation(Predicate predicate) { + for (org.jboss.jandex.AnnotationInstance annotation : annotationOverlay.annotations(jandexDeclaration.method())) { + if (predicate.test(new AnnotationInfoImpl(jandexIndex, annotationOverlay, annotation)) + && annotation.target() != null + && annotation.target().kind() == org.jboss.jandex.AnnotationTarget.Kind.METHOD_PARAMETER + && annotation.target().asMethodParameter().position() == jandexDeclaration.position()) { + return true; + } + } + return false; } @Override - public boolean equals(Object o) { - if (this == o) - return true; - if (o == null || getClass() != o.getClass()) - return false; - ParameterInfoImpl that = (ParameterInfoImpl) o; - return position == that.position - && Objects.equals(method, that.method); + public AnnotationInfo annotation(Class annotationType) { + DotName annotationName = DotName.createSimple(annotationType); + for (org.jboss.jandex.AnnotationInstance annotation : annotationOverlay.annotations(jandexDeclaration.method())) { + if (annotation.name().equals(annotationName) + && annotation.target() != null + && annotation.target().kind() == org.jboss.jandex.AnnotationTarget.Kind.METHOD_PARAMETER + && annotation.target().asMethodParameter().position() == jandexDeclaration.position()) { + return new AnnotationInfoImpl(jandexIndex, annotationOverlay, annotation); + } + } + return null; } @Override - public int hashCode() { - return Objects.hash(method, position); + public Collection repeatableAnnotation(Class annotationType) { + List result = new ArrayList<>(); + for (org.jboss.jandex.AnnotationInstance annotation : annotationOverlay.annotationsWithRepeatable( + jandexDeclaration.method(), annotationType)) { + if (annotation.target() != null + && annotation.target().kind() == org.jboss.jandex.AnnotationTarget.Kind.METHOD_PARAMETER + && annotation.target().asMethodParameter().position() == jandexDeclaration.position()) { + result.add(new AnnotationInfoImpl(jandexIndex, annotationOverlay, annotation)); + } + } + return Collections.unmodifiableList(result); + } + + @Override + public Collection annotations(Predicate predicate) { + List result = new ArrayList<>(); + for (org.jboss.jandex.AnnotationInstance annotation : annotationOverlay.annotations(jandexDeclaration.method())) { + if (annotation.target() != null + && annotation.target().kind() == org.jboss.jandex.AnnotationTarget.Kind.METHOD_PARAMETER + && annotation.target().asMethodParameter().position() == jandexDeclaration.position()) { + AnnotationInfo annotationInfo = new AnnotationInfoImpl(jandexIndex, annotationOverlay, annotation); + if (predicate.test(annotationInfo)) { + result.add(annotationInfo); + } + } + } + return Collections.unmodifiableList(result); + } + + @Override + public Collection annotations() { + List result = new ArrayList<>(); + for (org.jboss.jandex.AnnotationInstance annotation : annotationOverlay.annotations(jandexDeclaration.method())) { + if (annotation.target() != null + && annotation.target().kind() == org.jboss.jandex.AnnotationTarget.Kind.METHOD_PARAMETER + && annotation.target().asMethodParameter().position() == jandexDeclaration.position()) { + result.add(new AnnotationInfoImpl(jandexIndex, annotationOverlay, annotation)); + } + } + return Collections.unmodifiableList(result); + } + + @Override + public String toString() { + return "parameter " + name() + " of method " + jandexDeclaration.method(); } } diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ParameterizedTypeImpl.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ParameterizedTypeImpl.java index ee87ab384ace1..09c8d5ad41f88 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ParameterizedTypeImpl.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ParameterizedTypeImpl.java @@ -1,30 +1,29 @@ package io.quarkus.arc.processor.bcextensions; import java.util.List; -import java.util.stream.Collectors; import jakarta.enterprise.lang.model.types.ClassType; import jakarta.enterprise.lang.model.types.ParameterizedType; import jakarta.enterprise.lang.model.types.Type; class ParameterizedTypeImpl extends TypeImpl implements ParameterizedType { - ParameterizedTypeImpl(org.jboss.jandex.IndexView jandexIndex, AllAnnotationOverlays annotationOverlays, + ParameterizedTypeImpl(org.jboss.jandex.IndexView jandexIndex, org.jboss.jandex.MutableAnnotationOverlay annotationOverlay, org.jboss.jandex.ParameterizedType jandexType) { - super(jandexIndex, annotationOverlays, jandexType); + super(jandexIndex, annotationOverlay, jandexType); } @Override public ClassType genericClass() { org.jboss.jandex.Type jandexClassType = org.jboss.jandex.Type.create(jandexType.name(), org.jboss.jandex.Type.Kind.CLASS); - return new ClassTypeImpl(jandexIndex, annotationOverlays, (org.jboss.jandex.ClassType) jandexClassType); + return new ClassTypeImpl(jandexIndex, annotationOverlay, (org.jboss.jandex.ClassType) jandexClassType); } @Override public List typeArguments() { return jandexType.arguments() .stream() - .map(it -> fromJandexType(jandexIndex, annotationOverlays, it)) - .collect(Collectors.toUnmodifiableList()); + .map(it -> fromJandexType(jandexIndex, annotationOverlay, it)) + .toList(); } } diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/PrimitiveTypeImpl.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/PrimitiveTypeImpl.java index 63b2f9d1a970a..0474875d4e154 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/PrimitiveTypeImpl.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/PrimitiveTypeImpl.java @@ -3,9 +3,9 @@ import jakarta.enterprise.lang.model.types.PrimitiveType; class PrimitiveTypeImpl extends TypeImpl implements PrimitiveType { - PrimitiveTypeImpl(org.jboss.jandex.IndexView jandexIndex, AllAnnotationOverlays annotationOverlays, + PrimitiveTypeImpl(org.jboss.jandex.IndexView jandexIndex, org.jboss.jandex.MutableAnnotationOverlay annotationOverlay, org.jboss.jandex.PrimitiveType jandexType) { - super(jandexIndex, annotationOverlays, jandexType); + super(jandexIndex, annotationOverlay, jandexType); } @Override @@ -16,25 +16,16 @@ public String name() { @Override public PrimitiveKind primitiveKind() { org.jboss.jandex.PrimitiveType.Primitive primitive = jandexType.primitive(); - switch (primitive) { - case BOOLEAN: - return PrimitiveKind.BOOLEAN; - case BYTE: - return PrimitiveKind.BYTE; - case SHORT: - return PrimitiveKind.SHORT; - case INT: - return PrimitiveKind.INT; - case LONG: - return PrimitiveKind.LONG; - case FLOAT: - return PrimitiveKind.FLOAT; - case DOUBLE: - return PrimitiveKind.DOUBLE; - case CHAR: - return PrimitiveKind.CHAR; - default: - throw new IllegalStateException("Unknown primitive type " + primitive); - } + return switch (primitive) { + case BOOLEAN -> PrimitiveKind.BOOLEAN; + case BYTE -> PrimitiveKind.BYTE; + case SHORT -> PrimitiveKind.SHORT; + case INT -> PrimitiveKind.INT; + case LONG -> PrimitiveKind.LONG; + case FLOAT -> PrimitiveKind.FLOAT; + case DOUBLE -> PrimitiveKind.DOUBLE; + case CHAR -> PrimitiveKind.CHAR; + default -> throw new IllegalStateException("Unknown primitive type " + primitive); + }; } } diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/RecordComponentInfoImpl.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/RecordComponentInfoImpl.java index db936b15d019f..5fc347b8ebaf0 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/RecordComponentInfoImpl.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/RecordComponentInfoImpl.java @@ -1,25 +1,16 @@ package io.quarkus.arc.processor.bcextensions; -import java.util.Objects; - import jakarta.enterprise.lang.model.declarations.ClassInfo; import jakarta.enterprise.lang.model.declarations.FieldInfo; import jakarta.enterprise.lang.model.declarations.MethodInfo; import jakarta.enterprise.lang.model.declarations.RecordComponentInfo; import jakarta.enterprise.lang.model.types.Type; -import org.jboss.jandex.DotName; - class RecordComponentInfoImpl extends DeclarationInfoImpl implements RecordComponentInfo { - // only for equals/hashCode - private final DotName className; - private final String name; - - public RecordComponentInfoImpl(org.jboss.jandex.IndexView jandexIndex, AllAnnotationOverlays annotationOverlays, + public RecordComponentInfoImpl(org.jboss.jandex.IndexView jandexIndex, + org.jboss.jandex.MutableAnnotationOverlay annotationOverlay, org.jboss.jandex.RecordComponentInfo recordComponentInfo) { - super(jandexIndex, annotationOverlays, recordComponentInfo); - this.className = recordComponentInfo.declaringClass().name(); - this.name = recordComponentInfo.name(); + super(jandexIndex, annotationOverlay, recordComponentInfo); } @Override @@ -29,43 +20,21 @@ public String name() { @Override public Type type() { - return TypeImpl.fromJandexType(jandexIndex, annotationOverlays, jandexDeclaration.type()); + return TypeImpl.fromJandexType(jandexIndex, annotationOverlay, jandexDeclaration.type()); } @Override public FieldInfo field() { - return new FieldInfoImpl(jandexIndex, annotationOverlays, jandexDeclaration.field()); + return new FieldInfoImpl(jandexIndex, annotationOverlay, jandexDeclaration.field()); } @Override public MethodInfo accessor() { - return new MethodInfoImpl(jandexIndex, annotationOverlays, jandexDeclaration.accessor()); + return new MethodInfoImpl(jandexIndex, annotationOverlay, jandexDeclaration.accessor()); } @Override public ClassInfo declaringRecord() { - return new ClassInfoImpl(jandexIndex, annotationOverlays, jandexDeclaration.declaringClass()); - } - - @Override - AnnotationsOverlay annotationsOverlay() { - // we don't care about record components at all (yet) - return null; - } - - @Override - public boolean equals(Object o) { - if (this == o) - return true; - if (!(o instanceof RecordComponentInfoImpl)) - return false; - RecordComponentInfoImpl that = (RecordComponentInfoImpl) o; - return Objects.equals(className, that.className) - && Objects.equals(name, that.name); - } - - @Override - public int hashCode() { - return Objects.hash(className, name); + return new ClassInfoImpl(jandexIndex, annotationOverlay, jandexDeclaration.declaringClass()); } } diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ScopeInfoImpl.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ScopeInfoImpl.java index d0f28f59919b9..aa83a95a1b62d 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ScopeInfoImpl.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ScopeInfoImpl.java @@ -5,20 +5,20 @@ class ScopeInfoImpl implements ScopeInfo { private final org.jboss.jandex.IndexView jandexIndex; - private final AllAnnotationOverlays annotationOverlays; + private final org.jboss.jandex.MutableAnnotationOverlay annotationOverlay; private final io.quarkus.arc.processor.ScopeInfo arcScopeInfo; - ScopeInfoImpl(org.jboss.jandex.IndexView jandexIndex, AllAnnotationOverlays annotationOverlays, + ScopeInfoImpl(org.jboss.jandex.IndexView jandexIndex, org.jboss.jandex.MutableAnnotationOverlay annotationOverlay, io.quarkus.arc.processor.ScopeInfo arcScopeInfo) { this.jandexIndex = jandexIndex; - this.annotationOverlays = annotationOverlays; + this.annotationOverlay = annotationOverlay; this.arcScopeInfo = arcScopeInfo; } @Override public ClassInfo annotation() { org.jboss.jandex.ClassInfo jandexClass = jandexIndex.getClassByName(arcScopeInfo.getDotName()); - return new ClassInfoImpl(jandexIndex, annotationOverlays, jandexClass); + return new ClassInfoImpl(jandexIndex, annotationOverlay, jandexClass); } @Override diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/StereotypeInfoImpl.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/StereotypeInfoImpl.java index 5b6c306544f2a..9970c62cbfba1 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/StereotypeInfoImpl.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/StereotypeInfoImpl.java @@ -1,7 +1,6 @@ package io.quarkus.arc.processor.bcextensions; import java.util.Collection; -import java.util.stream.Collectors; import jakarta.enterprise.inject.build.compatible.spi.ScopeInfo; import jakarta.enterprise.inject.build.compatible.spi.StereotypeInfo; @@ -9,27 +8,27 @@ class StereotypeInfoImpl implements StereotypeInfo { private final org.jboss.jandex.IndexView jandexIndex; - private final AllAnnotationOverlays annotationOverlays; + private final org.jboss.jandex.MutableAnnotationOverlay annotationOverlay; private final io.quarkus.arc.processor.StereotypeInfo arcStereotype; - StereotypeInfoImpl(org.jboss.jandex.IndexView jandexIndex, AllAnnotationOverlays annotationOverlays, + StereotypeInfoImpl(org.jboss.jandex.IndexView jandexIndex, org.jboss.jandex.MutableAnnotationOverlay annotationOverlay, io.quarkus.arc.processor.StereotypeInfo arcStereotype) { this.jandexIndex = jandexIndex; - this.annotationOverlays = annotationOverlays; + this.annotationOverlay = annotationOverlay; this.arcStereotype = arcStereotype; } @Override public ScopeInfo defaultScope() { - return new ScopeInfoImpl(jandexIndex, annotationOverlays, arcStereotype.getDefaultScope()); + return new ScopeInfoImpl(jandexIndex, annotationOverlay, arcStereotype.getDefaultScope()); } @Override public Collection interceptorBindings() { return arcStereotype.getInterceptorBindings() .stream() - .map(it -> new AnnotationInfoImpl(jandexIndex, annotationOverlays, it)) - .collect(Collectors.toUnmodifiableList()); + .map(it -> (AnnotationInfo) new AnnotationInfoImpl(jandexIndex, annotationOverlay, it)) + .toList(); } @Override diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/TypeImpl.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/TypeImpl.java index cb0c32c32220e..4ee06d21bd255 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/TypeImpl.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/TypeImpl.java @@ -1,108 +1,114 @@ package io.quarkus.arc.processor.bcextensions; import java.lang.annotation.Annotation; +import java.util.ArrayList; import java.util.Collection; -import java.util.Objects; +import java.util.Collections; +import java.util.List; import java.util.function.Predicate; -import java.util.stream.Collectors; import jakarta.enterprise.lang.model.AnnotationInfo; import jakarta.enterprise.lang.model.types.Type; import org.jboss.jandex.DotName; -abstract class TypeImpl implements Type { - final org.jboss.jandex.IndexView jandexIndex; - final AllAnnotationOverlays annotationOverlays; +abstract class TypeImpl extends AnnotationTargetImpl implements Type { final JandexType jandexType; - TypeImpl(org.jboss.jandex.IndexView jandexIndex, AllAnnotationOverlays annotationOverlays, JandexType jandexType) { - this.jandexIndex = jandexIndex; - this.annotationOverlays = annotationOverlays; + TypeImpl(org.jboss.jandex.IndexView jandexIndex, org.jboss.jandex.MutableAnnotationOverlay annotationOverlay, + JandexType jandexType) { + super(jandexIndex, annotationOverlay, org.jboss.jandex.EquivalenceKey.of(jandexType)); this.jandexType = jandexType; } - static Type fromJandexType(org.jboss.jandex.IndexView jandexIndex, AllAnnotationOverlays annotationOverlays, + static Type fromJandexType(org.jboss.jandex.IndexView jandexIndex, + org.jboss.jandex.MutableAnnotationOverlay annotationOverlay, org.jboss.jandex.Type jandexType) { - switch (jandexType.kind()) { - case VOID: - return new VoidTypeImpl(jandexIndex, annotationOverlays, jandexType.asVoidType()); - case PRIMITIVE: - return new PrimitiveTypeImpl(jandexIndex, annotationOverlays, jandexType.asPrimitiveType()); - case CLASS: - return new ClassTypeImpl(jandexIndex, annotationOverlays, jandexType.asClassType()); - case ARRAY: - return new ArrayTypeImpl(jandexIndex, annotationOverlays, jandexType.asArrayType()); - case PARAMETERIZED_TYPE: - return new ParameterizedTypeImpl(jandexIndex, annotationOverlays, jandexType.asParameterizedType()); - case TYPE_VARIABLE: - return new TypeVariableImpl(jandexIndex, annotationOverlays, jandexType.asTypeVariable()); - case UNRESOLVED_TYPE_VARIABLE: - return new UnresolvedTypeVariableImpl(jandexIndex, annotationOverlays, jandexType.asUnresolvedTypeVariable()); - case WILDCARD_TYPE: - return new WildcardTypeImpl(jandexIndex, annotationOverlays, jandexType.asWildcardType()); - default: - throw new IllegalArgumentException("Unknown type " + jandexType); - } + return switch (jandexType.kind()) { + case VOID -> new VoidTypeImpl(jandexIndex, annotationOverlay, jandexType.asVoidType()); + case PRIMITIVE -> new PrimitiveTypeImpl(jandexIndex, annotationOverlay, jandexType.asPrimitiveType()); + case CLASS -> new ClassTypeImpl(jandexIndex, annotationOverlay, jandexType.asClassType()); + case ARRAY -> new ArrayTypeImpl(jandexIndex, annotationOverlay, jandexType.asArrayType()); + case PARAMETERIZED_TYPE -> + new ParameterizedTypeImpl(jandexIndex, annotationOverlay, jandexType.asParameterizedType()); + case TYPE_VARIABLE -> new TypeVariableImpl(jandexIndex, annotationOverlay, jandexType.asTypeVariable()); + case UNRESOLVED_TYPE_VARIABLE -> + new UnresolvedTypeVariableImpl(jandexIndex, annotationOverlay, jandexType.asUnresolvedTypeVariable()); + case WILDCARD_TYPE -> new WildcardTypeImpl(jandexIndex, annotationOverlay, jandexType.asWildcardType()); + default -> throw new IllegalArgumentException("Unknown type " + jandexType); + }; } @Override public boolean hasAnnotation(Class annotationType) { - return jandexType.hasAnnotation(DotName.createSimple(annotationType.getName())); + DotName annotationName = DotName.createSimple(annotationType); + for (org.jboss.jandex.AnnotationInstance annotation : jandexType.annotations()) { + if (annotation.runtimeVisible() && annotation.name().equals(annotationName)) { + return true; + } + } + return false; } @Override public boolean hasAnnotation(Predicate predicate) { - return jandexType.annotations() - .stream() - .anyMatch(it -> predicate.test(new AnnotationInfoImpl(jandexIndex, annotationOverlays, it))); + for (org.jboss.jandex.AnnotationInstance annotation : jandexType.annotations()) { + if (annotation.runtimeVisible() + && predicate.test(new AnnotationInfoImpl(jandexIndex, annotationOverlay, annotation))) { + return true; + } + } + return false; } @Override public AnnotationInfo annotation(Class annotationType) { - return new AnnotationInfoImpl(jandexIndex, annotationOverlays, - jandexType.annotation(DotName.createSimple(annotationType.getName()))); + org.jboss.jandex.AnnotationInstance jandexAnnotation = jandexType.annotation(DotName.createSimple(annotationType)); + if (jandexAnnotation == null || !jandexAnnotation.runtimeVisible()) { + return null; + } + return new AnnotationInfoImpl(jandexIndex, annotationOverlay, jandexAnnotation); } @Override public Collection repeatableAnnotation(Class annotationType) { - return jandexType.annotationsWithRepeatable(DotName.createSimple(annotationType.getName()), jandexIndex) - .stream() - .map(it -> new AnnotationInfoImpl(jandexIndex, annotationOverlays, it)) - .collect(Collectors.toUnmodifiableList()); + List result = new ArrayList<>(); + for (org.jboss.jandex.AnnotationInstance annotation : jandexType.annotationsWithRepeatable( + DotName.createSimple(annotationType), jandexIndex)) { + if (annotation.runtimeVisible()) { + result.add(new AnnotationInfoImpl(jandexIndex, annotationOverlay, annotation)); + } + } + return Collections.unmodifiableList(result); } @Override public Collection annotations(Predicate predicate) { - return jandexType.annotations() - .stream() - .map(it -> new AnnotationInfoImpl(jandexIndex, annotationOverlays, it)) - .filter(predicate) - .collect(Collectors.toUnmodifiableList()); + List result = new ArrayList<>(); + for (org.jboss.jandex.AnnotationInstance annotation : jandexType.annotations()) { + if (annotation.runtimeVisible()) { + AnnotationInfo annotationInfo = new AnnotationInfoImpl(jandexIndex, annotationOverlay, annotation); + if (predicate.test(annotationInfo)) { + result.add(annotationInfo); + } + } + } + return Collections.unmodifiableList(result); } @Override public Collection annotations() { - return annotations(it -> true); + List result = new ArrayList<>(); + for (org.jboss.jandex.AnnotationInstance annotation : jandexType.annotations()) { + if (annotation.runtimeVisible()) { + result.add(new AnnotationInfoImpl(jandexIndex, annotationOverlay, annotation)); + } + } + return Collections.unmodifiableList(result); } @Override public String toString() { return jandexType.toString(); } - - @Override - public boolean equals(Object o) { - if (this == o) - return true; - if (!(o instanceof TypeImpl)) - return false; - TypeImpl type = (TypeImpl) o; - return Objects.equals(jandexType, type.jandexType); - } - - @Override - public int hashCode() { - return Objects.hash(jandexType); - } } diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/TypeVariableImpl.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/TypeVariableImpl.java index c4b64644050a6..debbffb5ff7f2 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/TypeVariableImpl.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/TypeVariableImpl.java @@ -1,15 +1,14 @@ package io.quarkus.arc.processor.bcextensions; import java.util.List; -import java.util.stream.Collectors; import jakarta.enterprise.lang.model.types.Type; import jakarta.enterprise.lang.model.types.TypeVariable; class TypeVariableImpl extends TypeImpl implements TypeVariable { - TypeVariableImpl(org.jboss.jandex.IndexView jandexIndex, AllAnnotationOverlays annotationOverlays, + TypeVariableImpl(org.jboss.jandex.IndexView jandexIndex, org.jboss.jandex.MutableAnnotationOverlay annotationOverlay, org.jboss.jandex.TypeVariable jandexType) { - super(jandexIndex, annotationOverlays, jandexType); + super(jandexIndex, annotationOverlay, jandexType); } @Override @@ -21,7 +20,7 @@ public String name() { public List bounds() { return jandexType.bounds() .stream() - .map(it -> fromJandexType(jandexIndex, annotationOverlays, it)) - .collect(Collectors.toUnmodifiableList()); + .map(it -> fromJandexType(jandexIndex, annotationOverlay, it)) + .toList(); } } diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/TypesImpl.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/TypesImpl.java index 097da7fc562c2..2ec98181be219 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/TypesImpl.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/TypesImpl.java @@ -16,11 +16,11 @@ class TypesImpl implements Types { private final org.jboss.jandex.IndexView jandexIndex; - private final AllAnnotationOverlays annotationOverlays; + private final org.jboss.jandex.MutableAnnotationOverlay annotationOverlay; - TypesImpl(org.jboss.jandex.IndexView jandexIndex, AllAnnotationOverlays annotationOverlays) { + TypesImpl(org.jboss.jandex.IndexView jandexIndex, org.jboss.jandex.MutableAnnotationOverlay annotationOverlay) { this.jandexIndex = jandexIndex; - this.annotationOverlays = annotationOverlays; + this.annotationOverlay = annotationOverlay; } @Override @@ -61,7 +61,7 @@ public Type of(Class clazz) { org.jboss.jandex.Type jandexType = org.jboss.jandex.Type.create(DotName.createSimple(clazz.getName()), org.jboss.jandex.Type.Kind.CLASS); - return new ClassTypeImpl(jandexIndex, annotationOverlays, jandexType.asClassType()); + return new ClassTypeImpl(jandexIndex, annotationOverlay, jandexType.asClassType()); } @@ -69,21 +69,21 @@ public Type of(Class clazz) { public VoidType ofVoid() { org.jboss.jandex.Type jandexType = org.jboss.jandex.Type.create(DotName.createSimple("void"), org.jboss.jandex.Type.Kind.VOID); - return new VoidTypeImpl(jandexIndex, annotationOverlays, jandexType.asVoidType()); + return new VoidTypeImpl(jandexIndex, annotationOverlay, jandexType.asVoidType()); } @Override public PrimitiveType ofPrimitive(PrimitiveType.PrimitiveKind kind) { org.jboss.jandex.Type jandexType = org.jboss.jandex.Type.create(DotName.createSimple(kind.name().toLowerCase()), org.jboss.jandex.Type.Kind.PRIMITIVE); - return new PrimitiveTypeImpl(jandexIndex, annotationOverlays, jandexType.asPrimitiveType()); + return new PrimitiveTypeImpl(jandexIndex, annotationOverlay, jandexType.asPrimitiveType()); } @Override public ClassType ofClass(ClassInfo clazz) { org.jboss.jandex.Type jandexType = org.jboss.jandex.Type.create(((ClassInfoImpl) clazz).jandexDeclaration.name(), org.jboss.jandex.Type.Kind.CLASS); - return new ClassTypeImpl(jandexIndex, annotationOverlays, jandexType.asClassType()); + return new ClassTypeImpl(jandexIndex, annotationOverlay, jandexType.asClassType()); } @Override @@ -94,14 +94,14 @@ public ClassType ofClass(String name) { return null; } org.jboss.jandex.Type jandexType = org.jboss.jandex.Type.create(className, org.jboss.jandex.Type.Kind.CLASS); - return new ClassTypeImpl(jandexIndex, annotationOverlays, jandexType.asClassType()); + return new ClassTypeImpl(jandexIndex, annotationOverlay, jandexType.asClassType()); } @Override public ArrayType ofArray(Type componentType, int dimensions) { org.jboss.jandex.ArrayType jandexType = org.jboss.jandex.ArrayType.create(((TypeImpl) componentType).jandexType, dimensions); - return new ArrayTypeImpl(jandexIndex, annotationOverlays, jandexType); + return new ArrayTypeImpl(jandexIndex, annotationOverlay, jandexType); } @Override @@ -130,26 +130,26 @@ private ParameterizedType parameterizedType(DotName genericTypeName, Type... typ org.jboss.jandex.ParameterizedType jandexType = org.jboss.jandex.ParameterizedType.create(genericTypeName, jandexTypeArguments, null); - return new ParameterizedTypeImpl(jandexIndex, annotationOverlays, jandexType); + return new ParameterizedTypeImpl(jandexIndex, annotationOverlay, jandexType); } @Override public WildcardType wildcardWithUpperBound(Type upperBound) { org.jboss.jandex.WildcardType jandexType = org.jboss.jandex.WildcardType .createUpperBound(((TypeImpl) upperBound).jandexType); - return new WildcardTypeImpl(jandexIndex, annotationOverlays, jandexType); + return new WildcardTypeImpl(jandexIndex, annotationOverlay, jandexType); } @Override public WildcardType wildcardWithLowerBound(Type lowerBound) { org.jboss.jandex.WildcardType jandexType = org.jboss.jandex.WildcardType .createLowerBound(((TypeImpl) lowerBound).jandexType); - return new WildcardTypeImpl(jandexIndex, annotationOverlays, jandexType); + return new WildcardTypeImpl(jandexIndex, annotationOverlay, jandexType); } @Override public WildcardType wildcardUnbounded() { - org.jboss.jandex.WildcardType jandexType = org.jboss.jandex.WildcardType.create(null, true); - return new WildcardTypeImpl(jandexIndex, annotationOverlays, jandexType); + org.jboss.jandex.WildcardType jandexType = org.jboss.jandex.WildcardType.UNBOUNDED; + return new WildcardTypeImpl(jandexIndex, annotationOverlay, jandexType); } } diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/UnresolvedTypeVariableImpl.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/UnresolvedTypeVariableImpl.java index 286941114cba8..cd9e6b57b061b 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/UnresolvedTypeVariableImpl.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/UnresolvedTypeVariableImpl.java @@ -6,9 +6,10 @@ import jakarta.enterprise.lang.model.types.TypeVariable; class UnresolvedTypeVariableImpl extends TypeImpl implements TypeVariable { - UnresolvedTypeVariableImpl(org.jboss.jandex.IndexView jandexIndex, AllAnnotationOverlays annotationOverlays, + UnresolvedTypeVariableImpl(org.jboss.jandex.IndexView jandexIndex, + org.jboss.jandex.MutableAnnotationOverlay annotationOverlay, org.jboss.jandex.UnresolvedTypeVariable jandexType) { - super(jandexIndex, annotationOverlays, jandexType); + super(jandexIndex, annotationOverlay, jandexType); } @Override @@ -18,6 +19,6 @@ public String name() { @Override public List bounds() { - return List.of(fromJandexType(jandexIndex, annotationOverlays, org.jboss.jandex.ClassType.OBJECT_TYPE)); + return List.of(fromJandexType(jandexIndex, annotationOverlay, org.jboss.jandex.ClassType.OBJECT_TYPE)); } } diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/VoidTypeImpl.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/VoidTypeImpl.java index 49ccddb43fa2c..6ad302fd8d73b 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/VoidTypeImpl.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/VoidTypeImpl.java @@ -3,9 +3,9 @@ import jakarta.enterprise.lang.model.types.VoidType; class VoidTypeImpl extends TypeImpl implements VoidType { - VoidTypeImpl(org.jboss.jandex.IndexView jandexIndex, AllAnnotationOverlays annotationOverlays, + VoidTypeImpl(org.jboss.jandex.IndexView jandexIndex, org.jboss.jandex.MutableAnnotationOverlay annotationOverlay, org.jboss.jandex.VoidType jandexType) { - super(jandexIndex, annotationOverlays, jandexType); + super(jandexIndex, annotationOverlay, jandexType); } @Override diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/WildcardTypeImpl.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/WildcardTypeImpl.java index 5566a27acbc2f..b5491dc37a465 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/WildcardTypeImpl.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/WildcardTypeImpl.java @@ -6,9 +6,9 @@ class WildcardTypeImpl extends TypeImpl implements WildcardType { private final boolean hasUpperBound; - WildcardTypeImpl(org.jboss.jandex.IndexView jandexIndex, AllAnnotationOverlays annotationOverlays, + WildcardTypeImpl(org.jboss.jandex.IndexView jandexIndex, org.jboss.jandex.MutableAnnotationOverlay annotationOverlay, org.jboss.jandex.WildcardType jandexType) { - super(jandexIndex, annotationOverlays, jandexType); + super(jandexIndex, annotationOverlay, jandexType); this.hasUpperBound = jandexType.superBound() == null; } @@ -17,7 +17,7 @@ public Type upperBound() { if (!hasUpperBound) { return null; } - return fromJandexType(jandexIndex, annotationOverlays, jandexType.extendsBound()); + return fromJandexType(jandexIndex, annotationOverlay, jandexType.extendsBound()); } @Override @@ -25,6 +25,6 @@ public Type lowerBound() { if (hasUpperBound) { return null; } - return fromJandexType(jandexIndex, annotationOverlays, jandexType.superBound()); + return fromJandexType(jandexIndex, annotationOverlay, jandexType.superBound()); } } diff --git a/independent-projects/arc/processor/src/test/java/io/quarkus/arc/processor/SubclassSkipPredicateTest.java b/independent-projects/arc/processor/src/test/java/io/quarkus/arc/processor/SubclassSkipPredicateTest.java index 0035ce54cdb7b..ee7836ed9a1d8 100644 --- a/independent-projects/arc/processor/src/test/java/io/quarkus/arc/processor/SubclassSkipPredicateTest.java +++ b/independent-projects/arc/processor/src/test/java/io/quarkus/arc/processor/SubclassSkipPredicateTest.java @@ -27,7 +27,7 @@ public void testPredicate() throws IOException { IndexView index = Index.of(Base.class, Submarine.class, Long.class, Number.class); AssignabilityCheck assignabilityCheck = new AssignabilityCheck(index, null); SubclassSkipPredicate predicate = new SubclassSkipPredicate(assignabilityCheck::isAssignableFrom, null, - Collections.emptySet(), new AnnotationStore(Collections.emptyList(), null)); + Collections.emptySet(), new AnnotationStore(index, Collections.emptyList())); ClassInfo submarineClass = index.getClassByName(DotName.createSimple(Submarine.class.getName())); predicate.startProcessing(submarineClass, submarineClass); diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/ComponentsProvider.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/ComponentsProvider.java index 6ef91d6baa22f..e306643dffbc8 100644 --- a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/ComponentsProvider.java +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/ComponentsProvider.java @@ -9,7 +9,7 @@ public interface ComponentsProvider { static Logger LOG = Logger.getLogger(ComponentsProvider.class); - Components getComponents(); + Components getComponents(CurrentContextFactory currentContextFactory); static void unableToLoadRemovedBeanType(String type, Throwable problem) { LOG.warnf("Unable to load removed bean type [%s]: %s", type, problem.toString()); diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/ContextCreator.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/ContextCreator.java index cdd590104eedb..770eeb0b2776d 100644 --- a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/ContextCreator.java +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/ContextCreator.java @@ -7,10 +7,16 @@ */ public interface ContextCreator { + /** + * This key can be used to obtain the {@link CurrentContextFactory} from the map of parameters. + */ + String KEY_CURRENT_CONTEXT_FACTORY = "io.quarkus.arc.currentContextFactory"; + /** * * @param params * @return the context instance + * @see ContextCreator#KEY_CURRENT_CONTEXT_FACTORY */ InjectableContext create(Map params); diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/ArcContainerImpl.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/ArcContainerImpl.java index 25e8e75258b5d..c63710b5cbd86 100644 --- a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/ArcContainerImpl.java +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/ArcContainerImpl.java @@ -125,7 +125,7 @@ public ArcContainerImpl(CurrentContextFactory currentContextFactory, boolean str List components = new ArrayList<>(); for (ComponentsProvider componentsProvider : ServiceLoader.load(ComponentsProvider.class)) { - components.add(componentsProvider.getComponents()); + components.add(componentsProvider.getComponents(this.currentContextFactory)); } for (Components c : components) { diff --git a/independent-projects/arc/tcks/arquillian/src/main/java/io/quarkus/arc/arquillian/Deployer.java b/independent-projects/arc/tcks/arquillian/src/main/java/io/quarkus/arc/arquillian/Deployer.java index 6f02538e4e7df..274872e6c788e 100644 --- a/independent-projects/arc/tcks/arquillian/src/main/java/io/quarkus/arc/arquillian/Deployer.java +++ b/independent-projects/arc/tcks/arquillian/src/main/java/io/quarkus/arc/arquillian/Deployer.java @@ -131,7 +131,7 @@ private void generate() throws IOException, ExecutionException, InterruptedExcep .setBuildCompatibleExtensions(buildCompatibleExtensions) .setAdditionalBeanDefiningAnnotations(Set.of( new BeanDefiningAnnotation(DotName.createSimple(ExtraBean.class)))) - .addAnnotationTransformer(new AnnotationsTransformer() { + .addAnnotationTransformation(new AnnotationsTransformer() { @Override public boolean appliesTo(AnnotationTarget.Kind kind) { return kind == AnnotationTarget.Kind.CLASS; diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/ArcTestContainer.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/ArcTestContainer.java index 9aa839dd4eead..094bf5a6db279 100644 --- a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/ArcTestContainer.java +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/ArcTestContainer.java @@ -19,6 +19,7 @@ import jakarta.enterprise.inject.build.compatible.spi.BuildCompatibleExtension; +import org.jboss.jandex.AnnotationTransformation; import org.jboss.jandex.ClassInfo; import org.jboss.jandex.CompositeIndex; import org.jboss.jandex.DotName; @@ -83,7 +84,7 @@ public static class Builder { private final List qualifierRegistrars; private final List interceptorBindingRegistrars; private final List stereotypeRegistrars; - private final List annotationsTransformers; + private final List annotationsTransformers; private final List injectionsPointsTransformers; private final List observerTransformers; private final List beanDeploymentValidators; @@ -152,11 +153,20 @@ public Builder contextRegistrars(ContextRegistrar... registrars) { return this; } + /** + * @deprecated use {@link #annotationTransformations(AnnotationTransformation...)} + */ + @Deprecated(forRemoval = true) public Builder annotationsTransformers(AnnotationsTransformer... transformers) { Collections.addAll(this.annotationsTransformers, transformers); return this; } + public Builder annotationTransformations(AnnotationTransformation... transformations) { + Collections.addAll(this.annotationsTransformers, transformations); + return this; + } + public Builder injectionPointsTransformers(InjectionPointsTransformer... transformers) { Collections.addAll(this.injectionsPointsTransformers, transformers); return this; @@ -247,7 +257,7 @@ public ArcTestContainer build() { private final List qualifierRegistrars; private final List interceptorBindingRegistrars; private final List stereotypeRegistrars; - private final List annotationsTransformers; + private final List annotationsTransformers; private final List injectionPointsTransformers; private final List observerTransformers; private final List beanDeploymentValidators; @@ -444,7 +454,7 @@ private ClassLoader init(ExtensionContext context) { qualifierRegistrars.forEach(builder::addQualifierRegistrar); interceptorBindingRegistrars.forEach(builder::addInterceptorBindingRegistrar); stereotypeRegistrars.forEach(builder::addStereotypeRegistrar); - annotationsTransformers.forEach(builder::addAnnotationTransformer); + annotationsTransformers.forEach(builder::addAnnotationTransformation); injectionPointsTransformers.forEach(builder::addInjectionPointTransformer); observerTransformers.forEach(builder::addObserverTransformer); beanDeploymentValidators.forEach(builder::addBeanDeploymentValidator); diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/buildextension/annotations/AddObservesTest.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/buildextension/annotations/AddObservesTest.java index 67fc97c3f8995..15772311aa368 100644 --- a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/buildextension/annotations/AddObservesTest.java +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/buildextension/annotations/AddObservesTest.java @@ -24,7 +24,7 @@ public class AddObservesTest { @RegisterExtension public ArcTestContainer container = ArcTestContainer.builder().beanClasses(IWantToObserve.class) - .annotationsTransformers(new AnnotationsTransformer() { + .annotationTransformations(new AnnotationsTransformer() { @Override public boolean appliesTo(Kind kind) { diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/buildextension/annotations/AnnotationsTransformerBuilderTest.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/buildextension/annotations/AnnotationsTransformerBuilderTest.java index eb6c7445865ca..94042d0c9b082 100644 --- a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/buildextension/annotations/AnnotationsTransformerBuilderTest.java +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/buildextension/annotations/AnnotationsTransformerBuilderTest.java @@ -17,7 +17,7 @@ public class AnnotationsTransformerBuilderTest extends AbstractTransformerBuilde @RegisterExtension public ArcTestContainer container = ArcTestContainer.builder() .beanClasses(Seven.class, One.class, IWantToBeABean.class) - .annotationsTransformers( + .annotationTransformations( AnnotationsTransformer.builder() .appliesTo(Kind.CLASS) .whenContainsAny(Dependent.class) diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/buildextension/annotations/AnnotationsTransformerInterceptorBindingTest.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/buildextension/annotations/AnnotationsTransformerInterceptorBindingTest.java index 471f353d809e1..aac019b496c23 100644 --- a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/buildextension/annotations/AnnotationsTransformerInterceptorBindingTest.java +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/buildextension/annotations/AnnotationsTransformerInterceptorBindingTest.java @@ -17,7 +17,7 @@ public class AnnotationsTransformerInterceptorBindingTest { @RegisterExtension public ArcTestContainer container = ArcTestContainer.builder() .beanClasses(IWantToBeIntercepted.class, Simple.class, SimpleInterceptor.class) - .annotationsTransformers(new SimpleTransformer()) + .annotationTransformations(new SimpleTransformer()) .build(); @Test diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/buildextension/annotations/AnnotationsTransformerSpecificBuildersTest.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/buildextension/annotations/AnnotationsTransformerSpecificBuildersTest.java index 54a917288bc74..19b019064ff30 100644 --- a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/buildextension/annotations/AnnotationsTransformerSpecificBuildersTest.java +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/buildextension/annotations/AnnotationsTransformerSpecificBuildersTest.java @@ -16,7 +16,7 @@ public class AnnotationsTransformerSpecificBuildersTest extends AbstractTransfor @RegisterExtension public ArcTestContainer container = ArcTestContainer.builder() .beanClasses(Seven.class, One.class, IWantToBeABean.class) - .annotationsTransformers( + .annotationTransformations( AnnotationsTransformer.appliedToClass() .whenContainsAny(Dependent.class) .whenClass(c -> c.name().toString().equals(One.class.getName())) diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/buildextension/annotations/AnnotationsTransformerTest.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/buildextension/annotations/AnnotationsTransformerTest.java index 56d5cc8788669..76bc50eb7cbcc 100644 --- a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/buildextension/annotations/AnnotationsTransformerTest.java +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/buildextension/annotations/AnnotationsTransformerTest.java @@ -27,7 +27,7 @@ public class AnnotationsTransformerTest { @RegisterExtension public ArcTestContainer container = ArcTestContainer.builder() .beanClasses(Seven.class, One.class, IWantToBeABean.class) - .annotationsTransformers(new MyTransformer(), new DisabledTransformer()).build(); + .annotationTransformations(new MyTransformer(), new DisabledTransformer()).build(); @Test public void testVetoed() { diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/buildextension/beans/SyntheticBeanWithStereotypeTest.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/buildextension/beans/SyntheticBeanWithStereotypeTest.java index ef91c0ba05473..0ba36e319e4a9 100644 --- a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/buildextension/beans/SyntheticBeanWithStereotypeTest.java +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/buildextension/beans/SyntheticBeanWithStereotypeTest.java @@ -43,7 +43,7 @@ public class SyntheticBeanWithStereotypeTest { .beanClasses(ToBeStereotype.class, SimpleBinding.class, SimpleInterceptor.class) .additionalClasses(SomeBean.class) .stereotypeRegistrars(new MyStereotypeRegistrar()) - .annotationsTransformers(new MyAnnotationTrasnformer()) + .annotationTransformations(new MyAnnotationTrasnformer()) .beanRegistrars(new MyBeanRegistrar()) .build(); diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/buildextension/context/CustomContextTest.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/buildextension/context/CustomContextTest.java new file mode 100644 index 0000000000000..bb2f5ef047ea3 --- /dev/null +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/buildextension/context/CustomContextTest.java @@ -0,0 +1,312 @@ +package io.quarkus.arc.test.buildextension.context; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.lang.annotation.Annotation; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import java.util.Map; + +import jakarta.enterprise.context.NormalScope; +import jakarta.enterprise.context.spi.Contextual; +import jakarta.enterprise.context.spi.CreationalContext; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.arc.Arc; +import io.quarkus.arc.ArcContainer; +import io.quarkus.arc.ContextCreator; +import io.quarkus.arc.CurrentContextFactory; +import io.quarkus.arc.InjectableContext; +import io.quarkus.arc.processor.ContextConfigurator; +import io.quarkus.arc.processor.ContextRegistrar; +import io.quarkus.arc.test.ArcTestContainer; + +public class CustomContextTest { + + @RegisterExtension + public ArcTestContainer container = ArcTestContainer.builder() + .beanClasses(FieldScoped.class, MeadowScoped.class, Mina.class, InvalidNestedContext.class, + InvalidAbstractContext.class, InvalidConstructorContext.class, FieldContext.class, MeadowContext.class) + .contextRegistrars(new ContextRegistrar() { + @Override + public void register(RegistrationContext ctx) { + ContextConfigurator configurator = ctx.configure(FieldScoped.class); + assertThrows(IllegalArgumentException.class, () -> configurator.contextClass(InvalidNestedContext.class)); + assertThrows(IllegalArgumentException.class, () -> configurator.contextClass(InvalidAbstractContext.class)); + assertThrows(IllegalArgumentException.class, + () -> configurator.contextClass(InvalidConstructorContext.class)); + configurator.contextClass(FieldContext.class).done(); + } + }) + .contextRegistrars(new ContextRegistrar() { + @Override + public void register(RegistrationContext ctx) { + ctx.configure(MeadowScoped.class).creator(MeadowCreator.class).done(); + } + + }) + .build(); + + @Test + public void testCustomScope() { + ArcContainer arc = Arc.container(); + assertEquals("bac", arc.instance(Mina.class).get().bum()); + } + + @FieldScoped + public static class Mina { + + public String bum() { + return "bac"; + } + + } + + @MeadowScoped + public static class Flower { + + public void bloom() { + } + + } + + @NormalScope + @Inherited + @Target({ TYPE, METHOD, FIELD }) + @Retention(RUNTIME) + public @interface FieldScoped { + } + + @NormalScope + @Inherited + @Target({ TYPE, METHOD, FIELD }) + @Retention(RUNTIME) + public @interface MeadowScoped { + } + + public static class FieldContext implements InjectableContext { + + public FieldContext(CurrentContextFactory ccf) { + assertNotNull(ccf); + } + + @Override + public void destroy(Contextual contextual) { + throw new UnsupportedOperationException(); + } + + @Override + public Class getScope() { + return FieldScoped.class; + } + + @SuppressWarnings("unchecked") + @Override + public T get(Contextual contextual, CreationalContext creationalContext) { + return (T) new Mina(); + } + + @SuppressWarnings("unchecked") + @Override + public T get(Contextual contextual) { + return (T) new Mina(); + } + + @Override + public boolean isActive() { + return true; + } + + @Override + public void destroy() { + throw new UnsupportedOperationException(); + } + + @Override + public ContextState getState() { + throw new UnsupportedOperationException(); + } + + } + + public static class MeadowCreator implements ContextCreator { + + @Override + public InjectableContext create(Map params) { + assertNotNull(params.get(KEY_CURRENT_CONTEXT_FACTORY)); + return new MeadowContext(); + } + + } + + public static class MeadowContext implements InjectableContext { + + @Override + public void destroy(Contextual contextual) { + throw new UnsupportedOperationException(); + } + + @Override + public Class getScope() { + return MeadowScoped.class; + } + + @SuppressWarnings("unchecked") + @Override + public T get(Contextual contextual, CreationalContext creationalContext) { + return (T) new Flower(); + } + + @SuppressWarnings("unchecked") + @Override + public T get(Contextual contextual) { + return (T) new Flower(); + } + + @Override + public boolean isActive() { + return true; + } + + @Override + public void destroy() { + throw new UnsupportedOperationException(); + } + + @Override + public ContextState getState() { + throw new UnsupportedOperationException(); + } + + } + + class InvalidNestedContext implements InjectableContext { + + @Override + public void destroy(Contextual contextual) { + throw new UnsupportedOperationException(); + } + + @Override + public Class getScope() { + return FieldScoped.class; + } + + @Override + public T get(Contextual contextual, CreationalContext creationalContext) { + throw new UnsupportedOperationException(); + } + + @Override + public T get(Contextual contextual) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isActive() { + throw new UnsupportedOperationException(); + } + + @Override + public void destroy() { + throw new UnsupportedOperationException(); + } + + @Override + public ContextState getState() { + throw new UnsupportedOperationException(); + } + + } + + public abstract class InvalidAbstractContext implements InjectableContext { + + @Override + public void destroy(Contextual contextual) { + throw new UnsupportedOperationException(); + } + + @Override + public Class getScope() { + return FieldScoped.class; + } + + @Override + public T get(Contextual contextual, CreationalContext creationalContext) { + throw new UnsupportedOperationException(); + } + + @Override + public T get(Contextual contextual) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isActive() { + throw new UnsupportedOperationException(); + } + + @Override + public void destroy() { + throw new UnsupportedOperationException(); + } + + @Override + public ContextState getState() { + throw new UnsupportedOperationException(); + } + + } + + public class InvalidConstructorContext implements InjectableContext { + + public InvalidConstructorContext(Long age) { + } + + @Override + public void destroy(Contextual contextual) { + throw new UnsupportedOperationException(); + } + + @Override + public Class getScope() { + return FieldScoped.class; + } + + @Override + public T get(Contextual contextual, CreationalContext creationalContext) { + throw new UnsupportedOperationException(); + } + + @Override + public T get(Contextual contextual) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isActive() { + throw new UnsupportedOperationException(); + } + + @Override + public void destroy() { + throw new UnsupportedOperationException(); + } + + @Override + public ContextState getState() { + throw new UnsupportedOperationException(); + } + + } + +} diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/buildextension/stereotypes/AdditionalStereotypesTest.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/buildextension/stereotypes/AdditionalStereotypesTest.java index 42773d615a868..1e0f078d5a6c7 100644 --- a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/buildextension/stereotypes/AdditionalStereotypesTest.java +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/buildextension/stereotypes/AdditionalStereotypesTest.java @@ -39,7 +39,7 @@ public class AdditionalStereotypesTest { public ArcTestContainer container = ArcTestContainer.builder() .beanClasses(ToBeStereotype.class, SimpleBinding.class, SimpleInterceptor.class, SomeBean.class) .stereotypeRegistrars(new MyStereotypeRegistrar()) - .annotationsTransformers(new MyAnnotationTrasnformer()) + .annotationTransformations(new MyAnnotationTrasnformer()) .build(); @Test diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/cdi/bcextensions/ChangeObserverQualifierTest.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/cdi/bcextensions/ChangeObserverQualifierTest.java index 7dfe4e3265312..efc12e896eb74 100644 --- a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/cdi/bcextensions/ChangeObserverQualifierTest.java +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/cdi/bcextensions/ChangeObserverQualifierTest.java @@ -11,7 +11,6 @@ import jakarta.enterprise.event.Observes; import jakarta.enterprise.inject.build.compatible.spi.BuildCompatibleExtension; import jakarta.enterprise.inject.build.compatible.spi.Enhancement; -import jakarta.enterprise.inject.build.compatible.spi.Messages; import jakarta.enterprise.inject.build.compatible.spi.MethodConfig; import jakarta.inject.Inject; import jakarta.inject.Qualifier; @@ -34,12 +33,12 @@ public class ChangeObserverQualifierTest { public void test() { MyProducer myProducer = Arc.container().select(MyProducer.class).get(); myProducer.produce(); - assertEquals(MyConsumer.events, Set.of("qualified")); + assertEquals(Set.of("qualified"), MyConsumer.events); } public static class MyExtension implements BuildCompatibleExtension { @Enhancement(types = MyConsumer.class) - public void consumer(MethodConfig method, Messages messages) { + public void consumer(MethodConfig method) { switch (method.info().name()) { case "consume": method.parameters().get(0).addAnnotation(MyQualifier.class); diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/interceptors/bindings/transitive/with/transformer/TransitiveInterceptionWithTransformerApplicationTest.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/interceptors/bindings/transitive/with/transformer/TransitiveInterceptionWithTransformerApplicationTest.java index f28b55bfe6ac8..e82bde9b82f27 100644 --- a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/interceptors/bindings/transitive/with/transformer/TransitiveInterceptionWithTransformerApplicationTest.java +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/interceptors/bindings/transitive/with/transformer/TransitiveInterceptionWithTransformerApplicationTest.java @@ -23,7 +23,7 @@ public class TransitiveInterceptionWithTransformerApplicationTest { @RegisterExtension public ArcTestContainer container = ArcTestContainer.builder().beanClasses(PlainBinding.class, PlainInterceptor.class, MuchCoolerBinding.class, MuchCoolerInterceptor.class, DummyBean.class) - .annotationsTransformers(new TransitiveInterceptionWithTransformerApplicationTest.MyTransformer()).build(); + .annotationTransformations(new TransitiveInterceptionWithTransformerApplicationTest.MyTransformer()).build(); @Test public void testTransformersAreApplied() { diff --git a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/DirectoryClassPathElement.java b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/DirectoryClassPathElement.java deleted file mode 100644 index ea78be461995f..0000000000000 --- a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/DirectoryClassPathElement.java +++ /dev/null @@ -1,184 +0,0 @@ -package io.quarkus.bootstrap.classloading; - -import java.io.File; -import java.io.IOException; -import java.io.InterruptedIOException; -import java.net.MalformedURLException; -import java.net.URI; -import java.net.URL; -import java.nio.file.Files; -import java.nio.file.InvalidPathException; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.security.CodeSource; -import java.security.ProtectionDomain; -import java.security.cert.Certificate; -import java.util.HashSet; -import java.util.Set; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.stream.Stream; - -import io.quarkus.paths.DirectoryPathTree; -import io.quarkus.paths.OpenPathTree; - -/** - * A class path element that represents a file on the file system - * - * @deprecated in favor of {@link PathTreeClassPathElement} - */ -@Deprecated -public class DirectoryClassPathElement extends AbstractClassPathElement { - - private final Path root; - private final boolean runtime; - - public DirectoryClassPathElement(Path root, boolean runtime) { - assert root != null : "root is null"; - this.root = root.normalize(); - this.runtime = runtime; - } - - @Override - public T apply(Function func) { - return func.apply(new DirectoryPathTree(root)); - } - - @Override - public boolean isRuntime() { - return runtime; - } - - @Override - public Path getRoot() { - return root; - } - - @Override - public ClassPathResource getResource(String name) { - final Path file; - try { - file = root.resolve(name); - } catch (InvalidPathException ipe) { - // can't resolve the resource - return null; - } - Path normal = file.normalize(); - String cn = name; - if (File.separatorChar == '\\') { - cn = cn.replace('/', '\\'); - } - if (!normal.startsWith(file)) { - //don't allow directory escapes - return null; - } - if (normal.toString().equals(cn)) { - //this means that name is absolute (windows only, as the / would have been removed on linux) - //we don't allow absolute paths - return null; - } - if (!normal.endsWith(Paths.get(cn)) && !cn.isEmpty()) { - //make sure the case is correct - //if the file on disk does not match the case of name return null - return null; - } - - if (Files.exists(file)) { - return new ClassPathResource() { - @Override - public ClassPathElement getContainingElement() { - return DirectoryClassPathElement.this; - } - - @Override - public String getPath() { - return name; - } - - @Override - public URL getUrl() { - try { - URI uri = file.toUri(); - // the URLClassLoader doesn't add trailing slashes to directories, so we make sure we return - // the same URL as it would to avoid having QuarkusClassLoader return different URLs - // (one with a trailing slash and one without) for same resource - if (uri.getPath().endsWith("/")) { - String uriStr = uri.toString(); - return new URL(uriStr.substring(0, uriStr.length() - 1)); - } - return uri.toURL(); - } catch (MalformedURLException e) { - throw new RuntimeException(e); - } - } - - @Override - public byte[] getData() { - try { - try { - return Files.readAllBytes(file); - } catch (InterruptedIOException e) { - //if we are interrupted reading data we finish the op, then just re-interrupt the thread state - byte[] bytes = Files.readAllBytes(file); - Thread.currentThread().interrupt(); - return bytes; - } - } catch (IOException e) { - throw new RuntimeException("Unable to read " + file, e); - } - } - - @Override - public boolean isDirectory() { - return Files.isDirectory(file); - } - }; - } - return null; - } - - @Override - public Set getProvidedResources() { - try (Stream files = Files.walk(root)) { - Set paths = new HashSet<>(); - files.forEach(new Consumer() { - @Override - public void accept(Path path) { - if (!path.equals(root)) { - String st = root.relativize(path).toString(); - if (!path.getFileSystem().getSeparator().equals("/")) { - st = st.replace(path.getFileSystem().getSeparator(), "/"); - } - paths.add(st); - } - } - }); - return paths; - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - @Override - public ProtectionDomain getProtectionDomain() { - URL url = null; - try { - URI uri = root.toUri(); - url = uri.toURL(); - } catch (MalformedURLException e) { - throw new RuntimeException("Unable to create protection domain for " + root, e); - } - CodeSource codesource = new CodeSource(url, (Certificate[]) null); - return new ProtectionDomain(codesource, null); - } - - @Override - public void close() throws IOException { - //noop - } - - @Override - public String toString() { - return root.toAbsolutePath().toString(); - } -} diff --git a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/JarClassPathElement.java b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/JarClassPathElement.java deleted file mode 100644 index 6029f23a659bd..0000000000000 --- a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/JarClassPathElement.java +++ /dev/null @@ -1,319 +0,0 @@ -package io.quarkus.bootstrap.classloading; - -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.InterruptedIOException; -import java.io.UncheckedIOException; -import java.lang.reflect.Method; -import java.net.MalformedURLException; -import java.net.URISyntaxException; -import java.net.URL; -import java.nio.file.Path; -import java.security.CodeSource; -import java.security.ProtectionDomain; -import java.security.cert.Certificate; -import java.util.Enumeration; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReadWriteLock; -import java.util.concurrent.locks.ReentrantReadWriteLock; -import java.util.function.Function; -import java.util.jar.JarEntry; -import java.util.jar.JarFile; -import java.util.jar.Manifest; - -import org.jboss.logging.Logger; - -import io.quarkus.paths.OpenPathTree; -import io.quarkus.paths.PathTree; -import io.smallrye.common.io.jar.JarEntries; -import io.smallrye.common.io.jar.JarFiles; - -/** - * A class path element that represents a file on the file system - * - * @deprecated in favor of {@link PathTreeClassPathElement} - */ -@Deprecated -public class JarClassPathElement implements ClassPathElement { - - public static final int JAVA_VERSION; - - static { - int version = 8; - try { - Method versionMethod = Runtime.class.getMethod("version"); - Object v = versionMethod.invoke(null); - List list = (List) v.getClass().getMethod("version").invoke(v); - version = list.get(0); - } catch (Exception e) { - //version 8 - } - JAVA_VERSION = version; - //force this class to be loaded - //if quarkus is recompiled it needs to have already - //been loaded - //this is just a convenience for quarkus devs that means exit - //should work properly if you recompile while quarkus is running - new ZipFileMayHaveChangedException(null); - } - - private static final Logger log = Logger.getLogger(JarClassPathElement.class); - public static final String META_INF_VERSIONS = "META-INF/versions/"; - - private final File file; - private final URL jarPath; - private final Path root; - private final Lock readLock; - private final Lock writeLock; - private final boolean runtime; - - //Closing the jarFile requires the exclusive lock, while reading data from the jarFile requires the shared lock. - private final JarFile jarFile; - private volatile boolean closed; - - public JarClassPathElement(Path root, boolean runtime) { - try { - jarPath = root.toUri().toURL(); - this.root = root; - jarFile = JarFiles.create(file = root.toFile()); - ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); - this.readLock = readWriteLock.readLock(); - this.writeLock = readWriteLock.writeLock(); - } catch (IOException e) { - throw new UncheckedIOException("Error while reading file as JAR: " + root, e); - } - this.runtime = runtime; - } - - @Override - public boolean isRuntime() { - return runtime; - } - - @Override - public T apply(Function func) { - try (OpenPathTree openTree = PathTree.ofArchive(root).open()) { - return func.apply(openTree); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - } - - @Override - public Path getRoot() { - return root; - } - - @Override - public synchronized ClassPathResource getResource(String name) { - return withJarFile(new Function() { - @Override - public ClassPathResource apply(JarFile jarFile) { - JarEntry res = jarFile.getJarEntry(name); - if (res != null) { - return new ClassPathResource() { - @Override - public ClassPathElement getContainingElement() { - return JarClassPathElement.this; - } - - @Override - public String getPath() { - return name; - } - - @Override - public URL getUrl() { - try { - String realName = JarEntries.getRealName(res); - // Avoid ending the URL with / to avoid breaking compatibility - if (realName.endsWith("/")) { - realName = realName.substring(0, realName.length() - 1); - } - String urlFile = jarPath.getProtocol() + ":" + jarPath.getPath() + "!/" + realName; - return new URL("jar", null, urlFile); - } catch (MalformedURLException e) { - throw new UncheckedIOException(e); - } - } - - @Override - public byte[] getData() { - try { - return withJarFile(new Function() { - @Override - public byte[] apply(JarFile jarFile) { - try { - try { - return jarFile.getInputStream(res).readAllBytes(); - } catch (InterruptedIOException e) { - //if we are interrupted reading data we finish the op, then just re-interrupt the thread state - byte[] bytes = jarFile.getInputStream(res).readAllBytes(); - Thread.currentThread().interrupt(); - return bytes; - } - } catch (IOException e) { - if (!closed) { - throw new ZipFileMayHaveChangedException(e); - } - throw new RuntimeException("Unable to read " + name, e); - } - } - }); - } catch (ZipFileMayHaveChangedException e) { - //this is a weird corner case, that should not really affect end users, but is super annoying - //if you are actually working on Quarkus. If you rebuild quarkus while you have an application - //running reading from the rebuilt zip file will fail, but some of these classes are needed - //for a clean shutdown, so the Java process hangs and needs to be forcibly killed - //this effectively attempts to reopen the file, allowing shutdown to work. - //we need to do this here as close needs a write lock while withJarFile takes a readLock - try { - log.error("Failed to read " + name - + " attempting to re-open the zip file. It is likely a jar file changed on disk, you should shutdown your application", - e); - close(); - return getData(); - } catch (IOException ignore) { - throw new RuntimeException("Unable to read " + name, e.getCause()); - } - } - } - - @Override - public boolean isDirectory() { - return res.getName().endsWith("/"); - } - }; - } - return null; - - } - }); - } - - private T withJarFile(Function func) { - readLock.lock(); - try { - if (closed) { - //we still need this to work if it is closed, so shutdown hooks work - //once it is closed it simply does not hold on to any resources - try (JarFile jarFile = JarFiles.create(file)) { - return func.apply(jarFile); - } catch (IOException e) { - throw new RuntimeException(e); - } - } else { - return func.apply(jarFile); - } - } finally { - readLock.unlock(); - } - } - - @Override - public synchronized Set getProvidedResources() { - return withJarFile((new Function>() { - @Override - public Set apply(JarFile jarFile) { - Set paths = new HashSet<>(); - Enumeration entries = jarFile.entries(); - while (entries.hasMoreElements()) { - JarEntry entry = entries.nextElement(); - if (entry.getName().endsWith("/")) { - paths.add(entry.getName().substring(0, entry.getName().length() - 1)); - } else { - paths.add(entry.getName()); - } - } - //multi release jars can add additional entries - if (JarFiles.isMultiRelease(jarFile)) { - String[] copyToIterate = paths.toArray(new String[0]); - for (String i : copyToIterate) { - if (i.startsWith(META_INF_VERSIONS)) { - String part = i.substring(META_INF_VERSIONS.length()); - int slash = part.indexOf("/"); - if (slash != -1) { - try { - int ver = Integer.parseInt(part.substring(0, slash)); - if (ver <= JAVA_VERSION) { - paths.add(part.substring(slash + 1)); - } - } catch (NumberFormatException e) { - log.debug("Failed to parse META-INF/versions entry", e); - } - } - } - } - } - return paths; - } - })); - } - - @Override - public ProtectionDomain getProtectionDomain() { - final URL url; - try { - url = jarPath.toURI().toURL(); - } catch (URISyntaxException | MalformedURLException e) { - throw new RuntimeException("Unable to create protection domain for " + jarPath, e); - } - CodeSource codesource = new CodeSource(url, (Certificate[]) null); - return new ProtectionDomain(codesource, null, null, null); - } - - @Override - public Manifest getManifest() { - return withJarFile(new Function() { - @Override - public Manifest apply(JarFile jarFile) { - try { - return jarFile.getManifest(); - } catch (IOException e) { - log.warnf("Failed to parse manifest for %s", jarPath); - return null; - } - } - }); - } - - @Override - public void close() throws IOException { - writeLock.lock(); - try { - jarFile.close(); - closed = true; - } finally { - writeLock.unlock(); - } - } - - public static byte[] readStreamContents(InputStream inputStream) throws IOException { - try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { - byte[] buf = new byte[10000]; - int r; - while ((r = inputStream.read(buf)) > 0) { - out.write(buf, 0, r); - } - return out.toByteArray(); - } finally { - inputStream.close(); - } - } - - @Override - public String toString() { - return file.getName() + ": " + jarPath; - } - - static class ZipFileMayHaveChangedException extends RuntimeException { - public ZipFileMayHaveChangedException(Throwable cause) { - super(cause); - } - } -} diff --git a/independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/classloader/ClassLoadingInterruptTestCase.java b/independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/classloader/ClassLoadingInterruptTestCase.java index 87d545b083981..459d5857d4567 100644 --- a/independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/classloader/ClassLoadingInterruptTestCase.java +++ b/independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/classloader/ClassLoadingInterruptTestCase.java @@ -9,7 +9,7 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; -import io.quarkus.bootstrap.classloading.DirectoryClassPathElement; +import io.quarkus.bootstrap.classloading.ClassPathElement; import io.quarkus.bootstrap.classloading.QuarkusClassLoader; import io.quarkus.bootstrap.util.IoUtils; @@ -24,7 +24,7 @@ public void testClassLoaderWhenThreadInterrupted() throws Exception { jar.as(ExplodedExporter.class).exportExploded(path.toFile(), "tmp"); ClassLoader cl = QuarkusClassLoader.builder("test", getClass().getClassLoader(), false) - .addElement(new DirectoryClassPathElement(path.resolve("tmp"), true)) + .addElement(ClassPathElement.fromPath(path.resolve("tmp"), true)) .build(); Class c = cl.loadClass(InterruptClass.class.getName()); Assertions.assertNotEquals(c, InterruptClass.class); diff --git a/independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/classloader/ClassLoadingResourceUrlTestCase.java b/independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/classloader/ClassLoadingResourceUrlTestCase.java index 4504bc5aea370..66b4c658f018f 100644 --- a/independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/classloader/ClassLoadingResourceUrlTestCase.java +++ b/independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/classloader/ClassLoadingResourceUrlTestCase.java @@ -20,8 +20,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; -import io.quarkus.bootstrap.classloading.DirectoryClassPathElement; -import io.quarkus.bootstrap.classloading.JarClassPathElement; +import io.quarkus.bootstrap.classloading.ClassPathElement; import io.quarkus.bootstrap.classloading.MemoryClassPathElement; import io.quarkus.bootstrap.classloading.QuarkusClassLoader; import io.quarkus.bootstrap.util.IoUtils; @@ -43,7 +42,7 @@ public void testUrlReturnedFromClassLoaderDirectory(String testPath) throws Exce jar.as(ExplodedExporter.class).exportExploded(path.toFile(), "tmp"); ClassLoader cl = QuarkusClassLoader.builder("test", getClass().getClassLoader(), false) - .addElement(new DirectoryClassPathElement(path.resolve("tmp"), true)) + .addElement(ClassPathElement.fromPath(path.resolve("tmp"), true)) .build(); URL res = cl.getResource("a.txt"); Assertions.assertNotNull(res); @@ -81,7 +80,7 @@ public void testResourceAsStreamForDirectory(String testPath) throws Exception { try { jar.as(ExplodedExporter.class).exportExploded(tmpDir.toFile(), "tmpcltest"); final ClassLoader cl = QuarkusClassLoader.builder("test", getClass().getClassLoader(), false) - .addElement(new DirectoryClassPathElement(tmpDir.resolve("tmpcltest"), true)) + .addElement(ClassPathElement.fromPath(tmpDir.resolve("tmpcltest"), true)) .build(); try (final InputStream is = cl.getResourceAsStream("b/")) { @@ -109,7 +108,7 @@ public void testUrlReturnedFromClassLoaderJarFile(String testPath) throws Except jar.as(ZipExporter.class).exportTo(path.toFile(), true); ClassLoader cl = QuarkusClassLoader.builder("test", getClass().getClassLoader(), false) - .addElement(new JarClassPathElement(path, true)) + .addElement(ClassPathElement.fromPath(path, true)) .build(); URL res = cl.getResource("a.txt"); Assertions.assertNotNull(res); diff --git a/independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/classloader/MultiReleaseJarTestCase.java b/independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/classloader/MultiReleaseJarTestCase.java index 4ff78f79d4c42..2885f7db5cc21 100644 --- a/independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/classloader/MultiReleaseJarTestCase.java +++ b/independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/classloader/MultiReleaseJarTestCase.java @@ -1,7 +1,6 @@ package io.quarkus.bootstrap.classloader; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -18,12 +17,9 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.DisabledOnJre; -import org.junit.jupiter.api.condition.EnabledOnJre; -import org.junit.jupiter.api.condition.JRE; import org.junit.jupiter.api.io.TempDir; -import io.quarkus.bootstrap.classloading.JarClassPathElement; +import io.quarkus.bootstrap.classloading.ClassPathElement; import io.quarkus.bootstrap.classloading.QuarkusClassLoader; import io.quarkus.bootstrap.util.IoUtils; @@ -54,10 +50,9 @@ void setUp(@TempDir Path tempDirectory) throws IOException { } @Test - @DisabledOnJre(JRE.JAVA_8) public void shouldLoadMultiReleaseJarOnJDK9Plus() throws IOException { try (QuarkusClassLoader cl = QuarkusClassLoader.builder("test", getClass().getClassLoader(), false) - .addElement(new JarClassPathElement(jarPath, true)) + .addElement(ClassPathElement.fromPath(jarPath, true)) .build()) { URL resource = cl.getResource("foo.txt"); assertNotNull(resource, "foo.txt was not found in generated JAR"); @@ -70,20 +65,4 @@ public void shouldLoadMultiReleaseJarOnJDK9Plus() throws IOException { } } - @Test - @EnabledOnJre(JRE.JAVA_8) - public void shouldLoadMultiReleaseJarOnJDK8() throws IOException { - try (QuarkusClassLoader cl = QuarkusClassLoader.builder("test", getClass().getClassLoader(), false) - .addElement(new JarClassPathElement(jarPath, true)) - .build()) { - URL resource = cl.getResource("foo.txt"); - assertNotNull(resource, "foo.txt was not found in generated JAR"); - assertFalse(resource.toString().contains("META-INF/versions/9")); - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - try (InputStream is = cl.getResourceAsStream("foo.txt")) { - IoUtils.copy(baos, is); - } - assertEquals("Original", baos.toString()); - } - } } diff --git a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/IncubatingApplicationModelResolver.java b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/IncubatingApplicationModelResolver.java index a8ffbddb05703..118d9a4ab4390 100644 --- a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/IncubatingApplicationModelResolver.java +++ b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/IncubatingApplicationModelResolver.java @@ -20,10 +20,8 @@ import java.util.List; import java.util.Map; import java.util.Properties; -import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedDeque; -import java.util.concurrent.Phaser; import java.util.function.BiConsumer; import org.eclipse.aether.DefaultRepositorySystemSession; @@ -105,8 +103,6 @@ public static IncubatingApplicationModelResolver newInstance() { private final Map allExtensions = new ConcurrentHashMap<>(); private List conditionalDepsToProcess = new ArrayList<>(); - private final Collection errors = new ConcurrentLinkedDeque<>(); - private MavenArtifactResolver resolver; private List managedDeps; private ApplicationModelBuilder appBuilder; @@ -204,10 +200,9 @@ private List activateConditionalDeps() { private void processDeploymentDeps(DependencyNode root) { var app = new AppDep(root); - var phaser = new Phaser(1); - app.scheduleChildVisits(phaser, AppDep::scheduleDeploymentVisit); - phaser.arriveAndAwaitAdvance(); - assertNoErrors(); + final ModelResolutionTaskRunner taskRunner = new ModelResolutionTaskRunner(); + app.scheduleChildVisits(taskRunner, AppDep::scheduleDeploymentVisit); + taskRunner.waitForCompletion(); appBuilder.getApplicationArtifact().addDependencies(app.allDeps); for (var d : app.children) { d.addToModel(); @@ -218,88 +213,57 @@ private void processDeploymentDeps(DependencyNode root) { } } - private void assertNoErrors() { - if (!errors.isEmpty()) { - var sb = new StringBuilder( - "The following errors were encountered while processing Quarkus application dependencies:"); - log.error(sb); - var i = 1; - for (var error : errors) { - var prefix = i++ + ")"; - log.error(prefix, error); - sb.append(System.lineSeparator()).append(prefix).append(" ").append(error.getLocalizedMessage()); - for (var e : error.getStackTrace()) { - sb.append(System.lineSeparator()).append(e); - if (e.getClassName().contains("io.quarkus")) { - break; - } - } - } - throw new RuntimeException(sb.toString()); - } - } - private void injectDeployment(List activatedConditionalDeps) { final ConcurrentLinkedDeque injectQueue = new ConcurrentLinkedDeque<>(); - { - var phaser = new Phaser(1); - for (ExtensionDependency extDep : topExtensionDeps) { - phaser.register(); - CompletableFuture.runAsync(() -> { - var resolvedDep = appBuilder.getDependency(getKey(extDep.info.deploymentArtifact)); - try { - if (resolvedDep == null) { - extDep.collectDeploymentDeps(); - injectQueue.add(() -> extDep.injectDeploymentNode(null)); - } else { - // if resolvedDep isn't null, it means the deployment artifact is on the runtime classpath - // in which case we also clear the reloadable flag on it, in case it's coming from the workspace - resolvedDep.clearFlag(DependencyFlags.RELOADABLE); - } - } catch (BootstrapDependencyProcessingException e) { - errors.add(e); - } finally { - phaser.arriveAndDeregister(); - } - }); - } - // non-conditional deployment branches should be added before the activated conditional ones to have consistent - // dependency graph structures - phaser.arriveAndAwaitAdvance(); - assertNoErrors(); - } + collectDeploymentDeps(injectQueue); if (!activatedConditionalDeps.isEmpty()) { - var phaser = new Phaser(1); - for (ConditionalDependency cd : activatedConditionalDeps) { - phaser.register(); - CompletableFuture.runAsync(() -> { - var resolvedDep = appBuilder.getDependency(getKey(cd.conditionalDep.ext.info.deploymentArtifact)); - try { - if (resolvedDep == null) { - var extDep = cd.getExtensionDependency(); - extDep.collectDeploymentDeps(); - injectQueue.add(() -> extDep.injectDeploymentNode(cd.conditionalDep.ext.getParentDeploymentNode())); - } else { - // if resolvedDep isn't null, it means the deployment artifact is on the runtime classpath - // in which case we also clear the reloadable flag on it, in case it's coming from the workspace - resolvedDep.clearFlag(DependencyFlags.RELOADABLE); - } - } catch (BootstrapDependencyProcessingException e) { - errors.add(e); - } finally { - phaser.arriveAndDeregister(); - } - }); - } - phaser.arriveAndAwaitAdvance(); - assertNoErrors(); + collectConditionalDeploymentDeps(activatedConditionalDeps, injectQueue); } - for (var inject : injectQueue) { inject.run(); } } + private void collectConditionalDeploymentDeps(List activatedConditionalDeps, + ConcurrentLinkedDeque injectQueue) { + var taskRunner = new ModelResolutionTaskRunner(); + for (ConditionalDependency cd : activatedConditionalDeps) { + taskRunner.run(() -> { + var resolvedDep = appBuilder.getDependency(getKey(cd.conditionalDep.ext.info.deploymentArtifact)); + if (resolvedDep == null) { + var extDep = cd.getExtensionDependency(); + extDep.collectDeploymentDeps(); + injectQueue.add(() -> extDep.injectDeploymentNode(cd.conditionalDep.ext.getParentDeploymentNode())); + } else { + // if resolvedDep isn't null, it means the deployment artifact is on the runtime classpath + // in which case we also clear the reloadable flag on it, in case it's coming from the workspace + resolvedDep.clearFlag(DependencyFlags.RELOADABLE); + } + }); + } + taskRunner.waitForCompletion(); + } + + private void collectDeploymentDeps(ConcurrentLinkedDeque injectQueue) { + var taskRunner = new ModelResolutionTaskRunner(); + for (ExtensionDependency extDep : topExtensionDeps) { + taskRunner.run(() -> { + var resolvedDep = appBuilder.getDependency(getKey(extDep.info.deploymentArtifact)); + if (resolvedDep == null) { + extDep.collectDeploymentDeps(); + injectQueue.add(() -> extDep.injectDeploymentNode(null)); + } else { + // if resolvedDep isn't null, it means the deployment artifact is on the runtime classpath + // in which case we also clear the reloadable flag on it, in case it's coming from the workspace + resolvedDep.clearFlag(DependencyFlags.RELOADABLE); + } + }); + } + // non-conditional deployment branches should be added before the activated conditional ones to have consistent + // dependency graph structures + taskRunner.waitForCompletion(); + } + /** * Resolves and adds compile-only dependencies to the application model with the {@link DependencyFlags#COMPILE_ONLY} flag. * Compile-only dependencies are resolved as direct dependencies of the root with all the previously resolved dependencies @@ -458,10 +422,9 @@ private void processRuntimeDeps(DependencyNode root) { appRoot.walkingFlags |= COLLECT_RELOADABLE_MODULES; } - final Phaser phaser = new Phaser(1); - appRoot.scheduleChildVisits(phaser, AppDep::scheduleRuntimeVisit); - phaser.arriveAndAwaitAdvance(); - assertNoErrors(); + final ModelResolutionTaskRunner taskRunner = new ModelResolutionTaskRunner(); + appRoot.scheduleChildVisits(taskRunner, AppDep::scheduleRuntimeVisit); + taskRunner.waitForCompletion(); appBuilder.getApplicationArtifact().addDependencies(appRoot.allDeps); appRoot.setChildFlags(); } @@ -500,18 +463,9 @@ void addToModel() { } } - void scheduleDeploymentVisit(Phaser phaser) { - phaser.register(); - CompletableFuture.runAsync(() -> { - try { - visitDeploymentDependency(); - } catch (Throwable e) { - errors.add(e); - } finally { - phaser.arriveAndDeregister(); - } - }); - scheduleChildVisits(phaser, AppDep::scheduleDeploymentVisit); + void scheduleDeploymentVisit(ModelResolutionTaskRunner taskRunner) { + taskRunner.run(this::visitDeploymentDependency); + scheduleChildVisits(taskRunner, AppDep::scheduleDeploymentVisit); } void visitDeploymentDependency() { @@ -525,18 +479,9 @@ void visitDeploymentDependency() { } } - void scheduleRuntimeVisit(Phaser phaser) { - phaser.register(); - CompletableFuture.runAsync(() -> { - try { - visitRuntimeDependency(); - } catch (Throwable t) { - errors.add(t); - } finally { - phaser.arriveAndDeregister(); - } - }); - scheduleChildVisits(phaser, AppDep::scheduleRuntimeVisit); + void scheduleRuntimeVisit(ModelResolutionTaskRunner taskRunner) { + taskRunner.run(this::visitRuntimeDependency); + scheduleChildVisits(taskRunner, AppDep::scheduleRuntimeVisit); } void visitRuntimeDependency() { @@ -578,7 +523,8 @@ void visitRuntimeDependency() { } } - void scheduleChildVisits(Phaser phaser, BiConsumer childVisitor) { + void scheduleChildVisits(ModelResolutionTaskRunner taskRunner, + BiConsumer childVisitor) { var childNodes = node.getChildren(); List filtered = null; for (int i = 0; i < childNodes.size(); ++i) { @@ -605,7 +551,7 @@ void scheduleChildVisits(Phaser phaser, BiConsumer childVisitor) node.setChildren(filtered); } for (var child : children) { - childVisitor.accept(child, phaser); + childVisitor.accept(child, taskRunner); } } @@ -1081,10 +1027,9 @@ void activate() { if (collectReloadableModules) { conditionalDep.walkingFlags |= COLLECT_RELOADABLE_MODULES; } - var phaser = new Phaser(1); - conditionalDep.scheduleRuntimeVisit(phaser); - phaser.arriveAndAwaitAdvance(); - assertNoErrors(); + var taskRunner = new ModelResolutionTaskRunner(); + conditionalDep.scheduleRuntimeVisit(taskRunner); + taskRunner.waitForCompletion(); conditionalDep.setFlags(conditionalDep.walkingFlags); if (conditionalDep.parent.resolvedDep == null) { conditionalDep.parent.allDeps.add(conditionalDep.resolvedDep.getArtifactCoords()); diff --git a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/ModelResolutionTask.java b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/ModelResolutionTask.java new file mode 100644 index 0000000000000..93012967a3f6b --- /dev/null +++ b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/ModelResolutionTask.java @@ -0,0 +1,8 @@ +package io.quarkus.bootstrap.resolver.maven; + +/** + * Task related to resolution of an {@link io.quarkus.bootstrap.model.ApplicationModel} + */ +public interface ModelResolutionTask { + void run() throws Exception; +} diff --git a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/ModelResolutionTaskRunner.java b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/ModelResolutionTaskRunner.java new file mode 100644 index 0000000000000..e5ec42ce668ab --- /dev/null +++ b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/ModelResolutionTaskRunner.java @@ -0,0 +1,70 @@ +package io.quarkus.bootstrap.resolver.maven; + +import java.util.Collection; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentLinkedDeque; +import java.util.concurrent.Phaser; + +import org.jboss.logging.Logger; + +class ModelResolutionTaskRunner { + + private static final Logger log = Logger.getLogger(ModelResolutionTaskRunner.class); + + private final Phaser phaser = new Phaser(1); + + /** + * Errors caught while running tasks + */ + private final Collection errors = new ConcurrentLinkedDeque<>(); + + /** + * Runs a model resolution task asynchronously. This method may return before the task has completed. + * + * @param task task to run + */ + void run(ModelResolutionTask task) { + phaser.register(); + CompletableFuture.runAsync(() -> { + try { + task.run(); + } catch (Exception e) { + errors.add(e); + } finally { + phaser.arriveAndDeregister(); + } + }); + } + + /** + * Blocks until all the tasks have completed. + *

    + * In case some tasks failed with errors, this method will log each error and throw a {@link RuntimeException} + * with a corresponding message. + */ + void waitForCompletion() { + phaser.arriveAndAwaitAdvance(); + assertNoErrors(); + } + + private void assertNoErrors() { + if (!errors.isEmpty()) { + var sb = new StringBuilder( + "The following errors were encountered while processing Quarkus application dependencies:"); + log.error(sb); + var i = 1; + for (var error : errors) { + var prefix = i++ + ")"; + log.error(prefix, error); + sb.append(System.lineSeparator()).append(prefix).append(" ").append(error.getLocalizedMessage()); + for (var e : error.getStackTrace()) { + sb.append(System.lineSeparator()).append(e); + if (e.getClassName().contains("io.quarkus")) { + break; + } + } + } + throw new RuntimeException(sb.toString()); + } + } +} diff --git a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/workspace/WorkspaceLoader.java b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/workspace/WorkspaceLoader.java index 48408336ac644..db915361cde8a 100644 --- a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/workspace/WorkspaceLoader.java +++ b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/workspace/WorkspaceLoader.java @@ -8,9 +8,14 @@ import java.nio.file.Path; import java.nio.file.attribute.BasicFileAttributes; import java.util.ArrayList; -import java.util.HashMap; +import java.util.Collection; +import java.util.Deque; import java.util.List; import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedDeque; +import java.util.concurrent.Phaser; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import java.util.function.Function; @@ -41,6 +46,8 @@ public class WorkspaceLoader implements WorkspaceModelResolver, WorkspaceReader private static final String POM_XML = "pom.xml"; + private static final Model MISSING_MODEL = new Model(); + private static Path locateCurrentProjectPom(Path path) throws BootstrapMavenException { Path p = path; while (p != null) { @@ -53,11 +60,11 @@ private static Path locateCurrentProjectPom(Path path) throws BootstrapMavenExce throw new BootstrapMavenException("Failed to locate project pom.xml for " + path); } - private final List moduleQueue = new ArrayList<>(); - private final Map loadedPoms = new HashMap<>(); + private final Deque moduleQueue = new ConcurrentLinkedDeque<>(); + private final Map loadedPoms = new ConcurrentHashMap<>(); private final Function modelProvider; - private final Map loadedModules = new HashMap<>(); + private final Map loadedModules = new ConcurrentHashMap<>(); private final LocalWorkspace workspace = new LocalWorkspace(); private final Path currentProjectPom; @@ -102,7 +109,7 @@ private static Path locateCurrentProjectPom(Path path) throws BootstrapMavenExce private void addModulePom(Path pom) { if (pom != null) { - moduleQueue.add(new RawModule(pom)); + moduleQueue.push(new RawModule(pom)); } } @@ -152,11 +159,22 @@ LocalProject load() throws BootstrapMavenException { }; } - int i = 0; - while (i < moduleQueue.size()) { - var newModules = new ArrayList(); - while (i < moduleQueue.size()) { - loadModule(moduleQueue.get(i++), newModules); + while (!moduleQueue.isEmpty()) { + ConcurrentLinkedDeque newModules = new ConcurrentLinkedDeque<>(); + while (!moduleQueue.isEmpty()) { + final Phaser phaser = new Phaser(1); + while (!moduleQueue.isEmpty()) { + phaser.register(); + final RawModule module = moduleQueue.removeLast(); + CompletableFuture.runAsync(() -> { + try { + loadModule(module, newModules); + } finally { + phaser.arriveAndDeregister(); + } + }); + } + phaser.arriveAndAwaitAdvance(); } for (var newModule : newModules) { newModule.process(processor); @@ -169,7 +187,7 @@ LocalProject load() throws BootstrapMavenException { return currentProject.get(); } - private void loadModule(RawModule rawModule, List newModules) { + private void loadModule(RawModule rawModule, Collection newModules) { var moduleDir = rawModule.pom.getParent(); if (moduleDir == null) { moduleDir = getFsRootDir(); @@ -183,7 +201,7 @@ private void loadModule(RawModule rawModule, List newModules) { rawModule.model = readModel(rawModule.pom); } loadedPoms.put(moduleDir, rawModule.model); - if (rawModule.model == null) { + if (rawModule.model == MISSING_MODEL) { return; } @@ -212,9 +230,8 @@ private void loadModule(RawModule rawModule, List newModules) { parentDir = getFsRootDir(); } if (!loadedPoms.containsKey(parentDir)) { - var parent = new RawModule(parentPom); - rawModule.parent = parent; - moduleQueue.add(parent); + rawModule.parent = new RawModule(parentPom); + moduleQueue.push(rawModule.parent); } } } @@ -226,7 +243,7 @@ private static Path getFsRootDir() { private void queueModule(Path dir) { if (!loadedPoms.containsKey(dir)) { - moduleQueue.add(new RawModule(dir.resolve(POM_XML))); + moduleQueue.push(new RawModule(dir.resolve(POM_XML))); } } @@ -273,7 +290,7 @@ private static Model readModel(Path pom) { // which we don't support in this workspace loader log.warn("Module(s) under " + pom.getParent() + " will be handled as thirdparty dependencies because " + pom + " does not exist"); - return null; + return MISSING_MODEL; } catch (IOException e) { throw new UncheckedIOException("Failed to load POM from " + pom, e); } diff --git a/independent-projects/bootstrap/pom.xml b/independent-projects/bootstrap/pom.xml index 182488ef0c14b..0ff79277eaf33 100644 --- a/independent-projects/bootstrap/pom.xml +++ b/independent-projects/bootstrap/pom.xml @@ -37,13 +37,13 @@ 3.12.1 3.2.1 3.2.5 - 3.1.8 + 3.2.0 1.37 3.25.3 0.9.5 - 3.5.3.Final + 3.6.0.Final 5.10.2 3.9.6 0.9.0.M2 diff --git a/independent-projects/enforcer-rules/pom.xml b/independent-projects/enforcer-rules/pom.xml index c7e4d649e429c..10a937922354c 100644 --- a/independent-projects/enforcer-rules/pom.xml +++ b/independent-projects/enforcer-rules/pom.xml @@ -106,7 +106,7 @@ org.apache.groovy groovy - 4.0.20 + 4.0.21 diff --git a/independent-projects/extension-maven-plugin/pom.xml b/independent-projects/extension-maven-plugin/pom.xml index ffacc3a198c40..6d8afa21b3fb5 100644 --- a/independent-projects/extension-maven-plugin/pom.xml +++ b/independent-projects/extension-maven-plugin/pom.xml @@ -41,7 +41,7 @@ 3.12.1 3.2.1 3.2.5 - 2.17.0 + 2.17.1 1.4.1 5.10.2 @@ -391,7 +391,7 @@ org.mockito mockito-core - 5.11.0 + 5.12.0 test diff --git a/independent-projects/junit5-virtual-threads/pom.xml b/independent-projects/junit5-virtual-threads/pom.xml index 02f85408e2eaa..fd93be1a4291e 100644 --- a/independent-projects/junit5-virtual-threads/pom.xml +++ b/independent-projects/junit5-virtual-threads/pom.xml @@ -41,7 +41,7 @@ 3.12.1 3.2.1 3.2.5 - 3.1.8 + 3.2.0 2.23.0 1.9.0 @@ -201,7 +201,22 @@ clean install - + + quick-build-docs + + + quicklyDocs + + + + true + true + true + + + clean install + + quick-build-ci diff --git a/independent-projects/parent/pom.xml b/independent-projects/parent/pom.xml index 9350322de1046..46540f872be29 100644 --- a/independent-projects/parent/pom.xml +++ b/independent-projects/parent/pom.xml @@ -17,7 +17,7 @@ - 3.5.0 + 3.6.0 3.0.0 3.2.0 3.12.1 diff --git a/independent-projects/qute/pom.xml b/independent-projects/qute/pom.xml index b955103177b45..12e7dd43fc87b 100644 --- a/independent-projects/qute/pom.xml +++ b/independent-projects/qute/pom.xml @@ -40,9 +40,9 @@ UTF-8 5.10.2 3.25.3 - 3.1.8 + 3.2.0 1.8.0 - 3.5.3.Final + 3.6.0.Final 3.12.1 3.2.1 3.2.5 diff --git a/independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/beanparam/BeanParamParser.java b/independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/beanparam/BeanParamParser.java index fa2a9595cf73c..d16c9d11eb1c4 100644 --- a/independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/beanparam/BeanParamParser.java +++ b/independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/beanparam/BeanParamParser.java @@ -162,12 +162,12 @@ private static List parseInternal(ClassInfo beanParamClass, IndexView inde resultList.addAll(paramItemsForFieldsAndMethods(beanParamClass, FORM_PARAM, (annotationValue, fieldInfo) -> new FormParamItem(fieldInfo.name(), annotationValue, - fieldInfo.type().name().toString(), AsmUtil.getSignature(fieldInfo.type()), + fieldInfo.type(), AsmUtil.getSignature(fieldInfo.type()), fieldInfo.name(), partType(fieldInfo), fileName(fieldInfo), fieldInfo.hasDeclaredAnnotation(ENCODED), new FieldExtractor(null, fieldInfo.name(), fieldInfo.declaringClass().name().toString())), (annotationValue, getterMethod) -> new FormParamItem(getterMethod.name(), annotationValue, - getterMethod.returnType().name().toString(), + getterMethod.returnType(), AsmUtil.getSignature(getterMethod.returnType()), getterMethod.name(), partType(getterMethod), fileName(getterMethod), getterMethod.hasDeclaredAnnotation(ENCODED), @@ -176,13 +176,13 @@ private static List parseInternal(ClassInfo beanParamClass, IndexView inde resultList.addAll(paramItemsForFieldsAndMethods(beanParamClass, REST_FORM_PARAM, (annotationValue, fieldInfo) -> new FormParamItem(fieldInfo.name(), annotationValue != null ? annotationValue : fieldInfo.name(), - fieldInfo.type().name().toString(), AsmUtil.getSignature(fieldInfo.type()), + fieldInfo.type(), AsmUtil.getSignature(fieldInfo.type()), fieldInfo.name(), partType(fieldInfo), fileName(fieldInfo), fieldInfo.hasDeclaredAnnotation(ENCODED), new FieldExtractor(null, fieldInfo.name(), fieldInfo.declaringClass().name().toString())), (annotationValue, getterMethod) -> new FormParamItem(getterMethod.name(), annotationValue != null ? annotationValue : getterName(getterMethod), - getterMethod.returnType().name().toString(), + getterMethod.returnType(), AsmUtil.getSignature(getterMethod.returnType()), getterMethod.name(), partType(getterMethod), fileName(getterMethod), getterMethod.hasDeclaredAnnotation(ENCODED), diff --git a/independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/beanparam/FormParamItem.java b/independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/beanparam/FormParamItem.java index 2fada96647f7c..70f7007ccffd1 100644 --- a/independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/beanparam/FormParamItem.java +++ b/independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/beanparam/FormParamItem.java @@ -1,15 +1,17 @@ package org.jboss.resteasy.reactive.client.processor.beanparam; +import org.jboss.jandex.Type; + public class FormParamItem extends Item { private final String formParamName; - private final String paramType; + private final Type paramType; private final String paramSignature; private final String mimeType; private final String fileName; private final String sourceName; - public FormParamItem(String fieldName, String formParamName, String paramType, String paramSignature, + public FormParamItem(String fieldName, String formParamName, Type paramType, String paramSignature, String sourceName, String mimeType, String fileName, boolean encoded, @@ -27,7 +29,7 @@ public String getFormParamName() { return formParamName; } - public String getParamType() { + public Type getParamType() { return paramType; } diff --git a/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/EndpointIndexer.java b/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/EndpointIndexer.java index 50dd89d503d63..f4af2c7732735 100644 --- a/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/EndpointIndexer.java +++ b/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/EndpointIndexer.java @@ -110,6 +110,7 @@ import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.AnnotationTarget; +import org.jboss.jandex.AnnotationTransformation; import org.jboss.jandex.AnnotationValue; import org.jboss.jandex.ArrayType; import org.jboss.jandex.ClassInfo; @@ -257,7 +258,7 @@ protected EndpointIndexer(Builder builder) { this.classLevelExceptionMappers = builder.classLevelExceptionMappers; this.factoryCreator = builder.factoryCreator; this.resourceMethodCallback = builder.resourceMethodCallback; - this.annotationStore = new AnnotationStore(builder.annotationsTransformers); + this.annotationStore = new AnnotationStore(builder.index, builder.annotationsTransformers); this.applicationScanningResult = builder.applicationScanningResult; this.contextTypes = builder.contextTypes; this.parameterContainerTypes = builder.parameterContainerTypes; @@ -1679,7 +1680,7 @@ public static abstract class Builder, B private boolean hasRuntimeConverters; private Map> classLevelExceptionMappers; private Consumer resourceMethodCallback; - private Collection annotationsTransformers; + private Collection annotationsTransformers; private ApplicationScanningResult applicationScanningResult; private final Set contextTypes = new HashSet<>(DEFAULT_CONTEXT_TYPES); private final Set parameterContainerTypes = new HashSet<>(); @@ -1793,8 +1794,19 @@ public B setResourceMethodCallback(Consumer resourc return (B) this; } + /** + * @deprecated use {@link #setAnnotationTransformations(Collection)} + */ + @Deprecated(forRemoval = true) public B setAnnotationsTransformers(Collection annotationsTransformers) { - this.annotationsTransformers = annotationsTransformers; + List transformations = new ArrayList<>(annotationsTransformers.size()); + transformations.addAll(annotationsTransformers); + this.annotationsTransformers = transformations; + return (B) this; + } + + public B setAnnotationTransformations(Collection annotationTransformations) { + this.annotationsTransformers = annotationTransformations; return (B) this; } diff --git a/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/transformation/AbstractAnnotationsTransformation.java b/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/transformation/AbstractAnnotationsTransformation.java deleted file mode 100644 index 35c86b2451cbd..0000000000000 --- a/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/transformation/AbstractAnnotationsTransformation.java +++ /dev/null @@ -1,75 +0,0 @@ -package org.jboss.resteasy.reactive.common.processor.transformation; - -import java.lang.annotation.Annotation; -import java.util.Collection; -import java.util.Collections; -import java.util.function.Consumer; -import java.util.function.Predicate; - -import org.jboss.jandex.AnnotationInstance; -import org.jboss.jandex.AnnotationTarget; -import org.jboss.jandex.AnnotationValue; -import org.jboss.jandex.DotName; - -abstract class AbstractAnnotationsTransformation, C extends Collection> - implements AnnotationsTransformation { - - private final AnnotationTarget target; - private final Consumer resultConsumer; - protected final C modifiedAnnotations; - - /** - * - * @param annotations Mutable collection of annotations - * @param target - * @param resultConsumer - */ - public AbstractAnnotationsTransformation(C annotations, AnnotationTarget target, - Consumer resultConsumer) { - this.target = target; - this.resultConsumer = resultConsumer; - this.modifiedAnnotations = annotations; - } - - public T add(AnnotationInstance annotation) { - modifiedAnnotations.add(annotation); - return self(); - } - - public T addAll(Collection annotations) { - modifiedAnnotations.addAll(annotations); - return self(); - } - - public T addAll(AnnotationInstance... annotations) { - Collections.addAll(modifiedAnnotations, annotations); - return self(); - } - - public T add(Class annotationType, AnnotationValue... values) { - add(DotName.createSimple(annotationType.getName()), values); - return self(); - } - - public T add(DotName name, AnnotationValue... values) { - add(AnnotationInstance.create(name, target, values)); - return self(); - } - - public T remove(Predicate predicate) { - modifiedAnnotations.removeIf(predicate); - return self(); - } - - public T removeAll() { - modifiedAnnotations.clear(); - return self(); - } - - public void done() { - resultConsumer.accept(modifiedAnnotations); - } - - protected abstract T self(); - -} diff --git a/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/transformation/AnnotationStore.java b/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/transformation/AnnotationStore.java index 6dfe16c923b85..71e5692985502 100644 --- a/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/transformation/AnnotationStore.java +++ b/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/transformation/AnnotationStore.java @@ -1,20 +1,15 @@ package org.jboss.resteasy.reactive.common.processor.transformation; -import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; -import java.util.EnumMap; -import java.util.List; -import java.util.Objects; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; +import java.util.HashSet; +import java.util.Set; import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.AnnotationOverlay; import org.jboss.jandex.AnnotationTarget; -import org.jboss.jandex.AnnotationTarget.Kind; +import org.jboss.jandex.AnnotationTransformation; import org.jboss.jandex.DotName; -import org.jboss.jandex.FieldInfo; -import org.jboss.jandex.MethodInfo; +import org.jboss.jandex.IndexView; /** * Applies {@link AnnotationsTransformer}s and caches the results of transformations. @@ -24,34 +19,26 @@ */ public final class AnnotationStore { - private final ConcurrentMap> transformed; + private final AnnotationOverlay delegate; - private final EnumMap> transformersMap; + public AnnotationStore(IndexView index, Collection transformations) { + this.delegate = AnnotationOverlay.builder(index, transformations) + .compatibleMode() + .build(); + } - public AnnotationStore(Collection transformers) { - if (transformers == null || transformers.isEmpty()) { - this.transformed = null; - this.transformersMap = null; - } else { - this.transformed = new ConcurrentHashMap<>(); - this.transformersMap = new EnumMap<>(Kind.class); - this.transformersMap.put(Kind.CLASS, initTransformers(Kind.CLASS, transformers)); - this.transformersMap.put(Kind.METHOD, initTransformers(Kind.METHOD, transformers)); - this.transformersMap.put(Kind.FIELD, initTransformers(Kind.FIELD, transformers)); - } + public AnnotationOverlay overlay() { + return delegate; } /** * All {@link AnnotationsTransformer}s are applied and the result is cached. * * @param target - * @return the annotation instance for the given target + * @return the annotation instances for the given target */ public Collection getAnnotations(AnnotationTarget target) { - if (transformed != null) { - return transformed.computeIfAbsent(new AnnotationTargetKey(target), this::transform); - } - return getOriginalAnnotations(target); + return delegate.annotations(target.asDeclaration()); } /** @@ -62,147 +49,33 @@ public Collection getAnnotations(AnnotationTarget target) { * @see #getAnnotations(AnnotationTarget) */ public AnnotationInstance getAnnotation(AnnotationTarget target, DotName name) { - return Annotations.find(getAnnotations(target), name); + return delegate.annotation(target.asDeclaration(), name); } /** * * @param target * @param name - * @return {@code true} if the specified target contains the specified annotation, @{code false} otherwise + * @return {@code true} if the specified target contains the specified annotation, {@code false} otherwise * @see #getAnnotations(AnnotationTarget) */ public boolean hasAnnotation(AnnotationTarget target, DotName name) { - return Annotations.contains(getAnnotations(target), name); + return delegate.hasAnnotation(target.asDeclaration(), name); } /** * * @param target * @param names - * @return {@code true} if the specified target contains any of the specified annotations, @{code false} otherwise + * @return {@code true} if the specified target contains any of the specified annotations, {@code false} otherwise * @see #getAnnotations(AnnotationTarget) */ public boolean hasAnyAnnotation(AnnotationTarget target, Iterable names) { - return Annotations.containsAny(getAnnotations(target), names); - } - - private Collection transform(AnnotationTargetKey key) { - AnnotationTarget target = key.target; - Collection annotations = getOriginalAnnotations(target); - List transformers = transformersMap.get(target.kind()); - if (transformers.isEmpty()) { - return annotations; - } - TransformationContextImpl transformationContext = new TransformationContextImpl(target, annotations); - for (AnnotationsTransformer transformer : transformers) { - transformer.transform(transformationContext); - } - return transformationContext.getAnnotations(); - } - - private Collection getOriginalAnnotations(AnnotationTarget target) { - switch (target.kind()) { - case CLASS: - return target.asClass().declaredAnnotations(); - case METHOD: - // Note that the returning collection also contains method params annotations - return target.asMethod().annotations(); - case FIELD: - return target.asField().annotations(); - default: - throw new IllegalArgumentException("Unsupported annotation target"); - } - } - - private List initTransformers(Kind kind, Collection transformers) { - List found = new ArrayList<>(); - for (AnnotationsTransformer transformer : transformers) { - if (transformer.appliesTo(kind)) { - found.add(transformer); - } - } - if (found.isEmpty()) { - return Collections.emptyList(); - } - found.sort(AnnotationsTransformer::compare); - return found; - } - - static class TransformationContextImpl extends AnnotationsTransformationContext> - implements AnnotationsTransformer.TransformationContext { - - public TransformationContextImpl(AnnotationTarget target, - Collection annotations) { - super(target, annotations); + Set set = new HashSet<>(); + for (DotName name : names) { + set.add(name); } - - @Override - public Transformation transform() { - return new Transformation(new ArrayList<>(getAnnotations()), getTarget(), this::setAnnotations); - } - - } - - /** - * We cannot use annotation target directly as a key in a Map. Only {@link MethodInfo} overrides equals/hashCode. - */ - static final class AnnotationTargetKey { - - final AnnotationTarget target; - - public AnnotationTargetKey(AnnotationTarget target) { - this.target = target; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null) { - return false; - } - if (getClass() != obj.getClass()) { - return false; - } - AnnotationTargetKey other = (AnnotationTargetKey) obj; - if (target.kind() != other.target.kind()) { - return false; - } - switch (target.kind()) { - case METHOD: - return target.asMethod().equals(other.target); - case FIELD: - FieldInfo field = target.asField(); - FieldInfo otherField = other.target.asField(); - return Objects.equals(field.name(), otherField.name()) - && Objects.equals(field.declaringClass().name(), otherField.declaringClass().name()); - case CLASS: - return target.asClass().name().equals(other.target.asClass().name()); - default: - throw unsupportedAnnotationTarget(target); - } - } - - @Override - public int hashCode() { - switch (target.kind()) { - case METHOD: - return target.asMethod().hashCode(); - case FIELD: - return Objects.hash(target.asField().name(), target.asField().declaringClass().name()); - case CLASS: - return target.asClass().name().hashCode(); - default: - throw unsupportedAnnotationTarget(target); - } - } - - } - - private static IllegalArgumentException unsupportedAnnotationTarget(AnnotationTarget target) { - return new IllegalArgumentException("Unsupported annotation target: " + target.kind()); + return delegate.hasAnyAnnotation(target.asDeclaration(), set); } } diff --git a/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/transformation/Annotations.java b/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/transformation/Annotations.java index ecda964f1c85a..3308ecff9b3c9 100644 --- a/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/transformation/Annotations.java +++ b/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/transformation/Annotations.java @@ -1,16 +1,9 @@ package org.jboss.resteasy.reactive.common.processor.transformation; import java.util.Collection; -import java.util.Collections; -import java.util.HashSet; -import java.util.Set; -import java.util.function.Function; import org.jboss.jandex.AnnotationInstance; -import org.jboss.jandex.AnnotationTarget; -import org.jboss.jandex.AnnotationTarget.Kind; import org.jboss.jandex.DotName; -import org.jboss.jandex.MethodInfo; final class Annotations { @@ -66,64 +59,4 @@ public static boolean containsAny(Collection annotations, It return false; } - /** - * - * @param annotations - * @return the parameter annotations - */ - public static Set getParameterAnnotations(Collection annotations) { - return getAnnotations(Kind.METHOD_PARAMETER, annotations); - } - - /** - * - * @param annotations - * @return the annotations for the given kind - */ - public static Set getAnnotations(Kind kind, Collection annotations) { - return getAnnotations(kind, null, annotations); - } - - /** - * - * @param annotations - * @return the annotations for the given kind and name - */ - public static Set getAnnotations(Kind kind, DotName name, Collection annotations) { - if (annotations.isEmpty()) { - return Collections.emptySet(); - } - Set ret = new HashSet<>(); - for (AnnotationInstance annotation : annotations) { - if (kind != annotation.target().kind()) { - continue; - } - if (name != null && !annotation.name().equals(name)) { - continue; - } - ret.add(annotation); - } - return ret; - } - - /** - * - * @param transformedAnnotations - * @param method - * @param position - * @return the parameter annotations for the given position - */ - public static Set getParameterAnnotations( - Function> transformedAnnotations, MethodInfo method, - int position) { - Set annotations = new HashSet<>(); - for (AnnotationInstance annotation : transformedAnnotations.apply(method)) { - if (Kind.METHOD_PARAMETER == annotation.target().kind() - && annotation.target().asMethodParameter().position() == position) { - annotations.add(annotation); - } - } - return annotations; - } - } diff --git a/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/transformation/AnnotationsTransformationContext.java b/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/transformation/AnnotationsTransformationContext.java deleted file mode 100644 index 9b1754893e9d2..0000000000000 --- a/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/transformation/AnnotationsTransformationContext.java +++ /dev/null @@ -1,39 +0,0 @@ -package org.jboss.resteasy.reactive.common.processor.transformation; - -import java.util.Collection; - -import org.jboss.jandex.AnnotationInstance; -import org.jboss.jandex.AnnotationTarget; - -/** - * Transformation context base for an {@link AnnotationsTransformation}. - */ -abstract class AnnotationsTransformationContext> { - - protected final AnnotationTarget target; - private C annotations; - - /** - * - * @param target - * @param annotations Mutable collection of annotations - */ - public AnnotationsTransformationContext(AnnotationTarget target, - C annotations) { - this.target = target; - this.annotations = annotations; - } - - public AnnotationTarget getTarget() { - return target; - } - - public C getAnnotations() { - return annotations; - } - - void setAnnotations(C annotations) { - this.annotations = annotations; - } - -} diff --git a/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/transformation/AnnotationsTransformer.java b/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/transformation/AnnotationsTransformer.java index 922467543fc84..a85acbcfb5708 100644 --- a/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/transformation/AnnotationsTransformer.java +++ b/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/transformation/AnnotationsTransformer.java @@ -12,6 +12,7 @@ import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.AnnotationTarget; import org.jboss.jandex.AnnotationTarget.Kind; +import org.jboss.jandex.AnnotationTransformation; import org.jboss.jandex.DotName; /** @@ -22,7 +23,7 @@ * * @see Builder */ -public interface AnnotationsTransformer { +public interface AnnotationsTransformer extends AnnotationTransformation { int DEFAULT_PRIORITY = 1000; @@ -55,6 +56,46 @@ default boolean appliesTo(Kind kind) { */ void transform(TransformationContext transformationContext); + // --- + // implementation of `AnnotationTransformation` methods + + @Override + default int priority() { + return getPriority(); + } + + @Override + default boolean supports(Kind kind) { + return appliesTo(kind); + } + + @Override + default void apply(AnnotationTransformation.TransformationContext context) { + transform(new TransformationContext() { + @Override + public AnnotationTarget getTarget() { + return context.declaration(); + } + + @Override + public Collection getAnnotations() { + return context.annotations(); + } + + @Override + public Transformation transform() { + return new Transformation(context); + } + }); + } + + @Override + default boolean requiresCompatibleMode() { + return true; + } + + // --- + /** * * @return a new builder instance @@ -282,7 +323,7 @@ public int getPriority() { @Override public boolean appliesTo(Kind kind) { - return appliesTo != null ? appliesTo.test(kind) : true; + return appliesTo == null || appliesTo.test(kind); } @Override diff --git a/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/transformation/Transformation.java b/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/transformation/Transformation.java index d0536f4358706..1baa78f5c3573 100644 --- a/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/transformation/Transformation.java +++ b/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/transformation/Transformation.java @@ -1,21 +1,64 @@ package org.jboss.resteasy.reactive.common.processor.transformation; +import java.lang.annotation.Annotation; import java.util.Collection; -import java.util.function.Consumer; +import java.util.Collections; +import java.util.HashSet; +import java.util.function.Predicate; import org.jboss.jandex.AnnotationInstance; -import org.jboss.jandex.AnnotationTarget; +import org.jboss.jandex.AnnotationTransformation; +import org.jboss.jandex.AnnotationValue; +import org.jboss.jandex.DotName; -public final class Transformation extends AbstractAnnotationsTransformation> { +public final class Transformation implements AnnotationsTransformation { - public Transformation(Collection annotations, AnnotationTarget target, - Consumer> transformationConsumer) { - super(annotations, target, transformationConsumer); + private final AnnotationTransformation.TransformationContext ctx; + private final Collection modifiedAnnotations; + + Transformation(AnnotationTransformation.TransformationContext ctx) { + this.ctx = ctx; + this.modifiedAnnotations = new HashSet<>(ctx.annotations()); + } + + public Transformation add(AnnotationInstance annotation) { + modifiedAnnotations.add(annotation); + return this; } - @Override - protected Transformation self() { + public Transformation addAll(Collection annotations) { + modifiedAnnotations.addAll(annotations); return this; } + public Transformation addAll(AnnotationInstance... annotations) { + Collections.addAll(modifiedAnnotations, annotations); + return this; + } + + public Transformation add(Class annotationType, AnnotationValue... values) { + add(DotName.createSimple(annotationType.getName()), values); + return this; + } + + public Transformation add(DotName name, AnnotationValue... values) { + add(AnnotationInstance.create(name, ctx.declaration(), values)); + return this; + } + + public Transformation remove(Predicate predicate) { + modifiedAnnotations.removeIf(predicate); + return this; + } + + public Transformation removeAll() { + modifiedAnnotations.clear(); + return this; + } + + public void done() { + ctx.removeAll(); + ctx.addAll(modifiedAnnotations); + } + } diff --git a/independent-projects/resteasy-reactive/pom.xml b/independent-projects/resteasy-reactive/pom.xml index 9b8331da79859..ed63df6731bec 100644 --- a/independent-projects/resteasy-reactive/pom.xml +++ b/independent-projects/resteasy-reactive/pom.xml @@ -45,12 +45,12 @@ UTF-8 4.1.0 - 3.1.8 + 3.2.0 1.14.11 5.10.2 3.9.6 3.25.3 - 3.5.3.Final + 3.6.0.Final 3.0.6.Final 3.0.0 1.8.0 @@ -64,7 +64,7 @@ 4.5.7 5.4.0 1.0.0.Final - 2.17.0 + 2.17.1 2.6.0 3.0.2 3.0.3 @@ -72,7 +72,7 @@ 4.2.1 3.12.0 1.0.4 - 5.11.0 + 5.12.0 1.1.0 diff --git a/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/ResteasyReactiveDeploymentManager.java b/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/ResteasyReactiveDeploymentManager.java index 77d1917a5bf8b..9d9c50bc8fe3d 100644 --- a/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/ResteasyReactiveDeploymentManager.java +++ b/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/ResteasyReactiveDeploymentManager.java @@ -19,6 +19,7 @@ import jakarta.ws.rs.core.Application; +import org.jboss.jandex.AnnotationTransformation; import org.jboss.jandex.ClassInfo; import org.jboss.jandex.DotName; import org.jboss.jandex.IndexView; @@ -107,7 +108,7 @@ public static class ScanStep { private String applicationPath; private final List methodScanners = new ArrayList<>(); private final List featureScanners = new ArrayList<>(); - private final List annotationsTransformers = new ArrayList<>(); + private final List annotationsTransformers = new ArrayList<>(); public ScanStep(IndexView nonCalculatingIndex) { index = JandexUtil.createCalculatingIndex(nonCalculatingIndex); @@ -175,11 +176,20 @@ public ScanStep addFeatureScanner(FeatureScanner methodScanner) { return this; } + /** + * @deprecated use {@link #addAnnotationTransformation(AnnotationTransformation)} + */ + @Deprecated(forRemoval = true) public ScanStep addAnnotationsTransformer(AnnotationsTransformer annotationsTransformer) { this.annotationsTransformers.add(annotationsTransformer); return this; } + public ScanStep addAnnotationTransformation(AnnotationTransformation annotationTransformation) { + this.annotationsTransformers.add(annotationTransformation); + return this; + } + public String getApplicationPath() { return applicationPath; } @@ -210,7 +220,7 @@ public ScanResult scan() { .setIndex(index) .setApplicationIndex(index) .addContextTypes(contextTypes) - .setAnnotationsTransformers(annotationsTransformers) + .setAnnotationTransformations(annotationsTransformers) .setScannedResourcePaths(resources.getScannedResourcePaths()) .addParameterContainerTypes(parameterContainers) .setClassLevelExceptionMappers(new HashMap<>()) diff --git a/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/transformation/AnnotationTransformationTest.java b/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/transformation/AnnotationTransformationTest.java index f1c215f1e59cd..1fd21fc616b12 100644 --- a/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/transformation/AnnotationTransformationTest.java +++ b/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/transformation/AnnotationTransformationTest.java @@ -38,7 +38,7 @@ public class AnnotationTransformationTest { .addScanCustomizer(new Consumer() { @Override public void accept(ResteasyReactiveDeploymentManager.ScanStep scanStep) { - scanStep.addAnnotationsTransformer(new AnnotationsTransformer() { + scanStep.addAnnotationTransformation(new AnnotationsTransformer() { @Override public boolean appliesTo(AnnotationTarget.Kind kind) { return kind == AnnotationTarget.Kind.METHOD; diff --git a/independent-projects/tools/analytics-common/pom.xml b/independent-projects/tools/analytics-common/pom.xml index 7bce499adbd22..ca27a8dbd033a 100644 --- a/independent-projects/tools/analytics-common/pom.xml +++ b/independent-projects/tools/analytics-common/pom.xml @@ -16,7 +16,7 @@ 3.3.1 4.5.14 - 3.5.4 + 3.6.0 1.0.0.Final diff --git a/independent-projects/tools/pom.xml b/independent-projects/tools/pom.xml index 7d96d416624d4..ea20500ab22d4 100644 --- a/independent-projects/tools/pom.xml +++ b/independent-projects/tools/pom.xml @@ -49,17 +49,17 @@ 3.25.3 - 2.17.0 + 2.17.1 4.1.0 5.10.2 1.26.1 - 3.5.3.Final - 5.11.0 + 3.6.0.Final + 5.12.0 3.2.1 3.2.5 ${project.version} 36 - 3.1.8 + 3.2.0 2.0.2 4.2.1 diff --git a/integration-tests/hibernate-reactive-panache-kotlin/src/main/kotlin/io/quarkus/it/panache/reactive/kotlin/TestEndpoint.kt b/integration-tests/hibernate-reactive-panache-kotlin/src/main/kotlin/io/quarkus/it/panache/reactive/kotlin/TestEndpoint.kt index b7dfab5f4fc98..1dfba080f5150 100644 --- a/integration-tests/hibernate-reactive-panache-kotlin/src/main/kotlin/io/quarkus/it/panache/reactive/kotlin/TestEndpoint.kt +++ b/integration-tests/hibernate-reactive-panache-kotlin/src/main/kotlin/io/quarkus/it/panache/reactive/kotlin/TestEndpoint.kt @@ -1263,8 +1263,8 @@ class TestEndpoint { .onItem() .invoke { _ -> Assertions.fail("Did not throw " + exceptionClass.name) } .onFailure(exceptionClass) - .recoverWithItem { null } - .map { null } + .recoverWithItem { -> null } + .map { it: Any? -> null } } private fun testPersist(persistsTest: PersistTest): Uni { diff --git a/integration-tests/kafka-json-schema-apicurio2/pom.xml b/integration-tests/kafka-json-schema-apicurio2/pom.xml index 1637a01347502..dd0a1dc8d22ab 100644 --- a/integration-tests/kafka-json-schema-apicurio2/pom.xml +++ b/integration-tests/kafka-json-schema-apicurio2/pom.xml @@ -23,7 +23,7 @@ org.jetbrains.kotlin kotlin-scripting-compiler-embeddable - 1.9.23 + 2.0.0 org.json diff --git a/integration-tests/kafka-oauth-keycloak/pom.xml b/integration-tests/kafka-oauth-keycloak/pom.xml index 43f19d3917acf..31a2a1ce3161c 100644 --- a/integration-tests/kafka-oauth-keycloak/pom.xml +++ b/integration-tests/kafka-oauth-keycloak/pom.xml @@ -177,6 +177,7 @@ maven-surefire-plugin false + -Djava.security.manager=allow ${keycloak.docker.image} @@ -188,6 +189,7 @@ maven-failsafe-plugin false + -Djava.security.manager=allow ${keycloak.docker.image} ${project.basedir}/src/test/resources/keycloak/realms/kafka-authz-realm.json diff --git a/integration-tests/kafka-oauth-keycloak/src/test/java/io/quarkus/it/kafka/KafkaKeycloakTestResource.java b/integration-tests/kafka-oauth-keycloak/src/test/java/io/quarkus/it/kafka/KafkaKeycloakTestResource.java index e3319c8914171..42cb102a2f09e 100644 --- a/integration-tests/kafka-oauth-keycloak/src/test/java/io/quarkus/it/kafka/KafkaKeycloakTestResource.java +++ b/integration-tests/kafka-oauth-keycloak/src/test/java/io/quarkus/it/kafka/KafkaKeycloakTestResource.java @@ -29,7 +29,7 @@ public Map start() { client.createRealmFromPath(KEYCLOAK_REALM_JSON); //Start kafka container - this.kafka = new StrimziKafkaContainer("quay.io/strimzi/kafka:latest-kafka-3.0.0") + this.kafka = new StrimziKafkaContainer("quay.io/strimzi/kafka:latest-kafka-3.7.0") .withBrokerId(1) .withKafkaConfigurationMap(Map.of("listener.security.protocol.map", "JWT:SASL_PLAINTEXT,BROKER1:PLAINTEXT", diff --git a/integration-tests/keycloak-authorization/pom.xml b/integration-tests/keycloak-authorization/pom.xml index 08c35534b29df..6423b5716bffa 100644 --- a/integration-tests/keycloak-authorization/pom.xml +++ b/integration-tests/keycloak-authorization/pom.xml @@ -107,7 +107,7 @@ test - net.sourceforge.htmlunit + org.htmlunit htmlunit test diff --git a/integration-tests/keycloak-authorization/src/main/resources/application.properties b/integration-tests/keycloak-authorization/src/main/resources/application.properties index 18e8a230fc3cf..d9cc1995242d5 100644 --- a/integration-tests/keycloak-authorization/src/main/resources/application.properties +++ b/integration-tests/keycloak-authorization/src/main/resources/application.properties @@ -92,3 +92,5 @@ admin-url=${keycloak.url} # Configure Keycloak Admin Client quarkus.keycloak.admin-client.server-url=${admin-url} + +quarkus.log.category."org.htmlunit".level=ERROR diff --git a/integration-tests/keycloak-authorization/src/test/java/io/quarkus/it/keycloak/PolicyEnforcerTest.java b/integration-tests/keycloak-authorization/src/test/java/io/quarkus/it/keycloak/PolicyEnforcerTest.java index 0f18d3dfc0a78..3fc5fe50e69b1 100644 --- a/integration-tests/keycloak-authorization/src/test/java/io/quarkus/it/keycloak/PolicyEnforcerTest.java +++ b/integration-tests/keycloak-authorization/src/test/java/io/quarkus/it/keycloak/PolicyEnforcerTest.java @@ -8,16 +8,15 @@ import java.net.URL; import java.time.Duration; +import org.htmlunit.FailingHttpStatusCodeException; +import org.htmlunit.SilentCssErrorHandler; +import org.htmlunit.WebResponse; +import org.htmlunit.html.HtmlForm; +import org.htmlunit.html.HtmlPage; +import org.htmlunit.util.Cookie; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Test; -import com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException; -import com.gargoylesoftware.htmlunit.SilentCssErrorHandler; -import com.gargoylesoftware.htmlunit.WebResponse; -import com.gargoylesoftware.htmlunit.html.HtmlForm; -import com.gargoylesoftware.htmlunit.html.HtmlPage; -import com.gargoylesoftware.htmlunit.util.Cookie; - import io.quarkus.test.common.QuarkusTestResource; import io.quarkus.test.common.http.TestHTTPResource; import io.quarkus.test.junit.QuarkusTest; @@ -74,7 +73,7 @@ public void testUserHasSuperUserRoleWebTenant() throws Exception { } private void testWebAppTenantAllowed(String user) throws Exception { - try (final com.gargoylesoftware.htmlunit.WebClient webClient = createWebClient()) { + try (final org.htmlunit.WebClient webClient = createWebClient()) { HtmlPage page = webClient.getPage("http://localhost:8081/api-permission-webapp"); assertEquals("Sign in to quarkus", page.getTitleText()); @@ -97,7 +96,7 @@ private void testWebAppTenantAllowed(String user) throws Exception { } private void testWebAppTenantForbidden(String user) throws Exception { - try (final com.gargoylesoftware.htmlunit.WebClient webClient = createWebClient()) { + try (final org.htmlunit.WebClient webClient = createWebClient()) { HtmlPage page = webClient.getPage("http://localhost:8081/api-permission-webapp"); assertEquals("Sign in to quarkus", page.getTitleText()); @@ -122,8 +121,8 @@ private void testWebAppTenantForbidden(String user) throws Exception { } } - private com.gargoylesoftware.htmlunit.WebClient createWebClient() { - com.gargoylesoftware.htmlunit.WebClient webClient = new com.gargoylesoftware.htmlunit.WebClient(); + private org.htmlunit.WebClient createWebClient() { + org.htmlunit.WebClient webClient = new org.htmlunit.WebClient(); webClient.setCssErrorHandler(new SilentCssErrorHandler()); return webClient; } diff --git a/integration-tests/maven/src/test/java/io/quarkus/maven/it/DevMojoIT.java b/integration-tests/maven/src/test/java/io/quarkus/maven/it/DevMojoIT.java index 262bc40ceab09..f9a1957dd172a 100644 --- a/integration-tests/maven/src/test/java/io/quarkus/maven/it/DevMojoIT.java +++ b/integration-tests/maven/src/test/java/io/quarkus/maven/it/DevMojoIT.java @@ -34,6 +34,7 @@ import org.apache.commons.io.FileUtils; import org.apache.maven.shared.invoker.MavenInvocationException; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.DisabledOnOs; import org.junit.jupiter.api.condition.OS; @@ -588,6 +589,57 @@ public void testRestClientCustomHeadersExtension() throws MavenInvocationExcepti assertThat(devModeClient.getHttpResponse("/app/frontend")).isEqualTo("CustomValue1 CustomValue2"); } + @Test + public void testThatJUnitTestTemplatesWork() throws MavenInvocationException, IOException { + //we also check continuous testing + testDir = initProject("projects/test-template", "projects/test-template-processed"); + runAndCheck(); + + ContinuousTestingMavenTestUtils testingTestUtils = new ContinuousTestingMavenTestUtils(); + ContinuousTestingMavenTestUtils.TestStatus results = testingTestUtils.waitForNextCompletion(); + + //check that the tests in both modules run + Assertions.assertEquals(2, results.getTestsPassed()); + + // Re-running the tests when changes happen is covered by testThatChangesTriggerRerunsOfJUnitTestTemplates + } + + @Disabled("Not working; tracked by #40770") + @Test + public void testThatChangesTriggerRerunsOfJUnitTestTemplates() throws MavenInvocationException, IOException { + //we also check continuous testing + testDir = initProject("projects/test-template", "projects/test-template-processed"); + runAndCheck(); + + ContinuousTestingMavenTestUtils testingTestUtils = new ContinuousTestingMavenTestUtils(); + ContinuousTestingMavenTestUtils.TestStatus results = testingTestUtils.waitForNextCompletion(); + + //check that the tests in both modules run + Assertions.assertEquals(2, results.getTestsPassed()); + + // Edit the "Hello" message. + File source = new File(testDir, "src/main/java/org/acme/HelloResource.java"); + final String uuid = UUID.randomUUID().toString(); + filter(source, Collections.singletonMap("return \"hello\";", "return \"" + uuid + "\";")); + + // Wait until we get "uuid" + await() + .pollDelay(100, TimeUnit.MILLISECONDS) + .atMost(TestUtils.getDefaultTimeout(), TimeUnit.MINUTES) + .until(() -> devModeClient.getHttpResponse("/app/hello").contains(uuid)); + + await() + .pollDelay(100, TimeUnit.MILLISECONDS) + .pollInterval(1, TimeUnit.SECONDS) + .until(source::isFile); + + results = testingTestUtils.waitForNextCompletion(); + + //make sure the test is failing now + Assertions.assertEquals(0, results.getTestsPassed()); + Assertions.assertEquals(2, results.getTestsFailed()); + } + @Test public void testThatTheApplicationIsReloadedMultiModule() throws MavenInvocationException, IOException { //we also check continuous testing diff --git a/integration-tests/maven/src/test/resources-filtered/projects/test-template/pom.xml b/integration-tests/maven/src/test/resources-filtered/projects/test-template/pom.xml new file mode 100644 index 0000000000000..d41a45783c2b2 --- /dev/null +++ b/integration-tests/maven/src/test/resources-filtered/projects/test-template/pom.xml @@ -0,0 +1,121 @@ + + + 4.0.0 + + org.acme + quarkus-test-template + 1.0-SNAPSHOT + + + io.quarkus + quarkus-bom + @project.version@ + @project.version@ + ${compiler-plugin.version} + ${version.surefire.plugin} + ${maven.compiler.source} + ${maven.compiler.target} + UTF-8 + + + + + + \${quarkus.platform.group-id} + \${quarkus.platform.artifact-id} + \${quarkus.platform.version} + pom + import + + + + + + + io.quarkus + quarkus-resteasy + + + io.quarkus + quarkus-junit5 + test + + + io.rest-assured + rest-assured + test + + + + + + + maven-surefire-plugin + \${surefire-plugin.version} + + + org.jboss.logmanager.LogManager + \${maven.home} + + + + + io.quarkus + quarkus-maven-plugin + \${quarkus-plugin.version} + + + + build + + + + + + + + + + + native + + + native + + + + true + + + + + org.apache.maven.plugins + maven-surefire-plugin + + \${native.surefire.skip} + + + + maven-failsafe-plugin + \${surefire-plugin.version} + + + + integration-test + verify + + + + \${project.build.directory}/\${project.build.finalName}-runner + org.jboss.logmanager.LogManager + \${maven.home} + + + + + + + + + + diff --git a/integration-tests/maven/src/test/resources-filtered/projects/test-template/src/main/java/org/acme/HelloResource.java b/integration-tests/maven/src/test/resources-filtered/projects/test-template/src/main/java/org/acme/HelloResource.java new file mode 100644 index 0000000000000..fbb27b57b28fc --- /dev/null +++ b/integration-tests/maven/src/test/resources-filtered/projects/test-template/src/main/java/org/acme/HelloResource.java @@ -0,0 +1,16 @@ +package org.acme; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +@Path("/hello") +public class HelloResource { + + @GET + @Produces(MediaType.TEXT_PLAIN) + public String hello() { + return "hello"; + } +} diff --git a/integration-tests/maven/src/test/resources-filtered/projects/test-template/src/main/java/org/acme/MyApplication.java b/integration-tests/maven/src/test/resources-filtered/projects/test-template/src/main/java/org/acme/MyApplication.java new file mode 100644 index 0000000000000..a6d66f8b9eda2 --- /dev/null +++ b/integration-tests/maven/src/test/resources-filtered/projects/test-template/src/main/java/org/acme/MyApplication.java @@ -0,0 +1,9 @@ +package org.acme; + +import jakarta.ws.rs.ApplicationPath; +import jakarta.ws.rs.core.Application; + +@ApplicationPath("/app") +public class MyApplication extends Application { + +} diff --git a/integration-tests/maven/src/test/resources-filtered/projects/test-template/src/main/resources/META-INF/beans.xml b/integration-tests/maven/src/test/resources-filtered/projects/test-template/src/main/resources/META-INF/beans.xml new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/integration-tests/maven/src/test/resources-filtered/projects/test-template/src/main/resources/META-INF/resources/index.html b/integration-tests/maven/src/test/resources-filtered/projects/test-template/src/main/resources/META-INF/resources/index.html new file mode 100644 index 0000000000000..9eee1d163a6f2 --- /dev/null +++ b/integration-tests/maven/src/test/resources-filtered/projects/test-template/src/main/resources/META-INF/resources/index.html @@ -0,0 +1,154 @@ + + + + + getting-started - 1.0-SNAPSHOT + + + + +

    + +
    +
    +

    Congratulations, you have created a new Quarkus application.

    + +

    Why do you see this?

    + +

    This page is served by Quarkus. The source is in + src/main/resources/META-INF/resources/index.html.

    + +

    What can I do from here?

    + +

    If not already done, run the application in dev mode using: mvn compile quarkus:dev. +

    +
      +
    • Add REST resources, Servlets, functions and other services in src/main/java.
    • +
    • Your static assets are located in src/main/resources/META-INF/resources.
    • +
    • Configure your application in src/main/resources/META-INF/microprofile-config.properties. +
    • +
    + +

    Do you like Quarkus?

    +

    Go give it a star on GitHub.

    + +

    How do I get rid of this page?

    +

    Just delete the src/main/resources/META-INF/resources/index.html file.

    +
    +
    +
    +

    Application

    +
      +
    • GroupId: org.acme
    • +
    • ArtifactId: getting-started
    • +
    • Version: 1.0-SNAPSHOT
    • +
    +
    + +
    +
    + + + + \ No newline at end of file diff --git a/integration-tests/maven/src/test/resources-filtered/projects/test-template/src/main/resources/application.properties b/integration-tests/maven/src/test/resources-filtered/projects/test-template/src/main/resources/application.properties new file mode 100644 index 0000000000000..07a519e0e7928 --- /dev/null +++ b/integration-tests/maven/src/test/resources-filtered/projects/test-template/src/main/resources/application.properties @@ -0,0 +1 @@ +quarkus.test.continuous-testing=enabled diff --git a/integration-tests/maven/src/test/resources-filtered/projects/test-template/src/test/java/com/acme/TemplatedTest.java b/integration-tests/maven/src/test/resources-filtered/projects/test-template/src/test/java/com/acme/TemplatedTest.java new file mode 100644 index 0000000000000..3da0b8f6fdfc7 --- /dev/null +++ b/integration-tests/maven/src/test/resources-filtered/projects/test-template/src/test/java/com/acme/TemplatedTest.java @@ -0,0 +1,23 @@ +package com.acme; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.CoreMatchers.is; + +import org.junit.jupiter.api.TestTemplate; +import org.junit.jupiter.api.extension.ExtendWith; + +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +public class TemplatedTest { + + @TestTemplate + @ExtendWith(UserIdGeneratorTestInvocationContextProvider.class) + public void testHelloEndpoint(UserIdGeneratorTestCase testCase) { + given() + .when().get("/app/hello") + .then() + .statusCode(200) + .body(is(testCase.getExpectedBody())); + } +} diff --git a/integration-tests/maven/src/test/resources-filtered/projects/test-template/src/test/java/com/acme/UserIdGeneratorTestCase.java b/integration-tests/maven/src/test/resources-filtered/projects/test-template/src/test/java/com/acme/UserIdGeneratorTestCase.java new file mode 100644 index 0000000000000..d8c44acc15b88 --- /dev/null +++ b/integration-tests/maven/src/test/resources-filtered/projects/test-template/src/test/java/com/acme/UserIdGeneratorTestCase.java @@ -0,0 +1,14 @@ +package com.acme; + +public class UserIdGeneratorTestCase { + private static int GLOBAL_ID = 0; + private final int id = GLOBAL_ID++; + + public Object getExpectedBody() { + return "hello"; + } + + public String getDisplayName() { + return "simple test template test case" + id; + } +} diff --git a/integration-tests/maven/src/test/resources-filtered/projects/test-template/src/test/java/com/acme/UserIdGeneratorTestInvocationContextProvider.java b/integration-tests/maven/src/test/resources-filtered/projects/test-template/src/test/java/com/acme/UserIdGeneratorTestInvocationContextProvider.java new file mode 100644 index 0000000000000..ffa75399c9300 --- /dev/null +++ b/integration-tests/maven/src/test/resources-filtered/projects/test-template/src/test/java/com/acme/UserIdGeneratorTestInvocationContextProvider.java @@ -0,0 +1,67 @@ +package com.acme; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Stream; + +import org.junit.jupiter.api.extension.AfterTestExecutionCallback; +import org.junit.jupiter.api.extension.BeforeTestExecutionCallback; +import org.junit.jupiter.api.extension.Extension; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.ParameterContext; +import org.junit.jupiter.api.extension.ParameterResolver; +import org.junit.jupiter.api.extension.TestTemplateInvocationContext; +import org.junit.jupiter.api.extension.TestTemplateInvocationContextProvider; + +public class UserIdGeneratorTestInvocationContextProvider implements TestTemplateInvocationContextProvider { + @Override + public boolean supportsTestTemplate(ExtensionContext extensionContext) { + return true; + } + + @Override + public Stream provideTestTemplateInvocationContexts(ExtensionContext extensionContext) { + return Stream.of( + genericContext(new UserIdGeneratorTestCase()), + genericContext(new UserIdGeneratorTestCase())); + } + + private TestTemplateInvocationContext genericContext( + UserIdGeneratorTestCase userIdGeneratorTestCase) { + return new TestTemplateInvocationContext() { + @Override + public String getDisplayName(int invocationIndex) { + return userIdGeneratorTestCase.getDisplayName(); + } + + @Override + public List getAdditionalExtensions() { + return Arrays.asList(parameterResolver(), preProcessor(), postProcessor()); + } + + private BeforeTestExecutionCallback preProcessor() { + return context -> System.out.println("Pre-process parameter: " + userIdGeneratorTestCase.getDisplayName()); + } + + private AfterTestExecutionCallback postProcessor() { + return context -> System.out.println("Post-process parameter: " + userIdGeneratorTestCase.getDisplayName()); + } + + private ParameterResolver parameterResolver() { + return new ParameterResolver() { + @Override + public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) { + return parameterContext.getParameter() + .getType() + .equals(UserIdGeneratorTestCase.class); + } + + @Override + public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) { + return userIdGeneratorTestCase; + } + }; + } + }; + } +} diff --git a/integration-tests/oidc-code-flow/pom.xml b/integration-tests/oidc-code-flow/pom.xml index 32103961c4bc2..fb37f2ceb8635 100644 --- a/integration-tests/oidc-code-flow/pom.xml +++ b/integration-tests/oidc-code-flow/pom.xml @@ -76,7 +76,7 @@ test
    - net.sourceforge.htmlunit + org.htmlunit htmlunit test diff --git a/integration-tests/oidc-code-flow/src/main/java/io/quarkus/it/keycloak/CustomTenantResolver.java b/integration-tests/oidc-code-flow/src/main/java/io/quarkus/it/keycloak/CustomTenantResolver.java index 2915157d827e8..759473eea051a 100644 --- a/integration-tests/oidc-code-flow/src/main/java/io/quarkus/it/keycloak/CustomTenantResolver.java +++ b/integration-tests/oidc-code-flow/src/main/java/io/quarkus/it/keycloak/CustomTenantResolver.java @@ -52,7 +52,7 @@ public String resolve(RoutingContext context) { return "tenant-autorefresh"; } - if (path.contains("tenant-refresh")) { + if (path.endsWith("tenant-refresh")) { return "tenant-refresh"; } diff --git a/integration-tests/oidc-code-flow/src/main/java/io/quarkus/it/keycloak/GlobalOidcRedirectFilter.java b/integration-tests/oidc-code-flow/src/main/java/io/quarkus/it/keycloak/GlobalOidcRedirectFilter.java new file mode 100644 index 0000000000000..cc97c22ae618e --- /dev/null +++ b/integration-tests/oidc-code-flow/src/main/java/io/quarkus/it/keycloak/GlobalOidcRedirectFilter.java @@ -0,0 +1,19 @@ +package io.quarkus.it.keycloak; + +import jakarta.enterprise.context.ApplicationScoped; + +import io.quarkus.arc.Unremovable; +import io.quarkus.oidc.OidcRedirectFilter; + +@ApplicationScoped +@Unremovable +public class GlobalOidcRedirectFilter implements OidcRedirectFilter { + + @Override + public void filter(OidcRedirectContext context) { + if (context.redirectUri().contains("/session-expired-page")) { + context.additionalQueryParams().add("redirect-filtered", "true,"); + } + } + +} diff --git a/integration-tests/oidc-code-flow/src/main/java/io/quarkus/it/keycloak/SessionExpiredOidcRedirectFilter.java b/integration-tests/oidc-code-flow/src/main/java/io/quarkus/it/keycloak/SessionExpiredOidcRedirectFilter.java new file mode 100644 index 0000000000000..709b516e758ff --- /dev/null +++ b/integration-tests/oidc-code-flow/src/main/java/io/quarkus/it/keycloak/SessionExpiredOidcRedirectFilter.java @@ -0,0 +1,43 @@ +package io.quarkus.it.keycloak; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.jwt.Claims; + +import io.quarkus.arc.Unremovable; +import io.quarkus.oidc.AuthorizationCodeTokens; +import io.quarkus.oidc.OidcRedirectFilter; +import io.quarkus.oidc.Redirect; +import io.quarkus.oidc.Redirect.Location; +import io.quarkus.oidc.TenantFeature; +import io.quarkus.oidc.runtime.OidcUtils; +import io.smallrye.jwt.build.Jwt; + +@ApplicationScoped +@Unremovable +@TenantFeature("tenant-refresh") +@Redirect(Location.SESSION_EXPIRED_PAGE) +public class SessionExpiredOidcRedirectFilter implements OidcRedirectFilter { + + @Override + public void filter(OidcRedirectContext context) { + + if (!"tenant-refresh".equals(context.oidcTenantConfig().tenantId.get())) { + throw new RuntimeException("Invalid tenant id"); + } + + if (!context.redirectUri().contains("/session-expired-page")) { + throw new RuntimeException("Invalid redirect URI"); + } + + AuthorizationCodeTokens tokens = context.routingContext().get(AuthorizationCodeTokens.class.getName()); + String userName = OidcUtils.decodeJwtContent(tokens.getIdToken()).getString(Claims.preferred_username.name()); + String jwe = Jwt.preferredUserName(userName).jwe() + .encryptWithSecret(context.oidcTenantConfig().credentials.secret.get()); + OidcUtils.createCookie(context.routingContext(), context.oidcTenantConfig(), "session_expired", + jwe + "|" + context.oidcTenantConfig().tenantId.get(), 10); + + context.additionalQueryParams().add("session-expired", "true"); + } + +} diff --git a/integration-tests/oidc-code-flow/src/main/java/io/quarkus/it/keycloak/TenantRefresh.java b/integration-tests/oidc-code-flow/src/main/java/io/quarkus/it/keycloak/TenantRefresh.java index 4ea2986944d3f..c1c4646559d67 100644 --- a/integration-tests/oidc-code-flow/src/main/java/io/quarkus/it/keycloak/TenantRefresh.java +++ b/integration-tests/oidc-code-flow/src/main/java/io/quarkus/it/keycloak/TenantRefresh.java @@ -1,10 +1,19 @@ package io.quarkus.it.keycloak; import jakarta.inject.Inject; +import jakarta.ws.rs.CookieParam; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; +import jakarta.ws.rs.QueryParam; +import org.eclipse.microprofile.jwt.Claims; +import org.eclipse.microprofile.jwt.JsonWebToken; + +import io.quarkus.oidc.OidcTenantConfig; +import io.quarkus.oidc.runtime.OidcUtils; +import io.quarkus.oidc.runtime.TenantConfigBean; import io.quarkus.security.Authenticated; +import io.smallrye.jwt.auth.principal.DefaultJWTParser; import io.vertx.ext.web.RoutingContext; @Path("/tenant-refresh") @@ -12,9 +21,33 @@ public class TenantRefresh { @Inject RoutingContext context; + @Inject + TenantConfigBean tenantConfig; + @Authenticated @GET public String getTenantRefresh() { return "Tenant Refresh, refreshed: " + (context.get("refresh_token_grant_response") != null); } + + @GET + @Path("/session-expired-page") + public String sessionExpired(@CookieParam("session_expired") String sessionExpired, + @QueryParam("session-expired") boolean expired, @QueryParam("redirect-filtered") String filtered) + throws Exception { + if (expired && filtered.equals("true,")) { + // Cookie format: jwt| + + String[] pair = sessionExpired.split("\\|"); + OidcTenantConfig oidcConfig = tenantConfig.getStaticTenantsConfig().get(pair[1]).getOidcTenantConfig(); + JsonWebToken jwt = new DefaultJWTParser().decrypt(pair[0], oidcConfig.credentials.secret.get()); + + OidcUtils.removeCookie(context, oidcConfig, "session_expired"); + + return jwt.getClaim(Claims.preferred_username) + ", your session has expired. " + + "Please login again at http://localhost:8081/" + oidcConfig.tenantId.get(); + } + + throw new RuntimeException("Invalid session expired page redirect"); + } } diff --git a/integration-tests/oidc-code-flow/src/main/resources/application.properties b/integration-tests/oidc-code-flow/src/main/resources/application.properties index 0d61acc332ab5..41372ae76857a 100644 --- a/integration-tests/oidc-code-flow/src/main/resources/application.properties +++ b/integration-tests/oidc-code-flow/src/main/resources/application.properties @@ -1,4 +1,5 @@ quarkus.keycloak.devservices.create-realm=false +quarkus.keycloak.devservices.show-logs=true # Default tenant configurationf quarkus.oidc.client-id=quarkus-app quarkus.oidc.credentials.secret=secret @@ -75,7 +76,7 @@ quarkus.oidc.tenant-3.application-type=web-app quarkus.oidc.tenant-logout.auth-server-url=${keycloak.url}/realms/logout-realm quarkus.oidc.tenant-logout.client-id=quarkus-app -quarkus.oidc.tenant-logout.credentials.secret=secret +quarkus.oidc.tenant-logout.credentials.secret=eUk1p7UB3nFiXZGUXi0uph1Y9p34YhBU quarkus.oidc.tenant-logout.application-type=web-app quarkus.oidc.tenant-logout.authentication.cookie-path=/tenant-logout quarkus.oidc.tenant-logout.logout.path=/tenant-logout/logout @@ -85,11 +86,11 @@ quarkus.oidc.tenant-logout.token.refresh-expired=true quarkus.oidc.tenant-refresh.auth-server-url=${keycloak.url}/realms/logout-realm quarkus.oidc.tenant-refresh.client-id=quarkus-app -quarkus.oidc.tenant-refresh.credentials.secret=secret +quarkus.oidc.tenant-refresh.credentials.secret=eUk1p7UB3nFiXZGUXi0uph1Y9p34YhBU quarkus.oidc.tenant-refresh.application-type=web-app quarkus.oidc.tenant-refresh.authentication.cookie-path=/tenant-refresh quarkus.oidc.tenant-refresh.authentication.session-age-extension=2M -quarkus.oidc.tenant-refresh.authentication.session-expired-path=/session-expired-page +quarkus.oidc.tenant-refresh.authentication.session-expired-path=/tenant-refresh/session-expired-page quarkus.oidc.tenant-refresh.token.refresh-expired=true quarkus.oidc.tenant-autorefresh.auth-server-url=${quarkus.oidc.auth-server-url} @@ -204,4 +205,4 @@ quarkus.log.category."io.quarkus.resteasy.runtime.UnauthorizedExceptionMapper".l quarkus.log.category."io.quarkus.vertx.http.runtime.security.HttpAuthenticator".level=DEBUG quarkus.log.category."io.quarkus.vertx.http.runtime.security.HttpSecurityRecorder".level=DEBUG -quarkus.log.category."com.gargoylesoftware.htmlunit".level=ERROR +quarkus.log.category."org.htmlunit".level=ERROR diff --git a/integration-tests/oidc-code-flow/src/test/java/io/quarkus/it/keycloak/CodeFlowTest.java b/integration-tests/oidc-code-flow/src/test/java/io/quarkus/it/keycloak/CodeFlowTest.java index 1f62617f0c172..e4d0d732f453f 100644 --- a/integration-tests/oidc-code-flow/src/test/java/io/quarkus/it/keycloak/CodeFlowTest.java +++ b/integration-tests/oidc-code-flow/src/test/java/io/quarkus/it/keycloak/CodeFlowTest.java @@ -25,21 +25,20 @@ import javax.crypto.spec.SecretKeySpec; import org.hamcrest.Matchers; +import org.htmlunit.CookieManager; +import org.htmlunit.FailingHttpStatusCodeException; +import org.htmlunit.SilentCssErrorHandler; +import org.htmlunit.TextPage; +import org.htmlunit.WebClient; +import org.htmlunit.WebRequest; +import org.htmlunit.WebResponse; +import org.htmlunit.html.HtmlForm; +import org.htmlunit.html.HtmlPage; +import org.htmlunit.util.Cookie; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; -import com.gargoylesoftware.htmlunit.CookieManager; -import com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException; -import com.gargoylesoftware.htmlunit.SilentCssErrorHandler; -import com.gargoylesoftware.htmlunit.TextPage; -import com.gargoylesoftware.htmlunit.WebClient; -import com.gargoylesoftware.htmlunit.WebRequest; -import com.gargoylesoftware.htmlunit.WebResponse; -import com.gargoylesoftware.htmlunit.html.HtmlForm; -import com.gargoylesoftware.htmlunit.html.HtmlPage; -import com.gargoylesoftware.htmlunit.util.Cookie; - import io.quarkus.oidc.runtime.OidcUtils; import io.quarkus.test.common.QuarkusTestResource; import io.quarkus.test.junit.QuarkusTest; @@ -751,8 +750,18 @@ public Boolean call() throws Exception { if (statusCode == 302) { assertNull(getSessionCookie(webClient, "tenant-refresh")); - assertEquals("http://localhost:8081/session-expired-page", - webResponse.getResponseHeaderValue("location")); + String redirect = webResponse.getResponseHeaderValue("location"); + assertTrue(redirect.equals( + "http://localhost:8081/tenant-refresh/session-expired-page?redirect-filtered=true%2C&session-expired=true") + || redirect.equals( + "http://localhost:8081/tenant-refresh/session-expired-page?session-expired=true&redirect-filtered=true%2C")); + assertNotNull(webClient.getCookieManager().getCookie("session_expired")); + webResponse = webClient.loadWebResponse( + new WebRequest(URI.create(redirect).toURL())); + assertEquals( + "alice, your session has expired. Please login again at http://localhost:8081/tenant-refresh", + webResponse.getContentAsString()); + assertNull(webClient.getCookieManager().getCookie("session_expired")); return true; } diff --git a/integration-tests/oidc-code-flow/src/test/java/io/quarkus/it/keycloak/KeycloakRealmResourceManager.java b/integration-tests/oidc-code-flow/src/test/java/io/quarkus/it/keycloak/KeycloakRealmResourceManager.java index fd749a8f8668d..338208e6e502b 100644 --- a/integration-tests/oidc-code-flow/src/test/java/io/quarkus/it/keycloak/KeycloakRealmResourceManager.java +++ b/integration-tests/oidc-code-flow/src/test/java/io/quarkus/it/keycloak/KeycloakRealmResourceManager.java @@ -27,11 +27,11 @@ public class KeycloakRealmResourceManager implements QuarkusTestResourceLifecycl @Override public Map start() { - RealmRepresentation realm = createRealm(KEYCLOAK_REALM); + RealmRepresentation realm = createRealm(KEYCLOAK_REALM, "secret"); client.createRealm(realm); realms.add(realm); - RealmRepresentation logoutRealm = createRealm("logout-realm"); + RealmRepresentation logoutRealm = createRealm("logout-realm", "eUk1p7UB3nFiXZGUXi0uph1Y9p34YhBU"); // revoke refresh tokens so that they can only be used once logoutRealm.setRevokeRefreshToken(true); logoutRealm.setRefreshTokenMaxReuse(0); @@ -42,7 +42,7 @@ public Map start() { return Collections.emptyMap(); } - private static RealmRepresentation createRealm(String name) { + private static RealmRepresentation createRealm(String name, String defaultClientSecret) { RealmRepresentation realm = new RealmRepresentation(); realm.setRealm(name); @@ -62,7 +62,7 @@ private static RealmRepresentation createRealm(String name) { realm.getRoles().getRealm().add(new RoleRepresentation("admin", null, false)); realm.getRoles().getRealm().add(new RoleRepresentation("confidential", null, false)); - realm.getClients().add(createClient("quarkus-app")); + realm.getClients().add(createClient("quarkus-app", defaultClientSecret)); realm.getClients().add(createClientJwt("quarkus-app-jwt")); realm.getUsers().add(createUser("alice", "user")); realm.getUsers().add(createUser("admin", "user", "admin")); @@ -83,14 +83,14 @@ private static ClientRepresentation createClientJwt(String clientId) { return client; } - private static ClientRepresentation createClient(String clientId) { + private static ClientRepresentation createClient(String clientId, String secret) { ClientRepresentation client = new ClientRepresentation(); client.setClientId(clientId); client.setEnabled(true); client.setRedirectUris(Arrays.asList("*")); client.setClientAuthenticatorType("client-secret"); - client.setSecret("secret"); + client.setSecret(secret); return client; } diff --git a/integration-tests/oidc-tenancy/pom.xml b/integration-tests/oidc-tenancy/pom.xml index 3dd069ed3efd7..de7d04c6100c0 100644 --- a/integration-tests/oidc-tenancy/pom.xml +++ b/integration-tests/oidc-tenancy/pom.xml @@ -66,7 +66,7 @@ test - net.sourceforge.htmlunit + org.htmlunit htmlunit test diff --git a/integration-tests/oidc-tenancy/src/main/resources/application.properties b/integration-tests/oidc-tenancy/src/main/resources/application.properties index 88bf88c41c0e1..85b6a546f18f3 100644 --- a/integration-tests/oidc-tenancy/src/main/resources/application.properties +++ b/integration-tests/oidc-tenancy/src/main/resources/application.properties @@ -135,7 +135,7 @@ quarkus.http.auth.permission.authenticated.policy=authenticated smallrye.jwt.sign.key.location=/privateKey.pem smallrye.jwt.new-token.lifespan=5 -quarkus.log.category."com.gargoylesoftware.htmlunit.javascript.host.css.CSSStyleSheet".level=FATAL +quarkus.log.category."org.htmlunit".level=ERROR quarkus.http.auth.proactive=false quarkus.native.additional-build-args=-H:IncludeResources=.*\\.pem diff --git a/integration-tests/oidc-tenancy/src/test/java/io/quarkus/it/keycloak/BearerTokenAuthorizationTest.java b/integration-tests/oidc-tenancy/src/test/java/io/quarkus/it/keycloak/BearerTokenAuthorizationTest.java index 38a85d30756fb..fc2c666eb54b2 100644 --- a/integration-tests/oidc-tenancy/src/test/java/io/quarkus/it/keycloak/BearerTokenAuthorizationTest.java +++ b/integration-tests/oidc-tenancy/src/test/java/io/quarkus/it/keycloak/BearerTokenAuthorizationTest.java @@ -11,22 +11,25 @@ import java.io.IOException; import java.net.URI; import java.time.Duration; +import java.util.ArrayList; import java.util.List; +import java.util.SortedMap; +import java.util.TreeMap; import java.util.concurrent.Callable; import java.util.concurrent.TimeUnit; +import org.htmlunit.CookieManager; +import org.htmlunit.FailingHttpStatusCodeException; +import org.htmlunit.SilentCssErrorHandler; +import org.htmlunit.WebClient; +import org.htmlunit.WebRequest; +import org.htmlunit.WebResponse; +import org.htmlunit.html.HtmlForm; +import org.htmlunit.html.HtmlPage; +import org.htmlunit.util.Cookie; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; -import com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException; -import com.gargoylesoftware.htmlunit.SilentCssErrorHandler; -import com.gargoylesoftware.htmlunit.WebClient; -import com.gargoylesoftware.htmlunit.WebRequest; -import com.gargoylesoftware.htmlunit.WebResponse; -import com.gargoylesoftware.htmlunit.html.HtmlForm; -import com.gargoylesoftware.htmlunit.html.HtmlPage; -import com.gargoylesoftware.htmlunit.util.Cookie; - import io.quarkus.test.common.QuarkusTestResource; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.keycloak.client.KeycloakTestClient; @@ -307,7 +310,9 @@ public void testReAuthenticateWhenSwitchingTenants() throws IOException { page = loginForm.getInputByName("login").click(); assertEquals("tenant-web-app2:alice", page.getBody().asNormalizedText()); assertNull(getSessionCookie(webClient, "tenant-web-app")); - assertNotNull(getSessionCookie(webClient, "tenant-web-app2")); + List sessionCookieChunks = getSessionCookies(webClient, "tenant-web-app2"); + assertNotNull(sessionCookieChunks); + assertEquals(2, sessionCookieChunks.size()); webClient.getCookieManager().clearCookies(); } } @@ -932,4 +937,17 @@ private Cookie getSessionAtCookie(WebClient webClient, String tenantId) { private Cookie getSessionRtCookie(WebClient webClient, String tenantId) { return webClient.getCookieManager().getCookie("q_session_rt" + (tenantId == null ? "_Default_test" : "_" + tenantId)); } + + private List getSessionCookies(WebClient webClient, String tenantId) { + String sessionCookieNameChunk = "q_session" + (tenantId == null ? "" : "_" + tenantId) + "_chunk_"; + CookieManager cookieManager = webClient.getCookieManager(); + SortedMap sessionCookies = new TreeMap<>(); + for (Cookie cookie : cookieManager.getCookies()) { + if (cookie.getName().startsWith(sessionCookieNameChunk)) { + sessionCookies.put(cookie.getName(), cookie); + } + } + + return sessionCookies.isEmpty() ? null : new ArrayList(sessionCookies.values()); + } } diff --git a/integration-tests/oidc-token-propagation-reactive/pom.xml b/integration-tests/oidc-token-propagation-reactive/pom.xml index 214eac3166385..47b8de765c8a3 100644 --- a/integration-tests/oidc-token-propagation-reactive/pom.xml +++ b/integration-tests/oidc-token-propagation-reactive/pom.xml @@ -34,7 +34,7 @@ test - net.sourceforge.htmlunit + org.htmlunit htmlunit test diff --git a/integration-tests/oidc-token-propagation-reactive/src/test/java/io/quarkus/it/keycloak/OidcTokenReactivePropagationTest.java b/integration-tests/oidc-token-propagation-reactive/src/test/java/io/quarkus/it/keycloak/OidcTokenReactivePropagationTest.java index 7a4cb646a36f0..69bc7028d1677 100644 --- a/integration-tests/oidc-token-propagation-reactive/src/test/java/io/quarkus/it/keycloak/OidcTokenReactivePropagationTest.java +++ b/integration-tests/oidc-token-propagation-reactive/src/test/java/io/quarkus/it/keycloak/OidcTokenReactivePropagationTest.java @@ -2,14 +2,13 @@ import static org.junit.jupiter.api.Assertions.assertEquals; +import org.htmlunit.SilentCssErrorHandler; +import org.htmlunit.TextPage; +import org.htmlunit.WebClient; +import org.htmlunit.html.HtmlForm; +import org.htmlunit.html.HtmlPage; import org.junit.jupiter.api.Test; -import com.gargoylesoftware.htmlunit.SilentCssErrorHandler; -import com.gargoylesoftware.htmlunit.TextPage; -import com.gargoylesoftware.htmlunit.WebClient; -import com.gargoylesoftware.htmlunit.html.HtmlForm; -import com.gargoylesoftware.htmlunit.html.HtmlPage; - import io.quarkus.test.junit.QuarkusTest; import io.restassured.RestAssured; diff --git a/integration-tests/oidc-token-propagation/src/test/java/io/quarkus/it/keycloak/OidcTokenPropagationTest.java b/integration-tests/oidc-token-propagation/src/test/java/io/quarkus/it/keycloak/OidcTokenPropagationTest.java index 29a9327e6d87b..bcd717025d989 100644 --- a/integration-tests/oidc-token-propagation/src/test/java/io/quarkus/it/keycloak/OidcTokenPropagationTest.java +++ b/integration-tests/oidc-token-propagation/src/test/java/io/quarkus/it/keycloak/OidcTokenPropagationTest.java @@ -53,7 +53,7 @@ public void testGetUserNameWithAccessTokenPropagation() { //.statusCode(200) //.body(equalTo("alice")); .statusCode(500) - .body(containsString("Feature not enabled")); + .body(containsString("Client not allowed to exchange")); } @Test diff --git a/integration-tests/oidc-wiremock/pom.xml b/integration-tests/oidc-wiremock/pom.xml index 88b287f528278..dfddc45a735e1 100644 --- a/integration-tests/oidc-wiremock/pom.xml +++ b/integration-tests/oidc-wiremock/pom.xml @@ -44,7 +44,7 @@ test - net.sourceforge.htmlunit + org.htmlunit htmlunit test diff --git a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CodeFlowVerifyIdAndAccessTokenResource.java b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CodeFlowVerifyIdAndAccessTokenResource.java new file mode 100644 index 0000000000000..b3ffecf4c6bcf --- /dev/null +++ b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CodeFlowVerifyIdAndAccessTokenResource.java @@ -0,0 +1,40 @@ +package io.quarkus.it.keycloak; + +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +import org.eclipse.microprofile.jwt.JsonWebToken; + +import io.quarkus.oidc.IdToken; +import io.quarkus.oidc.runtime.DefaultTokenIntrospectionUserInfoCache; +import io.quarkus.security.Authenticated; +import io.vertx.ext.web.RoutingContext; + +@Path("/code-flow-verify-id-and-access-tokens") +public class CodeFlowVerifyIdAndAccessTokenResource { + + @Inject + @IdToken + JsonWebToken idToken; + + @Inject + JsonWebToken accessToken; + + @Inject + RoutingContext routingContext; + + @Inject + DefaultTokenIntrospectionUserInfoCache tokenCache; + + @GET + @Authenticated + public String access() { + return "access token verified: " + (routingContext.get("code_flow_access_token_result") != null) + + ", id_token issuer: " + idToken.getIssuer() + + ", access_token issuer: " + accessToken.getIssuer() + + ", id_token audience: " + idToken.getAudience().iterator().next() + + ", access_token audience: " + accessToken.getAudience().iterator().next() + + ", cache size: " + tokenCache.getCacheSize(); + } +} diff --git a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/OrderService.java b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/OrderService.java index 36d98d57b01d9..95c6a31e7443d 100644 --- a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/OrderService.java +++ b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/OrderService.java @@ -7,7 +7,7 @@ import jakarta.inject.Inject; import io.quarkus.oidc.AccessTokenCredential; -import io.quarkus.oidc.TenantFeature; +import io.quarkus.oidc.Tenant; import io.quarkus.oidc.TenantIdentityProvider; import io.quarkus.security.identity.SecurityIdentity; import io.quarkus.vertx.ConsumeEvent; @@ -21,7 +21,7 @@ public class OrderService { @Inject SecurityIdentity identity; - @TenantFeature("bearer") + @Tenant("bearer") @Inject TenantIdentityProvider identityProvider; diff --git a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/StartupService.java b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/StartupService.java index 911c4e55291a3..38ddd5b7e75ec 100644 --- a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/StartupService.java +++ b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/StartupService.java @@ -9,11 +9,12 @@ import java.util.concurrent.ConcurrentHashMap; import jakarta.enterprise.event.Observes; +import jakarta.inject.Inject; import jakarta.inject.Singleton; import io.quarkus.oidc.AccessTokenCredential; import io.quarkus.oidc.OIDCException; -import io.quarkus.oidc.TenantFeature; +import io.quarkus.oidc.Tenant; import io.quarkus.oidc.TenantIdentityProvider; import io.quarkus.runtime.StartupEvent; import io.quarkus.security.AuthenticationFailedException; @@ -27,16 +28,18 @@ public class StartupService { private static final String ISSUER = "https://server.example.com"; - @TenantFeature("bearer") + @Inject + @Tenant("bearer") TenantIdentityProvider identityProviderBearer; - @TenantFeature("bearer-role-claim-path") + @Inject + @Tenant("bearer-role-claim-path") TenantIdentityProvider identityProviderBearerRoleClaimPath; private final Map>> tenantToIdentityWithRole = new ConcurrentHashMap<>(); void onStartup(@Observes StartupEvent event, - @TenantFeature(DEFAULT_TENANT_ID) TenantIdentityProvider defaultTenantProvider, + @Tenant(DEFAULT_TENANT_ID) TenantIdentityProvider defaultTenantProvider, TenantIdentityProvider defaultTenantProviderDefaultQualifier) { assertDefaultTenantProviderInjection(defaultTenantProvider); assertDefaultTenantProviderInjection(defaultTenantProviderDefaultQualifier); diff --git a/integration-tests/oidc-wiremock/src/main/resources/application.properties b/integration-tests/oidc-wiremock/src/main/resources/application.properties index 15e351b94c6bf..a25886b891e32 100644 --- a/integration-tests/oidc-wiremock/src/main/resources/application.properties +++ b/integration-tests/oidc-wiremock/src/main/resources/application.properties @@ -24,10 +24,18 @@ quarkus.oidc.code-flow.logout.post-logout-uri-param=returnTo quarkus.oidc.code-flow.logout.extra-params.client_id=${quarkus.oidc.code-flow.client-id} quarkus.oidc.code-flow.credentials.secret=secret quarkus.oidc.code-flow.application-type=web-app -quarkus.oidc.code-flow.token.audience=https://server.example.com +quarkus.oidc.code-flow.token.audience=https://id.server.example.com quarkus.oidc.code-flow.token.refresh-expired=true quarkus.oidc.code-flow.token.refresh-token-time-skew=5M +quarkus.oidc.code-flow-verify-id-and-access-tokens.auth-server-url=${keycloak.url}/realms/quarkus/ +quarkus.oidc.code-flow-verify-id-and-access-tokens.client-id=quarkus-web-app +quarkus.oidc.code-flow-verify-id-and-access-tokens.authentication.user-info-required=false +quarkus.oidc.code-flow-verify-id-and-access-tokens.authentication.verify-access-token=true +quarkus.oidc.code-flow-verify-id-and-access-tokens.credentials.secret=secret +quarkus.oidc.code-flow-verify-id-and-access-tokens.application-type=web-app +quarkus.oidc.code-flow-verify-id-and-access-tokens.token.audience=any + quarkus.oidc.code-flow-encrypted-id-token-jwk.auth-server-url=${keycloak.url}/realms/quarkus/ quarkus.oidc.code-flow-encrypted-id-token-jwk.client-id=quarkus-web-app quarkus.oidc.code-flow-encrypted-id-token-jwk.credentials.secret=secret @@ -60,7 +68,7 @@ quarkus.oidc.code-flow-form-post.token-path=${keycloak.url}/realms/quarkus/token quarkus.oidc.code-flow-form-post.jwks-path=${keycloak.url}/realms/quarkus/protocol/openid-connect/certs quarkus.oidc.code-flow-form-post.logout.backchannel.path=/back-channel-logout quarkus.oidc.code-flow-form-post.logout.frontchannel.path=/code-flow-form-post/front-channel-logout -quarkus.oidc.code-flow-form-post.token.audience=https://server.example.com +quarkus.oidc.code-flow-form-post.token.audience=https://server.example.com,https://id.server.example.com quarkus.oidc.code-flow-user-info-only.auth-server-url=${keycloak.url}/realms/quarkus/ quarkus.oidc.code-flow-user-info-only.discovery-enabled=false diff --git a/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/CodeFlowAuthorizationTest.java b/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/CodeFlowAuthorizationTest.java index f41a520b1b9a2..759c8a6b65ed6 100644 --- a/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/CodeFlowAuthorizationTest.java +++ b/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/CodeFlowAuthorizationTest.java @@ -25,17 +25,17 @@ import javax.crypto.spec.SecretKeySpec; import org.hamcrest.Matchers; +import org.htmlunit.SilentCssErrorHandler; +import org.htmlunit.TextPage; +import org.htmlunit.WebClient; +import org.htmlunit.WebRequest; +import org.htmlunit.WebResponse; +import org.htmlunit.html.HtmlForm; +import org.htmlunit.html.HtmlPage; +import org.htmlunit.util.Cookie; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -import com.gargoylesoftware.htmlunit.SilentCssErrorHandler; -import com.gargoylesoftware.htmlunit.TextPage; -import com.gargoylesoftware.htmlunit.WebClient; -import com.gargoylesoftware.htmlunit.WebRequest; -import com.gargoylesoftware.htmlunit.WebResponse; -import com.gargoylesoftware.htmlunit.html.HtmlForm; -import com.gargoylesoftware.htmlunit.html.HtmlPage; -import com.gargoylesoftware.htmlunit.util.Cookie; import com.github.tomakehurst.wiremock.WireMockServer; import com.github.tomakehurst.wiremock.client.WireMock; @@ -93,6 +93,31 @@ public void testCodeFlow() throws IOException { clearCache(); } + @Test + public void testCodeFlowVerifyIdAndAccessToken() throws IOException { + defineCodeFlowLogoutStub(); + try (final WebClient webClient = createWebClient()) { + webClient.getOptions().setRedirectEnabled(true); + HtmlPage page = webClient.getPage("http://localhost:8081/code-flow-verify-id-and-access-tokens"); + + HtmlForm form = page.getFormByName("form"); + form.getInputByName("username").type("alice"); + form.getInputByName("password").type("alice"); + + TextPage textPage = form.getInputByValue("login").click(); + + assertEquals("access token verified: true," + + " id_token issuer: https://server.example.com," + + " access_token issuer: https://server.example.com," + + " id_token audience: https://id.server.example.com," + + " access_token audience: https://server.example.com," + + " cache size: 0", textPage.getContent()); + assertNotNull(getSessionCookie(webClient, "code-flow-verify-id-and-access-tokens")); + webClient.getCookieManager().clearCookies(); + } + clearCache(); + } + @Test public void testCodeFlowEncryptedIdTokenJwk() throws IOException { doTestCodeFlowEncryptedIdToken("code-flow-encrypted-id-token-jwk", KeyEncryptionAlgorithm.DIR); diff --git a/integration-tests/oidc/src/main/resources/upconfig.json b/integration-tests/oidc/src/main/resources/upconfig.json new file mode 100644 index 0000000000000..8487089bc90fd --- /dev/null +++ b/integration-tests/oidc/src/main/resources/upconfig.json @@ -0,0 +1,60 @@ +{ + "attributes": [ + { + "name": "username", + "displayName": "${username}", + "permissions": { + "view": ["admin", "user"], + "edit": ["admin", "user"] + }, + "validations": { + "length": { "min": 3, "max": 255 }, + "username-prohibited-characters": {}, + "up-username-not-idn-homograph": {} + } + }, + { + "name": "email", + "displayName": "${email}", + "permissions": { + "view": ["admin", "user"], + "edit": ["admin", "user"] + }, + "validations": { + "email" : {}, + "length": { "max": 255 } + } + }, + { + "name": "firstName", + "displayName": "${firstName}", + "permissions": { + "view": ["admin", "user"], + "edit": ["admin", "user"] + }, + "validations": { + "length": { "max": 255 }, + "person-name-prohibited-characters": {} + } + }, + { + "name": "lastName", + "displayName": "${lastName}", + "permissions": { + "view": ["admin", "user"], + "edit": ["admin", "user"] + }, + "validations": { + "length": { "max": 255 }, + "person-name-prohibited-characters": {} + } + } + ], + "groups": [ + { + "name": "user-metadata", + "displayHeader": "User metadata", + "displayDescription": "Attributes, which refer to user metadata" + } + ] +} \ No newline at end of file diff --git a/integration-tests/oidc/src/test/java/io/quarkus/it/keycloak/KeycloakXTestResourceLifecycleManager.java b/integration-tests/oidc/src/test/java/io/quarkus/it/keycloak/KeycloakXTestResourceLifecycleManager.java index 21c76533a6334..abe4321c0789d 100644 --- a/integration-tests/oidc/src/test/java/io/quarkus/it/keycloak/KeycloakXTestResourceLifecycleManager.java +++ b/integration-tests/oidc/src/test/java/io/quarkus/it/keycloak/KeycloakXTestResourceLifecycleManager.java @@ -51,10 +51,12 @@ public Map start() { keycloak = keycloak .withClasspathResourceMapping(SERVER_KEYSTORE, SERVER_KEYSTORE_MOUNTED_PATH, BindMode.READ_ONLY) .withClasspathResourceMapping(SERVER_TRUSTSTORE, SERVER_TRUSTSTORE_MOUNTED_PATH, BindMode.READ_ONLY) + .withClasspathResourceMapping("/upconfig.json", "/opt/keycloak/upconfig.json", BindMode.READ_ONLY) .withCommand("build --https-client-auth=required") .withCommand(String.format( "start --https-client-auth=required --hostname-strict=false --hostname-strict-https=false" - + " --https-key-store-file=%s --https-trust-store-file=%s --https-trust-store-password=password", + + " --https-key-store-file=%s --https-trust-store-file=%s --https-trust-store-password=password" + + " --spi-user-profile-declarative-user-profile-config-file=/opt/keycloak/upconfig.json", SERVER_KEYSTORE_MOUNTED_PATH, SERVER_TRUSTSTORE_MOUNTED_PATH)); keycloak.start(); LOGGER.info(keycloak.getLogs()); diff --git a/integration-tests/opentelemetry-reactive/src/main/java/io/quarkus/it/opentelemetry/reactive/CustomSecurityIdentityAugmentor.java b/integration-tests/opentelemetry-reactive/src/main/java/io/quarkus/it/opentelemetry/reactive/CustomSecurityIdentityAugmentor.java new file mode 100644 index 0000000000000..e3dead7ad460b --- /dev/null +++ b/integration-tests/opentelemetry-reactive/src/main/java/io/quarkus/it/opentelemetry/reactive/CustomSecurityIdentityAugmentor.java @@ -0,0 +1,43 @@ +package io.quarkus.it.opentelemetry.reactive; + +import java.util.Map; + +import jakarta.inject.Singleton; + +import io.quarkus.security.identity.AuthenticationRequestContext; +import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.security.identity.SecurityIdentityAugmentor; +import io.quarkus.security.runtime.QuarkusSecurityIdentity; +import io.quarkus.vertx.http.runtime.security.HttpSecurityUtils; +import io.smallrye.mutiny.Uni; + +@Singleton +public class CustomSecurityIdentityAugmentor implements SecurityIdentityAugmentor { + @Override + public Uni augment(SecurityIdentity securityIdentity, + AuthenticationRequestContext authenticationRequestContext) { + return augment(securityIdentity, authenticationRequestContext, Map.of()); + } + + @Override + public Uni augment(SecurityIdentity identity, AuthenticationRequestContext context, + Map attributes) { + var routingContext = HttpSecurityUtils.getRoutingContextAttribute(attributes); + if (routingContext != null) { + var augmentorScenario = routingContext.normalizedPath().contains("-augmentor"); + var configRolesMappingScenario = routingContext.normalizedPath().contains("roles-mapping-http-perm"); + if (augmentorScenario || configRolesMappingScenario) { + var builder = QuarkusSecurityIdentity.builder(identity); + if (augmentorScenario) { + builder.addRole("AUGMENTOR"); + } + if (configRolesMappingScenario) { + // this role is supposed to be re-mapped by HTTP roles mapping (not path-specific) + builder.addRole("ROLES-ALLOWED-MAPPING-ROLE"); + } + return Uni.createFrom().item(builder.build()); + } + } + return Uni.createFrom().item(identity); + } +} diff --git a/integration-tests/opentelemetry-reactive/src/main/java/io/quarkus/it/opentelemetry/reactive/EndUserResource.java b/integration-tests/opentelemetry-reactive/src/main/java/io/quarkus/it/opentelemetry/reactive/EndUserResource.java new file mode 100644 index 0000000000000..ea8765c35c87e --- /dev/null +++ b/integration-tests/opentelemetry-reactive/src/main/java/io/quarkus/it/opentelemetry/reactive/EndUserResource.java @@ -0,0 +1,112 @@ +package io.quarkus.it.opentelemetry.reactive; + +import jakarta.annotation.security.PermitAll; +import jakarta.annotation.security.RolesAllowed; +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.semconv.SemanticAttributes; + +@Path("/otel/enduser") +public class EndUserResource { + + @Inject + Tracer tracer; + + @Path("/no-authorization") + @GET + public String noAuthorization() { + return "/no-authorization"; + } + + @RolesAllowed("WRITER") + @Path("/roles-allowed-only-writer-role") + @GET + public String rolesAllowedOnlyWriterRole() { + return "/roles-allowed-only-writer-role"; + } + + @PermitAll + @Path("/permit-all-only") + @GET + public String permitAllOnly() { + return "/permit-all-only"; + } + + @Path("/no-authorization-augmentor") + @GET + public String noAuthorizationAugmentor() { + return "/no-authorization-augmentor"; + } + + @RolesAllowed("AUGMENTOR") + @Path("/roles-allowed-only-augmentor-role") + @GET + public String rolesAllowedOnlyAugmentorRole() { + return "/roles-allowed-only-augmentor-role"; + } + + @PermitAll + @Path("/permit-all-only-augmentor") + @GET + public String permitAllOnlyAugmentor() { + return "/permit-all-only-augmentor"; + } + + @RolesAllowed("WRITER-HTTP-PERM") + @Path("/roles-allowed-writer-http-perm-role") + @GET + public String rolesAllowedHttpPermWriterHttpPermRole() { + return "/roles-allowed-writer-http-perm-role"; + } + + @PermitAll + @Path("/roles-mapping-http-perm") + @GET + public String permitAllAnnotationConfigRolesMappingPermitAllHttpPerm() { + return "/roles-mapping-http-perm"; + } + + @RolesAllowed("HTTP-PERM-AUGMENTOR") + @Path("/roles-allowed-http-perm-augmentor-role") + @GET + public String rolesAllowedHttpPermHttpAugmentorPermRole() { + return "/roles-allowed-http-perm-augmentor-role"; + } + + @PermitAll + @Path("/roles-mapping-http-perm-augmentor") + @GET + public String permitAllAnnotationConfigRolesMappingPermitAllHttpPermAugmentor() { + return "/roles-mapping-http-perm-augmentor"; + } + + @Path("/jax-rs-http-perm") + @GET + public String jaxRsHttpPermOnly() { + return "/jax-rs-http-perm"; + } + + @RolesAllowed("READER") + @Path("/jax-rs-http-perm-annotation-reader-role") + @GET + public String jaxRsHttpPermRolesAllowedReaderRole() { + return "/jax-rs-http-perm-annotation-reader-role"; + } + + @RolesAllowed("READER") + @Path("/custom-span-reader-role") + @GET + public String customSpanReaderRole() { + var span = tracer.spanBuilder("custom-span").startSpan(); + try (var ignored = span.makeCurrent()) { + span.setAttribute("custom_attribute", "custom-value"); + span.setAttribute(SemanticAttributes.HTTP_TARGET, "custom-path"); + } finally { + span.end(); + } + return "/custom-span-reader-role"; + } +} diff --git a/integration-tests/opentelemetry-reactive/src/test/java/io/quarkus/it/opentelemetry/reactive/enduser/AbstractEndUserTest.java b/integration-tests/opentelemetry-reactive/src/test/java/io/quarkus/it/opentelemetry/reactive/enduser/AbstractEndUserTest.java new file mode 100644 index 0000000000000..841ab567c1b2d --- /dev/null +++ b/integration-tests/opentelemetry-reactive/src/test/java/io/quarkus/it/opentelemetry/reactive/enduser/AbstractEndUserTest.java @@ -0,0 +1,429 @@ +package io.quarkus.it.opentelemetry.reactive.enduser; + +import static io.quarkus.it.opentelemetry.reactive.Utils.getSpans; +import static io.restassured.RestAssured.get; +import static io.restassured.RestAssured.given; +import static java.net.HttpURLConnection.HTTP_OK; +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.awaitility.Awaitility.await; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.time.Duration; +import java.util.Map; + +import org.awaitility.Awaitility; +import org.hamcrest.Description; +import org.hamcrest.TypeSafeMatcher; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import io.opentelemetry.semconv.SemanticAttributes; +import io.restassured.response.ValidatableResponse; + +public abstract class AbstractEndUserTest { + + private static final String HTTP_PERM_AUGMENTOR_ROLE = "HTTP-PERM-AUGMENTOR"; + private static final String END_USER_ID_ATTR = SemanticAttributes.ENDUSER_ID.getKey(); + private static final String END_USER_ROLE_ATTR = SemanticAttributes.ENDUSER_ROLE.getKey(); + private static final String READER_ROLE = "READER"; + private static final String WRITER_ROLE = "WRITER"; + private static final String WRITER_HTTP_PERM_ROLE = "WRITER-HTTP-PERM"; + private static final String AUTH_FAILURE_ROLE = "AUTHZ-FAILURE-ROLE"; + private static final String AUGMENTOR_ROLE = "AUGMENTOR"; + + /** + * This is 'ROLES-ALLOWED-MAPPING-ROLE' role granted to the SecureIdentity by augmentor and + * remapped to 'ROLES-ALLOWED-MAPPING-ROLE-HTTP-PERM' role which allows to verify that the + * 'quarkus.http.auth.roles-mapping' config-level roles mapping is reflected in the End User attributes. + */ + private static final String HTTP_PERM_ROLES_ALLOWED_MAPPING_ROLE = "ROLES-ALLOWED-MAPPING-ROLE-HTTP-PERM"; + + @BeforeEach + @AfterEach + public void reset() { + given().get("/reset").then().statusCode(HTTP_OK); + await().atMost(5, SECONDS).until(() -> getSpans().isEmpty()); + } + + @Test + public void testAttributesWhenNoAuthorizationInPlace() { + var subPath = "/no-authorization"; + request(subPath, User.SCOTT).statusCode(200).body(is(subPath)); + + var spanData = waitForSpanWithSubPath(subPath); + + if (isProactiveAuthEnabled()) { + assertEndUserId(User.SCOTT, spanData); + + var roles = getRolesAttribute(spanData); + assertTrue(roles.contains(READER_ROLE)); + assertFalse(roles.contains(WRITER_ROLE)); + } else { + assertNoEndUserAttributes(spanData); + } + } + + @Test + public void testWhenRolesAllowedAnnotationOnlyUnauthorized() { + var subPath = "/roles-allowed-only-writer-role"; + // the endpoint is annotated with @RolesAllowed("WRITER") and no other authorization is in place + request(subPath, User.SCOTT).statusCode(403); + + var spanData = waitForSpanWithSubPath(subPath); + + assertEndUserId(User.SCOTT, spanData); + + var roles = getRolesAttribute(spanData); + assertTrue(roles.contains(READER_ROLE)); + assertFalse(roles.contains(WRITER_ROLE)); + } + + @Test + public void testWhenRolesAllowedAnnotationOnlyAuthorized() { + var subPath = "/roles-allowed-only-writer-role"; + // the endpoint is annotated with @RolesAllowed("WRITER") and no other authorization is in place + request(subPath, User.STUART).statusCode(200).body(is(subPath)); + + var spanData = waitForSpanWithSubPath(subPath); + + assertEndUserId(User.STUART, spanData); + var roles = getRolesAttribute(spanData); + assertTrue(roles.contains(READER_ROLE)); + assertTrue(roles.contains(WRITER_ROLE)); + } + + @Test + public void testWhenPermitAllOnly() { + var subPath = "/permit-all-only"; + // request to endpoint with @PermitAll annotation + request(subPath, User.STUART).statusCode(200).body(is(subPath)); + + var spanData = waitForSpanWithSubPath(subPath); + + if (isProactiveAuthEnabled()) { + assertEndUserId(User.STUART, spanData); + + var roles = getRolesAttribute(spanData); + assertTrue(roles.contains(READER_ROLE)); + assertTrue(roles.contains(WRITER_ROLE)); + } else { + assertNoEndUserAttributes(spanData); + } + } + + @Test + public void testWhenRolesAllowedAnnotationOnlyAuthorizedAugmentor() { + var subPath = "/roles-allowed-only-augmentor-role"; + // the endpoint is annotated with @RolesAllowed("AUGMENTOR") and no other authorization is in place + request(subPath, User.SCOTT).statusCode(200).body(is(subPath)); + + var spanData = waitForSpanWithSubPath(subPath); + + assertEndUserId(User.SCOTT, spanData); + + var roles = getRolesAttribute(spanData); + assertTrue(roles.contains(AUGMENTOR_ROLE)); + assertTrue(roles.contains(READER_ROLE)); + assertFalse(roles.contains(WRITER_ROLE)); + } + + @Test + public void testWhenPermitAllOnlyAugmentor() { + var subPath = "/permit-all-only-augmentor"; + // the endpoint is annotated with @PermitAll and no authorization is in place + request(subPath, User.STUART).statusCode(200).body(is(subPath)); + + var spanData = waitForSpanWithSubPath(subPath); + + if (isProactiveAuthEnabled()) { + assertEndUserId(User.STUART, spanData); + + var roles = getRolesAttribute(spanData); + assertTrue(roles.contains(AUGMENTOR_ROLE)); + assertTrue(roles.contains(READER_ROLE)); + assertTrue(roles.contains(WRITER_ROLE)); + } else { + assertNoEndUserAttributes(spanData); + } + } + + @Test + public void testAttributesWhenNoAuthorizationInPlaceAugmentor() { + var subPath = "/no-authorization-augmentor"; + // there is no authorization in place, therefore authentication happnes on demand + request(subPath, User.SCOTT).statusCode(200).body(is(subPath)); + + var spanData = waitForSpanWithSubPath(subPath); + + if (isProactiveAuthEnabled()) { + assertEndUserId(User.SCOTT, spanData); + + var roles = getRolesAttribute(spanData); + assertTrue(roles.contains(AUGMENTOR_ROLE)); + assertTrue(roles.contains(READER_ROLE)); + assertFalse(roles.contains(WRITER_ROLE)); + } else { + assertNoEndUserAttributes(spanData); + } + } + + @Test + public void testWhenConfigRolesMappingAndHttpPermAugmentor() { + var subPath = "/roles-mapping-http-perm-augmentor"; + // the endpoint is annotated with @PermitAll, HTTP permission 'permit-all' is in place; auth happens on demand + request(subPath, User.STUART).statusCode(200).body(is(subPath)); + + var spanData = waitForSpanWithSubPath(subPath); + + if (isProactiveAuthEnabled()) { + assertEndUserId(User.STUART, spanData); + + var roles = getRolesAttribute(spanData); + assertTrue(roles.contains(AUGMENTOR_ROLE)); + assertTrue(roles.contains(HTTP_PERM_ROLES_ALLOWED_MAPPING_ROLE)); + assertTrue(roles.contains(READER_ROLE)); + assertTrue(roles.contains(WRITER_ROLE)); + } else { + assertNoEndUserAttributes(spanData); + } + } + + @Test + public void testWhenConfigRolesMappingHttpPerm() { + var subPath = "/roles-mapping-http-perm"; + // request endpoint with both 'permit-all' HTTP permission and @PermitAll annotation + request(subPath, User.STUART).statusCode(200).body(is(subPath)); + + var spanData = waitForSpanWithSubPath(subPath); + + if (isProactiveAuthEnabled()) { + assertEndUserId(User.STUART, spanData); + + var roles = getRolesAttribute(spanData); + assertTrue(roles.contains(HTTP_PERM_ROLES_ALLOWED_MAPPING_ROLE)); + assertTrue(roles.contains(WRITER_ROLE)); + assertTrue(roles.contains(READER_ROLE)); + } else { + assertNoEndUserAttributes(spanData); + } + } + + @Test + public void testWhenRolesAllowedAnnotationHttpPermUnauthorized() { + var subPath = "/roles-allowed-writer-http-perm-role"; + // the endpoint is annotated with @RolesAllowed("WRITER-HTTP-PERM") + // the 'AUTHZ-FAILURE-ROLE' mapped from 'READER' by HTTP permission roles policy + request(subPath, User.SCOTT).statusCode(403); + + var spanData = waitForSpanWithSubPath(subPath); + + assertEndUserId(User.SCOTT, spanData); + + var roles = getRolesAttribute(spanData); + assertTrue(roles.contains(READER_ROLE)); + assertTrue(roles.contains(AUTH_FAILURE_ROLE)); + assertFalse(roles.contains(WRITER_ROLE)); + assertFalse(roles.contains(WRITER_HTTP_PERM_ROLE)); + } + + @Test + public void testWhenRolesAllowedAnnotationHttpPermAuthorized() { + var subPath = "/roles-allowed-writer-http-perm-role"; + // the endpoint is annotated with @RolesAllowed("WRITER-HTTP-PERM") + // the 'WRITER-HTTP-PERM' role is remapped from 'WRITER' role by HTTP permission roles policy + // the 'AUTHZ-FAILURE-ROLE' mapped from 'READER' by HTTP permission roles policy + request(subPath, User.STUART).statusCode(200).body(is(subPath)); + + var spanData = waitForSpanWithSubPath(subPath); + + assertEndUserId(User.STUART, spanData); + + var roles = getRolesAttribute(spanData); + assertTrue(roles.contains(READER_ROLE)); + assertTrue(roles.contains(AUTH_FAILURE_ROLE)); + assertTrue(roles.contains(WRITER_ROLE)); + assertTrue(roles.contains(WRITER_HTTP_PERM_ROLE)); + } + + @Test + public void testWhenRolesAllowedAnnotationHttpPermAuthorizedAugmentor() { + var subPath = "/roles-allowed-http-perm-augmentor-role"; + // the endpoint is annotated with @RolesAllowed("HTTP-PERM-AUGMENTOR") + // and role 'HTTP-PERM-AUGMENTOR' is mapped by HTTP perm roles policy from the 'AUGMENTOR' role + request(subPath, User.STUART).statusCode(200).body(is(subPath)); + + var spanData = waitForSpanWithSubPath(subPath); + + assertEndUserId(User.STUART, spanData); + + var roles = getRolesAttribute(spanData); + assertTrue(roles.contains(AUGMENTOR_ROLE)); + assertTrue(roles.contains(HTTP_PERM_AUGMENTOR_ROLE)); + assertTrue(roles.contains(WRITER_ROLE)); + assertTrue(roles.contains(READER_ROLE)); + } + + @Test + public void testJaxRsHttpPermOnlyAuthorized() { + var subPath = "/jax-rs-http-perm"; + // only JAX-RS HTTP Permission roles policy that requires 'WRITER' role is in place + request(subPath, User.STUART).statusCode(200).body(is(subPath)); + + var spanData = waitForSpanWithSubPath(subPath); + + assertEndUserId(User.STUART, spanData); + + var roles = getRolesAttribute(spanData); + assertTrue(roles.contains(READER_ROLE)); + assertTrue(roles.contains(WRITER_ROLE)); + } + + @Test + public void testJaxRsHttpPermOnlyUnauthorized() { + var subPath = "/jax-rs-http-perm"; + // only JAX-RS HTTP Permission roles policy that requires 'WRITER' role is in place + request(subPath, User.SCOTT).statusCode(403); + + var spanData = waitForSpanWithSubPath(subPath); + + assertEndUserId(User.SCOTT, spanData); + + var roles = getRolesAttribute(spanData); + assertTrue(roles.contains(READER_ROLE)); + assertFalse(roles.contains(WRITER_ROLE)); + } + + @Test + public void testJaxRsHttpPermAndRolesAllowedAnnotationAuthorized() { + var subPath = "/jax-rs-http-perm-annotation-reader-role"; + // both JAX-RS HTTP Permission roles policy that requires 'WRITER' role and @RolesAllowed("READER") are in place + request(subPath, User.STUART).statusCode(200).body(is(subPath)); + + var spanData = waitForSpanWithSubPath(subPath); + + assertEndUserId(User.STUART, spanData); + + var roles = getRolesAttribute(spanData); + assertTrue(roles.contains(READER_ROLE)); + assertTrue(roles.contains(WRITER_ROLE)); + } + + @Test + public void testJaxRsHttpPermAndRolesAllowedAnnotationUnauthorized() { + var subPath = "/jax-rs-http-perm-annotation-reader-role"; + // both JAX-RS HTTP Permission roles policy that requires 'WRITER' role and @RolesAllowed("READER") are in place + request(subPath, User.SCOTT).statusCode(403); + + var spanData = waitForSpanWithSubPath(subPath); + + assertEndUserId(User.SCOTT, spanData); + + var roles = getRolesAttribute(spanData); + assertTrue(roles.contains(READER_ROLE)); + assertFalse(roles.contains(WRITER_ROLE)); + } + + @Test + public void testCustomSpanContainsEndUserAttributes() { + var subPath = "/custom-span-reader-role"; + // the endpoint is annotated with @RolesAllowed("READER") and no other authorization is in place + request(subPath, User.SCOTT).statusCode(200).body(is(subPath)); + var spanData = waitForSpanWithSubPath(subPath); + + assertEndUserId(User.SCOTT, spanData); + + var roles = getRolesAttribute(spanData); + assertTrue(roles.contains(READER_ROLE)); + assertFalse(roles.contains(WRITER_ROLE)); + + // assert custom span also contains end user attributes + spanData = waitForSpanWithPath("custom-path"); + assertEquals("custom-value", spanData.get("custom_attribute"), spanData.toString()); + + assertEndUserId(User.SCOTT, spanData); + + roles = getRolesAttribute(spanData); + assertTrue(roles.contains(READER_ROLE)); + assertFalse(roles.contains(WRITER_ROLE)); + } + + protected abstract boolean isProactiveAuthEnabled(); + + enum User { + + SCOTT("reader"), // READER_ROLE + STUART("writer"); // READER_ROLE, WRITER_ROLE + + private final String password; + + User(String password) { + this.password = password; + } + + private String userName() { + return this.toString().toLowerCase(); + } + } + + private static void assertEndUserId(User requestUser, Map spanData) { + assertEquals(requestUser.userName(), spanData.get(END_USER_ID_ATTR), spanData.toString()); + } + + private static void assertNoEndUserAttributes(Map spanData) { + assertNull(spanData.get(END_USER_ID_ATTR), spanData.toString()); + assertNull(spanData.get(END_USER_ROLE_ATTR), spanData.toString()); + } + + private static String getRolesAttribute(Map spanData) { + var roles = (String) spanData.get(END_USER_ROLE_ATTR); + assertNotNull(roles, spanData.toString()); + return roles; + } + + private static ValidatableResponse request(String subPath, User requestUser) { + return given() + .when() + .auth().preemptive().basic(requestUser.userName(), requestUser.password) + .get(createResourcePath(subPath)) + .then(); + } + + private static String createResourcePath(String subPath) { + return "/otel/enduser" + subPath; + } + + @SuppressWarnings("unchecked") + private static Map getSpanByPath(final String path) { + return getSpans() + .stream() + .map(m -> (Map) m.get("attributes")) + .filter(m -> path.equals(m.get(SemanticAttributes.HTTP_TARGET.getKey()))) + .findFirst() + .orElse(Map.of()); + } + + private static Map waitForSpanWithSubPath(final String subPath) { + return waitForSpanWithPath(createResourcePath(subPath)); + } + + private static Map waitForSpanWithPath(final String path) { + Awaitility.await().atMost(Duration.ofSeconds(30)).until(() -> !getSpanByPath(path).isEmpty(), new TypeSafeMatcher<>() { + @Override + protected boolean matchesSafely(Boolean aBoolean) { + return Boolean.TRUE.equals(aBoolean); + } + + @Override + public void describeTo(Description description) { + description.appendText("Span with the 'http.target' attribute not found: " + path + " ; " + getSpans()); + } + }); + return getSpanByPath(path); + } +} diff --git a/integration-tests/opentelemetry-reactive/src/test/java/io/quarkus/it/opentelemetry/reactive/enduser/EagerAuthEndUserEnabledTest.java b/integration-tests/opentelemetry-reactive/src/test/java/io/quarkus/it/opentelemetry/reactive/enduser/EagerAuthEndUserEnabledTest.java new file mode 100644 index 0000000000000..1d894e70f9209 --- /dev/null +++ b/integration-tests/opentelemetry-reactive/src/test/java/io/quarkus/it/opentelemetry/reactive/enduser/EagerAuthEndUserEnabledTest.java @@ -0,0 +1,15 @@ +package io.quarkus.it.opentelemetry.reactive.enduser; + +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.TestProfile; + +@QuarkusTest +@TestProfile(EndUserProfile.class) +public class EagerAuthEndUserEnabledTest extends AbstractEndUserTest { + + @Override + protected boolean isProactiveAuthEnabled() { + return true; + } + +} diff --git a/integration-tests/opentelemetry-reactive/src/test/java/io/quarkus/it/opentelemetry/reactive/enduser/EndUserProfile.java b/integration-tests/opentelemetry-reactive/src/test/java/io/quarkus/it/opentelemetry/reactive/enduser/EndUserProfile.java new file mode 100644 index 0000000000000..3cd13f99dc8ac --- /dev/null +++ b/integration-tests/opentelemetry-reactive/src/test/java/io/quarkus/it/opentelemetry/reactive/enduser/EndUserProfile.java @@ -0,0 +1,33 @@ +package io.quarkus.it.opentelemetry.reactive.enduser; + +import java.util.HashMap; +import java.util.Map; + +import io.quarkus.test.junit.QuarkusTestProfile; + +public class EndUserProfile implements QuarkusTestProfile { + + @Override + public Map getConfigOverrides() { + Map config = new HashMap<>(Map.of("quarkus.otel.traces.eusp.enabled", "true", + "quarkus.http.auth.permission.roles-1.policy", "role-policy-1", + "quarkus.http.auth.permission.roles-1.paths", "/otel/enduser/roles-allowed-writer-http-perm-role", + "quarkus.http.auth.policy.role-policy-1.roles.WRITER", "WRITER-HTTP-PERM")); + config.put("quarkus.http.auth.policy.role-policy-1.roles.READER", "AUTHZ-FAILURE-ROLE"); + config.put("quarkus.http.auth.policy.role-policy-1.roles-allowed", "WRITER"); + config.put("quarkus.http.auth.permission.roles-2.policy", "role-policy-2"); + config.put("quarkus.http.auth.permission.roles-2.paths", "/otel/enduser/roles-allowed-http-perm-augmentor-role"); + config.put("quarkus.http.auth.policy.role-policy-2.roles-allowed", "AUGMENTOR"); + config.put("quarkus.http.auth.policy.role-policy-2.roles.AUGMENTOR", "HTTP-PERM-AUGMENTOR"); + config.put("quarkus.http.auth.permission.jax-rs.policy", "jax-rs"); + config.put("quarkus.http.auth.permission.jax-rs.paths", + "/otel/enduser/jax-rs-http-perm,/otel/enduser/jax-rs-http-perm-annotation-reader-role"); + config.put("quarkus.http.auth.permission.jax-rs.applies-to", "JAXRS"); + config.put("quarkus.http.auth.policy.jax-rs.roles-allowed", "WRITER"); + config.put("quarkus.http.auth.permission.permit-all.policy", "permit"); + config.put("quarkus.http.auth.permission.permit-all.paths", + "/otel/enduser/roles-mapping-http-perm,/otel/enduser/roles-mapping-http-perm-augmentor"); + config.put("quarkus.http.auth.roles-mapping.ROLES-ALLOWED-MAPPING-ROLE", "ROLES-ALLOWED-MAPPING-ROLE-HTTP-PERM"); + return config; + } +} diff --git a/integration-tests/opentelemetry-reactive/src/test/java/io/quarkus/it/opentelemetry/reactive/enduser/LazyAuthEndUserEnabledTest.java b/integration-tests/opentelemetry-reactive/src/test/java/io/quarkus/it/opentelemetry/reactive/enduser/LazyAuthEndUserEnabledTest.java new file mode 100644 index 0000000000000..9e85525ea99d7 --- /dev/null +++ b/integration-tests/opentelemetry-reactive/src/test/java/io/quarkus/it/opentelemetry/reactive/enduser/LazyAuthEndUserEnabledTest.java @@ -0,0 +1,13 @@ +package io.quarkus.it.opentelemetry.reactive.enduser; + +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.TestProfile; + +@QuarkusTest +@TestProfile(LazyAuthEndUserProfile.class) +public class LazyAuthEndUserEnabledTest extends AbstractEndUserTest { + @Override + protected boolean isProactiveAuthEnabled() { + return false; + } +} diff --git a/integration-tests/opentelemetry-reactive/src/test/java/io/quarkus/it/opentelemetry/reactive/enduser/LazyAuthEndUserProfile.java b/integration-tests/opentelemetry-reactive/src/test/java/io/quarkus/it/opentelemetry/reactive/enduser/LazyAuthEndUserProfile.java new file mode 100644 index 0000000000000..f62638221556b --- /dev/null +++ b/integration-tests/opentelemetry-reactive/src/test/java/io/quarkus/it/opentelemetry/reactive/enduser/LazyAuthEndUserProfile.java @@ -0,0 +1,14 @@ +package io.quarkus.it.opentelemetry.reactive.enduser; + +import java.util.HashMap; +import java.util.Map; + +public class LazyAuthEndUserProfile extends EndUserProfile { + + @Override + public Map getConfigOverrides() { + var config = new HashMap<>(super.getConfigOverrides()); + config.put("quarkus.http.auth.proactive", "false"); + return config; + } +} diff --git a/integration-tests/opentelemetry/pom.xml b/integration-tests/opentelemetry/pom.xml index 903728882aee0..d3ac1983b7125 100644 --- a/integration-tests/opentelemetry/pom.xml +++ b/integration-tests/opentelemetry/pom.xml @@ -44,7 +44,7 @@ io.quarkus - quarkus-security + quarkus-elytron-security-properties-file @@ -59,11 +59,6 @@ quarkus-junit5 test - - io.quarkus - quarkus-test-security - test - io.rest-assured rest-assured @@ -130,7 +125,7 @@ io.quarkus - quarkus-security-deployment + quarkus-elytron-security-properties-file-deployment ${project.version} pom test diff --git a/integration-tests/opentelemetry/src/main/java/io/quarkus/it/opentelemetry/CustomSecurityIdentityAugmentor.java b/integration-tests/opentelemetry/src/main/java/io/quarkus/it/opentelemetry/CustomSecurityIdentityAugmentor.java new file mode 100644 index 0000000000000..e5fe326b65d68 --- /dev/null +++ b/integration-tests/opentelemetry/src/main/java/io/quarkus/it/opentelemetry/CustomSecurityIdentityAugmentor.java @@ -0,0 +1,43 @@ +package io.quarkus.it.opentelemetry; + +import java.util.Map; + +import jakarta.inject.Singleton; + +import io.quarkus.security.identity.AuthenticationRequestContext; +import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.security.identity.SecurityIdentityAugmentor; +import io.quarkus.security.runtime.QuarkusSecurityIdentity; +import io.quarkus.vertx.http.runtime.security.HttpSecurityUtils; +import io.smallrye.mutiny.Uni; + +@Singleton +public class CustomSecurityIdentityAugmentor implements SecurityIdentityAugmentor { + @Override + public Uni augment(SecurityIdentity securityIdentity, + AuthenticationRequestContext authenticationRequestContext) { + return augment(securityIdentity, authenticationRequestContext, Map.of()); + } + + @Override + public Uni augment(SecurityIdentity identity, AuthenticationRequestContext context, + Map attributes) { + var routingContext = HttpSecurityUtils.getRoutingContextAttribute(attributes); + if (routingContext != null) { + var augmentorScenario = routingContext.normalizedPath().contains("-augmentor"); + var configRolesMappingScenario = routingContext.normalizedPath().contains("roles-mapping-http-perm"); + if (augmentorScenario || configRolesMappingScenario) { + var builder = QuarkusSecurityIdentity.builder(identity); + if (augmentorScenario) { + builder.addRole("AUGMENTOR"); + } + if (configRolesMappingScenario) { + // this role is supposed to be re-mapped by HTTP roles mapping (not path-specific) + builder.addRole("ROLES-ALLOWED-MAPPING-ROLE"); + } + return Uni.createFrom().item(builder.build()); + } + } + return Uni.createFrom().item(identity); + } +} diff --git a/integration-tests/opentelemetry/src/main/java/io/quarkus/it/opentelemetry/EndUserResource.java b/integration-tests/opentelemetry/src/main/java/io/quarkus/it/opentelemetry/EndUserResource.java new file mode 100644 index 0000000000000..e06ff171b8dfd --- /dev/null +++ b/integration-tests/opentelemetry/src/main/java/io/quarkus/it/opentelemetry/EndUserResource.java @@ -0,0 +1,112 @@ +package io.quarkus.it.opentelemetry; + +import jakarta.annotation.security.PermitAll; +import jakarta.annotation.security.RolesAllowed; +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.semconv.SemanticAttributes; + +@Path("/otel/enduser") +public class EndUserResource { + + @Inject + Tracer tracer; + + @Path("/no-authorization") + @GET + public String noAuthorization() { + return "/no-authorization"; + } + + @RolesAllowed("WRITER") + @Path("/roles-allowed-only-writer-role") + @GET + public String rolesAllowedOnlyWriterRole() { + return "/roles-allowed-only-writer-role"; + } + + @PermitAll + @Path("/permit-all-only") + @GET + public String permitAllOnly() { + return "/permit-all-only"; + } + + @Path("/no-authorization-augmentor") + @GET + public String noAuthorizationAugmentor() { + return "/no-authorization-augmentor"; + } + + @RolesAllowed("AUGMENTOR") + @Path("/roles-allowed-only-augmentor-role") + @GET + public String rolesAllowedOnlyAugmentorRole() { + return "/roles-allowed-only-augmentor-role"; + } + + @PermitAll + @Path("/permit-all-only-augmentor") + @GET + public String permitAllOnlyAugmentor() { + return "/permit-all-only-augmentor"; + } + + @RolesAllowed("WRITER-HTTP-PERM") + @Path("/roles-allowed-writer-http-perm-role") + @GET + public String rolesAllowedHttpPermWriterHttpPermRole() { + return "/roles-allowed-writer-http-perm-role"; + } + + @PermitAll + @Path("/roles-mapping-http-perm") + @GET + public String permitAllAnnotationConfigRolesMappingPermitAllHttpPerm() { + return "/roles-mapping-http-perm"; + } + + @RolesAllowed("HTTP-PERM-AUGMENTOR") + @Path("/roles-allowed-http-perm-augmentor-role") + @GET + public String rolesAllowedHttpPermHttpAugmentorPermRole() { + return "/roles-allowed-http-perm-augmentor-role"; + } + + @PermitAll + @Path("/roles-mapping-http-perm-augmentor") + @GET + public String permitAllAnnotationConfigRolesMappingPermitAllHttpPermAugmentor() { + return "/roles-mapping-http-perm-augmentor"; + } + + @Path("/jax-rs-http-perm") + @GET + public String jaxRsHttpPermOnly() { + return "/jax-rs-http-perm"; + } + + @RolesAllowed("READER") + @Path("/jax-rs-http-perm-annotation-reader-role") + @GET + public String jaxRsHttpPermRolesAllowedReaderRole() { + return "/jax-rs-http-perm-annotation-reader-role"; + } + + @RolesAllowed("READER") + @Path("/custom-span-reader-role") + @GET + public String customSpanReaderRole() { + var span = tracer.spanBuilder("custom-span").startSpan(); + try (var ignored = span.makeCurrent()) { + span.setAttribute("custom_attribute", "custom-value"); + span.setAttribute(SemanticAttributes.HTTP_TARGET, "custom-path"); + } finally { + span.end(); + } + return "/custom-span-reader-role"; + } +} diff --git a/integration-tests/opentelemetry/src/main/resources/application.properties b/integration-tests/opentelemetry/src/main/resources/application.properties index 054108ad58ab8..514f563c31933 100644 --- a/integration-tests/opentelemetry/src/main/resources/application.properties +++ b/integration-tests/opentelemetry/src/main/resources/application.properties @@ -8,3 +8,11 @@ quarkus.otel.bsp.export.timeout=5s pingpong/mp-rest/url=${test.url} simple/mp-rest/url=${test.url} + +quarkus.security.users.embedded.roles.stuart=READER,WRITER +quarkus.security.users.embedded.roles.scott=READER +quarkus.security.users.embedded.users.stuart=writer +quarkus.security.users.embedded.users.scott=reader +quarkus.security.users.embedded.plain-text=true +quarkus.security.users.embedded.enabled=true +quarkus.http.auth.basic=true diff --git a/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/AbstractEndUserTest.java b/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/AbstractEndUserTest.java new file mode 100644 index 0000000000000..afb0ebc55ee05 --- /dev/null +++ b/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/AbstractEndUserTest.java @@ -0,0 +1,435 @@ +package io.quarkus.it.opentelemetry; + +import static io.quarkus.it.opentelemetry.AbstractEndUserTest.User.SCOTT; +import static io.quarkus.it.opentelemetry.AbstractEndUserTest.User.STUART; +import static io.restassured.RestAssured.get; +import static io.restassured.RestAssured.given; +import static java.net.HttpURLConnection.HTTP_OK; +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.awaitility.Awaitility.await; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.time.Duration; +import java.util.List; +import java.util.Map; + +import org.awaitility.Awaitility; +import org.hamcrest.Description; +import org.hamcrest.TypeSafeMatcher; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import io.opentelemetry.semconv.SemanticAttributes; +import io.restassured.common.mapper.TypeRef; +import io.restassured.response.ValidatableResponse; + +public abstract class AbstractEndUserTest { + + private static final String HTTP_PERM_AUGMENTOR_ROLE = "HTTP-PERM-AUGMENTOR"; + private static final String END_USER_ID_ATTR = "attr_" + SemanticAttributes.ENDUSER_ID.getKey(); + private static final String END_USER_ROLE_ATTR = "attr_" + SemanticAttributes.ENDUSER_ROLE.getKey(); + private static final String READER_ROLE = "READER"; + private static final String WRITER_ROLE = "WRITER"; + private static final String WRITER_HTTP_PERM_ROLE = "WRITER-HTTP-PERM"; + private static final String AUTH_FAILURE_ROLE = "AUTHZ-FAILURE-ROLE"; + private static final String AUGMENTOR_ROLE = "AUGMENTOR"; + + /** + * This is 'ROLES-ALLOWED-MAPPING-ROLE' role granted to the SecureIdentity by augmentor and + * remapped to 'ROLES-ALLOWED-MAPPING-ROLE-HTTP-PERM' role which allows to verify that the + * 'quarkus.http.auth.roles-mapping' config-level roles mapping is reflected in the End User attributes. + */ + private static final String HTTP_PERM_ROLES_ALLOWED_MAPPING_ROLE = "ROLES-ALLOWED-MAPPING-ROLE-HTTP-PERM"; + + @BeforeEach + @AfterEach + public void reset() { + given().get("/reset").then().statusCode(HTTP_OK); + await().atMost(5, SECONDS).until(() -> getSpans().isEmpty()); + } + + @Test + public void testAttributesWhenNoAuthorizationInPlace() { + var subPath = "/no-authorization"; + request(subPath, SCOTT).statusCode(200).body(is(subPath)); + + var spanData = waitForSpanWithSubPath(subPath); + + if (isProactiveAuthEnabled()) { + assertEndUserId(SCOTT, spanData); + + var roles = getRolesAttribute(spanData); + assertTrue(roles.contains(READER_ROLE)); + assertFalse(roles.contains(WRITER_ROLE)); + } else { + assertNoEndUserAttributes(spanData); + } + } + + @Test + public void testWhenRolesAllowedAnnotationOnlyUnauthorized() { + var subPath = "/roles-allowed-only-writer-role"; + // the endpoint is annotated with @RolesAllowed("WRITER") and no other authorization is in place + request(subPath, SCOTT).statusCode(403); + + var spanData = waitForSpanWithSubPath(subPath); + + assertEndUserId(SCOTT, spanData); + + var roles = getRolesAttribute(spanData); + assertTrue(roles.contains(READER_ROLE)); + assertFalse(roles.contains(WRITER_ROLE)); + } + + @Test + public void testWhenRolesAllowedAnnotationOnlyAuthorized() { + var subPath = "/roles-allowed-only-writer-role"; + // the endpoint is annotated with @RolesAllowed("WRITER") and no other authorization is in place + request(subPath, STUART).statusCode(200).body(is(subPath)); + + var spanData = waitForSpanWithSubPath(subPath); + + assertEndUserId(STUART, spanData); + var roles = getRolesAttribute(spanData); + assertTrue(roles.contains(READER_ROLE)); + assertTrue(roles.contains(WRITER_ROLE)); + } + + @Test + public void testWhenPermitAllOnly() { + var subPath = "/permit-all-only"; + // request to endpoint with @PermitAll annotation + request(subPath, STUART).statusCode(200).body(is(subPath)); + + var spanData = waitForSpanWithSubPath(subPath); + + if (isProactiveAuthEnabled()) { + assertEndUserId(STUART, spanData); + + var roles = getRolesAttribute(spanData); + assertTrue(roles.contains(READER_ROLE)); + assertTrue(roles.contains(WRITER_ROLE)); + } else { + assertNoEndUserAttributes(spanData); + } + } + + @Test + public void testWhenRolesAllowedAnnotationOnlyAuthorizedAugmentor() { + var subPath = "/roles-allowed-only-augmentor-role"; + // the endpoint is annotated with @RolesAllowed("AUGMENTOR") and no other authorization is in place + request(subPath, SCOTT).statusCode(200).body(is(subPath)); + + var spanData = waitForSpanWithSubPath(subPath); + + assertEndUserId(SCOTT, spanData); + + var roles = getRolesAttribute(spanData); + assertTrue(roles.contains(AUGMENTOR_ROLE)); + assertTrue(roles.contains(READER_ROLE)); + assertFalse(roles.contains(WRITER_ROLE)); + } + + @Test + public void testWhenPermitAllOnlyAugmentor() { + var subPath = "/permit-all-only-augmentor"; + // the endpoint is annotated with @PermitAll and no authorization is in place + request(subPath, STUART).statusCode(200).body(is(subPath)); + + var spanData = waitForSpanWithSubPath(subPath); + + if (isProactiveAuthEnabled()) { + assertEndUserId(STUART, spanData); + + var roles = getRolesAttribute(spanData); + assertTrue(roles.contains(AUGMENTOR_ROLE)); + assertTrue(roles.contains(READER_ROLE)); + assertTrue(roles.contains(WRITER_ROLE)); + } else { + assertNoEndUserAttributes(spanData); + } + } + + @Test + public void testAttributesWhenNoAuthorizationInPlaceAugmentor() { + var subPath = "/no-authorization-augmentor"; + // there is no authorization in place, therefore authentication happnes on demand + request(subPath, SCOTT).statusCode(200).body(is(subPath)); + + var spanData = waitForSpanWithSubPath(subPath); + + if (isProactiveAuthEnabled()) { + assertEndUserId(SCOTT, spanData); + + var roles = getRolesAttribute(spanData); + assertTrue(roles.contains(AUGMENTOR_ROLE)); + assertTrue(roles.contains(READER_ROLE)); + assertFalse(roles.contains(WRITER_ROLE)); + } else { + assertNoEndUserAttributes(spanData); + } + } + + @Test + public void testWhenConfigRolesMappingAndHttpPermAugmentor() { + var subPath = "/roles-mapping-http-perm-augmentor"; + // the endpoint is annotated with @PermitAll, HTTP permission 'permit-all' is in place; auth happens on demand + request(subPath, STUART).statusCode(200).body(is(subPath)); + + var spanData = waitForSpanWithSubPath(subPath); + + if (isProactiveAuthEnabled()) { + assertEndUserId(STUART, spanData); + + var roles = getRolesAttribute(spanData); + assertTrue(roles.contains(AUGMENTOR_ROLE)); + assertTrue(roles.contains(HTTP_PERM_ROLES_ALLOWED_MAPPING_ROLE)); + assertTrue(roles.contains(READER_ROLE)); + assertTrue(roles.contains(WRITER_ROLE)); + } else { + assertNoEndUserAttributes(spanData); + } + } + + @Test + public void testWhenConfigRolesMappingHttpPerm() { + var subPath = "/roles-mapping-http-perm"; + // request endpoint with both 'permit-all' HTTP permission and @PermitAll annotation + request(subPath, STUART).statusCode(200).body(is(subPath)); + + var spanData = waitForSpanWithSubPath(subPath); + + if (isProactiveAuthEnabled()) { + assertEndUserId(STUART, spanData); + + var roles = getRolesAttribute(spanData); + assertTrue(roles.contains(HTTP_PERM_ROLES_ALLOWED_MAPPING_ROLE)); + assertTrue(roles.contains(WRITER_ROLE)); + assertTrue(roles.contains(READER_ROLE)); + } else { + assertNoEndUserAttributes(spanData); + } + } + + @Test + public void testWhenRolesAllowedAnnotationHttpPermUnauthorized() { + var subPath = "/roles-allowed-writer-http-perm-role"; + // the endpoint is annotated with @RolesAllowed("WRITER-HTTP-PERM") + // the 'AUTHZ-FAILURE-ROLE' mapped from 'READER' by HTTP permission roles policy + request(subPath, SCOTT).statusCode(403); + + var spanData = waitForSpanWithSubPath(subPath); + + assertEndUserId(SCOTT, spanData); + + var roles = getRolesAttribute(spanData); + assertTrue(roles.contains(READER_ROLE)); + assertTrue(roles.contains(AUTH_FAILURE_ROLE)); + assertFalse(roles.contains(WRITER_ROLE)); + assertFalse(roles.contains(WRITER_HTTP_PERM_ROLE)); + } + + @Test + public void testWhenRolesAllowedAnnotationHttpPermAuthorized() { + var subPath = "/roles-allowed-writer-http-perm-role"; + // the endpoint is annotated with @RolesAllowed("WRITER-HTTP-PERM") + // the 'WRITER-HTTP-PERM' role is remapped from 'WRITER' role by HTTP permission roles policy + // the 'AUTHZ-FAILURE-ROLE' mapped from 'READER' by HTTP permission roles policy + request(subPath, STUART).statusCode(200).body(is(subPath)); + + var spanData = waitForSpanWithSubPath(subPath); + + assertEndUserId(STUART, spanData); + + var roles = getRolesAttribute(spanData); + assertTrue(roles.contains(READER_ROLE)); + assertTrue(roles.contains(AUTH_FAILURE_ROLE)); + assertTrue(roles.contains(WRITER_ROLE)); + assertTrue(roles.contains(WRITER_HTTP_PERM_ROLE)); + } + + @Test + public void testWhenRolesAllowedAnnotationHttpPermAuthorizedAugmentor() { + var subPath = "/roles-allowed-http-perm-augmentor-role"; + // the endpoint is annotated with @RolesAllowed("HTTP-PERM-AUGMENTOR") + // and role 'HTTP-PERM-AUGMENTOR' is mapped by HTTP perm roles policy from the 'AUGMENTOR' role + request(subPath, STUART).statusCode(200).body(is(subPath)); + + var spanData = waitForSpanWithSubPath(subPath); + + assertEndUserId(STUART, spanData); + + var roles = getRolesAttribute(spanData); + assertTrue(roles.contains(AUGMENTOR_ROLE)); + assertTrue(roles.contains(HTTP_PERM_AUGMENTOR_ROLE)); + assertTrue(roles.contains(WRITER_ROLE)); + assertTrue(roles.contains(READER_ROLE)); + } + + @Test + public void testJaxRsHttpPermOnlyAuthorized() { + var subPath = "/jax-rs-http-perm"; + // only JAX-RS HTTP Permission roles policy that requires 'WRITER' role is in place + request(subPath, STUART).statusCode(200).body(is(subPath)); + + var spanData = waitForSpanWithSubPath(subPath); + + assertEndUserId(STUART, spanData); + + var roles = getRolesAttribute(spanData); + assertTrue(roles.contains(READER_ROLE)); + assertTrue(roles.contains(WRITER_ROLE)); + } + + @Test + public void testJaxRsHttpPermOnlyUnauthorized() { + var subPath = "/jax-rs-http-perm"; + // only JAX-RS HTTP Permission roles policy that requires 'WRITER' role is in place + request(subPath, SCOTT).statusCode(403); + + var spanData = waitForSpanWithSubPath(subPath); + + assertEndUserId(SCOTT, spanData); + + var roles = getRolesAttribute(spanData); + assertTrue(roles.contains(READER_ROLE)); + assertFalse(roles.contains(WRITER_ROLE)); + } + + @Test + public void testJaxRsHttpPermAndRolesAllowedAnnotationAuthorized() { + var subPath = "/jax-rs-http-perm-annotation-reader-role"; + // both JAX-RS HTTP Permission roles policy that requires 'WRITER' role and @RolesAllowed("READER") are in place + request(subPath, STUART).statusCode(200).body(is(subPath)); + + var spanData = waitForSpanWithSubPath(subPath); + + assertEndUserId(STUART, spanData); + + var roles = getRolesAttribute(spanData); + assertTrue(roles.contains(READER_ROLE)); + assertTrue(roles.contains(WRITER_ROLE)); + } + + @Test + public void testJaxRsHttpPermAndRolesAllowedAnnotationUnauthorized() { + var subPath = "/jax-rs-http-perm-annotation-reader-role"; + // both JAX-RS HTTP Permission roles policy that requires 'WRITER' role and @RolesAllowed("READER") are in place + request(subPath, SCOTT).statusCode(403); + + var spanData = waitForSpanWithSubPath(subPath); + + assertEndUserId(SCOTT, spanData); + + var roles = getRolesAttribute(spanData); + assertTrue(roles.contains(READER_ROLE)); + assertFalse(roles.contains(WRITER_ROLE)); + } + + @Test + public void testCustomSpanContainsEndUserAttributes() { + var subPath = "/custom-span-reader-role"; + // the endpoint is annotated with @RolesAllowed("READER") and no other authorization is in place + request(subPath, SCOTT).statusCode(200).body(is(subPath)); + var spanData = waitForSpanWithSubPath(subPath); + + assertEndUserId(SCOTT, spanData); + + var roles = getRolesAttribute(spanData); + assertTrue(roles.contains(READER_ROLE)); + assertFalse(roles.contains(WRITER_ROLE)); + + // assert custom span also contains end user attributes + spanData = waitForSpanWithPath("custom-path"); + assertEquals("custom-value", spanData.get("attr_custom_attribute"), spanData.toString()); + + assertEndUserId(SCOTT, spanData); + + roles = getRolesAttribute(spanData); + assertTrue(roles.contains(READER_ROLE)); + assertFalse(roles.contains(WRITER_ROLE)); + } + + protected abstract boolean isProactiveAuthEnabled(); + + enum User { + + SCOTT("reader"), // READER_ROLE + STUART("writer"); // READER_ROLE, WRITER_ROLE + + private final String password; + + User(String password) { + this.password = password; + } + + private String userName() { + return this.toString().toLowerCase(); + } + } + + private static void assertEndUserId(User requestUser, Map spanData) { + assertEquals(requestUser.userName(), spanData.get(END_USER_ID_ATTR), spanData.toString()); + } + + private static void assertNoEndUserAttributes(Map spanData) { + assertNull(spanData.get(END_USER_ID_ATTR), spanData.toString()); + assertNull(spanData.get(END_USER_ROLE_ATTR), spanData.toString()); + } + + private static String getRolesAttribute(Map spanData) { + var roles = (String) spanData.get(END_USER_ROLE_ATTR); + assertNotNull(roles, spanData.toString()); + return roles; + } + + private static ValidatableResponse request(String subPath, User requestUser) { + return given() + .when() + .auth().preemptive().basic(requestUser.userName(), requestUser.password) + .get(createResourcePath(subPath)) + .then(); + } + + private static String createResourcePath(String subPath) { + return "/otel/enduser" + subPath; + } + + private static List> getSpans() { + return get("/export").body().as(new TypeRef<>() { + }); + } + + private static Map getSpanByPath(final String path) { + return getSpans() + .stream() + .filter(m -> path.equals(m.get("attr_" + SemanticAttributes.HTTP_TARGET.getKey()))) + .findFirst() + .orElse(Map.of()); + } + + private static Map waitForSpanWithSubPath(final String subPath) { + return waitForSpanWithPath(createResourcePath(subPath)); + } + + private static Map waitForSpanWithPath(final String path) { + Awaitility.await().atMost(Duration.ofSeconds(30)).until(() -> !getSpanByPath(path).isEmpty(), new TypeSafeMatcher<>() { + @Override + protected boolean matchesSafely(Boolean aBoolean) { + return Boolean.TRUE.equals(aBoolean); + } + + @Override + public void describeTo(Description description) { + description.appendText("Span with the 'http.target' attribute not found: " + path + " ; " + getSpans()); + } + }); + return getSpanByPath(path); + } +} diff --git a/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/EagerAuthEndUserEnabledTest.java b/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/EagerAuthEndUserEnabledTest.java new file mode 100644 index 0000000000000..37d7c40b319b1 --- /dev/null +++ b/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/EagerAuthEndUserEnabledTest.java @@ -0,0 +1,16 @@ +package io.quarkus.it.opentelemetry; + +import io.quarkus.it.opentelemetry.util.EndUserProfile; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.TestProfile; + +@QuarkusTest +@TestProfile(EndUserProfile.class) +public class EagerAuthEndUserEnabledTest extends AbstractEndUserTest { + + @Override + protected boolean isProactiveAuthEnabled() { + return true; + } + +} diff --git a/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/LazyAuthEndUserEnabledTest.java b/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/LazyAuthEndUserEnabledTest.java new file mode 100644 index 0000000000000..82f4d329840ef --- /dev/null +++ b/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/LazyAuthEndUserEnabledTest.java @@ -0,0 +1,14 @@ +package io.quarkus.it.opentelemetry; + +import io.quarkus.it.opentelemetry.util.LazyAuthEndUserProfile; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.TestProfile; + +@QuarkusTest +@TestProfile(LazyAuthEndUserProfile.class) +public class LazyAuthEndUserEnabledTest extends AbstractEndUserTest { + @Override + protected boolean isProactiveAuthEnabled() { + return false; + } +} diff --git a/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/OpenTelemetryTest.java b/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/OpenTelemetryTest.java index a8df12fd2304b..8b9173127ea6d 100644 --- a/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/OpenTelemetryTest.java +++ b/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/OpenTelemetryTest.java @@ -27,7 +27,9 @@ import java.util.Map; import java.util.concurrent.TimeUnit; +import org.hamcrest.Matchers; import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -38,6 +40,7 @@ import io.opentelemetry.api.trace.TraceId; import io.opentelemetry.context.Context; import io.opentelemetry.context.propagation.TextMapSetter; +import io.opentelemetry.semconv.SemanticAttributes; import io.quarkus.it.opentelemetry.util.SocketClient; import io.quarkus.test.common.http.TestHTTPResource; import io.quarkus.test.junit.QuarkusTest; @@ -705,6 +708,34 @@ void testWrongHTTPVersion() { await().atMost(5, TimeUnit.SECONDS).until(() -> getSpans().size() == 1); } + /** + * Test no End User attributes are added when the feature is disabled. + */ + @Test + public void testNoEndUserAttributes() { + RestAssured + .given() + .auth().preemptive().basic("stuart", "writer") + .get("/otel/enduser/roles-allowed-only-writer-role") + .then() + .statusCode(200) + .body(Matchers.is("/roles-allowed-only-writer-role")); + RestAssured + .given() + .auth().preemptive().basic("scott", "reader") + .get("/otel/enduser/roles-allowed-only-writer-role") + .then() + .statusCode(403); + await().atMost(5, TimeUnit.SECONDS).until(() -> getSpans().size() > 1); + List> spans = getSpans(); + Assertions.assertTrue(spans + .stream() + .flatMap(m -> m.entrySet().stream()) + .filter(e -> ("attr_" + SemanticAttributes.ENDUSER_ID.getKey()).equals(e.getKey()) + || ("attr_" + SemanticAttributes.ENDUSER_ROLE.getKey()).equals(e.getKey())) + .findAny().isEmpty()); + } + private void verifyResource(Map spanData) { assertEquals("opentelemetry-integration-test", spanData.get("resource_service.name")); assertEquals("999-SNAPSHOT", spanData.get("resource_service.version")); diff --git a/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/util/EndUserProfile.java b/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/util/EndUserProfile.java new file mode 100644 index 0000000000000..0ca0a38cdd01f --- /dev/null +++ b/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/util/EndUserProfile.java @@ -0,0 +1,33 @@ +package io.quarkus.it.opentelemetry.util; + +import java.util.HashMap; +import java.util.Map; + +import io.quarkus.test.junit.QuarkusTestProfile; + +public class EndUserProfile implements QuarkusTestProfile { + + @Override + public Map getConfigOverrides() { + Map config = new HashMap<>(Map.of("quarkus.otel.traces.eusp.enabled", "true", + "quarkus.http.auth.permission.roles-1.policy", "role-policy-1", + "quarkus.http.auth.permission.roles-1.paths", "/otel/enduser/roles-allowed-writer-http-perm-role", + "quarkus.http.auth.policy.role-policy-1.roles.WRITER", "WRITER-HTTP-PERM")); + config.put("quarkus.http.auth.policy.role-policy-1.roles.READER", "AUTHZ-FAILURE-ROLE"); + config.put("quarkus.http.auth.policy.role-policy-1.roles-allowed", "WRITER"); + config.put("quarkus.http.auth.permission.roles-2.policy", "role-policy-2"); + config.put("quarkus.http.auth.permission.roles-2.paths", "/otel/enduser/roles-allowed-http-perm-augmentor-role"); + config.put("quarkus.http.auth.policy.role-policy-2.roles-allowed", "AUGMENTOR"); + config.put("quarkus.http.auth.policy.role-policy-2.roles.AUGMENTOR", "HTTP-PERM-AUGMENTOR"); + config.put("quarkus.http.auth.permission.jax-rs.policy", "jax-rs"); + config.put("quarkus.http.auth.permission.jax-rs.paths", + "/otel/enduser/jax-rs-http-perm,/otel/enduser/jax-rs-http-perm-annotation-reader-role"); + config.put("quarkus.http.auth.permission.jax-rs.applies-to", "JAXRS"); + config.put("quarkus.http.auth.policy.jax-rs.roles-allowed", "WRITER"); + config.put("quarkus.http.auth.permission.permit-all.policy", "permit"); + config.put("quarkus.http.auth.permission.permit-all.paths", + "/otel/enduser/roles-mapping-http-perm,/otel/enduser/roles-mapping-http-perm-augmentor"); + config.put("quarkus.http.auth.roles-mapping.ROLES-ALLOWED-MAPPING-ROLE", "ROLES-ALLOWED-MAPPING-ROLE-HTTP-PERM"); + return config; + } +} diff --git a/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/util/LazyAuthEndUserProfile.java b/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/util/LazyAuthEndUserProfile.java new file mode 100644 index 0000000000000..471bce77e748b --- /dev/null +++ b/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/util/LazyAuthEndUserProfile.java @@ -0,0 +1,14 @@ +package io.quarkus.it.opentelemetry.util; + +import java.util.HashMap; +import java.util.Map; + +public class LazyAuthEndUserProfile extends EndUserProfile { + + @Override + public Map getConfigOverrides() { + var config = new HashMap<>(super.getConfigOverrides()); + config.put("quarkus.http.auth.proactive", "false"); + return config; + } +} diff --git a/integration-tests/reactive-messaging-rabbitmq/src/main/resources/application.properties b/integration-tests/reactive-messaging-rabbitmq/src/main/resources/application.properties index fe4687906f478..324e04ff729ec 100644 --- a/integration-tests/reactive-messaging-rabbitmq/src/main/resources/application.properties +++ b/integration-tests/reactive-messaging-rabbitmq/src/main/resources/application.properties @@ -3,3 +3,6 @@ mp.messaging.outgoing.people-out.exchange.name=people mp.messaging.incoming.people-in.queue.name=people mp.messaging.incoming.people-in.exchange.name=people + +quarkus.messaging.health.people-in.readiness.enabled=false +quarkus.messaging.health.people-in.liveness.enabled=false diff --git a/integration-tests/reactive-messaging-rabbitmq/src/test/java/io/quarkus/it/rabbitmq/RabbitMQConnectorTest.java b/integration-tests/reactive-messaging-rabbitmq/src/test/java/io/quarkus/it/rabbitmq/RabbitMQConnectorTest.java index 94c6acab71d4f..e14541969e4e4 100644 --- a/integration-tests/reactive-messaging-rabbitmq/src/test/java/io/quarkus/it/rabbitmq/RabbitMQConnectorTest.java +++ b/integration-tests/reactive-messaging-rabbitmq/src/test/java/io/quarkus/it/rabbitmq/RabbitMQConnectorTest.java @@ -3,6 +3,10 @@ import static io.restassured.RestAssured.get; import static java.util.concurrent.TimeUnit.SECONDS; import static org.awaitility.Awaitility.await; +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.Matchers.containsInAnyOrder; import java.util.List; @@ -10,7 +14,9 @@ import org.junit.jupiter.api.Test; import io.quarkus.test.junit.QuarkusTest; +import io.restassured.RestAssured; import io.restassured.common.mapper.TypeRef; +import io.restassured.http.ContentType; @QuarkusTest public class RabbitMQConnectorTest { @@ -18,6 +24,19 @@ public class RabbitMQConnectorTest { protected static final TypeRef> TYPE_REF = new TypeRef>() { }; + @Test + public void testHealthCheck() { + RestAssured.when().get("/q/health").then() + .contentType(ContentType.JSON) + .header("Content-Type", containsString("charset=UTF-8")) + .body("status", is("UP"), + "checks.status", containsInAnyOrder("UP", "UP", "UP"), + "checks.name", containsInAnyOrder("SmallRye Reactive Messaging - liveness check", + "SmallRye Reactive Messaging - readiness check", + "SmallRye Reactive Messaging - startup check"), + "checks.data", not(containsString("people-in"))); + } + @Test public void test() { await().atMost(30, SECONDS) diff --git a/integration-tests/rest-csrf/pom.xml b/integration-tests/rest-csrf/pom.xml index 2288c04348353..772a01b8a30b3 100644 --- a/integration-tests/rest-csrf/pom.xml +++ b/integration-tests/rest-csrf/pom.xml @@ -43,7 +43,7 @@ test - net.sourceforge.htmlunit + org.htmlunit htmlunit test diff --git a/integration-tests/rest-csrf/src/test/java/io/quarkus/it/csrf/CsrfReactiveTest.java b/integration-tests/rest-csrf/src/test/java/io/quarkus/it/csrf/CsrfReactiveTest.java index 3409acb530fb2..7c06d6536f9f5 100644 --- a/integration-tests/rest-csrf/src/test/java/io/quarkus/it/csrf/CsrfReactiveTest.java +++ b/integration-tests/rest-csrf/src/test/java/io/quarkus/it/csrf/CsrfReactiveTest.java @@ -12,17 +12,16 @@ import java.util.Base64; import java.util.List; +import org.htmlunit.FailingHttpStatusCodeException; +import org.htmlunit.SilentCssErrorHandler; +import org.htmlunit.TextPage; +import org.htmlunit.WebClient; +import org.htmlunit.html.DomElement; +import org.htmlunit.html.HtmlForm; +import org.htmlunit.html.HtmlPage; +import org.htmlunit.util.Cookie; import org.junit.jupiter.api.Test; -import com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException; -import com.gargoylesoftware.htmlunit.SilentCssErrorHandler; -import com.gargoylesoftware.htmlunit.TextPage; -import com.gargoylesoftware.htmlunit.WebClient; -import com.gargoylesoftware.htmlunit.html.DomElement; -import com.gargoylesoftware.htmlunit.html.HtmlForm; -import com.gargoylesoftware.htmlunit.html.HtmlPage; -import com.gargoylesoftware.htmlunit.util.Cookie; - import io.quarkus.test.common.http.TestHTTPResource; import io.quarkus.test.junit.QuarkusTest; import io.restassured.RestAssured; diff --git a/integration-tests/smallrye-jwt-oidc-webapp/pom.xml b/integration-tests/smallrye-jwt-oidc-webapp/pom.xml index 566417b2b8876..2d89d6642b982 100644 --- a/integration-tests/smallrye-jwt-oidc-webapp/pom.xml +++ b/integration-tests/smallrye-jwt-oidc-webapp/pom.xml @@ -136,7 +136,7 @@ - net.sourceforge.htmlunit + org.htmlunit htmlunit test diff --git a/integration-tests/smallrye-jwt-oidc-webapp/src/test/java/io/quarkus/it/keycloak/SmallRyeJwtOidcWebAppTest.java b/integration-tests/smallrye-jwt-oidc-webapp/src/test/java/io/quarkus/it/keycloak/SmallRyeJwtOidcWebAppTest.java index d2e4089bb9f47..45c4ab108d033 100644 --- a/integration-tests/smallrye-jwt-oidc-webapp/src/test/java/io/quarkus/it/keycloak/SmallRyeJwtOidcWebAppTest.java +++ b/integration-tests/smallrye-jwt-oidc-webapp/src/test/java/io/quarkus/it/keycloak/SmallRyeJwtOidcWebAppTest.java @@ -4,13 +4,12 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import org.hamcrest.Matchers; +import org.htmlunit.SilentCssErrorHandler; +import org.htmlunit.WebClient; +import org.htmlunit.html.HtmlForm; +import org.htmlunit.html.HtmlPage; import org.junit.jupiter.api.Test; -import com.gargoylesoftware.htmlunit.SilentCssErrorHandler; -import com.gargoylesoftware.htmlunit.WebClient; -import com.gargoylesoftware.htmlunit.html.HtmlForm; -import com.gargoylesoftware.htmlunit.html.HtmlPage; - import io.quarkus.test.common.QuarkusTestResource; import io.quarkus.test.junit.QuarkusTest; import io.restassured.RestAssured; diff --git a/integration-tests/test-extension/extension-that-defines-junit-test-extensions/deployment/src/main/resources/application.properties b/integration-tests/test-extension/extension-that-defines-junit-test-extensions/deployment/src/main/resources/application.properties index 3ed057571a910..e69de29bb2d1d 100644 --- a/integration-tests/test-extension/extension-that-defines-junit-test-extensions/deployment/src/main/resources/application.properties +++ b/integration-tests/test-extension/extension-that-defines-junit-test-extensions/deployment/src/main/resources/application.properties @@ -1,193 +0,0 @@ -# Log settings -# log level in lower case for testing -quarkus.log.level=info -quarkus.log.file.enable=true -quarkus.log.file.level=INFO -quarkus.log.file.format=%d{HH:mm:ss} %-5p [%c{2.}]] (%t) %s%e%n - -# Resource path to DSAPublicKey base64 encoded bytes -quarkus.root.dsa-key-location=/DSAPublicKey.encoded - -# Have the TestProcessor validate the build time configuration below -quarkus.root.validate-build-config=true - - -### Configuration settings for the TestBuildTimeConfig config root -quarkus.bt.bt-string-opt=btStringOptValue -quarkus.bt.bt-sbv=StringBasedValue -# This is not set so that we should get the @ConfigItem defaultValue -#quarkus.bt.bt-sbv-with-default=StringBasedValue -quarkus.bt.all-values.oov=configPart1+configPart2 -quarkus.bt.all-values.ovo=configPart1+configPart2 -# This is not set so that we should get the @ConfigItem defaultValue -#quarkus.bt.bt-oov-with-default=ObjectOfValue -quarkus.bt.all-values.long-primitive=1234567891 -quarkus.bt.all-values.double-primitive=3.1415926535897932384 -quarkus.bt.all-values.long-value=1234567892 -quarkus.bt.all-values.opt-long-value=1234567893 -quarkus.bt.all-values.opt-double-value=3.1415926535897932384 -quarkus.bt.all-values.optional-long-value=1234567894 -quarkus.bt.all-values.nested-config-map.key1.nested-value=value1 -quarkus.bt.all-values.nested-config-map.key1.oov=value1.1+value1.2 -quarkus.bt.all-values.nested-config-map.key2.nested-value=value2 -quarkus.bt.all-values.nested-config-map.key2.oov=value2.1+value2.2 -quarkus.bt.all-values.string-list=value1,value2 -quarkus.bt.all-values.long-list=1,2,3 -quarkus.bt.bt-config-value=${test.record.expansion} -test.record.expansion=value -quarkus.bt.bt-config-value-empty= - -### Duplicate settings for the TestBuildAndRunTimeConfig. May be able to drop if ConfigRoot inheritance is added -quarkus.btrt.bt-string-opt=btStringOptValue -quarkus.btrt.bt-sbv=StringBasedValue -quarkus.btrt.all-values.oov=configPart1+configPart2 -quarkus.btrt.all-values.ovo=configPart1+configPart2 -quarkus.btrt.all-values.long-primitive=1234567891 -quarkus.btrt.all-values.double-primitive=3.1415926535897932384 -quarkus.btrt.all-values.long-value=1234567892 -quarkus.btrt.all-values.opt-long-value=1234567893 -quarkus.btrt.all-values.opt-double-value=3.1415926535897932384 -quarkus.btrt.all-values.optional-long-value=1234567894 -quarkus.btrt.all-values.nested-config-map.key1.nested-value=value1 -quarkus.btrt.all-values.nested-config-map.key1.oov=value1.1+value1.2 -quarkus.btrt.all-values.nested-config-map.key2.nested-value=value2 -quarkus.btrt.all-values.nested-config-map.key2.oov=value2.1+value2.2 -quarkus.btrt.all-values.string-list=value1,value2 -quarkus.btrt.all-values.long-list=1,2,3 -# The expansion value is not available in runtime so we need to set it directly. -quarkus.btrt.all-values.expanded-default=1234 - -### Configuration settings for the TestRunTimeConfig config root -quarkus.rt.rt-string-opt=rtStringOptValue -quarkus.rt.rt-string-opt-with-default=rtStringOptWithDefaultValue -quarkus.rt.all-values.oov=configPart1+configPart2 -quarkus.rt.all-values.ovo=configPart1+configPart2 -quarkus.rt.all-values.long-primitive=12345678911 -quarkus.rt.all-values.double-primitive=3.1415926535897932384 -quarkus.rt.all-values.long-value=12345678921 -quarkus.rt.all-values.opt-long-value=12345678931 -quarkus.rt.all-values.opt-double-value=3.1415926535897932384 -quarkus.rt.all-values.optional-long-value=12345678941 -quarkus.rt.all-values.nested-config-map.key1.nested-value=value1 -quarkus.rt.all-values.nested-config-map.key1.oov=value1.1+value1.2 -quarkus.rt.all-values.nested-config-map.key2.nested-value=value2 -quarkus.rt.all-values.nested-config-map.key2.oov=value2.1+value2.2 -quarkus.rt.all-values.string-list=value1,value2 -quarkus.rt.all-values.long-list=1,2,3 -# A nested map of properties -quarkus.rt.all-values.string-map.key1=value1 -quarkus.rt.all-values.string-map.key2=value2 -quarkus.rt.all-values.string-map.key3=value3 -# And list form -quarkus.rt.all-values.string-list-map.key1=value1,value2,value3 -quarkus.rt.all-values.string-list-map.key2=value4,value5 -quarkus.rt.all-values.string-list-map.key3=value6 -# A root map of properties -quarkus.rt.string-map.key1=value1 -quarkus.rt.string-map.key2=value2 -quarkus.rt.string-map.key3=value3 -# And list form -quarkus.rt.string-list-map.key1=value1 -quarkus.rt.string-list-map.key2=value2,value3 -quarkus.rt.string-list-map.key3=value4,value5,value6 - -### run time configuration using enhanced converters -quarkus.rt.my-enum=enum-two -quarkus.rt.my-enums=enum-one,enum-two -quarkus.rt.my-optional-enums=optional -quarkus.rt.no-hyphenate-first-enum=ENUM_ONE -quarkus.rt.no-hyphenate-second-enum=Enum_Two -quarkus.rt.primitive-boolean=YES -quarkus.rt.object-boolean=NO -quarkus.rt.primitive-integer=two -quarkus.rt.object-integer=nine -quarkus.rt.one-to-nine=one,two,three,four,five,six,seven,eight,nine -quarkus.rt.map-of-numbers.key1=one -quarkus.rt.map-of-numbers.key2=two - -### map configurations -quarkus.rt.leaf-map.key.first=first-key-value -quarkus.rt.leaf-map.key.second=second-key-value -quarkus.rt.config-group-map.key.group.nested-value=value -quarkus.rt.config-group-map.key.group.oov=value2.1+value2.2 - -### build time and run time configuration using enhanced converters -quarkus.btrt.map-of-numbers.key1=one -quarkus.btrt.map-of-numbers.key2=two -quarkus.btrt.my-enum=optional -quarkus.btrt.my-enums=optional,enum-one,enum-two - -### anonymous root property -quarkus.test-property=foo - -### map of map of strings -quarkus.rt.map-map.outer-key.inner-key=1234 -quarkus.btrt.map-map.outer-key.inner-key=1234 -quarkus.bt.map-map.outer-key.inner-key=1234 - -# Test config root with "RuntimeConfig" suffix -quarkus.foo.bar=huhu - -### named map with profiles -quarkus.btrt.map-map.main-profile.property=1234 -%test.quarkus.btrt.map-map.test-profile.property=5678 - -### ordinal and default values source -config_ordinal=1000 -my.prop=1234 -%prod.my.prop=1234 -%dev.my.prop=5678 -%test.my.prop=1234 - -### Unknown properties -quarkus.unknown.prop=1234 -quarkus.http.non-application-root-path=/1234 -quarkus.http.ssl-port=4443 -# This is how Env Source will output property names (for maps) -QUARKUS_HTTP_NON_APPLICATION_ROOT_PATH=/1234 -quarkus.http.non.application.root.path=/1234 -QUARKUS_HTTP_SSL_PORT=4443 -quarkus.http.ssl.port=4443 -quarkus.arc.unremovable-types=foo -# The YAML source may add an indexed property (depending on how the YAML is laid out). This is not supported by @ConfigRoot -quarkus.arc.unremovable-types[0]=foo - -### Do not record env values in build time -bt.ok.to.record=properties -%test.bt.profile.record=properties - -### mappings -quarkus.mapping.bt.value=value -quarkus.mapping.bt.group.value=value -quarkus.mapping.bt.present.value=present -quarkus.mapping.bt.groups[0].value=first -quarkus.mapping.bt.groups[1].value=second - -quarkus.mapping.btrt.value=value -quarkus.mapping.btrt.group.value=value - -quarkus.mapping.rt.value=value -quarkus.mapping.rt.group.value=value - -### prefix -my.prefix.prop=1234 -my.prefix.map.prop=1234 -my.prefix.nested.nested-value=nested-1234 -my.prefix.nested.oov=nested-1234+nested-5678 -my.prefix.named.prop=1234 -my.prefix.named.map.prop=1234 -my.prefix.named.nested.nested-value=nested-1234 -my.prefix.named.nested.oov=nested-1234+nested-5678 - -my.prefix.bt.prop=1234 -my.prefix.bt.nested.nested-value=nested-1234 -my.prefix.bt.nested.oov=nested-1234+nested-5678 - -another.another-prefix.prop=5678 -another.another-prefix.map.prop=5678 - -proprietary.root.config.value=1234 -proprietary.mapping.config.value=1234 -proprietary.should.not.report.unknown=1234 - -unremoveable.value=1234 diff --git a/integration-tests/test-extension/tests/pom.xml b/integration-tests/test-extension/tests/pom.xml index c00c53b56d37c..801e54684ca19 100644 --- a/integration-tests/test-extension/tests/pom.xml +++ b/integration-tests/test-extension/tests/pom.xml @@ -176,6 +176,29 @@ uber-jar + + native-image + + + native + + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + + true + + + + + + diff --git a/integration-tests/test-extension/tests/src/test/java/io/quarkus/it/extension/it/TestParameterDevModeIT.java b/integration-tests/test-extension/tests/src/test/java/io/quarkus/it/extension/it/TestParameterDevModeIT.java index 08717caa64c40..83355e35ca946 100644 --- a/integration-tests/test-extension/tests/src/test/java/io/quarkus/it/extension/it/TestParameterDevModeIT.java +++ b/integration-tests/test-extension/tests/src/test/java/io/quarkus/it/extension/it/TestParameterDevModeIT.java @@ -7,7 +7,6 @@ import org.apache.maven.shared.invoker.MavenInvocationException; import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.DisabledIfSystemProperty; @@ -21,7 +20,6 @@ *

    * mvn install -Dit.test=DevMojoIT#methodName */ -@Disabled // because of https://github.com/quarkiverse/quarkus-pact/issues/73 @DisabledIfSystemProperty(named = "quarkus.test.native", matches = "true") public class TestParameterDevModeIT extends RunAndCheckMojoTestBase { diff --git a/integration-tests/test-extension/tests/src/test/java/io/quarkus/it/extension/it/TestParameterTestModeIT.java b/integration-tests/test-extension/tests/src/test/java/io/quarkus/it/extension/it/TestParameterTestModeIT.java index 1a9867be18681..b5f2bb42f7c00 100644 --- a/integration-tests/test-extension/tests/src/test/java/io/quarkus/it/extension/it/TestParameterTestModeIT.java +++ b/integration-tests/test-extension/tests/src/test/java/io/quarkus/it/extension/it/TestParameterTestModeIT.java @@ -20,8 +20,8 @@ *

    * mvn install -Dit.test=DevMojoIT#methodName */ -@Disabled // because of https://github.com/quarkiverse/quarkus-pact/issues/73 @DisabledIfSystemProperty(named = "quarkus.test.native", matches = "true") +@Disabled("The base function now works via quarkus:test, but the test infrastructure for seeing how many tests ran needs the dev ui to be running") public class TestParameterTestModeIT extends RunAndCheckMojoTestBase { @Override diff --git a/integration-tests/test-extension/tests/src/test/java/io/quarkus/it/extension/it/TestTemplateCanSeeByteCodeChangesDevModeIT.java b/integration-tests/test-extension/tests/src/test/java/io/quarkus/it/extension/it/TestTemplateCanSeeByteCodeChangesDevModeIT.java new file mode 100644 index 0000000000000..993fdbaf83130 --- /dev/null +++ b/integration-tests/test-extension/tests/src/test/java/io/quarkus/it/extension/it/TestTemplateCanSeeByteCodeChangesDevModeIT.java @@ -0,0 +1,65 @@ +package io.quarkus.it.extension.it; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.FileNotFoundException; +import java.io.IOException; + +import org.apache.maven.shared.invoker.MavenInvocationException; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledIfSystemProperty; + +import io.quarkus.maven.it.RunAndCheckMojoTestBase; +import io.quarkus.maven.it.continuoustesting.ContinuousTestingMavenTestUtils; + +/** + * Be aware! This test will not run if the name does not start with 'Test'. + *

    + * NOTE to anyone diagnosing failures in this test, to run a single method use: + *

    + * mvn install -Dit.test=TestTemplateDevModeIT#methodName + */ +@DisabledIfSystemProperty(named = "quarkus.test.native", matches = "true") +@Disabled // Tracked by #27821 +public class TestTemplateCanSeeByteCodeChangesDevModeIT extends RunAndCheckMojoTestBase { + + /* + * We have a few tests that will run in parallel, so set a unique port + */ + protected int getPort() { + return 8090; + } + + protected void runAndCheck(boolean performCompile, String... options) + throws MavenInvocationException, FileNotFoundException { + run(performCompile, options); + + try { + String resp = devModeClient.getHttpResponse(); + assertThat(resp).containsIgnoringCase("ready").containsIgnoringCase("application") + .containsIgnoringCase("SNAPSHOT"); + } catch (Exception e) { + e.printStackTrace(); + } + + // There's no json endpoints, so nothing else to check + } + + @Test + public void testThatTheTestsPassed() throws MavenInvocationException, IOException { + //we also check continuous testing + String executionDir = "projects/project-using-test-template-from-extension-with-bytecode-changes-processed"; + testDir = initProject("projects/project-using-test-template-from-extension-with-bytecode-changes", executionDir); + runAndCheck(); + + ContinuousTestingMavenTestUtils testingTestUtils = new ContinuousTestingMavenTestUtils(getPort()); + ContinuousTestingMavenTestUtils.TestStatus results = testingTestUtils.waitForNextCompletion(); + // This is a bit brittle when we add tests, but failures are often so catastrophic they're not even reported as failures, + // so we need to check the pass count explicitly + Assertions.assertEquals(0, results.getTestsFailed()); + Assertions.assertEquals(9, results.getTestsPassed()); + } + +} diff --git a/integration-tests/test-extension/tests/src/test/java/io/quarkus/it/extension/it/TestTemplateTestModeIT.java b/integration-tests/test-extension/tests/src/test/java/io/quarkus/it/extension/it/TestTemplateCanSeeByteCodeChangesTestModeIT.java similarity index 96% rename from integration-tests/test-extension/tests/src/test/java/io/quarkus/it/extension/it/TestTemplateTestModeIT.java rename to integration-tests/test-extension/tests/src/test/java/io/quarkus/it/extension/it/TestTemplateCanSeeByteCodeChangesTestModeIT.java index 594bab163a6d1..2cc02f9485e62 100644 --- a/integration-tests/test-extension/tests/src/test/java/io/quarkus/it/extension/it/TestTemplateTestModeIT.java +++ b/integration-tests/test-extension/tests/src/test/java/io/quarkus/it/extension/it/TestTemplateCanSeeByteCodeChangesTestModeIT.java @@ -21,7 +21,7 @@ */ @Disabled // Tracked by #27821 @DisabledIfSystemProperty(named = "quarkus.test.native", matches = "true") -public class TestTemplateTestModeIT extends RunAndCheckMojoTestBase { +public class TestTemplateCanSeeByteCodeChangesTestModeIT extends RunAndCheckMojoTestBase { /* * We have a few tests that will run in parallel, so set a unique port diff --git a/integration-tests/test-extension/tests/src/test/java/io/quarkus/it/extension/it/TestTemplateDevModeIT.java b/integration-tests/test-extension/tests/src/test/java/io/quarkus/it/extension/it/TestTemplateDevModeIT.java index db8a2f637092e..f9b3ec9475d53 100644 --- a/integration-tests/test-extension/tests/src/test/java/io/quarkus/it/extension/it/TestTemplateDevModeIT.java +++ b/integration-tests/test-extension/tests/src/test/java/io/quarkus/it/extension/it/TestTemplateDevModeIT.java @@ -21,15 +21,15 @@ *

    * mvn install -Dit.test=TestTemplateDevModeIT#methodName */ +@Disabled("NPE in JUnit stack; See discussion in https://github.com/quarkiverse/quarkiverse/issues/94, should be re-enabled when https://github.com/quarkusio/quarkus/pull/40751 is merged") @DisabledIfSystemProperty(named = "quarkus.test.native", matches = "true") -@Disabled // Tracked by #27821 public class TestTemplateDevModeIT extends RunAndCheckMojoTestBase { /* * We have a few tests that will run in parallel, so set a unique port */ protected int getPort() { - return 8090; + return 8092; } protected void runAndCheck(boolean performCompile, String... options) @@ -50,8 +50,8 @@ protected void runAndCheck(boolean performCompile, String... options) @Test public void testThatTheTestsPassed() throws MavenInvocationException, IOException { //we also check continuous testing - String executionDir = "projects/project-using-test-template-from-extension-processed"; - testDir = initProject("projects/project-using-test-template-from-extension", executionDir); + String executionDir = "projects/project-using-test-template-from-extension-with-bytecode-changes-processed"; + testDir = initProject("projects/project-using-test-template-from-extension-with-bytecode-changes", executionDir); runAndCheck(); ContinuousTestingMavenTestUtils testingTestUtils = new ContinuousTestingMavenTestUtils(getPort()); @@ -59,7 +59,7 @@ public void testThatTheTestsPassed() throws MavenInvocationException, IOExceptio // This is a bit brittle when we add tests, but failures are often so catastrophic they're not even reported as failures, // so we need to check the pass count explicitly Assertions.assertEquals(0, results.getTestsFailed()); - Assertions.assertEquals(9, results.getTestsPassed()); + Assertions.assertEquals(3, results.getTestsPassed()); } } diff --git a/integration-tests/test-extension/tests/src/test/resources-filtered/projects/project-using-test-callback-from-extension/src/main/resources/application.properties b/integration-tests/test-extension/tests/src/test/resources-filtered/projects/project-using-test-callback-from-extension/src/main/resources/application.properties index 442095ca8410c..8d698e657885b 100644 --- a/integration-tests/test-extension/tests/src/test/resources-filtered/projects/project-using-test-callback-from-extension/src/main/resources/application.properties +++ b/integration-tests/test-extension/tests/src/test/resources-filtered/projects/project-using-test-callback-from-extension/src/main/resources/application.properties @@ -1 +1,3 @@ -quarkus.test.continuous-testing=enabled \ No newline at end of file +quarkus.test.continuous-testing=enabled +# this should not be needed, but something in the tests is setting this to 1234 and confusing the test framework, so set it here to match +quarkus.http.non-application-root-path=1234 \ No newline at end of file diff --git a/integration-tests/test-extension/tests/src/test/resources-filtered/projects/project-using-test-parameter-injection/src/main/resources/application.properties b/integration-tests/test-extension/tests/src/test/resources-filtered/projects/project-using-test-parameter-injection/src/main/resources/application.properties index 9e0b5c5fcead3..8d698e657885b 100644 --- a/integration-tests/test-extension/tests/src/test/resources-filtered/projects/project-using-test-parameter-injection/src/main/resources/application.properties +++ b/integration-tests/test-extension/tests/src/test/resources-filtered/projects/project-using-test-parameter-injection/src/main/resources/application.properties @@ -1,2 +1,3 @@ quarkus.test.continuous-testing=enabled -quarkus.rest-client.alpaca-api.url=http://localhost:8085/ +# this should not be needed, but something in the tests is setting this to 1234 and confusing the test framework, so set it here to match +quarkus.http.non-application-root-path=1234 \ No newline at end of file diff --git a/integration-tests/test-extension/tests/src/test/resources-filtered/projects/project-using-test-template-from-extension-with-bytecode-changes/pom.xml b/integration-tests/test-extension/tests/src/test/resources-filtered/projects/project-using-test-template-from-extension-with-bytecode-changes/pom.xml new file mode 100644 index 0000000000000..77a81d2ea5993 --- /dev/null +++ b/integration-tests/test-extension/tests/src/test/resources-filtered/projects/project-using-test-template-from-extension-with-bytecode-changes/pom.xml @@ -0,0 +1,127 @@ + + + 4.0.0 + org.acme + project-using-test-template-from-extension + 999-SNAPSHOT + + ${compiler-plugin.version} + false + quarkus-bom + 17 + UTF-8 + UTF-8 + 3.2.5 + @project.version@ + + + + + io.quarkus + ${quarkus.bom.artifact-id} + ${quarkus.version} + pom + import + + + + + + io.quarkus + quarkus-vertx-http + + + + io.quarkus + quarkus-rest-client-jackson + + + io.quarkus + quarkus-junit5 + test + + + + io.quarkus + integration-test-extension-that-defines-junit-test-extensions + ${quarkus.version} + test + + + + + + + io.quarkus + quarkus-maven-plugin + ${quarkus.version} + true + + + + build + generate-code + generate-code-tests + + + + + + maven-compiler-plugin + \${compiler-plugin.version} + + + -parameters + + + + + maven-surefire-plugin + \${surefire-plugin.version} + + + org.jboss.logmanager.LogManager + \${maven.home} + + + + + + + + native + + + native + + + + + + maven-failsafe-plugin + \${surefire-plugin.version} + + + + integration-test + verify + + + + \${project.build.directory}/\${project.build.finalName}-runner + org.jboss.logmanager.LogManager + \${maven.home} + + + + + + + + + true + + + + diff --git a/integration-tests/test-extension/tests/src/test/resources-filtered/projects/project-using-test-template-from-extension-with-bytecode-changes/src/main/resources/META-INF/resources/index.html b/integration-tests/test-extension/tests/src/test/resources-filtered/projects/project-using-test-template-from-extension-with-bytecode-changes/src/main/resources/META-INF/resources/index.html new file mode 100644 index 0000000000000..b80fe3dc62642 --- /dev/null +++ b/integration-tests/test-extension/tests/src/test/resources-filtered/projects/project-using-test-template-from-extension-with-bytecode-changes/src/main/resources/META-INF/resources/index.html @@ -0,0 +1,16 @@ + + + + + + getting-started - 1.0-SNAPSHOT + + + +

    + The application is ready. +
    + + + + \ No newline at end of file diff --git a/integration-tests/test-extension/tests/src/test/resources-filtered/projects/project-using-test-template-from-extension-with-bytecode-changes/src/main/resources/application.properties b/integration-tests/test-extension/tests/src/test/resources-filtered/projects/project-using-test-template-from-extension-with-bytecode-changes/src/main/resources/application.properties new file mode 100644 index 0000000000000..8d698e657885b --- /dev/null +++ b/integration-tests/test-extension/tests/src/test/resources-filtered/projects/project-using-test-template-from-extension-with-bytecode-changes/src/main/resources/application.properties @@ -0,0 +1,3 @@ +quarkus.test.continuous-testing=enabled +# this should not be needed, but something in the tests is setting this to 1234 and confusing the test framework, so set it here to match +quarkus.http.non-application-root-path=1234 \ No newline at end of file diff --git a/integration-tests/test-extension/tests/src/test/resources-filtered/projects/project-using-test-template-from-extension-with-bytecode-changes/src/test/java/org/acme/NormalQuarkusTest.java b/integration-tests/test-extension/tests/src/test/resources-filtered/projects/project-using-test-template-from-extension-with-bytecode-changes/src/test/java/org/acme/NormalQuarkusTest.java new file mode 100644 index 0000000000000..74c7976a691a0 --- /dev/null +++ b/integration-tests/test-extension/tests/src/test/resources-filtered/projects/project-using-test-template-from-extension-with-bytecode-changes/src/test/java/org/acme/NormalQuarkusTest.java @@ -0,0 +1,23 @@ +package org.acme; + +import java.lang.annotation.Annotation; +import java.util.Arrays; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.QuarkusTest; + +/** + * Sense check - do we see the added annotation without parameterization? + */ +@QuarkusTest +public class NormalQuarkusTest { + + @Test + void executionAnnotationCheckingTestTemplate() { + Annotation[] myAnnotations = this.getClass().getAnnotations(); + Assertions.assertTrue(Arrays.toString(myAnnotations).contains("AnnotationAddedByExtension"), + "The test execution does not see the annotation, only sees " + Arrays.toString(myAnnotations)); + } +} diff --git a/integration-tests/test-extension/tests/src/test/resources-filtered/projects/project-using-test-template-from-extension-with-bytecode-changes/src/test/java/org/acme/TemplatedNormalTest.java b/integration-tests/test-extension/tests/src/test/resources-filtered/projects/project-using-test-template-from-extension-with-bytecode-changes/src/test/java/org/acme/TemplatedNormalTest.java new file mode 100644 index 0000000000000..e58a235635a33 --- /dev/null +++ b/integration-tests/test-extension/tests/src/test/resources-filtered/projects/project-using-test-template-from-extension-with-bytecode-changes/src/test/java/org/acme/TemplatedNormalTest.java @@ -0,0 +1,67 @@ +package org.acme; + +import java.lang.annotation.Annotation; +import java.util.Arrays; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.TestTemplate; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.ExtensionContext; + +// No QuarkusTest annotation + +/** + * It's likely we would never expect this to work; unit tests which don't have a @QuarkusTest + * annotation would not be able to + * benefit from bytecode manipulations from extensions. + */ +public class TemplatedNormalTest { + + @TestTemplate + @ExtendWith(MyContextProvider.class) + void verificationTestTemplate(ExtensionContext context) { + Annotation[] contextAnnotations = context.getRequiredTestClass() + .getAnnotations(); + Annotation[] myAnnotations = this.getClass() + .getAnnotations(); + + Assertions.assertEquals(myAnnotations.length, contextAnnotations.length, + "The test template sees a different version of the class than the test execution" + + Arrays.toString(myAnnotations) + " vs " + Arrays.toString( + contextAnnotations)); + } + + @TestTemplate + @ExtendWith(MyContextProvider.class) + void classloaderIntrospectionTestTemplate(ExtensionContext context) { + ClassLoader loader = this.getClass() + .getClassLoader(); + ClassLoader contextLoader = context.getRequiredTestClass() + .getClassLoader(); + + Assertions.assertEquals(loader, contextLoader, + "The test template is using a different classloader to the actual test."); + } + + @TestTemplate + @ExtendWith(MyContextProvider.class) + void contextAnnotationCheckingTestTemplate(ExtensionContext context) { + Annotation[] contextAnnotations = context.getRequiredTestClass() + .getAnnotations(); + Assertions.assertTrue(Arrays.toString(contextAnnotations) + .contains("AnnotationAddedByExtension"), + "The JUnit extension context does not see the annotation, only sees " + Arrays.toString( + contextAnnotations)); + } + + @TestTemplate + @ExtendWith(MyContextProvider.class) + void executionAnnotationCheckingTestTemplate(ExtensionContext context) { + Annotation[] myAnnotations = this.getClass() + .getAnnotations(); + Assertions.assertTrue(Arrays.toString(myAnnotations) + .contains("AnnotationAddedByExtension"), + "The test execution does not see the annotation, only sees " + Arrays.toString( + myAnnotations)); + } +} diff --git a/integration-tests/test-extension/tests/src/test/resources-filtered/projects/project-using-test-template-from-extension-with-bytecode-changes/src/test/java/org/acme/TemplatedQuarkusTest.java b/integration-tests/test-extension/tests/src/test/resources-filtered/projects/project-using-test-template-from-extension-with-bytecode-changes/src/test/java/org/acme/TemplatedQuarkusTest.java new file mode 100644 index 0000000000000..1858ba4bb78a9 --- /dev/null +++ b/integration-tests/test-extension/tests/src/test/resources-filtered/projects/project-using-test-template-from-extension-with-bytecode-changes/src/test/java/org/acme/TemplatedQuarkusTest.java @@ -0,0 +1,54 @@ +package org.acme; + +import java.lang.annotation.Annotation; +import java.util.Arrays; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.TestTemplate; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.ExtensionContext; + +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +public class TemplatedQuarkusTest { + + @TestTemplate + @ExtendWith(MyContextProvider.class) + void verificationTestTemplate(ExtensionContext context) { + Annotation[] contextAnnotations = context.getRequiredTestClass().getAnnotations(); + Annotation[] myAnnotations = this.getClass().getAnnotations(); + + Assertions.assertEquals(myAnnotations.length, contextAnnotations.length, + "The test template sees a different version of the class than the test execution" + + Arrays.toString(myAnnotations) + " (execution) vs " + Arrays.toString(contextAnnotations) + + " (context)"); + } + + @TestTemplate + @ExtendWith(MyContextProvider.class) + void classloaderIntrospectionTestTemplate(ExtensionContext context) { + ClassLoader loader = this.getClass().getClassLoader(); + ClassLoader contextLoader = context.getRequiredTestClass().getClassLoader(); + + Assertions.assertEquals(loader, contextLoader, + "The test template is using a different classloader to the actual test. Execution loader: " + loader + + " vs template context loader " + contextLoader); + } + + @TestTemplate + @ExtendWith(MyContextProvider.class) + void contextAnnotationCheckingTestTemplate(ExtensionContext context) { + Annotation[] contextAnnotations = context.getRequiredTestClass().getAnnotations(); + Assertions.assertTrue(Arrays.toString(contextAnnotations).contains("AnnotationAddedByExtension"), + "The JUnit extension context does not see the annotation, only sees " + Arrays.toString(contextAnnotations)); + } + + @TestTemplate + @ExtendWith(MyContextProvider.class) + void executionAnnotationCheckingTestTemplate(ExtensionContext context) { + Annotation[] myAnnotations = this.getClass().getAnnotations(); + Assertions.assertTrue(Arrays.toString(myAnnotations).contains("AnnotationAddedByExtension"), + "The test execution does not see the annotation, only sees " + Arrays.toString(myAnnotations)); + } +} diff --git a/integration-tests/test-extension/tests/src/test/resources-filtered/projects/project-using-test-template-from-extension/src/test/java/org/acme/NormalQuarkusTest.java b/integration-tests/test-extension/tests/src/test/resources-filtered/projects/project-using-test-template-from-extension/src/test/java/org/acme/NormalQuarkusTest.java index 74c7976a691a0..41be0f283f6b0 100644 --- a/integration-tests/test-extension/tests/src/test/resources-filtered/projects/project-using-test-template-from-extension/src/test/java/org/acme/NormalQuarkusTest.java +++ b/integration-tests/test-extension/tests/src/test/resources-filtered/projects/project-using-test-template-from-extension/src/test/java/org/acme/NormalQuarkusTest.java @@ -1,8 +1,5 @@ package org.acme; -import java.lang.annotation.Annotation; -import java.util.Arrays; - import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -16,8 +13,6 @@ public class NormalQuarkusTest { @Test void executionAnnotationCheckingTestTemplate() { - Annotation[] myAnnotations = this.getClass().getAnnotations(); - Assertions.assertTrue(Arrays.toString(myAnnotations).contains("AnnotationAddedByExtension"), - "The test execution does not see the annotation, only sees " + Arrays.toString(myAnnotations)); + Assertions.assertTrue(true); } } diff --git a/integration-tests/test-extension/tests/src/test/resources-filtered/projects/project-using-test-template-from-extension/src/test/java/org/acme/TemplatedNormalTest.java b/integration-tests/test-extension/tests/src/test/resources-filtered/projects/project-using-test-template-from-extension/src/test/java/org/acme/TemplatedNormalTest.java index e58a235635a33..273a628ce214e 100644 --- a/integration-tests/test-extension/tests/src/test/resources-filtered/projects/project-using-test-template-from-extension/src/test/java/org/acme/TemplatedNormalTest.java +++ b/integration-tests/test-extension/tests/src/test/resources-filtered/projects/project-using-test-template-from-extension/src/test/java/org/acme/TemplatedNormalTest.java @@ -1,8 +1,5 @@ package org.acme; -import java.lang.annotation.Annotation; -import java.util.Arrays; - import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.TestTemplate; import org.junit.jupiter.api.extension.ExtendWith; @@ -19,49 +16,8 @@ public class TemplatedNormalTest { @TestTemplate @ExtendWith(MyContextProvider.class) - void verificationTestTemplate(ExtensionContext context) { - Annotation[] contextAnnotations = context.getRequiredTestClass() - .getAnnotations(); - Annotation[] myAnnotations = this.getClass() - .getAnnotations(); - - Assertions.assertEquals(myAnnotations.length, contextAnnotations.length, - "The test template sees a different version of the class than the test execution" - + Arrays.toString(myAnnotations) + " vs " + Arrays.toString( - contextAnnotations)); - } - - @TestTemplate - @ExtendWith(MyContextProvider.class) - void classloaderIntrospectionTestTemplate(ExtensionContext context) { - ClassLoader loader = this.getClass() - .getClassLoader(); - ClassLoader contextLoader = context.getRequiredTestClass() - .getClassLoader(); - - Assertions.assertEquals(loader, contextLoader, - "The test template is using a different classloader to the actual test."); - } - - @TestTemplate - @ExtendWith(MyContextProvider.class) - void contextAnnotationCheckingTestTemplate(ExtensionContext context) { - Annotation[] contextAnnotations = context.getRequiredTestClass() - .getAnnotations(); - Assertions.assertTrue(Arrays.toString(contextAnnotations) - .contains("AnnotationAddedByExtension"), - "The JUnit extension context does not see the annotation, only sees " + Arrays.toString( - contextAnnotations)); - } + void trivialTestTemplate(ExtensionContext context) { + Assertions.assertTrue(context != null); - @TestTemplate - @ExtendWith(MyContextProvider.class) - void executionAnnotationCheckingTestTemplate(ExtensionContext context) { - Annotation[] myAnnotations = this.getClass() - .getAnnotations(); - Assertions.assertTrue(Arrays.toString(myAnnotations) - .contains("AnnotationAddedByExtension"), - "The test execution does not see the annotation, only sees " + Arrays.toString( - myAnnotations)); } } diff --git a/integration-tests/test-extension/tests/src/test/resources-filtered/projects/project-using-test-template-from-extension/src/test/java/org/acme/TemplatedQuarkusTest.java b/integration-tests/test-extension/tests/src/test/resources-filtered/projects/project-using-test-template-from-extension/src/test/java/org/acme/TemplatedQuarkusTest.java index 1858ba4bb78a9..735f61ce0c751 100644 --- a/integration-tests/test-extension/tests/src/test/resources-filtered/projects/project-using-test-template-from-extension/src/test/java/org/acme/TemplatedQuarkusTest.java +++ b/integration-tests/test-extension/tests/src/test/resources-filtered/projects/project-using-test-template-from-extension/src/test/java/org/acme/TemplatedQuarkusTest.java @@ -1,8 +1,5 @@ package org.acme; -import java.lang.annotation.Annotation; -import java.util.Arrays; - import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.TestTemplate; import org.junit.jupiter.api.extension.ExtendWith; @@ -15,40 +12,8 @@ public class TemplatedQuarkusTest { @TestTemplate @ExtendWith(MyContextProvider.class) - void verificationTestTemplate(ExtensionContext context) { - Annotation[] contextAnnotations = context.getRequiredTestClass().getAnnotations(); - Annotation[] myAnnotations = this.getClass().getAnnotations(); - - Assertions.assertEquals(myAnnotations.length, contextAnnotations.length, - "The test template sees a different version of the class than the test execution" - + Arrays.toString(myAnnotations) + " (execution) vs " + Arrays.toString(contextAnnotations) - + " (context)"); - } - - @TestTemplate - @ExtendWith(MyContextProvider.class) - void classloaderIntrospectionTestTemplate(ExtensionContext context) { - ClassLoader loader = this.getClass().getClassLoader(); - ClassLoader contextLoader = context.getRequiredTestClass().getClassLoader(); - - Assertions.assertEquals(loader, contextLoader, - "The test template is using a different classloader to the actual test. Execution loader: " + loader - + " vs template context loader " + contextLoader); - } - - @TestTemplate - @ExtendWith(MyContextProvider.class) - void contextAnnotationCheckingTestTemplate(ExtensionContext context) { - Annotation[] contextAnnotations = context.getRequiredTestClass().getAnnotations(); - Assertions.assertTrue(Arrays.toString(contextAnnotations).contains("AnnotationAddedByExtension"), - "The JUnit extension context does not see the annotation, only sees " + Arrays.toString(contextAnnotations)); + void trivialTestTemplate(ExtensionContext context) { + Assertions.assertTrue(context != null); } - @TestTemplate - @ExtendWith(MyContextProvider.class) - void executionAnnotationCheckingTestTemplate(ExtensionContext context) { - Annotation[] myAnnotations = this.getClass().getAnnotations(); - Assertions.assertTrue(Arrays.toString(myAnnotations).contains("AnnotationAddedByExtension"), - "The test execution does not see the annotation, only sees " + Arrays.toString(myAnnotations)); - } } diff --git a/pom.xml b/pom.xml index b9d30ce544269..4534465eeceaa 100644 --- a/pom.xml +++ b/pom.xml @@ -55,7 +55,7 @@ jdbc:postgresql:hibernate_orm_test 4.5.3 - 0.0.105 + 0.0.106 false false @@ -64,15 +64,15 @@ 0.8.12 - 6.11.0 + 6.12.1 5.4.0 - 1.63.0 + 1.64.0 1.2.1 3.25.0 ${protoc.version} - 2.39.0 + 2.39.1 7.8.0 @@ -167,7 +167,7 @@ io.quarkus.bot build-reporter-maven-extension - 3.6.0 + 3.7.0 diff --git a/test-framework/junit5-component/src/main/java/io/quarkus/test/component/QuarkusComponentTestExtension.java b/test-framework/junit5-component/src/main/java/io/quarkus/test/component/QuarkusComponentTestExtension.java index 97b3627c620e2..4516f822f1e8f 100644 --- a/test-framework/junit5-component/src/main/java/io/quarkus/test/component/QuarkusComponentTestExtension.java +++ b/test-framework/junit5-component/src/main/java/io/quarkus/test/component/QuarkusComponentTestExtension.java @@ -13,6 +13,7 @@ import java.lang.reflect.Modifier; import java.lang.reflect.Parameter; import java.lang.reflect.ParameterizedType; +import java.net.URI; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; @@ -632,12 +633,12 @@ public void writeResource(Resource resource) throws IOException { extensionContext.getRoot().getStore(NAMESPACE).put(KEY_GENERATED_RESOURCES, generatedResources); - builder.addAnnotationTransformer(AnnotationsTransformer.appliedToField().whenContainsAny(qualifiers) + builder.addAnnotationTransformation(AnnotationsTransformer.appliedToField().whenContainsAny(qualifiers) .whenContainsNone(DotName.createSimple(Inject.class)).thenTransform(t -> t.add(Inject.class))); - builder.addAnnotationTransformer(new JaxrsSingletonTransformer()); + builder.addAnnotationTransformation(new JaxrsSingletonTransformer()); for (AnnotationsTransformer transformer : configuration.annotationsTransformers) { - builder.addAnnotationTransformer(transformer); + builder.addAnnotationTransformation(transformer); } // Register: @@ -1197,13 +1198,20 @@ private File getTestOutputDirectory(Class testClass) { if (outputDirectory != null) { testOutputDirectory = new File(outputDirectory); } else { + // All below string transformations work with _URL encoded_ paths, where e.g. + // a space is replaced with %20. At the end, we feed this back to URI.create + // to make sure the encoding is dealt with properly, so we don't have to do this + // ourselves. Directly passing a URL-encoded string to the File() constructor + // does not work properly. + // org.acme.Foo -> org/acme/Foo.class String testClassResourceName = fromClassNameToResourceName(testClass.getName()); - // org/acme/Foo.class -> /some/path/to/project/target/test-classes/org/acme/Foo.class - String testPath = testClass.getClassLoader().getResource(testClassResourceName).getFile(); - // /some/path/to/project/target/test-classes/org/acme/Foo.class -> /some/path/to/project/target/test-classes - String testClassesRootPath = testPath.substring(0, testPath.length() - testClassResourceName.length()); - testOutputDirectory = new File(testClassesRootPath); + // org/acme/Foo.class -> file:/some/path/to/project/target/test-classes/org/acme/Foo.class + String testPath = testClass.getClassLoader().getResource(testClassResourceName).toString(); + // file:/some/path/to/project/target/test-classes/org/acme/Foo.class -> file:/some/path/to/project/target/test-classes + String testClassesRootPath = testPath.substring(0, testPath.length() - testClassResourceName.length() - 1); + // resolve back to File instance + testOutputDirectory = new File(URI.create(testClassesRootPath)); } if (!testOutputDirectory.canWrite()) { throw new IllegalStateException("Invalid test output directory: " + testOutputDirectory); diff --git a/test-framework/junit5-internal/src/main/java/io/quarkus/test/ProdModeTestBuildStep.java b/test-framework/junit5-internal/src/main/java/io/quarkus/test/ProdModeTestBuildStep.java index 53e649fad5126..f87c76b549161 100644 --- a/test-framework/junit5-internal/src/main/java/io/quarkus/test/ProdModeTestBuildStep.java +++ b/test-framework/junit5-internal/src/main/java/io/quarkus/test/ProdModeTestBuildStep.java @@ -4,7 +4,7 @@ import io.quarkus.builder.BuildStep; -// needs to be in a class of it's own in order to avoid java.lang.IncompatibleClassChangeError +// needs to be in a class of its own in order to avoid java.lang.IncompatibleClassChangeError public abstract class ProdModeTestBuildStep implements BuildStep { private final Map testContext; diff --git a/test-framework/junit5-internal/src/main/java/io/quarkus/test/QuarkusProdModeTest.java b/test-framework/junit5-internal/src/main/java/io/quarkus/test/QuarkusProdModeTest.java index bb75fc407f5ff..f2f9de9fd5b8f 100644 --- a/test-framework/junit5-internal/src/main/java/io/quarkus/test/QuarkusProdModeTest.java +++ b/test-framework/junit5-internal/src/main/java/io/quarkus/test/QuarkusProdModeTest.java @@ -74,7 +74,7 @@ public class QuarkusProdModeTest implements BeforeAllCallback, AfterAllCallback, BeforeEachCallback, TestWatcher, InvocationInterceptor { - private static final String EXPECTED_OUTPUT_FROM_SUCCESSFULLY_STARTED = "features"; + private static final String EXPECTED_OUTPUT_FROM_SUCCESSFULLY_STARTED = "Installed features"; private static final int DEFAULT_HTTP_PORT_INT = 8081; private static final String DEFAULT_HTTP_PORT = "" + DEFAULT_HTTP_PORT_INT; private static final String QUARKUS_HTTP_PORT_PROPERTY = "quarkus.http.port"; diff --git a/test-framework/junit5-internal/src/main/java/io/quarkus/test/QuarkusUnitTest.java b/test-framework/junit5-internal/src/main/java/io/quarkus/test/QuarkusUnitTest.java index f8b0fffd80577..9b37b88520986 100644 --- a/test-framework/junit5-internal/src/main/java/io/quarkus/test/QuarkusUnitTest.java +++ b/test-framework/junit5-internal/src/main/java/io/quarkus/test/QuarkusUnitTest.java @@ -734,6 +734,9 @@ public void afterAll(ExtensionContext extensionContext) throws Exception { rootLogger.setHandlers(originalHandlers); inMemoryLogHandler.clearRecords(); inMemoryLogHandler.setFilter(null); + if (testMethodInvokers != null) { + testMethodInvokers.clear(); + } try { if (runningQuarkusApplication != null) { diff --git a/test-framework/junit5-internal/src/test/java/io/quarkus/test/QuarkusProdModeTestConfusingLogTest.java b/test-framework/junit5-internal/src/test/java/io/quarkus/test/QuarkusProdModeTestConfusingLogTest.java new file mode 100644 index 0000000000000..2450c61632824 --- /dev/null +++ b/test-framework/junit5-internal/src/test/java/io/quarkus/test/QuarkusProdModeTestConfusingLogTest.java @@ -0,0 +1,110 @@ +package io.quarkus.test; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import com.sun.net.httpserver.HttpServer; + +import io.quarkus.runtime.Quarkus; +import io.quarkus.runtime.annotations.QuarkusMain; + +public class QuarkusProdModeTestConfusingLogTest { + + @RegisterExtension + static final QuarkusProdModeTest simpleApp = new QuarkusProdModeTest() + .withApplicationRoot(jar -> jar.addClass(Main.class)) + .setApplicationName("simple-app") + .setApplicationVersion("0.1-SNAPSHOT") + .setRun(true); + + static HttpClient client; + + @BeforeAll + static void setUp() { + // No tear down, because there's no way to shut down the client explicitly before Java 21 :( + // We'll just hope no connection is left hanging. + client = HttpClient.newBuilder() + .connectTimeout(Duration.ofMillis(100)) + .build(); + } + + @Test + public void shouldWaitForAppActuallyStarted() { + thenAppIsRunning(); + + whenStopApp(); + thenAppIsNotRunning(); + + whenStartApp(); + thenAppIsRunning(); + } + + private void whenStopApp() { + simpleApp.stop(); + } + + private void whenStartApp() { + simpleApp.start(); + } + + private void thenAppIsNotRunning() { + assertNotNull(simpleApp.getExitCode(), "App is running"); + assertThrows(IOException.class, this::tryReachApp, "App's HTTP server is still running"); + } + + private void thenAppIsRunning() { + assertNull(simpleApp.getExitCode(), "App is not running"); + assertDoesNotThrow(this::tryReachApp, "App's HTTP server is not reachable"); + } + + private void tryReachApp() throws IOException, InterruptedException { + String response = client.send(HttpRequest.newBuilder().uri(URI.create("http://localhost:8081/test")).GET().build(), + HttpResponse.BodyHandlers.ofString()) + .body(); + // If the app is reachable, this is the expected response. + assertEquals("OK", response, "App returned unexpected response"); + } + + @QuarkusMain + public static class Main { + public static void main(String[] args) { + // Use an unrelated log to trick QuarkusProdModeTest into thinking the app started + System.out.println( + "HHH000511: The -9999.-9999.-9999 version for [org.hibernate.dialect.PostgreSQLDialect] is no longer supported, hence certain features may not work properly. The minimum supported version is 12.0.0. Check the community dialects project for available legacy versions."); + try { + // Delay the actual app start so there's a decent chance of QuarkusProdModeTest + // being ahead of the app -- otherwise we wouldn't reproduce the bug. + Thread.sleep(500); + // Expose an endpoint proving the app is up + HttpServer server = HttpServer.create(new InetSocketAddress(8081), 0); + server.createContext("/test", exchange -> { + String response = "OK"; + exchange.sendResponseHeaders(200, response.length()); + OutputStream os = exchange.getResponseBody(); + os.write(response.getBytes()); + os.close(); + }); + server.start(); + Quarkus.run(args); + } catch (InterruptedException | IOException e) { + throw new RuntimeException(e); + } + } + } +} diff --git a/test-framework/junit5/pom.xml b/test-framework/junit5/pom.xml index 132c4db1b6531..449f8fda37df5 100644 --- a/test-framework/junit5/pom.xml +++ b/test-framework/junit5/pom.xml @@ -49,10 +49,8 @@ quarkus-core - com.thoughtworks.xstream - xstream - - 1.4.20 + org.jboss.marshalling + jboss-marshalling diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusTestExtension.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusTestExtension.java index f2707e915346b..15fa6c360e67b 100644 --- a/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusTestExtension.java +++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusTestExtension.java @@ -40,7 +40,6 @@ import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; -import java.util.function.Supplier; import java.util.regex.Pattern; import org.eclipse.microprofile.config.spi.ConfigProviderResolver; @@ -52,7 +51,6 @@ import org.jboss.jandex.Type; import org.jboss.logging.Logger; import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.TestInfo; import org.junit.jupiter.api.extension.AfterAllCallback; import org.junit.jupiter.api.extension.AfterEachCallback; import org.junit.jupiter.api.extension.AfterTestExecutionCallback; @@ -106,7 +104,7 @@ import io.quarkus.test.junit.callback.QuarkusTestContext; import io.quarkus.test.junit.callback.QuarkusTestMethodContext; import io.quarkus.test.junit.internal.DeepClone; -import io.quarkus.test.junit.internal.SerializationWithXStreamFallbackDeepClone; +import io.quarkus.test.junit.internal.NewSerializingDeepClone; public class QuarkusTestExtension extends AbstractJvmQuarkusTestExtension implements BeforeEachCallback, BeforeTestExecutionCallback, AfterTestExecutionCallback, AfterEachCallback, @@ -355,7 +353,7 @@ private void shutdownHangDetection() { } private void populateDeepCloneField(StartupAction startupAction) { - deepClone = new SerializationWithXStreamFallbackDeepClone(startupAction.getClassLoader()); + deepClone = new NewSerializingDeepClone(originalCl, startupAction.getClassLoader()); } private void populateTestMethodInvokers(ClassLoader quarkusClassLoader) { @@ -962,49 +960,13 @@ private Object runExtensionMethod(ReflectiveInvocationContext invocation Parameter[] parameters = invocationContext.getExecutable().getParameters(); for (int i = 0; i < originalArguments.size(); i++) { Object arg = originalArguments.get(i); - boolean cloneRequired = false; - Object replacement = null; Class argClass = parameters[i].getType(); - if (arg != null) { - Class theclass = argClass; - while (theclass.isArray()) { - theclass = theclass.getComponentType(); - } - if (theclass.isPrimitive()) { - cloneRequired = false; - } else if (TestInfo.class.isAssignableFrom(theclass)) { - TestInfo info = (TestInfo) arg; - Method newTestMethod = info.getTestMethod().isPresent() - ? determineTCCLExtensionMethod(info.getTestMethod().get(), testClassFromTCCL) - : null; - replacement = new TestInfoImpl(info.getDisplayName(), info.getTags(), - Optional.of(testClassFromTCCL), - Optional.ofNullable(newTestMethod)); - } else if (clonePattern.matcher(theclass.getName()).matches()) { - cloneRequired = true; - } else { - try { - cloneRequired = runningQuarkusApplication.getClassLoader() - .loadClass(theclass.getName()) != theclass; - } catch (ClassNotFoundException e) { - if (arg instanceof Supplier) { - cloneRequired = true; - } else { - throw e; - } - } - } - } - if (replacement != null) { - argumentsFromTccl.add(replacement); - } else if (cloneRequired) { - argumentsFromTccl.add(deepClone.clone(arg)); - } else if (testMethodInvokerToUse != null) { + if (testMethodInvokerToUse != null) { argumentsFromTccl.add(testMethodInvokerToUse.getClass().getMethod("methodParamInstance", String.class) .invoke(testMethodInvokerToUse, argClass.getName())); } else { - argumentsFromTccl.add(arg); + argumentsFromTccl.add(deepClone.clone(arg)); } } @@ -1014,7 +976,7 @@ private Object runExtensionMethod(ReflectiveInvocationContext invocation .invoke(testMethodInvokerToUse, effectiveTestInstance, newMethod, argumentsFromTccl, extensionContext.getRequiredTestClass().getName()); } else { - return newMethod.invoke(effectiveTestInstance, argumentsFromTccl.toArray(new Object[0])); + return newMethod.invoke(effectiveTestInstance, argumentsFromTccl.toArray(Object[]::new)); } } catch (InvocationTargetException e) { diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/internal/CustomListConverter.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/internal/CustomListConverter.java deleted file mode 100644 index ddb8642d0056c..0000000000000 --- a/test-framework/junit5/src/main/java/io/quarkus/test/junit/internal/CustomListConverter.java +++ /dev/null @@ -1,63 +0,0 @@ -package io.quarkus.test.junit.internal; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Set; -import java.util.function.Predicate; - -import com.thoughtworks.xstream.converters.collections.CollectionConverter; -import com.thoughtworks.xstream.mapper.Mapper; - -/** - * A custom List converter that always uses ArrayList for unmarshalling. - * This is probably not semantically correct 100% of the time, but it's likely fine - * for all the cases where we are using marshalling / unmarshalling. - * - * The reason for doing this is to avoid XStream causing illegal access issues - * for internal JDK lists - */ -public class CustomListConverter extends CollectionConverter { - - // if we wanted to be 100% sure, we'd list all the List.of methods, but I think it's pretty safe to say - // that the JDK won't add custom implementations for the other classes - - private final Predicate supported = new Predicate() { - - private final Set JDK_LIST_CLASS_NAMES = Set.of( - List.of().getClass().getName(), - List.of(Integer.MAX_VALUE).getClass().getName(), - Arrays.asList(Integer.MAX_VALUE).getClass().getName(), - Collections.unmodifiableList(List.of()).getClass().getName(), - Collections.emptyList().getClass().getName(), - List.of(Integer.MIN_VALUE, Integer.MAX_VALUE).subList(0, 1).getClass().getName()); - - @Override - public boolean test(String className) { - return JDK_LIST_CLASS_NAMES.contains(className); - } - }.or(new Predicate<>() { - - private static final String GUAVA_LISTS_PACKAGE = "com.google.common.collect.Lists"; - - @Override - public boolean test(String className) { - return className.startsWith(GUAVA_LISTS_PACKAGE); - } - }); - - public CustomListConverter(Mapper mapper) { - super(mapper); - } - - @Override - public boolean canConvert(Class type) { - return (type != null) && supported.test(type.getName()); - } - - @Override - protected Object createCollection(Class type) { - return new ArrayList<>(); - } -} diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/internal/CustomMapConverter.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/internal/CustomMapConverter.java deleted file mode 100644 index fe93cb8594587..0000000000000 --- a/test-framework/junit5/src/main/java/io/quarkus/test/junit/internal/CustomMapConverter.java +++ /dev/null @@ -1,41 +0,0 @@ -package io.quarkus.test.junit.internal; - -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import java.util.Set; - -import com.thoughtworks.xstream.converters.collections.MapConverter; -import com.thoughtworks.xstream.mapper.Mapper; - -/** - * A custom Map converter that always uses HashMap for unmarshalling. - * This is probably not semantically correct 100% of the time, but it's likely fine - * for all the cases where we are using marshalling / unmarshalling. - * - * The reason for doing this is to avoid XStream causing illegal access issues - * for internal JDK maps - */ -public class CustomMapConverter extends MapConverter { - - // if we wanted to be 100% sure, we'd list all the Set.of methods, but I think it's pretty safe to say - // that the JDK won't add custom implementations for the other classes - private final Set SUPPORTED_CLASS_NAMES = Set.of( - Map.of().getClass().getName(), - Map.of(Integer.MAX_VALUE, Integer.MAX_VALUE).getClass().getName(), - Collections.emptyMap().getClass().getName()); - - public CustomMapConverter(Mapper mapper) { - super(mapper); - } - - @Override - public boolean canConvert(Class type) { - return (type != null) && SUPPORTED_CLASS_NAMES.contains(type.getName()); - } - - @Override - protected Object createCollection(Class type) { - return new HashMap<>(); - } -} diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/internal/CustomMapEntryConverter.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/internal/CustomMapEntryConverter.java deleted file mode 100644 index f20a7fe3e3f36..0000000000000 --- a/test-framework/junit5/src/main/java/io/quarkus/test/junit/internal/CustomMapEntryConverter.java +++ /dev/null @@ -1,55 +0,0 @@ -package io.quarkus.test.junit.internal; - -import java.util.AbstractMap; -import java.util.Map; -import java.util.Set; - -import com.thoughtworks.xstream.converters.MarshallingContext; -import com.thoughtworks.xstream.converters.UnmarshallingContext; -import com.thoughtworks.xstream.converters.collections.MapConverter; -import com.thoughtworks.xstream.io.HierarchicalStreamReader; -import com.thoughtworks.xstream.io.HierarchicalStreamWriter; -import com.thoughtworks.xstream.mapper.Mapper; - -/** - * A custom Map.Entry converter that always uses AbstractMap.SimpleEntry for unmarshalling. - * This is probably not semantically correct 100% of the time, but it's likely fine - * for all the cases where we are using marshalling / unmarshalling. - * - * The reason for doing this is to avoid XStream causing illegal access issues - * for internal JDK types - */ -@SuppressWarnings({ "rawtypes", "unchecked" }) -public class CustomMapEntryConverter extends MapConverter { - - private final Set SUPPORTED_CLASS_NAMES = Set - .of(Map.entry(Integer.MAX_VALUE, Integer.MAX_VALUE).getClass().getName()); - - public CustomMapEntryConverter(Mapper mapper) { - super(mapper); - } - - @Override - public boolean canConvert(Class type) { - return (type != null) && SUPPORTED_CLASS_NAMES.contains(type.getName()); - } - - @Override - public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context) { - var entryName = mapper().serializedClass(Map.Entry.class); - var entry = (Map.Entry) source; - writer.startNode(entryName); - writeCompleteItem(entry.getKey(), context, writer); - writeCompleteItem(entry.getValue(), context, writer); - writer.endNode(); - } - - @Override - public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) { - reader.moveDown(); - var key = readCompleteItem(reader, context, null); - var value = readCompleteItem(reader, context, null); - reader.moveUp(); - return new AbstractMap.SimpleEntry(key, value); - } -} diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/internal/CustomSetConverter.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/internal/CustomSetConverter.java deleted file mode 100644 index 88d434cfaf34a..0000000000000 --- a/test-framework/junit5/src/main/java/io/quarkus/test/junit/internal/CustomSetConverter.java +++ /dev/null @@ -1,40 +0,0 @@ -package io.quarkus.test.junit.internal; - -import java.util.Collections; -import java.util.HashSet; -import java.util.Set; - -import com.thoughtworks.xstream.converters.collections.CollectionConverter; -import com.thoughtworks.xstream.mapper.Mapper; - -/** - * A custom Set converter that always uses HashSet for unmarshalling. - * This is probably not semantically correct 100% of the time, but it's likely fine - * for all the cases where we are using marshalling / unmarshalling. - * - * The reason for doing this is to avoid XStream causing illegal access issues - * for internal JDK sets - */ -public class CustomSetConverter extends CollectionConverter { - - // if we wanted to be 100% sure, we'd list all the Set.of methods, but I think it's pretty safe to say - // that the JDK won't add custom implementations for the other classes - private final Set SUPPORTED_CLASS_NAMES = Set.of( - Set.of().getClass().getName(), - Set.of(Integer.MAX_VALUE).getClass().getName(), - Collections.emptySet().getClass().getName()); - - public CustomSetConverter(Mapper mapper) { - super(mapper); - } - - @Override - public boolean canConvert(Class type) { - return (type != null) && SUPPORTED_CLASS_NAMES.contains(type.getName()); - } - - @Override - protected Object createCollection(Class type) { - return new HashSet<>(); - } -} diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/internal/NewSerializingDeepClone.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/internal/NewSerializingDeepClone.java new file mode 100644 index 0000000000000..682a196e00c71 --- /dev/null +++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/internal/NewSerializingDeepClone.java @@ -0,0 +1,113 @@ +package io.quarkus.test.junit.internal; + +import java.io.IOException; +import java.io.Serializable; +import java.io.UncheckedIOException; +import java.lang.reflect.Method; +import java.util.Set; +import java.util.function.Supplier; + +import org.jboss.marshalling.cloner.ClassCloner; +import org.jboss.marshalling.cloner.ClonerConfiguration; +import org.jboss.marshalling.cloner.ObjectCloner; +import org.jboss.marshalling.cloner.ObjectCloners; +import org.junit.jupiter.api.TestInfo; + +/** + * A deep-clone implementation using JBoss Marshalling's fast object cloner. + */ +public final class NewSerializingDeepClone implements DeepClone { + private final ObjectCloner cloner; + + public NewSerializingDeepClone(final ClassLoader sourceLoader, final ClassLoader targetLoader) { + ClonerConfiguration cc = new ClonerConfiguration(); + cc.setSerializabilityChecker(clazz -> clazz != Object.class); + cc.setClassCloner(new ClassCloner() { + public Class clone(final Class original) { + if (isUncloneable(original)) { + return original; + } + try { + return targetLoader.loadClass(original.getName()); + } catch (ClassNotFoundException ignored) { + return original; + } + } + + public Class cloneProxy(final Class proxyClass) { + // not really supported + return proxyClass; + } + }); + cc.setCloneTable( + (original, objectCloner, classCloner) -> { + if (EXTRA_IDENTITY_CLASSES.contains(original.getClass())) { + // avoid copying things that do not need to be copied + return original; + } else if (isUncloneable(original.getClass())) { + if (original instanceof Supplier s) { + // sneaky + return (Supplier) () -> clone(s.get()); + } else { + return original; + } + } else if (original instanceof TestInfo info) { + // copy the test info correctly + return new TestInfoImpl(info.getDisplayName(), info.getTags(), + info.getTestClass().map(this::cloneClass), + info.getTestMethod().map(this::cloneMethod)); + } else if (original == sourceLoader) { + return targetLoader; + } + // let the default cloner handle it + return null; + }); + cloner = ObjectCloners.getSerializingObjectClonerFactory().createCloner(cc); + } + + private static boolean isUncloneable(Class clazz) { + return clazz.isHidden() && !Serializable.class.isAssignableFrom(clazz); + } + + private Class cloneClass(Class clazz) { + try { + return (Class) cloner.clone(clazz); + } catch (IOException | ClassNotFoundException e) { + return null; + } + } + + private Method cloneMethod(Method method) { + try { + Class declaring = (Class) cloner.clone(method.getDeclaringClass()); + Class[] argTypes = (Class[]) cloner.clone(method.getParameterTypes()); + return declaring.getDeclaredMethod(method.getName(), argTypes); + } catch (Exception e) { + return null; + } + } + + public Object clone(final Object objectToClone) { + try { + return cloner.clone(objectToClone); + } catch (IOException e) { + throw new UncheckedIOException(e); + } catch (ClassNotFoundException e) { + throw new IllegalStateException(e); + } + } + + /** + * Classes which do not need to be cloned. + */ + private static final Set> EXTRA_IDENTITY_CLASSES = Set.of( + Object.class, + byte[].class, + short[].class, + int[].class, + long[].class, + char[].class, + boolean[].class, + float[].class, + double[].class); +} diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/internal/SerializationDeepClone.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/internal/SerializationDeepClone.java deleted file mode 100644 index 3da2c0c16e372..0000000000000 --- a/test-framework/junit5/src/main/java/io/quarkus/test/junit/internal/SerializationDeepClone.java +++ /dev/null @@ -1,46 +0,0 @@ -package io.quarkus.test.junit.internal; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.ObjectInputStream; -import java.io.ObjectOutputStream; -import java.io.ObjectStreamClass; - -/** - * Cloning strategy that just serializes and deserializes using plain old java serialization. - */ -class SerializationDeepClone implements DeepClone { - - private final ClassLoader classLoader; - - SerializationDeepClone(ClassLoader classLoader) { - this.classLoader = classLoader; - } - - @Override - public Object clone(Object objectToClone) { - ByteArrayOutputStream byteOut = new ByteArrayOutputStream(512); - try (ObjectOutputStream objOut = new ObjectOutputStream(byteOut)) { - objOut.writeObject(objectToClone); - try (ObjectInputStream objIn = new ClassLoaderAwareObjectInputStream(byteOut)) { - return objIn.readObject(); - } - } catch (IOException | ClassNotFoundException e) { - throw new IllegalStateException("Unable to deep clone object of type '" + objectToClone.getClass().getName() - + "'. Please report the issue on the Quarkus issue tracker.", e); - } - } - - private class ClassLoaderAwareObjectInputStream extends ObjectInputStream { - - public ClassLoaderAwareObjectInputStream(ByteArrayOutputStream byteOut) throws IOException { - super(new ByteArrayInputStream(byteOut.toByteArray())); - } - - @Override - protected Class resolveClass(ObjectStreamClass desc) throws ClassNotFoundException { - return Class.forName(desc.getName(), true, classLoader); - } - } -} diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/internal/SerializationWithXStreamFallbackDeepClone.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/internal/SerializationWithXStreamFallbackDeepClone.java deleted file mode 100644 index 36da89a82e804..0000000000000 --- a/test-framework/junit5/src/main/java/io/quarkus/test/junit/internal/SerializationWithXStreamFallbackDeepClone.java +++ /dev/null @@ -1,35 +0,0 @@ -package io.quarkus.test.junit.internal; - -import java.io.Serializable; -import java.util.Optional; - -import org.jboss.logging.Logger; - -/** - * Cloning strategy delegating to {@link SerializationDeepClone}, falling back to {@link XStreamDeepClone} in case of error. - */ -public class SerializationWithXStreamFallbackDeepClone implements DeepClone { - - private static final Logger LOG = Logger.getLogger(SerializationWithXStreamFallbackDeepClone.class); - - private final SerializationDeepClone serializationDeepClone; - private final XStreamDeepClone xStreamDeepClone; - - public SerializationWithXStreamFallbackDeepClone(ClassLoader classLoader) { - this.serializationDeepClone = new SerializationDeepClone(classLoader); - this.xStreamDeepClone = new XStreamDeepClone(classLoader); - } - - @Override - public Object clone(Object objectToClone) { - if (objectToClone instanceof Serializable) { - try { - return serializationDeepClone.clone(objectToClone); - } catch (RuntimeException re) { - LOG.debugf("SerializationDeepClone failed (will fall back to XStream): %s", - Optional.ofNullable(re.getCause()).orElse(re)); - } - } - return xStreamDeepClone.clone(objectToClone); - } -} diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/TestInfoImpl.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/internal/TestInfoImpl.java similarity index 95% rename from test-framework/junit5/src/main/java/io/quarkus/test/junit/TestInfoImpl.java rename to test-framework/junit5/src/main/java/io/quarkus/test/junit/internal/TestInfoImpl.java index 498cc5ff64447..7cc0be697b719 100644 --- a/test-framework/junit5/src/main/java/io/quarkus/test/junit/TestInfoImpl.java +++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/internal/TestInfoImpl.java @@ -1,4 +1,4 @@ -package io.quarkus.test.junit; +package io.quarkus.test.junit.internal; import java.lang.reflect.Method; import java.util.Optional; diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/internal/XStreamDeepClone.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/internal/XStreamDeepClone.java deleted file mode 100644 index 9951f96734d44..0000000000000 --- a/test-framework/junit5/src/main/java/io/quarkus/test/junit/internal/XStreamDeepClone.java +++ /dev/null @@ -1,61 +0,0 @@ -package io.quarkus.test.junit.internal; - -import java.util.function.Supplier; - -import com.thoughtworks.xstream.XStream; - -/** - * Super simple cloning strategy that just serializes to XML and deserializes it using xstream - */ -class XStreamDeepClone implements DeepClone { - - private final Supplier xStreamSupplier; - - XStreamDeepClone(ClassLoader classLoader) { - // avoid doing any work eagerly since the cloner is rarely used - xStreamSupplier = () -> { - XStream result = new XStream(); - result.allowTypesByRegExp(new String[] { ".*" }); - result.setClassLoader(classLoader); - result.registerConverter(new CustomListConverter(result.getMapper())); - result.registerConverter(new CustomSetConverter(result.getMapper())); - result.registerConverter(new CustomMapConverter(result.getMapper())); - result.registerConverter(new CustomMapEntryConverter(result.getMapper())); - - return result; - }; - } - - @Override - public Object clone(Object objectToClone) { - if (objectToClone == null) { - return null; - } - - if (objectToClone instanceof Supplier) { - return handleSupplier((Supplier) objectToClone); - } - - return doClone(objectToClone); - } - - private Supplier handleSupplier(final Supplier supplier) { - return new Supplier() { - @Override - public Object get() { - return doClone(supplier.get()); - } - }; - } - - private Object doClone(Object objectToClone) { - XStream xStream = xStreamSupplier.get(); - final String serialized = xStream.toXML(objectToClone); - final Object result = xStream.fromXML(serialized); - if (result == null) { - throw new IllegalStateException("Unable to deep clone object of type '" + objectToClone.getClass().getName() - + "'. Please report the issue on the Quarkus issue tracker."); - } - return result; - } -} diff --git a/test-framework/maven/src/main/java/io/quarkus/maven/it/continuoustesting/TestModeContinuousTestingMavenTestUtils.java b/test-framework/maven/src/main/java/io/quarkus/maven/it/continuoustesting/TestModeContinuousTestingMavenTestUtils.java index 15b4530ea586b..d08e2c7cf7b1d 100644 --- a/test-framework/maven/src/main/java/io/quarkus/maven/it/continuoustesting/TestModeContinuousTestingMavenTestUtils.java +++ b/test-framework/maven/src/main/java/io/quarkus/maven/it/continuoustesting/TestModeContinuousTestingMavenTestUtils.java @@ -22,10 +22,11 @@ public class TestModeContinuousTestingMavenTestUtils extends ContinuousTestingMa // Example output we look for // 1 test failed (1 passing, 0 skipped), 1 test was run in 217ms. Tests completed at 21:22:34 due to changes to HelloResource$Blah.class and 1 other files. // All 2 tests are passing (0 skipped), 2 tests were run in 1413ms. Tests completed at 21:22:33. + // All 1 test is passing (0 skipped), ... // Windows log, despite `quarkus.console.basic=true', might contain terminal control symbols, colour decorations. // e.g. the matcher is then fighting: 1 test failed (1 passing, 0 skipped) private static final Pattern ALL_PASSING = Pattern.compile( - "(?:\\e\\[[\\d;]+m)*All (\\d+) tests are passing \\((\\d+) skipped\\)", + "(?:\\e\\[[\\d;]+m)*All (\\d+) tests? (?:are|is) passing \\((\\d+) skipped\\)", Pattern.MULTILINE); private static final Pattern SOME_PASSING = Pattern .compile( diff --git a/test-framework/oidc-server/src/main/java/io/quarkus/test/oidc/server/OidcWiremockTestResource.java b/test-framework/oidc-server/src/main/java/io/quarkus/test/oidc/server/OidcWiremockTestResource.java index 9f76443d13692..bd7bd43903bfd 100644 --- a/test-framework/oidc-server/src/main/java/io/quarkus/test/oidc/server/OidcWiremockTestResource.java +++ b/test-framework/oidc-server/src/main/java/io/quarkus/test/oidc/server/OidcWiremockTestResource.java @@ -43,6 +43,8 @@ public class OidcWiremockTestResource implements QuarkusTestResourceLifecycleMan "https://server.example.com"); private static final String TOKEN_AUDIENCE = System.getProperty("quarkus.test.oidc.token.audience", "https://server.example.com"); + private static final String ID_TOKEN_AUDIENCE = System.getProperty("quarkus.test.oidc.idtoken.audience", + "https://id.server.example.com"); private static final String TOKEN_SUBJECT = "123456"; private static final String BEARER_TOKEN_TYPE = "Bearer"; private static final String ID_TOKEN_TYPE = "ID"; @@ -385,10 +387,11 @@ public static String generateJwtToken(String userName, Set groups, Strin } public static String generateJwtToken(String userName, Set groups, String sub, String type) { + final String audience = ID_TOKEN_TYPE.equals(type) ? ID_TOKEN_AUDIENCE : TOKEN_AUDIENCE; JwtClaimsBuilder builder = Jwt.preferredUserName(userName) .groups(groups) .issuer(TOKEN_ISSUER) - .audience(TOKEN_AUDIENCE) + .audience(audience) .claim("sid", "session-id") .subject(sub); if (type != null) {