diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 293c230e2cd9cf..725df600a395dd 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -6,7 +6,7 @@ updates: interval: daily time: "23:00" timezone: Europe/Paris - open-pull-requests-limit: 6 + open-pull-requests-limit: 10 labels: - area/dependencies allow: diff --git a/.github/develocity-preapproved-developers.json b/.github/develocity-preapproved-developers.json deleted file mode 100644 index 670c4d84f00ac1..00000000000000 --- a/.github/develocity-preapproved-developers.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "preapproved-developers": [ - "alesj", - "aloubyansky", - "aureamunoz", - "brunobat", - "cescoffier", - "DavideD", - "dmlloyd", - "ebullient", - "emmanuelbernard", - "evanchooly", - "FroMage", - "galderz", - "gastaldi", - "geoand", - "gsmet", - "gwenneg", - "holly-cummins", - "ia3andy", - "iocanel", - "jmartisk", - "johnaohara", - "jponge", - "karesti", - "Karm", - "Ladicek", - "machi1990", - "manovotn", - "manusa", - "maxandersen", - "metacosm", - "MichalMaler", - "michalvavrik", - "michelle-purcell", - "MikeEdgar", - "mkouba", - "n1hility", - "ozangunalp", - "patriot1burke", - "pedroigor", - "phillip-kruger", - "ppalaga", - "radcortez", - "rsvoboda", - "Sanne", - "sberyozkin", - "Sgitario", - "stalep", - "starksm64", - "stuartwdouglas", - "tsegismont", - "yrodiere", - "zakkak" - ] -} \ No newline at end of file diff --git a/.github/native-tests.json b/.github/native-tests.json index d137a6435f5a11..6542e8537d8b9c 100644 --- a/.github/native-tests.json +++ b/.github/native-tests.json @@ -111,7 +111,7 @@ { "category": "Misc3", "timeout": 80, - "test-modules": "kubernetes-client, openshift-client, kubernetes-service-binding-jdbc, smallrye-config, smallrye-graphql, smallrye-graphql-client, smallrye-metrics", + "test-modules": "kubernetes-client, openshift-client, kubernetes-service-binding-jdbc, smallrye-config, smallrye-graphql, smallrye-graphql-client, smallrye-graphql-client-keycloak, smallrye-metrics", "os-name": "ubuntu-latest" }, { diff --git a/.github/workflows/ci-actions-incremental.yml b/.github/workflows/ci-actions-incremental.yml index 33e0b6c4b65c1c..92b758f0d09ef5 100644 --- a/.github/workflows/ci-actions-incremental.yml +++ b/.github/workflows/ci-actions-incremental.yml @@ -732,6 +732,7 @@ jobs: path: | **/target/*-reports/TEST-*.xml target/build-report.json + **/target/gradle-build-scan-url.txt LICENSE.txt retention-days: 2 - name: Save Build Scan @@ -926,7 +927,7 @@ jobs: path: | **/target/*-reports/TEST-*.xml target/build-report.json - target/gradle-build-scan-url.txt + **/target/gradle-build-scan-url.txt LICENSE.txt retention-days: 2 - name: Save Build Scan @@ -1023,7 +1024,7 @@ jobs: **/target/*-reports/TEST-*.xml **/build/test-results/test/TEST-*.xml target/build-report.json - target/gradle-build-scan-url.txt + **/target/gradle-build-scan-url.txt LICENSE.txt retention-days: 2 - name: Save Build Scan diff --git a/.github/workflows/develocity-publish-build-scans.yml b/.github/workflows/develocity-publish-build-scans.yml index 6bafd0273332af..c391c6bc686088 100644 --- a/.github/workflows/develocity-publish-build-scans.yml +++ b/.github/workflows/develocity-publish-build-scans.yml @@ -11,27 +11,24 @@ defaults: jobs: publish-build-scans: - if: github.repository == 'quarkusio/quarkus' && github.event.workflow_run.event == 'pull_request' + if: github.repository == 'quarkusio/quarkus' && github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion != 'cancelled' runs-on: ubuntu-latest permissions: pull-requests: write + checks: write steps: - - uses: actions/checkout@v4 - - name: Extract preapproved developers list - id: extract-preapproved-developers - run: | - echo "preapproved-developpers<> $GITHUB_OUTPUT - cat .github/develocity-preapproved-developers.json >> $GITHUB_OUTPUT - echo >> $GITHUB_OUTPUT - echo "EOF" >> $GITHUB_OUTPUT - name: Publish Maven Build Scans uses: gradle/github-actions/maven-build-scan/publish@v1-beta - if: ${{ contains(fromJson(steps.extract-preapproved-developers.outputs.preapproved-developpers).preapproved-developers, github.event.workflow_run.actor.login) }} with: develocity-url: 'https://ge.quarkus.io' develocity-access-key: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} skip-comment: true - - name: Push to summary - if: ${{ contains(fromJson(steps.extract-preapproved-developers.outputs.preapproved-developpers).preapproved-developers, github.event.workflow_run.actor.login) }} + - name: Inject build scans in reports + uses: quarkusio/action-helpers@main + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + action: inject-build-scans + workflow-run-id: ${{ github.event.workflow_run.id }} + - name: Output JSON file run: | - cat build-metadata.json >> ${GITHUB_STEP_SUMMARY} + if [ -f build-metadata.json ]; then jq '.' build-metadata.json >> $GITHUB_STEP_SUMMARY; fi diff --git a/.mvn/gradle-enterprise-custom-user-data.groovy b/.mvn/gradle-enterprise-custom-user-data.groovy index a076e487182941..8e78dcf99e5977 100644 --- a/.mvn/gradle-enterprise-custom-user-data.groovy +++ b/.mvn/gradle-enterprise-custom-user-data.groovy @@ -96,3 +96,23 @@ if (System.env.GITHUB_ACTIONS) { } } +// Check runtime Maven version and Maven Wrapper version are aligned +def runtimeInfo = (org.apache.maven.rtinfo.RuntimeInformation) session.lookup("org.apache.maven.rtinfo.RuntimeInformation") +def runtimeMavenVersion = runtimeInfo?.getMavenVersion() +Properties mavenWrapperProperties = new Properties() +File mavenWrapperPropertiesFile = new File(".mvn/wrapper/maven-wrapper.properties") +if(mavenWrapperPropertiesFile.exists()) { + mavenWrapperPropertiesFile.withInputStream { + mavenWrapperProperties.load(it) + } + // assuming the wrapper properties contains: + // distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/VERSION/apache-maven-VERSION-bin.zip + if(regexp = mavenWrapperProperties."distributionUrl" =~ /.*\/apache-maven-(.*)-bin\.zip/) { + def wrapperMavenVersion = regexp.group(1) + if (runtimeMavenVersion && wrapperMavenVersion && wrapperMavenVersion != runtimeMavenVersion) { + log.warn("Maven Wrapper is configured with a different version (" + wrapperMavenVersion + ") than the runtime version (" + runtimeMavenVersion + "). This will negatively impact build consistency and build caching.") + buildScan.tag("misaligned-maven-version") + buildScan.value("wrapper-maven-version", wrapperMavenVersion) + } + } +} diff --git a/.mvn/gradle-enterprise.xml b/.mvn/gradle-enterprise.xml index 0f9d7558936b53..27ac5553de6a2c 100644 --- a/.mvn/gradle-enterprise.xml +++ b/.mvn/gradle-enterprise.xml @@ -27,7 +27,7 @@ true - #{env['CI'] != null} + #{env['CI'] != null and env['GRADLE_ENTERPRISE_ACCESS_KEY'] != null and env['GRADLE_ENTERPRISE_ACCESS_KEY'] != ''} diff --git a/bom/application/pom.xml b/bom/application/pom.xml index d505c6550a77fd..92e17e9b35b189 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -25,14 +25,15 @@ 1 1.1.5 2.1.4.Final - 3.0.2.Final + 3.1.0.Final 6.2.6.Final 0.33.0 0.2.4 0.1.15 0.1.5 - 1.30.1 - 1.30.0-alpha + 1.31.0 + 1.31.0-alpha + 1.21.0-alpha 5.0.3.Final 1.11.5 2.1.12 @@ -56,12 +57,12 @@ 3.7.0 2.6.0 6.2.6 - 4.3.1 + 4.4.0 2.1.0 1.0.13 3.0.1 3.7.2 - 4.10.1 + 4.11.0 2.4.0 2.1.2 2.1.1 @@ -107,7 +108,7 @@ 7.0.0.Final 2.1 8.0.0.Final - 8.10.4 + 8.11.1 2.2.21 2.2.5.Final 2.2.2.Final @@ -138,7 +139,7 @@ 2.2 5.10.0 1.5.0 - 14.0.20.Final + 14.0.21.Final 4.6.5.Final 3.1.5 4.1.100.Final @@ -161,7 +162,7 @@ 1.7.3 0.27.0 1.6.0 - 4.0.3 + 4.1.0 3.2.0 4.2.0 3.0.2.Final @@ -175,9 +176,9 @@ 4.11.1 1.8.0 0.34.1 - 3.25.5 + 3.25.6 3.14.9 - 1.17.2 + 1.17.6 0.2.1 4.9.2 5.2.SP7 @@ -197,8 +198,8 @@ 2.1 4.7.5 1.1.0 - 1.24.0 - 1.10.0 + 1.25.0 + 1.11.0 2.10.1 1.1.1.Final 2.20.0 @@ -206,11 +207,11 @@ 1.11.3 2.4.14.Final 0.1.18.Final - 1.19.1 - 3.3.3 + 1.19.3 + 3.3.4 2.0.0 - 1.3.0 + 1.4.4 2.7 2.4 2.4.0 @@ -3178,6 +3179,12 @@ ${dekorate.version} noapt + + io.dekorate + prometheus-annotations + ${dekorate.version} + noapt + io.dekorate s2i-annotations @@ -3304,48 +3311,10 @@ com.github.docker-java - docker-java - ${docker-java.version} - - - com.github.docker-java - docker-java-api - ${docker-java.version} - - - com.github.docker-java - docker-java-core - ${docker-java.version} - - - com.github.docker-java - docker-java-transport - ${docker-java.version} - - - com.github.docker-java - docker-java-transport-httpclient5 - ${docker-java.version} - - - com.github.docker-java - docker-java-transport-jersey - ${docker-java.version} - - - com.github.docker-java - docker-java-transport-netty - ${docker-java.version} - - - com.github.docker-java - docker-java-transport-okhttp - ${docker-java.version} - - - com.github.docker-java - docker-java-transport-zerodep + docker-java-bom ${docker-java.version} + pom + import com.google.cloud.functions @@ -6214,6 +6183,23 @@ ${mime4j.version} + + + io.opentelemetry.semconv + opentelemetry-semconv + ${opentelemetry-semconv.version} + + + io.opentelemetry + opentelemetry-bom + + + io.opentelemetry + opentelemetry-api + + + + io.quarkus diff --git a/build-parent/pom.xml b/build-parent/pom.xml index f8e0262254b759..0a0960fb02faeb 100644 --- a/build-parent/pom.xml +++ b/build-parent/pom.xml @@ -38,7 +38,7 @@ 2.5.10 2.70.0 - 3.25.5 + 3.25.6 2.0.3.Final 6.0.1 @@ -106,7 +106,7 @@ quay.io/keycloak/keycloak:${keycloak.version} quay.io/keycloak/keycloak:${keycloak.wildfly.version}-legacy - 6.0.9 + 6.0.10 3.24.2 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 73cd3cff70287d..5983f094813df0 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/Capability.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/Capability.java @@ -32,10 +32,12 @@ public interface Capability { String LRA_PARTICIPANT = QUARKUS_PREFIX + ".lra.participant"; String JACKSON = QUARKUS_PREFIX + ".jackson"; + String JSONB = QUARKUS_PREFIX + ".jsonb"; - String KOTLIN = QUARKUS_PREFIX + ".kotlin"; + String JAXB = QUARKUS_PREFIX + ".jaxb"; + String JAXP = QUARKUS_PREFIX + ".jaxp"; - String JSONB = QUARKUS_PREFIX + ".jsonb"; + String KOTLIN = QUARKUS_PREFIX + ".kotlin"; String HAL = QUARKUS_PREFIX + ".hal"; diff --git a/core/deployment/src/main/java/io/quarkus/deployment/CodeGenProvider.java b/core/deployment/src/main/java/io/quarkus/deployment/CodeGenProvider.java index c0c759cbb110ef..e5910422f46af6 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/CodeGenProvider.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/CodeGenProvider.java @@ -22,11 +22,27 @@ public interface CodeGenProvider { /** * File extension that CodeGenProvider will generate code from + * Deprecated: use inputExtensions instead * * @return file extension */ + @Deprecated + default String inputExtension() { + return null; + } + + /** + * File extensions that CodeGenProvider will generate code from + * + * @return file extensions + */ @NotNull - String inputExtension(); + default String[] inputExtensions() { + if (inputExtension() != null) { + return new String[] { inputExtension() }; + } + return new String[] {}; + } /** * Name of the directory containing input files for a given {@link CodeGenProvider} implementation diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/CodeGenWatcher.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/CodeGenWatcher.java index 76b76cfe8dd0e8..a6ad84813053c2 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/dev/CodeGenWatcher.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/CodeGenWatcher.java @@ -41,19 +41,21 @@ class CodeGenWatcher { final Config config = CodeGenerator.getConfig(curatedApplication.getApplicationModel(), LaunchMode.DEVELOPMENT, properties, deploymentClassLoader); for (CodeGenData codeGen : codeGens) { - watchers.add(new FSWatchUtil.Watcher(codeGen.sourceDir, codeGen.provider.inputExtension(), - modifiedPaths -> { - codeGenLock.lock(); - try { - CodeGenerator.trigger(deploymentClassLoader, - codeGen, - curatedApplication.getApplicationModel(), config, false); - } catch (Exception any) { - log.warn("Code generation failed", any); - } finally { - codeGenLock.unlock(); - } - })); + for (String ext : codeGen.provider.inputExtensions()) { + watchers.add(new FSWatchUtil.Watcher(codeGen.sourceDir, ext, + modifiedPaths -> { + codeGenLock.lock(); + try { + CodeGenerator.trigger(deploymentClassLoader, + codeGen, + curatedApplication.getApplicationModel(), config, false); + } catch (Exception any) { + log.warn("Code generation failed", any); + } finally { + codeGenLock.unlock(); + } + })); + } } fsWatchUtil = new FSWatchUtil(); fsWatchUtil.observe(watchers, 500); 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 0a92c1909a8fe9..24a1c175a30593 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 @@ -1128,7 +1128,9 @@ private RuntimeUpdatesProcessor setWatchedFilePathsInternal(Map // First find all matching paths from all roots try (final Stream walk = Files.walk(root)) { walk.forEach(path -> { - if (path.equals(root)) { + if (path.equals(root) + // Never watch directories + || Files.isDirectory(path)) { return; } // Use the relative path to match the watched file diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/builditem/CompiledJavaVersionBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/builditem/CompiledJavaVersionBuildItem.java index 39c5385a9348eb..88dadbcc63dd47 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/builditem/CompiledJavaVersionBuildItem.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/builditem/CompiledJavaVersionBuildItem.java @@ -30,6 +30,8 @@ public interface JavaVersion { Status isJava17OrHigher(); + Status isJava21OrHigher(); + Status isJava19OrHigher(); enum Status { @@ -58,6 +60,11 @@ public Status isJava17OrHigher() { return Status.UNKNOWN; } + @Override + public Status isJava21OrHigher() { + return Status.UNKNOWN; + } + @Override public Status isJava19OrHigher() { return Status.UNKNOWN; @@ -69,6 +76,7 @@ final class Known implements JavaVersion { private static final int JAVA_11_MAJOR = 55; private static final int JAVA_17_MAJOR = 61; private static final int JAVA_19_MAJOR = 63; + private static final int JAVA_21_MAJOR = 66; private final int determinedMajor; @@ -96,6 +104,11 @@ public Status isJava19OrHigher() { return higherOrEqualStatus(JAVA_19_MAJOR); } + @Override + public Status isJava21OrHigher() { + return higherOrEqualStatus(JAVA_21_MAJOR); + } + private Status higherOrEqualStatus(int javaMajor) { return determinedMajor >= javaMajor ? Status.TRUE : Status.FALSE; } diff --git a/core/deployment/src/test/java/io/quarkus/deployment/runnerjar/ExcludedArtifactsTest.java b/core/deployment/src/test/java/io/quarkus/deployment/runnerjar/ExcludedArtifactsTest.java index 89935d33638582..b0db1dfb0cbc06 100644 --- a/core/deployment/src/test/java/io/quarkus/deployment/runnerjar/ExcludedArtifactsTest.java +++ b/core/deployment/src/test/java/io/quarkus/deployment/runnerjar/ExcludedArtifactsTest.java @@ -78,13 +78,5 @@ protected void assertAppModel(ApplicationModel model) throws Exception { expected.add(new ArtifactDependency(ArtifactCoords.jar("io.quarkus.bootstrap.test", "dep-g", "1"), DependencyFlags.RUNTIME_CP, DependencyFlags.DEPLOYMENT_CP)); assertEquals(expected, getDependenciesWithFlag(model, DependencyFlags.RUNTIME_CP)); - - expected = new HashSet<>(); - expected.add(new ArtifactDependency(ArtifactCoords.jar("io.quarkus.bootstrap.test", "ext-a-dep", "1"), - DependencyFlags.REMOVED)); - expected.add(new ArtifactDependency(ArtifactCoords.jar("org.banned", "dep-e", "1"), DependencyFlags.REMOVED)); - expected.add(new ArtifactDependency(ArtifactCoords.jar("org.banned.too", "dep-d", "1"), DependencyFlags.REMOVED)); - expected.add(new ArtifactDependency(ArtifactCoords.jar("org.banned", "dep-f", "1"), DependencyFlags.REMOVED)); - assertEquals(expected, getDependenciesWithFlag(model, DependencyFlags.REMOVED)); } } 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 004ecae8dfbaae..04cdd47aba0cdb 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 @@ -495,7 +495,8 @@ private List extractEnumValues(TypeMirror realTypeMirror, boolean useHyp if (rawJavaDoc != null && !rawJavaDoc.isBlank()) { // Show enum constant description as a Tooltip String javaDoc = enumJavaDocParser.parseConfigDescription(rawJavaDoc); - acceptedValues.add(String.format(Constants.TOOLTIP, enumValue, javaDoc)); + acceptedValues.add(String.format(Constants.TOOLTIP, enumValue, + javaDoc.replace("

", Constants.EMPTY).replace("

", Constants.EMPTY))); } else { acceptedValues.add(Constants.CODE_DELIMITER + enumValue + Constants.CODE_DELIMITER); diff --git a/core/runtime/src/main/java/io/quarkus/runtime/configuration/DeprecatedRuntimePropertiesRecorder.java b/core/runtime/src/main/java/io/quarkus/runtime/configuration/DeprecatedRuntimePropertiesRecorder.java index 6ff8eb37c3f829..994e2fbd5c0dc0 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/configuration/DeprecatedRuntimePropertiesRecorder.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/configuration/DeprecatedRuntimePropertiesRecorder.java @@ -7,6 +7,7 @@ import org.jboss.logging.Logger; import io.quarkus.runtime.annotations.Recorder; +import io.smallrye.config.SmallRyeConfig; @Recorder public class DeprecatedRuntimePropertiesRecorder { @@ -17,6 +18,11 @@ public void reportDeprecatedProperties(Set deprecatedRuntimeProperties) Config config = ConfigProvider.getConfig(); for (String property : config.getPropertyNames()) { if (deprecatedRuntimeProperties.contains(property)) { + String configSourceName = ((SmallRyeConfig) config).getConfigValue(property).getConfigSourceName(); + // this condition can be removed when support of the @ConfigRoot annotation on classes is removed + if ("DefaultValuesConfigSource".equals(configSourceName)) { + continue; + } log.warnf("The '%s' config property is deprecated and should not be used anymore", property); } } diff --git a/core/runtime/src/main/java/io/quarkus/runtime/util/ContainerRuntimeUtil.java b/core/runtime/src/main/java/io/quarkus/runtime/util/ContainerRuntimeUtil.java index ed538474b8b57f..607ead4f24980c 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/util/ContainerRuntimeUtil.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/util/ContainerRuntimeUtil.java @@ -191,7 +191,10 @@ private static boolean getRootlessStateFor(ContainerRuntime containerRuntime) { final Predicate stringPredicate; // Docker includes just "rootless" under SecurityOptions, while podman includes "rootless: " if (containerRuntime == ContainerRuntime.DOCKER) { - stringPredicate = line -> line.trim().equals("rootless"); + // We also treat Docker Desktop as "rootless" since the way it binds mounts does not + // transparently map the host user ID and GID + // see https://docs.docker.com/desktop/faqs/linuxfaqs/#how-do-i-enable-file-sharing + stringPredicate = line -> line.trim().equals("rootless") || line.contains("desktop-linux"); } else { stringPredicate = line -> line.trim().equals("rootless: true"); } diff --git a/devtools/cli/src/main/java/io/quarkus/cli/CreateExtension.java b/devtools/cli/src/main/java/io/quarkus/cli/CreateExtension.java index 9cbbc9889bd349..91f913675aab77 100644 --- a/devtools/cli/src/main/java/io/quarkus/cli/CreateExtension.java +++ b/devtools/cli/src/main/java/io/quarkus/cli/CreateExtension.java @@ -1,7 +1,9 @@ package io.quarkus.cli; +import java.util.ArrayList; import java.util.Map; import java.util.TreeMap; +import java.util.stream.Collectors; import io.quarkus.cli.common.OutputOptionMixin; import io.quarkus.cli.common.PropertiesOptions; @@ -13,6 +15,7 @@ import io.quarkus.devtools.commands.data.QuarkusCommandOutcome; import io.quarkus.devtools.commands.handlers.CreateExtensionCommandHandler; import io.quarkus.devtools.project.BuildTool; +import io.quarkus.devtools.project.JavaVersion; import io.quarkus.devtools.project.QuarkusProject; import io.quarkus.maven.dependency.ArtifactCoords; import io.quarkus.registry.catalog.ExtensionCatalog; @@ -68,6 +71,12 @@ }) public class CreateExtension extends BaseCreateCommand { + static class VersionCandidates extends ArrayList { + VersionCandidates() { + super(JavaVersion.JAVA_VERSIONS_LTS.stream().map(String::valueOf).collect(Collectors.toList())); + } + } + @CommandLine.Spec protected CommandLine.Model.CommandSpec spec; @@ -77,6 +86,11 @@ public class CreateExtension extends BaseCreateCommand { @CommandLine.ArgGroup(order = 1, heading = "%nQuarkus version:%n") TargetQuarkusPlatformGroup targetQuarkusVersion = new TargetQuarkusPlatformGroup(); + // Ideally we should use TargetLanguageGroup once we support creating extensions with Kotlin + @CommandLine.Option(names = { + "--java" }, description = "Target Java version.\n Valid values: ${COMPLETION-CANDIDATES}", completionCandidates = VersionCandidates.class, defaultValue = JavaVersion.DEFAULT_JAVA_VERSION_FOR_EXTENSION) + String javaVersion; + @CommandLine.ArgGroup(order = 2, exclusive = false, heading = "%nGenerated artifacts%n") ExtensionNameGenerationGroup nameGeneration = new ExtensionNameGenerationGroup(); @@ -117,6 +131,7 @@ public Integer call() throws Exception { .quarkusBomGroupId(quarkusBom.getGroupId()) .quarkusBomArtifactId(quarkusBom.getArtifactId()) .quarkusBomVersion(quarkusBom.getVersion()) + .javaVersion(javaVersion) .withCodestart(codeGeneration.withCodestart()) .withoutUnitTest(codeGeneration.skipUnitTest()) .withoutDevModeTest(codeGeneration.skipDevModeTest()) diff --git a/devtools/maven/src/main/java/io/quarkus/maven/CreateExtensionMojo.java b/devtools/maven/src/main/java/io/quarkus/maven/CreateExtensionMojo.java index 8487aa12aa3ad8..99b628b4f6eede 100644 --- a/devtools/maven/src/main/java/io/quarkus/maven/CreateExtensionMojo.java +++ b/devtools/maven/src/main/java/io/quarkus/maven/CreateExtensionMojo.java @@ -179,6 +179,12 @@ public class CreateExtensionMojo extends AbstractMojo { @Parameter(property = "quarkusBomVersion") String quarkusBomVersion; + /** + * Version of Java used to build the project. + */ + @Parameter(property = "javaVersion") + private String javaVersion; + /** * Indicates whether to generate an extension codestart */ @@ -281,7 +287,8 @@ public void execute() throws MojoExecutionException { .quarkusVersion(quarkusVersion) .quarkusBomGroupId(quarkusBomGroupId) .quarkusBomArtifactId(quarkusBomArtifactId) - .quarkusBomGroupId(quarkusBomVersion) + .quarkusBomVersion(quarkusBomVersion) + .javaVersion(javaVersion) .withCodestart(withCodestart) .withoutUnitTest(withoutTests || withoutUnitTest) .withoutDevModeTest(withoutTests || withoutDevModeTest) diff --git a/devtools/maven/src/main/java/io/quarkus/maven/CreateProjectMojo.java b/devtools/maven/src/main/java/io/quarkus/maven/CreateProjectMojo.java index 254dded675ed4c..3025394fba8f48 100644 --- a/devtools/maven/src/main/java/io/quarkus/maven/CreateProjectMojo.java +++ b/devtools/maven/src/main/java/io/quarkus/maven/CreateProjectMojo.java @@ -110,6 +110,9 @@ public class CreateProjectMojo extends AbstractMojo { @Parameter(property = "platformVersion", required = false) private String bomVersion; + /** + * Version of Java used to build the project. + */ @Parameter(property = "javaVersion") private String javaVersion; diff --git a/docs/src/main/asciidoc/building-native-image.adoc b/docs/src/main/asciidoc/building-native-image.adoc index 96e4e1640cdb01..6e3712384afd57 100644 --- a/docs/src/main/asciidoc/building-native-image.adoc +++ b/docs/src/main/asciidoc/building-native-image.adoc @@ -532,7 +532,7 @@ The project generation has also provided a `Dockerfile.native` in the `src/main/ [source,dockerfile] ---- -FROM registry.access.redhat.com/ubi8/ubi-minimal:8.8 +FROM registry.access.redhat.com/ubi8/ubi-minimal:8.9 WORKDIR /work/ RUN chown 1001 /work \ && chmod "g+rwX" /work \ diff --git a/docs/src/main/asciidoc/config-reference.adoc b/docs/src/main/asciidoc/config-reference.adoc index e3c22718953a04..8d68f8462abdf3 100644 --- a/docs/src/main/asciidoc/config-reference.adoc +++ b/docs/src/main/asciidoc/config-reference.adoc @@ -79,17 +79,33 @@ nor `\_` with `_`: `FOO_BAR_0_` or `FOO_BAR_0__BAZ` [CAUTION] ==== -In some situations, looking up the exact property name is impossible. For instance, when looking -up a configuration that is part of a `Map`, and the property name contains a dynamic segment (the `Map` key). In this -case, Quarkus relies upon each source’s list of property names. These must be converted back to their most likely -dotted format for Environment Variables. +In some situations, looking up the exact property name is impossible. This is the case for configuration names that +contain user defined path segments. -By default, the underscore `_` of an Environment Variable name always maps to a dot `.`. If the property name contains -a dash or some other special character, that property name can be specified in another Config Source, with the expected -dotted format. It will provide additional information to perform a two-way conversion and match the property names. +Applying the conversion rules for Environment Variables names, `quarkus.datasource."datasource-name".jdbc.url` becomes +`QUARKUS_DATASOURCE\__DATASOURCE_NAME__JDBC_URL`. The configuration will work as expected if both properties are +available in the Config system. + +If only `QUARKUS_DATASOURCE\__DATASOURCE_NAME__JDBC_URL` is present, the Config system needs to reconvert the +configuration name to its most likely dotted format. This works fine for fixed configuration segments, but not for +names that contain dynamic segments. In this case, Quarkus is unable to determine if `DATASOURCE_NAME` should be +converted to `datasource.name` or `datasource-name` (or any other special character separator). + +For this reason, such properties always require their dotted version name in another source (the value can be left +empty) to disambiguate the Environment Variable name. It will provide additional information to perform a two-way +conversion and match the property names together. + +[source,properties] +---- +# value can be left empty +quarkus.datasource."datasource-name".jdbc.url= +---- + +[source,bash] +---- +EXPORT QUARKUS_DATASOURCE__DATASOURCE_NAME__JDBC_URL=jdbc:postgresql://localhost:5432/database +---- -To correctly lookup `Map` properties where `FOO_BAR_BAZ` is the property name and `BAR_BAZ` is the key, add -`foo.bar-baz` in a source with an ordinal lower than the EnvConfigSource (`300`). ==== @@ -101,7 +117,7 @@ To correctly lookup `Map` properties where `FOO_BAR_BAZ` is the property name an ---- QUARKUS_DATASOURCE_PASSWORD=youshallnotpass <1> ---- -<1> The name `QUARKUS_DATASOURCE_PASSWORD` the same conversion rules used for <>. +<1> The name `QUARKUS_DATASOURCE_PASSWORD` follows the same conversion rules used for <>. For `dev` mode, this file can be placed in the root of the project, but it is advised to **not** check it in to version control because it typically contains passwords, access tokens, API keys or other secrets. diff --git a/docs/src/main/asciidoc/config-yaml.adoc b/docs/src/main/asciidoc/config-yaml.adoc index 9a84dcfb9f5156..0b73e8106fd3e9 100644 --- a/docs/src/main/asciidoc/config-yaml.adoc +++ b/docs/src/main/asciidoc/config-yaml.adoc @@ -211,3 +211,45 @@ quarkus: ---- YAML `null` keys are not included in the assembly of the configuration property name, allowing them to be used at any level for disambiguating configuration keys. + +Although Quarkus primarily uses `.properties` file extension for configuration, the snakeyaml library, which is used for parsing YAML in Quarkus, can also parse JSON structures. This means you can use YAML files with JSON content inside. + +YAML and JSON structures can be read in an application.yaml file. + +Certainly, here's a step-by-step guide on how to use complex configuration structures with Quarkus: + +* Define Your Configuration Interface. + +[source,java] +---- +@ConfigMapping(prefix = "server") +public interface ServiceConfig { + + List environments(); + + interface Environment { + String name(); + String services(); + } +} +---- + +* Create the appropriate JSON structure and store it in a YAML file. + +[source,yaml] +---- +{ + "server": { + "environments": [ + { + "name": "dev", + "services": "bookstore" + }, + { + "name": "batch", + "services": "warehouse" + } + ] + } +} +---- diff --git a/docs/src/main/asciidoc/container-image.adoc b/docs/src/main/asciidoc/container-image.adoc index 953637e0a9159b..eb5e7c3ebff196 100644 --- a/docs/src/main/asciidoc/container-image.adoc +++ b/docs/src/main/asciidoc/container-image.adoc @@ -47,7 +47,7 @@ For example, the presence of `src/main/jib/foo/bar` would result in `/foo/bar` There are cases where the built container image may need to have Java debugging conditionally enabled at runtime. -When the base image has not been changed (and therefore `ubi8/openjdk-11-runtime` or `ubi8/openjdk-17-runtime` is used), then the `quarkus.jib.jvm-arguments` configuration property can be used in order to +When the base image has not been changed (and therefore `ubi8/openjdk-11-runtime`, `ubi8/openjdk-17-runtime`, or `ubi8/openjdk-21-runtime` is used), then the `quarkus.jib.jvm-arguments` configuration property can be used in order to make the JVM listen on the debug port at startup. The exact configuration is: @@ -63,7 +63,7 @@ Other base images might provide launch scripts that enable debugging when an env The `quarkus.jib.jvm-entrypoint` configuration property can be used to completely override the container entry point and can thus be used to either hard code the JVM debug configuration or point to a script that handles the details. -For example, if the base images `ubi8/openjdk-11-runtime` or `ubi8/openjdk-17-runtime` are used to build the container, the entry point can be hard-coded on the application properties file. +For example, if the base images `ubi8/openjdk-11-runtime`, `ubi8/openjdk-17-runtime` or `ubi8/openjdk-21-runtime` are used to build the container, the entry point can be hard-coded on the application properties file. .Example application.properties [source,properties] @@ -88,7 +88,7 @@ java \ -jar quarkus-run.jar ---- -NOTE: `/home/jboss` is the WORKDIR for all quarkus binaries in the base images `ubi8/openjdk-11-runtime` and `ubi8/openjdk-17-runtime` (https://catalog.redhat.com/software/containers/ubi8/openjdk-17/618bdbf34ae3739687568813?container-tabs=dockerfile[Dockerfile for ubi8/openjdk-17-runtime, window="_blank"]) +NOTE: `/home/jboss` is the WORKDIR for all quarkus binaries in the base images `ubi8/openjdk-11-runtime`, `ubi8/openjdk-17-runtime` and `ubi8/openjdk-21-runtime` (https://catalog.redhat.com/software/containers/ubi8/openjdk-17/618bdbf34ae3739687568813?container-tabs=dockerfile[Dockerfile for ubi8/openjdk-17-runtime, window="_blank"]) ==== Multi-module projects and layering diff --git a/docs/src/main/asciidoc/hibernate-orm.adoc b/docs/src/main/asciidoc/hibernate-orm.adoc index c76ac5894639bf..3439cbae1e4697 100644 --- a/docs/src/main/asciidoc/hibernate-orm.adoc +++ b/docs/src/main/asciidoc/hibernate-orm.adoc @@ -1242,3 +1242,82 @@ to tell Quarkus it should be used in the default persistence unit. + For <>, use `@PersistenceUnitExtension("nameOfYourPU")` <2> Implement `org.hibernate.engine.jdbc.spi.StatementInspector`. + +[[json_xml_serialization_deserialization]] +== Customizing JSON/XML serialization/deserialization + +By default, Quarkus will try to automatically configure format mappers depending on available extensions. +Globally configured `ObjectMapper` (or `Jsonb`) will be used for serialization/deserialization operations when Jackson (or JSON-B) is available. +Jackson will take precedence if both Jackson and JSON-B are available at the same time. + +JSON and XML serialization/deserialization in Hibernate ORM can be customized by implementing a `org.hibernate.type.format.FormatMapper` +and annotating the implementation with the appropriate qualifiers: + +[source,java] +---- +@JsonFormat // <1> +@PersistenceUnitExtension // <2> +public class MyJsonFormatMapper implements FormatMapper { // <3> + @Override + public String inspect(String sql) { + // ... + return sql; + } + @Override + public T fromString(CharSequence charSequence, JavaType javaType, WrapperOptions wrapperOptions) { + // ... + } + + @Override + public String toString(T value, JavaType javaType, WrapperOptions wrapperOptions) { + // ... + } +} +---- +<1> Annotate the format mapper implementation with the `@JsonFormat` qualifier +to tell Quarkus that this mapper is specific to JSON serialization/deserialization. ++ +<2> Annotate the format mapper implementation with the `@PersistenceUnitExtension` qualifier +to tell Quarkus it should be used in the default persistence unit. ++ +For <>, use `@PersistenceUnitExtension("nameOfYourPU")` +<3> Implement `org.hibernate.type.format.FormatMapper`. + +In case of a custom XML format mapper, a different CDI qualifier must be applied: + +[source,java] +---- +@XmlFormat // <1> +@PersistenceUnitExtension // <2> +public class MyJsonFormatMapper implements FormatMapper { // <3> + @Override + public String inspect(String sql) { + // ... + return sql; + } + @Override + public T fromString(CharSequence charSequence, JavaType javaType, WrapperOptions wrapperOptions) { + // ... + } + + @Override + public String toString(T value, JavaType javaType, WrapperOptions wrapperOptions) { + // ... + } +} +---- +<1> Annotate the format mapper implementation with the `@XmlFormat` qualifier +to tell Quarkus that this mapper is specific to XML serialization/deserialization. ++ +<2> Annotate the format mapper implementation with the `@PersistenceUnitExtension` qualifier +to tell Quarkus it should be used in the default persistence unit. ++ +For <>, use `@PersistenceUnitExtension("nameOfYourPU")` +<3> Implement `org.hibernate.type.format.FormatMapper`. + +[NOTE] +==== +Format mappers *must* have both `@PersistenceUnitExtension` and either `@JsonFormat` or `@XmlFormat` CDI qualifiers applied. + +Having multiple JSON (or XML) format mappers registered for the same persistence unit will result in an exception, because of the ambiguity. +==== diff --git a/docs/src/main/asciidoc/kotlin.adoc b/docs/src/main/asciidoc/kotlin.adoc index ef117f0c8d0fb5..0d7bc89658eb4c 100644 --- a/docs/src/main/asciidoc/kotlin.adoc +++ b/docs/src/main/asciidoc/kotlin.adoc @@ -470,9 +470,9 @@ import io.fabric8.kubernetes.client.utils.Serialization import com.fasterxml.jackson.module.kotlin.KotlinModule ... - -Serialization.jsonMapper().registerModule(KotlinModule()) -Serialization.yamlMapper().registerModule(KotlinModule()) +val kotlinModule = KotlinModule.Builder().build() +Serialization.jsonMapper().registerModule(kotlinModule) +Serialization.yamlMapper().registerModule(kotlinModule) ---- _Please test this carefully on compilation to native images and fallback to Java-compatible Jackson bindings if you experience problems._ diff --git a/docs/src/main/asciidoc/quarkus-runtime-base-image.adoc b/docs/src/main/asciidoc/quarkus-runtime-base-image.adoc index 9d94a898fc410a..ba9f0ff6c8b9ab 100644 --- a/docs/src/main/asciidoc/quarkus-runtime-base-image.adoc +++ b/docs/src/main/asciidoc/quarkus-runtime-base-image.adoc @@ -39,7 +39,7 @@ In this case, you need to use a multi-stage `dockerfile` to copy the required li [source, dockerfile] ---- # First stage - install the dependencies in an intermediate container -FROM registry.access.redhat.com/ubi8/ubi-minimal:8.8 as BUILD +FROM registry.access.redhat.com/ubi8/ubi-minimal:8.9 as BUILD RUN microdnf install freetype # Second stage - copy the dependencies @@ -62,7 +62,7 @@ If you need to have access to the full AWT support, you need more than just `lib [source, dockerfile] ---- # First stage - install the dependencies in an intermediate container -FROM registry.access.redhat.com/ubi8/ubi-minimal:8.8 as BUILD +FROM registry.access.redhat.com/ubi8/ubi-minimal:8.9 as BUILD RUN microdnf install freetype fontconfig # Second stage - copy the dependencies @@ -112,7 +112,7 @@ To use this base image, use the following `Dockerfile`: [source, dockerfile] ---- -FROM registry.access.redhat.com/ubi8/ubi-minimal:8.8 +FROM registry.access.redhat.com/ubi8/ubi-minimal:8.9 WORKDIR /work/ RUN chown 1001 /work \ && chmod "g+rwX" /work \ diff --git a/docs/src/main/asciidoc/qute-reference.adoc b/docs/src/main/asciidoc/qute-reference.adoc index d25cfa49e0de51..e978cc97db37a0 100644 --- a/docs/src/main/asciidoc/qute-reference.adoc +++ b/docs/src/main/asciidoc/qute-reference.adoc @@ -2425,7 +2425,11 @@ TIP: Quarkus detects possible namespace collisions and fails the build if a spec === Global Variables The `io.quarkus.qute.TemplateGlobal` annotation can be used to denote static fields and methods that supply _global variables_ which are accessible in any template. -Internally, each global variable is added to the data map of any `TemplateInstance` via the `TemplateInstance#data(String, Object)` method. + +Global variables are: + +* added to the data map of any `TemplateInstance` during initialization, +* accessible with the `global:` namespace. .Global Variables Definition [source,java] @@ -2454,11 +2458,11 @@ public class Globals { [source,html] ---- User: {currentUser} <1> -Age: {age} <2> +Age: {global:age} <2> Colors: {#each myColors}{it}{#if it_hasNext}, {/if}{/each} <3> ---- <1> `currentUser` resolves to `Globals#user()`. -<2> `age` resolves to `Globals#age`. +<2> The `global:` namespace is used; `age` resolves to `Globals#age`. <3> `myColors` resolves to `Globals#myColors()`. NOTE: Note that global variables implicitly add <> to all templates and so any expression that references a global variable is validated during build. @@ -2473,7 +2477,7 @@ Colors: RED, BLUE ==== Resolving Conflicts -Global variables may conflict with regular data objects. +If not accessed via the `global:` namespace the global variables may conflict with regular data objects. <> override the global variables automatically. For example, the following definition overrides the global variable supplied by the `Globals#user()` method: diff --git a/docs/src/main/asciidoc/reactive-event-bus.adoc b/docs/src/main/asciidoc/reactive-event-bus.adoc index b1c03416d0770d..d0c2f3bc2ab52a 100644 --- a/docs/src/main/asciidoc/reactive-event-bus.adoc +++ b/docs/src/main/asciidoc/reactive-event-bus.adoc @@ -6,6 +6,7 @@ https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc = Using the event bus include::_attributes.adoc[] :categories: messaging +:keywords: vertx vert.x :summary: This guide explains how different beans can interact using the event bus. :topics: messaging,event-bus,vert.x :extensions: io.quarkus:quarkus-vertx diff --git a/docs/src/main/asciidoc/redis-reference.adoc b/docs/src/main/asciidoc/redis-reference.adoc index 940a6ca793280c..61f5d47becac09 100644 --- a/docs/src/main/asciidoc/redis-reference.adoc +++ b/docs/src/main/asciidoc/redis-reference.adoc @@ -221,6 +221,24 @@ We recommend the latter, and if possible, using secrets or an environment variab The associated environment variable is `QUARKUS_REDIS_PASSWORD`, or `QUARKUS_REDIS__PASSWORD` for named clients. +=== Connection pooling + +Connections to Redis are always pooled. +By default, maximum number of connections in the pool is 6. +This can be configured using `quarkus.redis.max-pool-size`. + +When the connection pool is depleted, attempts to obtain a connection are put into a queue. +By default, maximum number of attempts waiting in the queue to obtain a Redis connection is 24. +This can be configured using `quarkus.redis.max-pool-waiting`. + +Executing certain commands modifies the server-side state and the behavior of the connection. +Such connections cannot be reused and when closed, they are not put back into the pool; instead, they are truly closed. +The commands that cause this behavior are: + +* subscription commands (`SUBSCRIBE`, `UNSUBSCRIBE` etc.) +* `SELECT` +* `AUTH` + == Use Redis data sources Quarkus exposes a high-level API on top of Redis. diff --git a/docs/src/main/asciidoc/rest-client-reactive.adoc b/docs/src/main/asciidoc/rest-client-reactive.adoc index 4d1af8480e4d30..b3c063bccdda2c 100644 --- a/docs/src/main/asciidoc/rest-client-reactive.adoc +++ b/docs/src/main/asciidoc/rest-client-reactive.adoc @@ -883,6 +883,107 @@ If you use a `CompletionStage`, you would need to call the service's method to r This difference comes from the laziness aspect of Mutiny and its subscription protocol. More details about this can be found in https://smallrye.io/smallrye-mutiny/latest/reference/uni-and-multi/[the Mutiny documentation]. +=== Server-Sent Event (SSE) support + +Consuming SSE events is possible simply by declaring the result type as a `io.smallrye.mutiny.Multi`. + +The simplest example is: + +[source, java] +---- +package org.acme.rest.client; + +import io.smallrye.mutiny.Multi; +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +@Path("/sse") +@RegisterRestClient(configKey = "some-api") +public interface SseClient { + @GET + @Produces(MediaType.SERVER_SENT_EVENTS) + Multi get(); +} +---- + +[NOTE] +==== +All the IO involved in streaming the SSE results is done in a non-blocking manner. +==== + +Results are not limited to strings - for example when the server returns JSON payload for each event, Quarkus automatically deserializes it into the generic type used in the `Multi`. + +[TIP] +==== +Users can also access the entire SSE event by using the `org.jboss.resteasy.reactive.client.SseEvent` type. + +A simple example where the event payloads are `Long` values is the following: + +[source, java] +---- +package org.acme.rest.client; + +import io.smallrye.mutiny.Uni; +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; +import org.jboss.resteasy.reactive.client.SseEvent; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.QueryParam; + +@Path("/sse") +@RegisterRestClient(configKey = "some-api") +public interface SseClient { + @GET + @Produces(MediaType.SERVER_SENT_EVENTS) + Multi> get(); +} +---- +==== + +==== Filtering out events + +On occasion, the stream of SSE events may contain some events that should not be returned by the client - an example of this is having the server send heartbeat events in order to keep the underlying TCP connection open. +The REST Client supports filtering out such events by providing the `@org.jboss.resteasy.reactive.client.SseEventFilter`. + +Here is an example of filtering out heartbeat events: + +[source,java] +---- +package org.acme.rest.client; + +import io.smallrye.mutiny.Uni; +import java.util.function.Predicate; +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; +import org.jboss.resteasy.reactive.client.SseEvent; +import org.jboss.resteasy.reactive.client.SseEventFilter; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.QueryParam; + +@Path("/sse") +@RegisterRestClient(configKey = "some-api") +public interface SseClient { + + @GET + @Produces(MediaType.SERVER_SENT_EVENTS) + @SseEventFilter(HeartbeatFilter.class) + Multi> get(); + + + class HeartbeatFilter implements Predicate> { + + @Override + public boolean test(SseEvent event) { + return !"heartbeat".equals(event.id()); + } + } +} +---- + == Custom headers support There are a few ways in which you can specify custom headers for your REST calls: diff --git a/docs/src/main/asciidoc/resteasy-reactive.adoc b/docs/src/main/asciidoc/resteasy-reactive.adoc index 7d9792a76c145a..5a4afac21f3d0e 100644 --- a/docs/src/main/asciidoc/resteasy-reactive.adoc +++ b/docs/src/main/asciidoc/resteasy-reactive.adoc @@ -1301,6 +1301,8 @@ public class Person { private final Long id; private final String first; private final String last; + @SecureField(rolesAllowed = ${role:admin}") <1> + private String address; public Person(Long id, String first, String last) { this.id = id; @@ -1319,8 +1321,20 @@ public class Person { public String getLast() { return last; } + + public String getAddress() { + return address; + } + + public void setAddress(String address) { + this.address = address; + } } ---- +<1> The `io.quarkus.resteasy.reactive.jackson.SecureField.rolesAllowed` property supports xref:config-reference.adoc#property-expressions[property expressions] +exactly in the same fashion the `jakarta.annotation.security.RolesAllowed` annotation does. For more information, please +refer to the xref:security-authorize-web-endpoints-reference.adoc#standard-security-annotations[Standard security annotations] +section of the Authorization of web endpoints guide. A very simple Jakarta REST Resource that uses `Person` could be: @@ -1337,7 +1351,7 @@ public class Person { @Path("{id}") @GET public Person getPerson(Long id) { - return new Person(id, "foo", "bar"); + return new Person(id, "foo", "bar", "Brick Lane"); } } ---- @@ -1350,7 +1364,8 @@ performs an HTTP GET on `/person/1` they will receive: { "id": 1, "first": "foo", - "last": "bar" + "last": "bar", + "address", "Brick Lane" } ---- @@ -1369,6 +1384,8 @@ Any user however that does not have the `admin` role will receive: NOTE: No additional configuration needs to be applied for this secure serialization to take place. However, users can use the `@io.quarkus.resteasy.reactive.jackson.EnableSecureSerialization` and `@io.quarkus.resteasy.reactive.jackson.DisableSecureSerialization` annotation to opt in or out for specific Jakarta REST Resource classes or methods. +WARNING: Configuration expressions set with the `SecureField.rolesAllowed` property are validated during application startup even when the `@io.quarkus.resteasy.reactive.jackson.DisableSecureSerialization` annotation is used. + ===== @JsonView support Jakarta REST methods can be annotated with https://fasterxml.github.io/jackson-annotations/javadoc/2.10/com/fasterxml/jackson/annotation/JsonView.html[@JsonView] @@ -2989,6 +3006,10 @@ public class RuntimeResource { } } ---- +[IMPORTANT] +==== +This feature does not work when using native build. +==== == RESTEasy Reactive client diff --git a/docs/src/main/asciidoc/security-architecture.adoc b/docs/src/main/asciidoc/security-architecture.adoc index 9ed77730be8342..4f0cd7267d4c03 100644 --- a/docs/src/main/asciidoc/security-architecture.adoc +++ b/docs/src/main/asciidoc/security-architecture.adoc @@ -57,7 +57,10 @@ For more information, see the xref:security-customization.adoc#security-identity == Supported authentication mechanisms -To learn more about security authentication in Quarkus and the supported mechanisms and protocols, see the Quarkus xref:security-authentication-mechanisms.adoc[Authentication mechanisms in Quarkus] guide. +The Quarkus Security framework supports multiple authentication mechanisms, which can also be combined. +Some supported authentication mechanisms are built into Quarkus, while others require you to add an extension. + +To learn about security authentication in Quarkus and the supported mechanisms and protocols, see the Quarkus xref:security-authentication-mechanisms.adoc[Authentication mechanisms in Quarkus] guide. == Proactive authentication @@ -67,7 +70,7 @@ For more information, see the Quarkus xref:security-proactive-authentication.ado == Quarkus Security customization -Quarkus Security is also highly customizable. +Quarkus Security is customizable. You can customize the following core security components of Quarkus: * `HttpAuthenticationMechanism` 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 41a9d39bf3feda..698225dd0f3a31 100644 --- a/docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc +++ b/docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc @@ -68,6 +68,8 @@ It is an exact path match because it does not end with `*`. <3> This permission set references the previously defined policy. `roles1` is an example name; you can call the permission sets whatever you want. +WARNING: The `/forbidden` exact path in the example above will not secure the `/forbidden/` path. Don't forget to add new exact path for the `/forbidden/` path. + === Custom HttpSecurityPolicy Sometimes it might be useful to register your own named policy. You can get it done by creating application scoped CDI @@ -123,10 +125,12 @@ Otherwise, it queries for an exact match and only matches that specific path: [source,properties] ---- -quarkus.http.auth.permission.permit1.paths=/public/*,/css/*,/js/*,/robots.txt +quarkus.http.auth.permission.permit1.paths=/public*,/css/*,/js/*,/robots.txt <1> quarkus.http.auth.permission.permit1.policy=permit quarkus.http.auth.permission.permit1.methods=GET,HEAD ---- +<1> The `$$*$$` wildcard at the end of the path matches zero or more path segments, but never any word starting from the `/public` path. +For that reason, a path like `/public-info` is not matched by this pattern. === Matching a path but not a method @@ -170,6 +174,59 @@ quarkus.http.auth.permission.public.policy=permit ---- ==== +=== Matching multiple sub-paths: longest path to the `*` wildcard wins + +Previous examples shown how you can match all sub-paths when a path ends with the `$$*$$` wildcard. +The `$$*$$` wildcard can also be used in the middle of the path, in which case it represents exactly one path segment. +You can't combine this wildcard with any other path segment character, therefore the `$$*$$` wildcard will always be +enclosed with path separators as in the `/public/$$*$$/about-us` path. + +What happens if multiple path patterns matches same request path? +Matching is always done on the "longest sub-path to the `$$*$$` wildcard wins" basis. +Every path segment character is considered more specific than the `$$*$$` wildcard. + +Here is a simple example: + +[source,properties] +---- +quarkus.http.auth.permission.secured.paths=/api/*/detail <1> +quarkus.http.auth.permission.secured.policy=authenticated +quarkus.http.auth.permission.public.paths=/api/public-product/detail <2> +quarkus.http.auth.permission.public.policy=permit +---- +<1> Request paths like `/api/product/detail` can only be accessed by authenticated users. +<2> The path `/api/public-product/detail` is more specific, therefore accessible by anyone. + +[IMPORTANT] +==== +All paths secured with the authorization using configuration should be tested. +Writing path patterns with multiple wildcards can be cumbersome. +Please make sure paths are authorized as you intended. +==== + +In the following example, paths are ordered from the most specific to the least specific one: + +.Request path `/one/two/three/four/five` matches ordered from the most specific to the least specific path + +[source, text] +---- +/one/two/three/four/five +/one/two/three/four/* +/one/two/three/*/five +/one/two/three/*/* +/one/two/*/four/five +/one/*/three/four/five +/*/two/three/four/five +/*/two/three/*/five +/* +---- + +[IMPORTANT] +==== +The `$$*$$` wildcard at the end of the path matches zero or more path segments. +The `$$*$$` wildcard placed anywhere else matches exactly one path segment. +==== + === Matching multiple paths: most specific method wins When a path is registered with multiple permission sets, the permission sets explicitly specifying an HTTP method that matches the request take precedence. diff --git a/docs/src/main/asciidoc/security-basic-authentication-howto.adoc b/docs/src/main/asciidoc/security-basic-authentication-howto.adoc index 6477a57b342bb5..331874887dc40e 100644 --- a/docs/src/main/asciidoc/security-basic-authentication-howto.adoc +++ b/docs/src/main/asciidoc/security-basic-authentication-howto.adoc @@ -15,23 +15,39 @@ Enable xref:security-basic-authentication.adoc[Basic authentication] for your Qu == Prerequisites -* You have installed at least one extension that provides an `IdentityProvider` based on username and password, such as xref:security-jdbc.adoc[Elytron JDBC]. +* 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`)] +** 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)`] + +The following procedure outlines how you can enable Basic authentication for your application by using the `elytron-security-properties-file` extension. == Procedure -. Enable Basic authentication by setting the `quarkus.http.auth.basic` property to `true`. +. In the `application.properties` file, set the `quarkus.http.auth.basic` property to `true`. + [source,properties] ---- quarkus.http.auth.basic=true ---- -. For testing purposes, you can configure the required user credentials, user name, secret, and roles, in the `application.properties` file. +. **Optional:** In a non-production environment only and purely for testing Quarkus Security in your applications: +.. To enable authentication for the embedded realm, set the `quarkus.security.users.embedded.enabled` property to `true`. ++ +[source,properties] +---- +security.users.embedded.enabled=true +---- + +.. You can also configure the required user credentials, user name, secret, and roles. For example: + [source,properties] ---- quarkus.http.auth.basic=true +quarkus.security.users.embedded.enabled=true quarkus.security.users.embedded.plain-text=true quarkus.security.users.embedded.users.alice=alice <1> quarkus.security.users.embedded.users.bob=bob <2> diff --git a/docs/src/main/asciidoc/security-overview.adoc b/docs/src/main/asciidoc/security-overview.adoc index 9e26731e6d328a..066c08635b3ed1 100644 --- a/docs/src/main/asciidoc/security-overview.adoc +++ b/docs/src/main/asciidoc/security-overview.adoc @@ -45,7 +45,7 @@ To get started with security in Quarkus, consider securing your Quarkus applicat Complete the steps in the xref:security-getting-started-tutorial.adoc[Getting Started with Security using Basic authentication and Jakarta Persistence] tutorial. -After successfully securing your Quarkus application with Basic authentication, you can increase the security further by adding more advanced authentication mechanisms, for example, the xref:security-oidc-code-flow-authentication.adoc[OpenID Connect (OIDC) authorization code flow mechanism]. +After successfully securing your Quarkus application with Basic authentication, you can increase the security further by adding more advanced authentication mechanisms, for example, the Quarkus xref:security-oidc-code-flow-authentication.adoc[OpenID Connect (OIDC) authorization code flow mechanism] guide. == Quarkus Security testing @@ -57,7 +57,7 @@ Guidance for testing Quarkus Security features and ensuring that your Quarkus ap === Cross-origin resource sharing To make your Quarkus application accessible to another application running on a different domain, you need to configure cross-origin resource sharing (CORS). -For more information about the CORS filter that Quarkus provides, see the Quarkus xref:security-cors.adoc#cors-filter[CORS filter] section of the "Cross-origin resource sharing" guide. +For more information about the CORS filter that Quarkus provides, see the xref:security-cors.adoc#cors-filter[CORS filter] section of the Quarkus "Cross-origin resource sharing" guide. [[csrf-prevention]] === Cross-Site Request Forgery (CSRF) prevention @@ -69,31 +69,31 @@ For more information, see the Quarkus xref:security-csrf-prevention.adoc[Cross-S === SameSite cookies You can add a link:https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite[SameSite] cookie property to any of the cookies set by a Quarkus endpoint. -For more information, see the Quarkus xref:http-reference.adoc#same-site-cookie[SameSite cookies] guide. +For more information, see the xref:http-reference.adoc#same-site-cookie[SameSite cookies] section of the Quarkus "HTTP reference" guide. [[secrets-engines]] === Secrets engines -Secrets engines are components that store, generate, or encrypt data. +You can use secrets engines with Quarkus to store, generate, or encrypt data. -Quarkus provides comprehensive HashiCorp Vault support. -For more information, see the link:{vault-guide}[Quarkus and HashiCorp Vault] documentation. +Quarkus provides additional extensions in Quarkiverse for securely storing credentials, for example, link:{vault-guide}[Quarkus and HashiCorp Vault]. == Secrets in environment properties Quarkus provides support to store secrets in environment properties. -See xref:config.adoc#secrets-in-environment-properties[store secrets in an environment properties file]. +For more information, see the Quarkus xref:config.adoc#secrets-in-environment-properties[store secrets in an environment properties file] guide. [[secure-serialization]] === Secure serialization If your Quarkus Security architecture includes RESTEasy Reactive and Jackson, Quarkus can limit the fields that are included in JSON serialization based on the configured security. -For more information, see the Quarkus xref:resteasy-reactive.adoc#secure-serialization[Writing REST services with RESTEasy Reactive] guide. +For more information, see the xref:resteasy-reactive.adoc#secure-serialization[JSON serialisation] section of the Quarkus “Writing REST services with RESTEasy Reactive” guide. + [[rest-data-panache]] === Secure auto-generated resources by REST Data with Panache If you use the REST Data with Panache extension to auto-generate your resources, you can still use security annotations within the package `jakarta.annotation.security`. -For more information, see the xref:rest-data-panache.adoc#securing-endpoints[Securing auto-generated resources] section of the Quarkus "Generating Jakarta REST resources with Panache" guide. +For more information, see the xref:rest-data-panache.adoc#securing-endpoints[Securing endpoints] section of the Quarkus "Generating Jakarta REST resources with Panache" guide. == Security vulnerability detection @@ -102,6 +102,7 @@ For information about security vulnerabilities, see the xref:security-vulnerabil == References +* xref:security-basic-authentication.adoc[Basic authentication] * xref:security-getting-started-tutorial.adoc[Getting Started with Security using Basic authentication and Jakarta Persistence] * xref:security-oidc-code-flow-authentication-tutorial.adoc[Protect a web application by using OIDC authorization code flow] * xref:security-oidc-bearer-token-authentication-tutorial.adoc[Protect a service application by using OIDC Bearer token authentication] diff --git a/docs/src/main/asciidoc/update-quarkus.adoc b/docs/src/main/asciidoc/update-quarkus.adoc index 8b355b27f1f1c1..0d6fc592957eba 100644 --- a/docs/src/main/asciidoc/update-quarkus.adoc +++ b/docs/src/main/asciidoc/update-quarkus.adoc @@ -50,7 +50,7 @@ Confirm the version number using `quarkus -v`. ---- quarkus update ---- -Optional: To specify a particular stream, use the `stream` option; for example: `--stream=3.0` +Optional: To specify a particular stream, use the `--stream` option; for example: `--stream=3.2` ifndef::devtools-no-maven[] ifdef::devtools-wrapped[+] **** @@ -63,7 +63,7 @@ ifdef::devtools-wrapped[+] ---- ./mvnw {quarkus-platform-groupid}:quarkus-maven-plugin:{quarkus-version}:update -N ---- -Optional: To specify a particular stream, use the `Dstream` option; for example: `-Dstream=3.0` +Optional: To specify a particular stream, use the `-Dstream` option; for example: `-Dstream=3.2` endif::[] ifndef::devtools-no-gradle[] ifdef::devtools-wrapped[+] diff --git a/docs/src/main/asciidoc/vertx-reference.adoc b/docs/src/main/asciidoc/vertx-reference.adoc index eed2eadb4d316f..5802a913f61dd3 100644 --- a/docs/src/main/asciidoc/vertx-reference.adoc +++ b/docs/src/main/asciidoc/vertx-reference.adoc @@ -6,6 +6,7 @@ https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc = Vert.x Reference Guide include::_attributes.adoc[] :categories: miscellaneous +:keywords: vertx event verticle :summary: This reference guide provides advanced details about the usage and the configuration of the Vert.x instance used by Quarkus. https://vertx.io[Vert.x] is a toolkit for building reactive applications. @@ -515,6 +516,22 @@ public String consume(String name) { ---- <1> Receive the messages sent to the `greeting` address +The address value can be a property expression. +In this case, the configured value is used instead: `@ConsumeEvent("${my.consumer.address}")`. +Additionally, the property expression can specify a default value: `@ConsumeEvent("${my.consumer.address:defaultAddress}")`. + +.Config Property Example +[source, java] +---- +@ConsumeEvent("${my.consumer.address}") // <1> +public String consume(String name) { + return name.toLowerCase(); +} +---- +<1> Receive the messages sent to the address configured with the `my.consumer.address` key. + +NOTE: If no config property with the specified key exists and no default value is set then the application startup fails. + === Process events asynchronously The previous examples use synchronous processing. diff --git a/docs/src/main/asciidoc/vertx.adoc b/docs/src/main/asciidoc/vertx.adoc index 09fb4a2a4b7067..c5bbb73681ebd6 100644 --- a/docs/src/main/asciidoc/vertx.adoc +++ b/docs/src/main/asciidoc/vertx.adoc @@ -6,6 +6,7 @@ https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc = Using Eclipse Vert.x API from a Quarkus Application include::_attributes.adoc[] :categories: miscellaneous +:keywords: vertx event verticle :summary: This guide explains how to use Vert.x in Quarkus to build reactive applications. https://vertx.io[Vert.x] is a toolkit for building reactive applications. diff --git a/docs/src/main/asciidoc/virtual-threads.adoc b/docs/src/main/asciidoc/virtual-threads.adoc index eba2cde1575509..2ee3545979f746 100644 --- a/docs/src/main/asciidoc/virtual-threads.adoc +++ b/docs/src/main/asciidoc/virtual-threads.adoc @@ -349,10 +349,10 @@ To containerize your Quarkus application that use `@RunOnVirtualThread`, add the quarkus.container-image.build=true quarkus.container-image.group= quarkus.container-image.name= -quarkus.jib.base-jvm-image=eclipse-temurin:21-ubi9-minimal <1> +quarkus.jib.base-jvm-image=registry.access.redhat.com/ubi8/openjdk-21-runtime <1> quarkus.jib.platforms=linux/amd64,linux/arm64 <2> ---- -<1> Make sure you use a base image supporting virtual threads. Here we use an image providing Java 21. +<1> Make sure you use a base image supporting virtual threads. Here we use an image providing Java 21. Quarkus picks an image providing Java 21+ automatically if you do not set one. <2> Select the target architecture. You can select more than one to build multi-archs images. Then, build your container as you would do usually. diff --git a/extensions/avro/deployment/src/main/java/io/quarkus/avro/deployment/AvroCodeGenProviderBase.java b/extensions/avro/deployment/src/main/java/io/quarkus/avro/deployment/AvroCodeGenProviderBase.java index b759abf4bf1977..b9f29f2a61fb84 100644 --- a/extensions/avro/deployment/src/main/java/io/quarkus/avro/deployment/AvroCodeGenProviderBase.java +++ b/extensions/avro/deployment/src/main/java/io/quarkus/avro/deployment/AvroCodeGenProviderBase.java @@ -4,6 +4,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashSet; @@ -11,6 +12,7 @@ import java.util.Locale; import java.util.Set; import java.util.stream.Collectors; +import java.util.stream.Stream; import org.apache.avro.generic.GenericData; import org.eclipse.microprofile.config.Config; @@ -86,7 +88,8 @@ private Collection gatherAllFiles(Path importPath) throws CodeGenException } try { return Files.find(importPath, 20, - (path, ignored) -> Files.isRegularFile(path) && path.toString().endsWith("." + inputExtension())) + (path, ignored) -> Files.isRegularFile(path) + && Arrays.stream(inputExtensions()).anyMatch(ext -> path.toString().endsWith("." + ext))) .map(Path::toAbsolutePath) .collect(Collectors.toList()); } catch (IOException e) { @@ -194,9 +197,11 @@ private boolean getBooleanProperty(String propName, boolean defaultValue) { } public String[] getImports(Config config) { - return config.getOptionalValue("avro.codegen." + inputExtension() + ".imports", String.class) - .map(i -> i.split(",")) - .orElse(EMPTY); + return Arrays.stream(inputExtensions()) + .flatMap(ext -> config.getOptionalValue("avro.codegen." + ext + ".imports", String.class) + .map(i -> Arrays.stream(i.split(","))).stream()) + .reduce(Stream.empty(), Stream::concat) + .toArray(String[]::new); } @Override diff --git a/extensions/avro/deployment/src/main/java/io/quarkus/avro/deployment/AvroIDLCodeGenProvider.java b/extensions/avro/deployment/src/main/java/io/quarkus/avro/deployment/AvroIDLCodeGenProvider.java index fd89cec14c1a9d..ccbc6ac761f232 100644 --- a/extensions/avro/deployment/src/main/java/io/quarkus/avro/deployment/AvroIDLCodeGenProvider.java +++ b/extensions/avro/deployment/src/main/java/io/quarkus/avro/deployment/AvroIDLCodeGenProvider.java @@ -18,8 +18,8 @@ public String providerId() { } @Override - public String inputExtension() { - return "avdl"; + public String[] inputExtensions() { + return new String[] { "avdl" }; } @Override diff --git a/extensions/avro/deployment/src/main/java/io/quarkus/avro/deployment/AvroProtocolCodeGenProvider.java b/extensions/avro/deployment/src/main/java/io/quarkus/avro/deployment/AvroProtocolCodeGenProvider.java index 41e414648e4aed..c7df5be5c07884 100644 --- a/extensions/avro/deployment/src/main/java/io/quarkus/avro/deployment/AvroProtocolCodeGenProvider.java +++ b/extensions/avro/deployment/src/main/java/io/quarkus/avro/deployment/AvroProtocolCodeGenProvider.java @@ -22,8 +22,8 @@ public String providerId() { } @Override - public String inputExtension() { - return "avpr"; + public String[] inputExtensions() { + return new String[] { "avpr" }; } @Override diff --git a/extensions/avro/deployment/src/main/java/io/quarkus/avro/deployment/AvroSchemaCodeGenProvider.java b/extensions/avro/deployment/src/main/java/io/quarkus/avro/deployment/AvroSchemaCodeGenProvider.java index 505c865fae661d..2ceaf8d038dede 100644 --- a/extensions/avro/deployment/src/main/java/io/quarkus/avro/deployment/AvroSchemaCodeGenProvider.java +++ b/extensions/avro/deployment/src/main/java/io/quarkus/avro/deployment/AvroSchemaCodeGenProvider.java @@ -26,8 +26,8 @@ public String providerId() { } @Override - public String inputExtension() { - return "avsc"; + public String[] inputExtensions() { + return new String[] { "avsc" }; } void init() { diff --git a/extensions/cache/deployment/src/test/java/io/quarkus/cache/test/runtime/DuplicatedContextHandlingTest.java b/extensions/cache/deployment/src/test/java/io/quarkus/cache/test/runtime/DuplicatedContextHandlingTest.java new file mode 100644 index 00000000000000..32229c90c02edf --- /dev/null +++ b/extensions/cache/deployment/src/test/java/io/quarkus/cache/test/runtime/DuplicatedContextHandlingTest.java @@ -0,0 +1,178 @@ +package io.quarkus.cache.test.runtime; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.context.control.ActivateRequestContext; +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.cache.CacheResult; +import io.quarkus.test.QuarkusUnitTest; +import io.smallrye.mutiny.Uni; +import io.vertx.core.Context; +import io.vertx.core.Vertx; +import io.vertx.core.impl.ContextInternal; + +public class DuplicatedContextHandlingTest { + + @RegisterExtension + static final QuarkusUnitTest TEST = new QuarkusUnitTest().withApplicationRoot(jar -> jar + .addClass(CachedService.class)); + + @Inject + CachedService cachedService; + + @Inject + Vertx vertx; + + @Test + @ActivateRequestContext + void testDuplicatedContextHandlingWhenCalledFromNoContext() { + cachedService.direct(false).await().indefinitely(); + cachedService.direct(true).await().indefinitely(); + } + + @Test + @ActivateRequestContext + void testDuplicatedContextHandlingWhenCalledOnContext() throws InterruptedException { + ContextInternal context = (ContextInternal) vertx.getOrCreateContext(); + if (context.isDuplicate()) { + context = context.duplicate(); + } + + CountDownLatch latch = new CountDownLatch(1); + Context tmp = context; + context.runOnContext(x -> { + cachedService.direct(false) + .invoke(() -> { + if (!tmp.equals(Vertx.currentContext())) { + throw new AssertionError("Expected to go back on the caller context"); + } + }) + .subscribe().with(y -> latch.countDown()); + }); + Assertions.assertTrue(latch.await(1, TimeUnit.SECONDS)); + + CountDownLatch latch2 = new CountDownLatch(1); + context.runOnContext(x -> { + cachedService.direct(true) + .invoke(() -> { + if (!tmp.equals(Vertx.currentContext())) { + throw new AssertionError("Expected to go back on the caller context"); + } + }) + .subscribe().with(y -> latch2.countDown()); + }); + Assertions.assertTrue(latch2.await(1, TimeUnit.SECONDS)); + + CountDownLatch latch3 = new CountDownLatch(1); + context.runOnContext(x -> { + cachedService.direct(false) + .invoke(() -> { + if (!tmp.equals(Vertx.currentContext())) { + throw new AssertionError("Expected to go back on the caller context"); + } + }) + .subscribe().with(y -> latch3.countDown()); + }); + Assertions.assertTrue(latch3.await(1, TimeUnit.SECONDS)); + + } + + @Test + @ActivateRequestContext + void testDuplicatedContextHandlingWhenCalledOnDifferentContexts() throws InterruptedException { + ContextInternal context = (ContextInternal) vertx.getOrCreateContext(); + context = context.duplicate(); + var context2 = context.duplicate(); + + CountDownLatch latch = new CountDownLatch(1); + Context tmp = context; + context.runOnContext(x -> { + cachedService.direct(false) + .invoke(() -> { + if (!tmp.equals(Vertx.currentContext())) { + throw new AssertionError("Expected to go back on the caller context"); + } + }) + .subscribe().with(y -> latch.countDown()); + }); + Assertions.assertTrue(latch.await(1, TimeUnit.SECONDS)); + + CountDownLatch latch2 = new CountDownLatch(1); + context2.runOnContext(x -> { + cachedService.direct(false) + .invoke(() -> { + if (!context2.equals(Vertx.currentContext())) { + throw new AssertionError("Expected to go back on the caller context"); + } + }) + .subscribe().with(y -> latch2.countDown()); + }); + Assertions.assertTrue(latch2.await(1, TimeUnit.SECONDS)); + } + + @Test + @ActivateRequestContext + void testDuplicatedContextHandlingWhenCalledContextAndAnsweredFromAnotherContext() throws InterruptedException { + ContextInternal context = (ContextInternal) vertx.getOrCreateContext(); + context = context.duplicate(); + var context2 = context.duplicate(); + + CountDownLatch latch = new CountDownLatch(1); + Context tmp = context; + context.runOnContext(x -> { + cachedService.directOnAnotherContext(false) + .invoke(() -> { + if (!tmp.equals(Vertx.currentContext())) { + throw new AssertionError("Expected to go back on the caller context"); + } + }) + .subscribe().with(y -> latch.countDown()); + }); + Assertions.assertTrue(latch.await(1, TimeUnit.SECONDS)); + + CountDownLatch latch2 = new CountDownLatch(1); + context2.runOnContext(x -> { + cachedService.directOnAnotherContext(false) + .invoke(() -> { + if (!context2.equals(Vertx.currentContext())) { + throw new AssertionError("Expected to go back on the caller context"); + } + }) + .subscribe().with(y -> latch2.countDown()); + }); + Assertions.assertTrue(latch2.await(1, TimeUnit.SECONDS)); + } + + @ApplicationScoped + public static class CachedService { + + volatile boolean timedout = false; + + @CacheResult(cacheName = "duplicated-context-cache", lockTimeout = 100) + public Uni direct(boolean timeout) { + if (!timeout || timedout) { + return Uni.createFrom().item("foo"); + } + timedout = true; + return Uni.createFrom().nothing(); + } + + @CacheResult(cacheName = "duplicated-context-cache", lockTimeout = 100) + public Uni directOnAnotherContext(boolean timeout) { + if (!timeout || timedout) { + return Uni.createFrom().item("foo") + .emitOn(c -> ((ContextInternal) Vertx.currentContext().owner()).duplicate().runOnContext(x -> c.run())); + } + timedout = true; + return Uni.createFrom().nothing(); + } + } + +} diff --git a/extensions/container-image/container-image-docker/deployment/src/test/java/io/quarkus/container/image/docker/deployment/RedHatOpenJDKRuntimeBaseProviderTest.java b/extensions/container-image/container-image-docker/deployment/src/test/java/io/quarkus/container/image/docker/deployment/RedHatOpenJDKRuntimeBaseProviderTest.java index 88afd20df90e4b..6114615670116e 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/deployment/src/test/java/io/quarkus/container/image/docker/deployment/RedHatOpenJDKRuntimeBaseProviderTest.java @@ -16,7 +16,7 @@ void testImageWithJava11() { Path path = getPath("openjdk-11-runtime"); var result = sut.determine(path); assertThat(result).hasValueSatisfying(v -> { - assertThat(v.getBaseImage()).isEqualTo("registry.access.redhat.com/ubi8/openjdk-11-runtime:1.17"); + assertThat(v.getBaseImage()).isEqualTo("registry.access.redhat.com/ubi8/openjdk-11-runtime:1.18"); assertThat(v.getJavaVersion()).isEqualTo(11); }); } @@ -26,11 +26,21 @@ 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.17"); + assertThat(v.getBaseImage()).isEqualTo("registry.access.redhat.com/ubi8/openjdk-17-runtime:1.18"); assertThat(v.getJavaVersion()).isEqualTo(17); }); } + @Test + 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.18"); + assertThat(v.getJavaVersion()).isEqualTo(21); + }); + } + @Test void testUnhandled() { Path path = getPath("ubi-java11"); 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/deployment/src/test/java/io/quarkus/container/image/docker/deployment/UbiMinimalBaseProviderTest.java index de17e9675c3cad..29c266279ac38b 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/deployment/src/test/java/io/quarkus/container/image/docker/deployment/UbiMinimalBaseProviderTest.java @@ -16,7 +16,7 @@ void testImageWithJava11() { Path path = getPath("ubi-java11"); var result = sut.determine(path); assertThat(result).hasValueSatisfying(v -> { - assertThat(v.getBaseImage()).isEqualTo("registry.access.redhat.com/ubi8/ubi-minimal:8.8"); + assertThat(v.getBaseImage()).isEqualTo("registry.access.redhat.com/ubi8/ubi-minimal:8.9"); assertThat(v.getJavaVersion()).isEqualTo(11); }); } @@ -26,11 +26,21 @@ 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"); + assertThat(v.getBaseImage()).isEqualTo("registry.access.redhat.com/ubi8/ubi-minimal:8.9"); assertThat(v.getJavaVersion()).isEqualTo(17); }); } + @Test + 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); + }); + } + @Test void testUnhandled() { Path path = getPath("openjdk-11-runtime"); diff --git a/extensions/container-image/container-image-docker/deployment/src/test/resources/openjdk-11-runtime b/extensions/container-image/container-image-docker/deployment/src/test/resources/openjdk-11-runtime index abba4268c79280..eb3b9e643de4c9 100644 --- a/extensions/container-image/container-image-docker/deployment/src/test/resources/openjdk-11-runtime +++ b/extensions/container-image/container-image-docker/deployment/src/test/resources/openjdk-11-runtime @@ -1,4 +1,4 @@ -FROM registry.access.redhat.com/ubi8/openjdk-11-runtime:1.17 +FROM registry.access.redhat.com/ubi8/openjdk-11-runtime:1.18 ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en' diff --git a/extensions/container-image/container-image-docker/deployment/src/test/resources/openjdk-17-runtime b/extensions/container-image/container-image-docker/deployment/src/test/resources/openjdk-17-runtime index 4542ffb9d471fa..14f5d447dbd362 100644 --- a/extensions/container-image/container-image-docker/deployment/src/test/resources/openjdk-17-runtime +++ b/extensions/container-image/container-image-docker/deployment/src/test/resources/openjdk-17-runtime @@ -1,5 +1,5 @@ # Use Java 17 base image -FROM registry.access.redhat.com/ubi8/openjdk-17-runtime:1.17 +FROM registry.access.redhat.com/ubi8/openjdk-17-runtime:1.18 ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en' diff --git a/extensions/container-image/container-image-docker/deployment/src/test/resources/openjdk-21-runtime b/extensions/container-image/container-image-docker/deployment/src/test/resources/openjdk-21-runtime new file mode 100644 index 00000000000000..8d11343e7b78e6 --- /dev/null +++ b/extensions/container-image/container-image-docker/deployment/src/test/resources/openjdk-21-runtime @@ -0,0 +1,18 @@ +# Use Java 17 base image +FROM registry.access.redhat.com/ubi8/openjdk-21-runtime:1.18 + +ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en' + +# Append additional options to the java process, you can add -XshowSettings:vm to also display the heap size. +ENV JAVA_OPTS_APPEND="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager" + +# We make four distinct layers so if there are application changes the library layers can be re-used +COPY --chown=185 target/quarkus-app/lib/ /deployments/lib/ +COPY --chown=185 target/quarkus-app/*.jar /deployments/ +COPY --chown=185 target/quarkus-app/app/ /deployments/app/ +COPY --chown=185 target/quarkus-app/quarkus/ /deployments/quarkus/ + +EXPOSE 8080 +USER 185 + +ENTRYPOINT [ "java", "-jar", "/deployments/quarkus-run.jar" ] diff --git a/extensions/container-image/container-image-docker/deployment/src/test/resources/ubi-java11 b/extensions/container-image/container-image-docker/deployment/src/test/resources/ubi-java11 index 9ad8990cf8fb5a..64397357e06289 100644 --- a/extensions/container-image/container-image-docker/deployment/src/test/resources/ubi-java11 +++ b/extensions/container-image/container-image-docker/deployment/src/test/resources/ubi-java11 @@ -1,4 +1,4 @@ -FROM registry.access.redhat.com/ubi8/ubi-minimal:8.8 +FROM registry.access.redhat.com/ubi8/ubi-minimal:8.9 ARG JAVA_PACKAGE=java-11-openjdk-headless ARG RUN_JAVA_VERSION=1.3.8 diff --git a/extensions/container-image/container-image-docker/deployment/src/test/resources/ubi-java17 b/extensions/container-image/container-image-docker/deployment/src/test/resources/ubi-java17 index 5ae6e1e2f3ac4a..77d59a96bc997e 100644 --- a/extensions/container-image/container-image-docker/deployment/src/test/resources/ubi-java17 +++ b/extensions/container-image/container-image-docker/deployment/src/test/resources/ubi-java17 @@ -1,4 +1,4 @@ -FROM registry.access.redhat.com/ubi8/ubi-minimal +FROM registry.access.redhat.com/ubi8/ubi-minimal:8.9 ARG JAVA_PACKAGE=java-17-openjdk-headless ARG RUN_JAVA_VERSION=1.3.8 diff --git a/extensions/container-image/container-image-docker/deployment/src/test/resources/ubi-java21 b/extensions/container-image/container-image-docker/deployment/src/test/resources/ubi-java21 new file mode 100644 index 00000000000000..ee1e916b5d106a --- /dev/null +++ b/extensions/container-image/container-image-docker/deployment/src/test/resources/ubi-java21 @@ -0,0 +1,31 @@ +FROM registry.access.redhat.com/ubi8/ubi-minimal:8.9 + +ARG JAVA_PACKAGE=java-21-openjdk-headless +ARG RUN_JAVA_VERSION=1.3.8 +ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en' +# Install java and the run-java script +# Also set up permissions for user `1001` +RUN microdnf install curl ca-certificates ${JAVA_PACKAGE} \ + && microdnf update \ + && microdnf clean all \ + && mkdir /deployments \ + && chown 1001 /deployments \ + && chmod "g+rwX" /deployments \ + && chown 1001:root /deployments \ + && curl https://repo1.maven.org/maven2/io/fabric8/run-java-sh/${RUN_JAVA_VERSION}/run-java-sh-${RUN_JAVA_VERSION}-sh.sh -o /deployments/run-java.sh \ + && chown 1001 /deployments/run-java.sh \ + && chmod 540 /deployments/run-java.sh \ + && echo "securerandom.source=file:/dev/urandom" >> /etc/alternatives/jre/lib/security/java.security + +# Configure the JAVA_OPTIONS, you can add -XshowSettings:vm to also display the heap size. +ENV JAVA_OPTIONS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager" +# We make four distinct layers so if there are application changes the library layers can be re-used +COPY --chown=1001 target/quarkus-app/lib/ /deployments/lib/ +COPY --chown=1001 target/quarkus-app/*.jar /deployments/ +COPY --chown=1001 target/quarkus-app/app/ /deployments/app/ +COPY --chown=1001 target/quarkus-app/quarkus/ /deployments/quarkus/ + +EXPOSE 8080 +USER 1001 + +ENTRYPOINT [ "/deployments/run-java.sh" ] 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 53a676a68153af..b79d0303741fe8 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 @@ -16,9 +16,12 @@ public class ContainerImageJibConfig { /** * The base image to be used when a container image is being produced for the jar build. * - * When the application is built against Java 17 or higher, {@code registry.access.redhat.com/ubi8/openjdk-17-runtime:1.17} + * When the application is built against Java 21 or higher, {@code registry.access.redhat.com/ubi8/openjdk-21-runtime:1.18} * is used as the default. - * Otherwise {@code registry.access.redhat.com/ubi8/openjdk-11-runtime:1.17} is used as the default. + * When the application is built against Java 17 or higher (but less than 21), + * {@code registry.access.redhat.com/ubi8/openjdk-17-runtime:1.18} + * is used as the default. + * Otherwise {@code registry.access.redhat.com/ubi8/openjdk-11-runtime:1.18} is used as the default. */ @ConfigItem public Optional baseJvmImage; 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 d6ac1b57c58c46..c3cb77ddc4a164 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 @@ -92,8 +92,10 @@ public class JibProcessor { private static final IsClassPredicate IS_CLASS_PREDICATE = new IsClassPredicate(); private static final String BINARY_NAME_IN_CONTAINER = "application"; - private static final String JAVA_17_BASE_IMAGE = "registry.access.redhat.com/ubi8/openjdk-17-runtime:1.17"; - private static final String JAVA_11_BASE_IMAGE = "registry.access.redhat.com/ubi8/openjdk-11-runtime:1.17"; + private static final String JAVA_21_BASE_IMAGE = "registry.access.redhat.com/ubi8/openjdk-21-runtime:1.18"; + private static final String JAVA_17_BASE_IMAGE = "registry.access.redhat.com/ubi8/openjdk-17-runtime:1.18"; + private static final String JAVA_11_BASE_IMAGE = "registry.access.redhat.com/ubi8/openjdk-11-runtime:1.18"; + private static final String DEFAULT_BASE_IMAGE_USER = "185"; private static final String OPENTELEMETRY_CONTEXT_CONTEXT_STORAGE_PROVIDER_SYS_PROP = "io.opentelemetry.context.contextStorageProvider"; @@ -134,6 +136,9 @@ private String determineBaseJvmImage(ContainerImageJibConfig jibConfig, Compiled } var javaVersion = compiledJavaVersion.getJavaVersion(); + if (javaVersion.isJava21OrHigher() == CompiledJavaVersionBuildItem.JavaVersion.Status.TRUE) { + return JAVA_21_BASE_IMAGE; + } if (javaVersion.isJava17OrHigher() == CompiledJavaVersionBuildItem.JavaVersion.Status.TRUE) { return JAVA_17_BASE_IMAGE; } diff --git a/extensions/container-image/container-image-openshift/deployment/src/main/java/io/quarkus/container/image/openshift/deployment/ContainerImageOpenshiftConfig.java b/extensions/container-image/container-image-openshift/deployment/src/main/java/io/quarkus/container/image/openshift/deployment/ContainerImageOpenshiftConfig.java index 9636ba877651d2..f049d4a6692fed 100644 --- a/extensions/container-image/container-image-openshift/deployment/src/main/java/io/quarkus/container/image/openshift/deployment/ContainerImageOpenshiftConfig.java +++ b/extensions/container-image/container-image-openshift/deployment/src/main/java/io/quarkus/container/image/openshift/deployment/ContainerImageOpenshiftConfig.java @@ -15,8 +15,10 @@ @ConfigRoot(name = "openshift", phase = ConfigPhase.BUILD_TIME) public class ContainerImageOpenshiftConfig { - public static final String DEFAULT_BASE_JVM_JDK11_IMAGE = "registry.access.redhat.com/ubi8/openjdk-11:1.17"; - public static final String DEFAULT_BASE_JVM_JDK17_IMAGE = "registry.access.redhat.com/ubi8/openjdk-17:1.17"; + public static final String DEFAULT_BASE_JVM_JDK11_IMAGE = "registry.access.redhat.com/ubi8/openjdk-11:1.18"; + public static final String DEFAULT_BASE_JVM_JDK17_IMAGE = "registry.access.redhat.com/ubi8/openjdk-17:1.18"; + public static final String DEFAULT_BASE_JVM_JDK21_IMAGE = "registry.access.redhat.com/ubi8/openjdk-21:1.18"; + public static final String DEFAULT_BASE_NATIVE_IMAGE = "quay.io/quarkus/ubi-quarkus-native-binary-s2i:2.0"; public static final String DEFAULT_NATIVE_TARGET_FILENAME = "application"; @@ -29,11 +31,12 @@ public class ContainerImageOpenshiftConfig { public static final String FALLBACK_NATIVE_BINARY_DIRECTORY = "/home/quarkus/"; public static String getDefaultJvmImage(CompiledJavaVersionBuildItem.JavaVersion version) { - switch (version.isJava17OrHigher()) { - case TRUE: - return DEFAULT_BASE_JVM_JDK17_IMAGE; - default: - return DEFAULT_BASE_JVM_JDK11_IMAGE; + if (version.isJava21OrHigher() == CompiledJavaVersionBuildItem.JavaVersion.Status.TRUE) { + return DEFAULT_BASE_JVM_JDK21_IMAGE; + } else if (version.isJava17OrHigher() == CompiledJavaVersionBuildItem.JavaVersion.Status.TRUE) { + return DEFAULT_BASE_JVM_JDK17_IMAGE; + } else { + return DEFAULT_BASE_JVM_JDK11_IMAGE; } } @@ -48,9 +51,11 @@ public static String getDefaultJvmImage(CompiledJavaVersionBuildItem.JavaVersion * The value of this property is used to create an ImageStream for the builder image used in the Openshift build. * When it references images already available in the internal Openshift registry, the corresponding streams are used * instead. - * When the application is built against Java 17 or higher, {@code registry.access.redhat.com/ubi8/openjdk-17:1.17} + * When the application is built against Java 21 or higher, {@code registry.access.redhat.com/ubi8/openjdk-21:1.18} + * is used as the default. + * When the application is built against Java [17, 20], {@code registry.access.redhat.com/ubi8/openjdk-17:1.18} * is used as the default. - * Otherwise {@code registry.access.redhat.com/ubi8/openjdk-11:1.17} is used as the default. + * Otherwise {@code registry.access.redhat.com/ubi8/openjdk-11:1.18} is used as the default. */ @ConfigItem public Optional baseJvmImage; diff --git a/extensions/container-image/container-image-openshift/deployment/src/main/java/io/quarkus/container/image/openshift/deployment/S2iConfig.java b/extensions/container-image/container-image-openshift/deployment/src/main/java/io/quarkus/container/image/openshift/deployment/S2iConfig.java index a636a645f74791..0c88e16cba29ee 100644 --- a/extensions/container-image/container-image-openshift/deployment/src/main/java/io/quarkus/container/image/openshift/deployment/S2iConfig.java +++ b/extensions/container-image/container-image-openshift/deployment/src/main/java/io/quarkus/container/image/openshift/deployment/S2iConfig.java @@ -41,9 +41,9 @@ public static String getDefaultJvmImage(CompiledJavaVersionBuildItem.JavaVersion /** * The base image to be used when a container image is being produced for the jar build. * - * When the application is built against Java 17 or higher, {@code registry.access.redhat.com/ubi8/openjdk-17:1.17} + * When the application is built against Java 17 or higher, {@code registry.access.redhat.com/ubi8/openjdk-17:1.18} * is used as the default. - * Otherwise {@code registry.access.redhat.com/ubi8/openjdk-11:1.17} is used as the default. + * Otherwise {@code registry.access.redhat.com/ubi8/openjdk-11:1.18} is used as the default. */ @ConfigItem public Optional baseJvmImage; diff --git a/extensions/container-image/container-image-s2i/deployment/src/main/java/io/quarkus/container/image/s2i/deployment/ContainerImageS2iConfig.java b/extensions/container-image/container-image-s2i/deployment/src/main/java/io/quarkus/container/image/s2i/deployment/ContainerImageS2iConfig.java index 361aba90822904..10e9f127f17b8f 100644 --- a/extensions/container-image/container-image-s2i/deployment/src/main/java/io/quarkus/container/image/s2i/deployment/ContainerImageS2iConfig.java +++ b/extensions/container-image/container-image-s2i/deployment/src/main/java/io/quarkus/container/image/s2i/deployment/ContainerImageS2iConfig.java @@ -15,15 +15,17 @@ public class ContainerImageS2iConfig { public static final String DEFAULT_BASE_JVM_JDK11_IMAGE = "registry.access.redhat.com/ubi8/openjdk-11"; public static final String DEFAULT_BASE_JVM_JDK17_IMAGE = "registry.access.redhat.com/ubi8/openjdk-17"; + public static final String DEFAULT_BASE_JVM_JDK21_IMAGE = "registry.access.redhat.com/ubi8/openjdk-21"; public static final String DEFAULT_BASE_NATIVE_IMAGE = "quay.io/quarkus/ubi-quarkus-native-binary-s2i:2.0"; public static final String DEFAULT_NATIVE_TARGET_FILENAME = "application"; public static String getDefaultJvmImage(CompiledJavaVersionBuildItem.JavaVersion version) { - switch (version.isJava17OrHigher()) { - case TRUE: - return DEFAULT_BASE_JVM_JDK17_IMAGE; - default: - return DEFAULT_BASE_JVM_JDK11_IMAGE; + if (version.isJava21OrHigher() == CompiledJavaVersionBuildItem.JavaVersion.Status.TRUE) { + return DEFAULT_BASE_JVM_JDK21_IMAGE; + } else if (version.isJava17OrHigher() == CompiledJavaVersionBuildItem.JavaVersion.Status.TRUE) { + return DEFAULT_BASE_JVM_JDK17_IMAGE; + } else { + return DEFAULT_BASE_JVM_JDK11_IMAGE; } } diff --git a/extensions/grpc/codegen/src/main/java/io/quarkus/grpc/deployment/GrpcCodeGen.java b/extensions/grpc/codegen/src/main/java/io/quarkus/grpc/deployment/GrpcCodeGen.java index 6c5417239d0a39..fb6c31f12404ef 100644 --- a/extensions/grpc/codegen/src/main/java/io/quarkus/grpc/deployment/GrpcCodeGen.java +++ b/extensions/grpc/codegen/src/main/java/io/quarkus/grpc/deployment/GrpcCodeGen.java @@ -70,8 +70,8 @@ public String providerId() { } @Override - public String inputExtension() { - return "proto"; + public String[] inputExtensions() { + return new String[] { "proto" }; } @Override diff --git a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/ClassNames.java b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/ClassNames.java index 0639230faa6f68..ab0e5cd4e2ea08 100644 --- a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/ClassNames.java +++ b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/ClassNames.java @@ -57,6 +57,9 @@ private static DotName createConstant(String fqcn) { public static final DotName INTERCEPTOR = createConstant("org.hibernate.Interceptor"); public static final DotName STATEMENT_INSPECTOR = createConstant("org.hibernate.resource.jdbc.spi.StatementInspector"); + public static final DotName FORMAT_MAPPER = createConstant("org.hibernate.type.format.FormatMapper"); + public static final DotName JSON_FORMAT = createConstant("io.quarkus.hibernate.orm.JsonFormat"); + public static final DotName XML_FORMAT = createConstant("io.quarkus.hibernate.orm.XmlFormat"); public static final List GENERATORS = List.of( createConstant("org.hibernate.generator.internal.CurrentTimestampGeneration"), diff --git a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmCdiProcessor.java b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmCdiProcessor.java index 63d691520a6e80..fc207f65444c32 100644 --- a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmCdiProcessor.java +++ b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmCdiProcessor.java @@ -61,7 +61,8 @@ public class HibernateOrmCdiProcessor { ClassNames.TENANT_RESOLVER, ClassNames.TENANT_CONNECTION_RESOLVER, ClassNames.INTERCEPTOR, - ClassNames.STATEMENT_INSPECTOR); + ClassNames.STATEMENT_INSPECTOR, + ClassNames.FORMAT_MAPPER); @BuildStep AnnotationsTransformerBuildItem convertJpaResourceAnnotationsToQualifier( @@ -247,11 +248,13 @@ void generateDataSourceBeans(HibernateOrmRecorder recorder, @BuildStep void registerAnnotations(BuildProducer additionalBeans, BuildProducer beanDefiningAnnotations) { - // add the @PersistenceUnit and @PersistenceUnitExtension classes + // add the @PersistenceUnit, @PersistenceUnitExtension, @JsonFormat and @XmlFormat classes // otherwise they won't be registered as qualifiers additionalBeans.produce(AdditionalBeanBuildItem.builder() .addBeanClasses(ClassNames.QUARKUS_PERSISTENCE_UNIT.toString(), - ClassNames.PERSISTENCE_UNIT_EXTENSION.toString()) + ClassNames.PERSISTENCE_UNIT_EXTENSION.toString(), + ClassNames.JSON_FORMAT.toString(), + ClassNames.XML_FORMAT.toString()) .build()); // Register the default scope for @PersistenceUnitExtension and make such beans unremovable by default 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 8a45d4ceca3c57..209e04ed82595f 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 @@ -326,7 +326,7 @@ public void configurationDescriptorBuilding( hibernateOrmConfig.database.ormCompatibilityVersion, Collections.emptyMap()), null, jpaModel.getXmlMappings(persistenceXmlDescriptorBuildItem.getDescriptor().getName()), - false, true)); + false, true, capabilities)); } if (impliedPU.shouldGenerateImpliedBlockingPersistenceUnit()) { @@ -416,6 +416,7 @@ public BytecodeRecorderConstantDefinitionBuildItem pregenProxies( JpaModelIndexBuildItem indexBuildItem, TransformedClassesBuildItem transformedClassesBuildItem, List persistenceUnitDescriptorBuildItems, + List additionalJpaModelBuildItems, BuildProducer generatedClassBuildItemBuildProducer, LiveReloadBuildItem liveReloadBuildItem) { Set managedClassAndPackageNames = new HashSet<>(jpaModel.getEntityClassNames()); @@ -426,6 +427,11 @@ public BytecodeRecorderConstantDefinitionBuildItem pregenProxies( // is used for packages too, and it relies (indirectly) on getManagedClassNames(). managedClassAndPackageNames.addAll(pud.getManagedClassNames()); } + + for (AdditionalJpaModelBuildItem additionalJpaModelBuildItem : additionalJpaModelBuildItems) { + managedClassAndPackageNames.add(additionalJpaModelBuildItem.getClassName()); + } + PreGeneratedProxies proxyDefinitions = generatedProxies(managedClassAndPackageNames, indexBuildItem.getIndex(), transformedClassesBuildItem, generatedClassBuildItemBuildProducer, liveReloadBuildItem); @@ -1118,7 +1124,7 @@ private static void producePersistenceUnitDescriptorFromConfig( persistenceUnitConfig.unsupportedProperties), persistenceUnitConfig.multitenantSchemaDatasource.orElse(null), xmlMappings, - false, false)); + false, false, capabilities)); } private static void collectDialectConfig(String persistenceUnitName, diff --git a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/PersistenceUnitDescriptorBuildItem.java b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/PersistenceUnitDescriptorBuildItem.java index 9400a67468d6c8..5693396c0bbba0 100644 --- a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/PersistenceUnitDescriptorBuildItem.java +++ b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/PersistenceUnitDescriptorBuildItem.java @@ -2,12 +2,16 @@ import java.util.Collection; import java.util.List; +import java.util.Optional; import org.hibernate.jpa.boot.internal.ParsedPersistenceXmlDescriptor; import io.quarkus.builder.item.MultiBuildItem; +import io.quarkus.deployment.Capabilities; +import io.quarkus.deployment.Capability; import io.quarkus.hibernate.orm.runtime.boot.QuarkusPersistenceUnitDefinition; import io.quarkus.hibernate.orm.runtime.boot.xml.RecordableXmlMapping; +import io.quarkus.hibernate.orm.runtime.customized.FormatMapperKind; import io.quarkus.hibernate.orm.runtime.integration.HibernateOrmIntegrationStaticDescriptor; import io.quarkus.hibernate.orm.runtime.recording.RecordedConfig; @@ -30,12 +34,14 @@ public final class PersistenceUnitDescriptorBuildItem extends MultiBuildItem { private final List xmlMappings; private final boolean isReactive; private final boolean fromPersistenceXml; + private final Optional jsonMapper; + private final Optional xmlMapper; public PersistenceUnitDescriptorBuildItem(ParsedPersistenceXmlDescriptor descriptor, String configurationName, RecordedConfig config, String multiTenancySchemaDataSource, List xmlMappings, - boolean isReactive, boolean fromPersistenceXml) { + boolean isReactive, boolean fromPersistenceXml, Capabilities capabilities) { this.descriptor = descriptor; this.configurationName = configurationName; this.config = config; @@ -43,6 +49,8 @@ public PersistenceUnitDescriptorBuildItem(ParsedPersistenceXmlDescriptor descrip this.xmlMappings = xmlMappings; this.isReactive = isReactive; this.fromPersistenceXml = fromPersistenceXml; + this.jsonMapper = json(capabilities); + this.xmlMapper = xml(capabilities); } public Collection getManagedClassNames() { @@ -80,6 +88,23 @@ public boolean isFromPersistenceXml() { public QuarkusPersistenceUnitDefinition asOutputPersistenceUnitDefinition( List integrationStaticDescriptors) { return new QuarkusPersistenceUnitDefinition(descriptor, configurationName, config, - xmlMappings, isReactive, fromPersistenceXml, integrationStaticDescriptors); + xmlMappings, isReactive, fromPersistenceXml, jsonMapper, xmlMapper, integrationStaticDescriptors); + } + + private Optional json(Capabilities capabilities) { + if (capabilities.isPresent(Capability.JACKSON)) { + return Optional.of(FormatMapperKind.JACKSON); + } + if (capabilities.isPresent(Capability.JSONB)) { + return Optional.of(FormatMapperKind.JSONB); + } + return Optional.empty(); + } + + private Optional xml(Capabilities capabilities) { + if (capabilities.isPresent(Capability.JAXB)) { + return Optional.of(FormatMapperKind.JAXB); + } + return Optional.empty(); } } diff --git a/extensions/hibernate-orm/runtime/pom.xml b/extensions/hibernate-orm/runtime/pom.xml index 264d3f851db533..a9c86cf7c75b27 100644 --- a/extensions/hibernate-orm/runtime/pom.xml +++ b/extensions/hibernate-orm/runtime/pom.xml @@ -143,6 +143,21 @@ io.quarkus quarkus-caffeine
+ + io.quarkus + quarkus-jackson + true + + + io.quarkus + quarkus-jsonb + true + + + io.quarkus + quarkus-jaxb + true + diff --git a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/JsonFormat.java b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/JsonFormat.java new file mode 100644 index 00000000000000..54d036ba9f92b2 --- /dev/null +++ b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/JsonFormat.java @@ -0,0 +1,37 @@ +package io.quarkus.hibernate.orm; + +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 static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import jakarta.enterprise.util.AnnotationLiteral; +import jakarta.inject.Qualifier; + +import org.hibernate.type.format.FormatMapper; + +/** + * CDI qualifier for beans implementing a {@link FormatMapper}. + *

+ * This mapper will be used by Hibernate ORM for serialization and deserialization of JSON properties. + *

+ * Must be used in a combination with a {@link PersistenceUnitExtension} qualifier to define the persistence + * unit the mapper should be associated with. + */ +@Target({ TYPE, FIELD, METHOD, PARAMETER }) +@Retention(RUNTIME) +@Documented +@Qualifier +public @interface JsonFormat { + class Literal extends AnnotationLiteral implements JsonFormat { + public static JsonFormat INSTANCE = new Literal(); + + private Literal() { + } + } +} diff --git a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/XmlFormat.java b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/XmlFormat.java new file mode 100644 index 00000000000000..89cd4e0f1ed9ba --- /dev/null +++ b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/XmlFormat.java @@ -0,0 +1,37 @@ +package io.quarkus.hibernate.orm; + +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 static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import jakarta.enterprise.util.AnnotationLiteral; +import jakarta.inject.Qualifier; + +import org.hibernate.type.format.FormatMapper; + +/** + * CDI qualifier for beans implementing a {@link FormatMapper}. + *

+ * This mapper will be used by Hibernate ORM for serialization and deserialization of XML properties. + *

+ * Must be used in a combination with a {@link PersistenceUnitExtension} qualifier to define the persistence + * unit the mapper should be associated with. + */ +@Target({ TYPE, FIELD, METHOD, PARAMETER }) +@Retention(RUNTIME) +@Documented +@Qualifier +public @interface XmlFormat { + class Literal extends AnnotationLiteral implements XmlFormat { + public static XmlFormat INSTANCE = new Literal(); + + private Literal() { + } + } +} diff --git a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/PersistenceUnitUtil.java b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/PersistenceUnitUtil.java index caf85a8de72495..b91894c052e683 100644 --- a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/PersistenceUnitUtil.java +++ b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/PersistenceUnitUtil.java @@ -1,5 +1,7 @@ package io.quarkus.hibernate.orm.runtime; +import java.lang.annotation.Annotation; +import java.util.Arrays; import java.util.Comparator; import java.util.Locale; @@ -22,8 +24,10 @@ public static boolean isDefaultPersistenceUnit(String name) { } public static InjectableInstance singleExtensionInstanceForPersistenceUnit(Class beanType, - String persistenceUnitName) { - InjectableInstance instance = extensionInstanceForPersistenceUnit(beanType, persistenceUnitName); + String persistenceUnitName, + Annotation... additionalQualifiers) { + InjectableInstance instance = extensionInstanceForPersistenceUnit(beanType, persistenceUnitName, + additionalQualifiers); if (instance.isAmbiguous()) { throw new IllegalStateException(String.format(Locale.ROOT, "Multiple instances of %1$s were found for persistence unit %2$s. " @@ -33,9 +37,15 @@ public static InjectableInstance singleExtensionInstanceForPersistenceUni return instance; } - public static InjectableInstance extensionInstanceForPersistenceUnit(Class beanType, String persistenceUnitName) { - return Arc.container().select(beanType, - new PersistenceUnitExtension.Literal(persistenceUnitName)); + public static InjectableInstance extensionInstanceForPersistenceUnit(Class beanType, String persistenceUnitName, + Annotation... additionalQualifiers) { + if (additionalQualifiers.length == 0) { + return Arc.container().select(beanType, new PersistenceUnitExtension.Literal(persistenceUnitName)); + } else { + Annotation[] qualifiers = Arrays.copyOf(additionalQualifiers, additionalQualifiers.length + 1); + qualifiers[additionalQualifiers.length] = new PersistenceUnitExtension.Literal(persistenceUnitName); + return Arc.container().select(beanType, qualifiers); + } } public static class PersistenceUnitNameComparator implements Comparator { diff --git a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/boot/FastBootEntityManagerFactoryBuilder.java b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/boot/FastBootEntityManagerFactoryBuilder.java index 52db112ee6e5f5..0e1d9307ad5805 100644 --- a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/boot/FastBootEntityManagerFactoryBuilder.java +++ b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/boot/FastBootEntityManagerFactoryBuilder.java @@ -30,8 +30,11 @@ import org.hibernate.tool.schema.spi.CommandAcceptanceException; import org.hibernate.tool.schema.spi.DelayedDropRegistryNotAvailableImpl; import org.hibernate.tool.schema.spi.SchemaManagementToolCoordinator; +import org.hibernate.type.format.FormatMapper; import io.quarkus.arc.InjectableInstance; +import io.quarkus.hibernate.orm.JsonFormat; +import io.quarkus.hibernate.orm.XmlFormat; import io.quarkus.hibernate.orm.runtime.PersistenceUnitUtil; import io.quarkus.hibernate.orm.runtime.RuntimeSettings; import io.quarkus.hibernate.orm.runtime.migration.MultiTenancyStrategy; @@ -211,6 +214,17 @@ protected void populate(String persistenceUnitName, SessionFactoryOptionsBuilder if (!statementInspectorInstance.isUnsatisfied()) { options.applyStatementInspector(statementInspectorInstance.get()); } + + InjectableInstance jsonFormatMapper = PersistenceUnitUtil.singleExtensionInstanceForPersistenceUnit( + FormatMapper.class, persistenceUnitName, JsonFormat.Literal.INSTANCE); + if (!jsonFormatMapper.isUnsatisfied()) { + options.applyJsonFormatMapper(jsonFormatMapper.get()); + } + InjectableInstance xmlFormatMapper = PersistenceUnitUtil.singleExtensionInstanceForPersistenceUnit( + FormatMapper.class, persistenceUnitName, XmlFormat.Literal.INSTANCE); + if (!xmlFormatMapper.isUnsatisfied()) { + options.applyXmlFormatMapper(xmlFormatMapper.get()); + } } private static class ServiceRegistryCloser implements SessionFactoryObserver { diff --git a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/boot/FastBootMetadataBuilder.java b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/boot/FastBootMetadataBuilder.java index 3eabdd8e7bc79c..33f24af8d789bd 100644 --- a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/boot/FastBootMetadataBuilder.java +++ b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/boot/FastBootMetadataBuilder.java @@ -399,6 +399,15 @@ private MergedSettings mergeSettings(QuarkusPersistenceUnitDefinition puDefiniti } } + // If there's any mapping lib that we can work with available we'll set the default mapper: + if (puDefinition.getJsonMapperCreator().isPresent()) { + cfg.put(AvailableSettings.JSON_FORMAT_MAPPER, puDefinition.getJsonMapperCreator().get().create()); + } + // If there's any mapping lib that we can work with available we'll set the default mapper: + if (puDefinition.getXmlMapperCreator().isPresent()) { + cfg.put(AvailableSettings.XML_FORMAT_MAPPER, puDefinition.getXmlMapperCreator().get().create()); + } + return mergedSettings; } diff --git a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/boot/QuarkusPersistenceUnitDefinition.java b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/boot/QuarkusPersistenceUnitDefinition.java index a59c32123a15bd..ce7bf595a82ba1 100644 --- a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/boot/QuarkusPersistenceUnitDefinition.java +++ b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/boot/QuarkusPersistenceUnitDefinition.java @@ -2,10 +2,12 @@ import java.util.List; import java.util.Objects; +import java.util.Optional; import org.hibernate.jpa.boot.spi.PersistenceUnitDescriptor; import io.quarkus.hibernate.orm.runtime.boot.xml.RecordableXmlMapping; +import io.quarkus.hibernate.orm.runtime.customized.FormatMapperKind; import io.quarkus.hibernate.orm.runtime.integration.HibernateOrmIntegrationStaticDescriptor; import io.quarkus.hibernate.orm.runtime.recording.RecordedConfig; import io.quarkus.runtime.annotations.RecordableConstructor; @@ -21,12 +23,16 @@ public final class QuarkusPersistenceUnitDefinition { private final List xmlMappings; private final boolean isReactive; private final boolean fromPersistenceXml; + private final Optional jsonMapperCreator; + private final Optional xmlMapperCreator; private final List integrationStaticDescriptors; public QuarkusPersistenceUnitDefinition(PersistenceUnitDescriptor persistenceUnitDescriptor, String configurationName, RecordedConfig config, List xmlMappings, boolean isReactive, boolean fromPersistenceXml, + Optional jsonMapperCreator, + Optional xmlMapperCreator, List integrationStaticDescriptors) { Objects.requireNonNull(persistenceUnitDescriptor); Objects.requireNonNull(config); @@ -36,6 +42,8 @@ public QuarkusPersistenceUnitDefinition(PersistenceUnitDescriptor persistenceUni this.xmlMappings = xmlMappings; this.isReactive = isReactive; this.fromPersistenceXml = fromPersistenceXml; + this.jsonMapperCreator = jsonMapperCreator; + this.xmlMapperCreator = xmlMapperCreator; this.integrationStaticDescriptors = integrationStaticDescriptors; } @@ -45,6 +53,8 @@ public QuarkusPersistenceUnitDefinition(RuntimePersistenceUnitDescriptor actualH List xmlMappings, boolean reactive, boolean fromPersistenceXml, + Optional jsonMapperCreator, + Optional xmlMapperCreator, List integrationStaticDescriptors) { Objects.requireNonNull(actualHibernateDescriptor); Objects.requireNonNull(config); @@ -53,6 +63,8 @@ public QuarkusPersistenceUnitDefinition(RuntimePersistenceUnitDescriptor actualH this.xmlMappings = xmlMappings; this.isReactive = reactive; this.fromPersistenceXml = fromPersistenceXml; + this.jsonMapperCreator = jsonMapperCreator; + this.xmlMapperCreator = xmlMapperCreator; this.integrationStaticDescriptors = integrationStaticDescriptors; } @@ -81,6 +93,14 @@ public boolean isFromPersistenceXml() { return fromPersistenceXml; } + public Optional getJsonMapperCreator() { + return jsonMapperCreator; + } + + public Optional getXmlMapperCreator() { + return xmlMapperCreator; + } + public List getIntegrationStaticDescriptors() { return integrationStaticDescriptors; } diff --git a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/customized/FormatMapperKind.java b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/customized/FormatMapperKind.java new file mode 100644 index 00000000000000..1dec3cf5413e46 --- /dev/null +++ b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/customized/FormatMapperKind.java @@ -0,0 +1,38 @@ +package io.quarkus.hibernate.orm.runtime.customized; + +import jakarta.json.bind.Jsonb; + +import org.hibernate.type.format.FormatMapper; +import org.hibernate.type.format.jackson.JacksonJsonFormatMapper; +import org.hibernate.type.format.jakartajson.JsonBJsonFormatMapper; +import org.hibernate.type.format.jaxb.JaxbXmlFormatMapper; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import io.quarkus.arc.Arc; + +public enum FormatMapperKind { + JACKSON { + @Override + public FormatMapper create() { + // NOTE: we are not creating a Jackson based XML mapper since that one + // requires an additional lib (jackson-dataformat-xml-2.15.2) being available + // as well as an XmlMapper instance instead of an ObjectMapper... + return new JacksonJsonFormatMapper(Arc.container().instance(ObjectMapper.class).get()); + } + }, + JSONB { + @Override + public FormatMapper create() { + return new JsonBJsonFormatMapper(Arc.container().instance(Jsonb.class).get()); + } + }, + JAXB { + @Override + public FormatMapper create() { + return new JaxbXmlFormatMapper(); + } + }; + + public abstract FormatMapper create(); +} 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 9dff2f96a0122f..d8c2130bf24459 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 @@ -40,6 +40,7 @@ import io.quarkus.datasource.deployment.spi.DefaultDataSourceDbKindBuildItem; import io.quarkus.datasource.runtime.DataSourceBuildTimeConfig; import io.quarkus.datasource.runtime.DataSourcesBuildTimeConfig; +import io.quarkus.deployment.Capabilities; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.annotations.BuildSteps; @@ -131,6 +132,7 @@ public void buildReactivePersistenceUnit( ApplicationArchivesBuildItem applicationArchivesBuildItem, LaunchModeBuildItem launchMode, JpaModelBuildItem jpaModel, + Capabilities capabilities, BuildProducer systemProperties, BuildProducer nativeImageResources, BuildProducer hotDeploymentWatchedFiles, @@ -191,7 +193,7 @@ public void buildReactivePersistenceUnit( persistenceUnitConfig.unsupportedProperties), null, jpaModel.getXmlMappings(reactivePU.getName()), - true, false)); + true, false, capabilities)); } } diff --git a/extensions/jaxb/runtime/pom.xml b/extensions/jaxb/runtime/pom.xml index 6498c57c868e2b..591b9481a3e67d 100644 --- a/extensions/jaxb/runtime/pom.xml +++ b/extensions/jaxb/runtime/pom.xml @@ -53,6 +53,9 @@ javax.xml.bind:jaxb-api org.jboss.spec.javax.xml.bind:jboss-jaxb-api_2.3_spec + + io.quarkus.jaxb + diff --git a/extensions/jaxp/runtime/pom.xml b/extensions/jaxp/runtime/pom.xml index 3b8b0f44298e6e..c670dfaf44b4d1 100644 --- a/extensions/jaxp/runtime/pom.xml +++ b/extensions/jaxp/runtime/pom.xml @@ -24,6 +24,11 @@ io.quarkus quarkus-extension-maven-plugin + + + io.quarkus.jaxp + + maven-compiler-plugin diff --git a/extensions/keycloak-admin-client-reactive/deployment/src/main/java/io/quarkus/keycloak/admin/client/reactive/KeycloakAdminClientReactiveProcessor.java b/extensions/keycloak-admin-client-reactive/deployment/src/main/java/io/quarkus/keycloak/admin/client/reactive/KeycloakAdminClientReactiveProcessor.java index 0a486c5e024d1d..27c8d4a6f95c26 100644 --- a/extensions/keycloak-admin-client-reactive/deployment/src/main/java/io/quarkus/keycloak/admin/client/reactive/KeycloakAdminClientReactiveProcessor.java +++ b/extensions/keycloak-admin-client-reactive/deployment/src/main/java/io/quarkus/keycloak/admin/client/reactive/KeycloakAdminClientReactiveProcessor.java @@ -25,6 +25,7 @@ import io.quarkus.keycloak.admin.client.common.KeycloakAdminClientInjectionEnabled; import io.quarkus.keycloak.admin.client.reactive.runtime.ResteasyReactiveClientProvider; import io.quarkus.keycloak.admin.client.reactive.runtime.ResteasyReactiveKeycloakAdminClientRecorder; +import io.quarkus.runtime.TlsConfig; public class KeycloakAdminClientReactiveProcessor { @@ -53,8 +54,8 @@ public void nativeImage(BuildProducer serviceProviderP @Record(ExecutionTime.STATIC_INIT) @Produce(ServiceStartBuildItem.class) @BuildStep - public void integrate(ResteasyReactiveKeycloakAdminClientRecorder recorder) { - recorder.setClientProvider(); + public void integrate(ResteasyReactiveKeycloakAdminClientRecorder recorder, TlsConfig tlsConfig) { + recorder.setClientProvider(tlsConfig.trustAll); } @Record(ExecutionTime.RUNTIME_INIT) diff --git a/extensions/keycloak-admin-client-reactive/runtime/src/main/java/io/quarkus/keycloak/admin/client/reactive/runtime/ResteasyReactiveClientProvider.java b/extensions/keycloak-admin-client-reactive/runtime/src/main/java/io/quarkus/keycloak/admin/client/reactive/runtime/ResteasyReactiveClientProvider.java index a8bb66a6d0096c..c39ffee71d45a3 100644 --- a/extensions/keycloak-admin-client-reactive/runtime/src/main/java/io/quarkus/keycloak/admin/client/reactive/runtime/ResteasyReactiveClientProvider.java +++ b/extensions/keycloak-admin-client-reactive/runtime/src/main/java/io/quarkus/keycloak/admin/client/reactive/runtime/ResteasyReactiveClientProvider.java @@ -30,9 +30,15 @@ public class ResteasyReactiveClientProvider implements ResteasyClientProvider { private static final List HANDLED_MEDIA_TYPES = List.of(MediaType.APPLICATION_JSON); private static final int PROVIDER_PRIORITY = Priorities.USER + 100; // ensures that it will be used first + private final boolean tlsTrustAll; + + public ResteasyReactiveClientProvider(boolean tlsTrustAll) { + this.tlsTrustAll = tlsTrustAll; + } + @Override public Client newRestEasyClient(Object messageHandler, SSLContext sslContext, boolean disableTrustManager) { - ClientBuilderImpl clientBuilder = new ClientBuilderImpl().trustAll(disableTrustManager); + ClientBuilderImpl clientBuilder = new ClientBuilderImpl().trustAll(tlsTrustAll || disableTrustManager); return registerJacksonProviders(clientBuilder).build(); } diff --git a/extensions/keycloak-admin-client-reactive/runtime/src/main/java/io/quarkus/keycloak/admin/client/reactive/runtime/ResteasyReactiveKeycloakAdminClientRecorder.java b/extensions/keycloak-admin-client-reactive/runtime/src/main/java/io/quarkus/keycloak/admin/client/reactive/runtime/ResteasyReactiveKeycloakAdminClientRecorder.java index 61d7605485442a..12458c795592f4 100644 --- a/extensions/keycloak-admin-client-reactive/runtime/src/main/java/io/quarkus/keycloak/admin/client/reactive/runtime/ResteasyReactiveKeycloakAdminClientRecorder.java +++ b/extensions/keycloak-admin-client-reactive/runtime/src/main/java/io/quarkus/keycloak/admin/client/reactive/runtime/ResteasyReactiveKeycloakAdminClientRecorder.java @@ -21,8 +21,8 @@ public ResteasyReactiveKeycloakAdminClientRecorder( this.keycloakAdminClientConfigRuntimeValue = keycloakAdminClientConfigRuntimeValue; } - public void setClientProvider() { - Keycloak.setClientProvider(new ResteasyReactiveClientProvider()); + public void setClientProvider(boolean tlsTrustAll) { + Keycloak.setClientProvider(new ResteasyReactiveClientProvider(tlsTrustAll)); } public Supplier createAdminClient() { diff --git a/extensions/keycloak-admin-client/deployment/src/main/java/io/quarkus/keycloak/adminclient/deployment/KeycloakAdminClientProcessor.java b/extensions/keycloak-admin-client/deployment/src/main/java/io/quarkus/keycloak/adminclient/deployment/KeycloakAdminClientProcessor.java index 056b3c1d5bf93d..5ac5f6fef32376 100644 --- a/extensions/keycloak-admin-client/deployment/src/main/java/io/quarkus/keycloak/adminclient/deployment/KeycloakAdminClientProcessor.java +++ b/extensions/keycloak-admin-client/deployment/src/main/java/io/quarkus/keycloak/adminclient/deployment/KeycloakAdminClientProcessor.java @@ -25,6 +25,7 @@ import io.quarkus.keycloak.admin.client.common.AutoCloseableDestroyer; import io.quarkus.keycloak.admin.client.common.KeycloakAdminClientInjectionEnabled; import io.quarkus.keycloak.adminclient.ResteasyKeycloakAdminClientRecorder; +import io.quarkus.runtime.TlsConfig; public class KeycloakAdminClientProcessor { @@ -48,8 +49,8 @@ ReflectiveClassBuildItem reflect() { @Record(ExecutionTime.STATIC_INIT) @Produce(ServiceStartBuildItem.class) @BuildStep - public void integrate(ResteasyKeycloakAdminClientRecorder recorder) { - recorder.setClientProvider(); + public void integrate(ResteasyKeycloakAdminClientRecorder recorder, TlsConfig tlsConfig) { + recorder.setClientProvider(tlsConfig.trustAll); } @Record(ExecutionTime.RUNTIME_INIT) diff --git a/extensions/keycloak-admin-client/runtime/src/main/java/io/quarkus/keycloak/adminclient/ResteasyKeycloakAdminClientRecorder.java b/extensions/keycloak-admin-client/runtime/src/main/java/io/quarkus/keycloak/adminclient/ResteasyKeycloakAdminClientRecorder.java index 9dda7e9c3c4753..75fb6d29248964 100644 --- a/extensions/keycloak-admin-client/runtime/src/main/java/io/quarkus/keycloak/adminclient/ResteasyKeycloakAdminClientRecorder.java +++ b/extensions/keycloak-admin-client/runtime/src/main/java/io/quarkus/keycloak/adminclient/ResteasyKeycloakAdminClientRecorder.java @@ -58,13 +58,13 @@ public Keycloak get() { }; } - public void setClientProvider() { + public void setClientProvider(boolean tlsTrustAll) { Keycloak.setClientProvider(new ResteasyClientClassicProvider() { @Override public Client newRestEasyClient(Object customJacksonProvider, SSLContext sslContext, boolean disableTrustManager) { // point here is to use default Quarkus providers rather than org.keycloak.admin.client.JacksonProvider // as it doesn't work properly in native mode - return ClientBuilderWrapper.create(sslContext, disableTrustManager).build(); + return ClientBuilderWrapper.create(sslContext, tlsTrustAll || disableTrustManager).build(); } }); } diff --git a/extensions/kubernetes/vanilla/deployment/pom.xml b/extensions/kubernetes/vanilla/deployment/pom.xml index cb2668a662062c..40b8b5b205e414 100644 --- a/extensions/kubernetes/vanilla/deployment/pom.xml +++ b/extensions/kubernetes/vanilla/deployment/pom.xml @@ -90,6 +90,26 @@ + + io.dekorate + prometheus-annotations + noapt + + + io.sundr + * + + + com.sun + tools + + + io.fabric8 + kubernetes-client + + + + io.quarkus quarkus-junit5-internal diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/AddServiceMonitorResourceDecorator.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/AddServiceMonitorResourceDecorator.java new file mode 100644 index 00000000000000..bc75666b48f15d --- /dev/null +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/AddServiceMonitorResourceDecorator.java @@ -0,0 +1,46 @@ +package io.quarkus.kubernetes.deployment; + +import io.dekorate.kubernetes.decorator.ResourceProvidingDecorator; +import io.dekorate.prometheus.model.ServiceMonitorBuilder; +import io.fabric8.kubernetes.api.model.KubernetesListBuilder; +import io.fabric8.kubernetes.api.model.ObjectMeta; + +public class AddServiceMonitorResourceDecorator extends ResourceProvidingDecorator { + + private final String scheme; + private final String targetPort; + private final String path; + private final int interval; + private final boolean honorLabels; + + public AddServiceMonitorResourceDecorator(String scheme, String targetPort, String path, int interval, + boolean honorLabels) { + this.scheme = scheme; + this.targetPort = targetPort; + this.path = path; + this.interval = interval; + this.honorLabels = honorLabels; + } + + @Override + public void visit(KubernetesListBuilder list) { + ObjectMeta meta = getMandatoryDeploymentMetadata(list, ANY); + list.addToItems(new ServiceMonitorBuilder() + .withNewMetadata() + .withName(meta.getName()) + .withLabels(meta.getLabels()) + .endMetadata() + .withNewSpec() + .withNewSelector() + .addToMatchLabels(meta.getLabels()) + .endSelector() + .addNewEndpoint() + .withScheme(scheme) + .withNewTargetPort(targetPort) + .withPath(path) + .withInterval(interval + "s") + .withHonorLabels(honorLabels) + .endEndpoint() + .endSpec()); + } +} 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 afb73d57cdea4b..f3ec0a8eb039ab 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 @@ -996,12 +996,21 @@ private static List createAnnotationDecorators(Optional { - String path = m.metricsEndpoint(); - String prefix = config.getPrometheusConfig().prefix; - if (port.isPresent() && path != null) { + // Add metrics annotations + metricsConfiguration.ifPresent(m -> { + String path = m.metricsEndpoint(); + String prefix = config.getPrometheusConfig().prefix; + if (port.isPresent() && path != null) { + if (config.getPrometheusConfig().generateServiceMonitor) { + result.add(new DecoratorBuildItem(target, new AddServiceMonitorResourceDecorator( + config.getPrometheusConfig().scheme.orElse("http"), + config.getPrometheusConfig().port.orElse(String.valueOf(port.get().getContainerPort())), + config.getPrometheusConfig().path.orElse(path), + 10, + true))); + } + + if (config.getPrometheusConfig().annotations) { result.add(new DecoratorBuildItem(target, new AddAnnotationDecorator(name, config.getPrometheusConfig().scrape.orElse(prefix + "/scrape"), "true", PROMETHEUS_ANNOTATION_TARGETS))); @@ -1014,8 +1023,8 @@ private static List createAnnotationDecorators(Optional header = Optional.empty(); + /** + * HTTP Authorization header scheme. + */ + @ConfigItem(defaultValue = OidcConstants.BEARER_SCHEME) + public String authorizationScheme = OidcConstants.BEARER_SCHEME; + /** * Required signature algorithm. * OIDC providers support many signature algorithms but if necessary you can restrict @@ -1697,6 +1703,14 @@ public boolean isSubjectRequired() { public void setSubjectRequired(boolean subjectRequired) { this.subjectRequired = subjectRequired; } + + public String getAuthorizationScheme() { + return authorizationScheme; + } + + public void setAuthorizationScheme(String authorizationScheme) { + this.authorizationScheme = authorizationScheme; + } } public static enum ApplicationType { diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/BearerAuthenticationMechanism.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/BearerAuthenticationMechanism.java index 78f4f2fe978dec..f6c22753ab98e0 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/BearerAuthenticationMechanism.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/BearerAuthenticationMechanism.java @@ -1,10 +1,11 @@ package io.quarkus.oidc.runtime; +import java.util.function.Function; + import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpResponseStatus; import io.quarkus.oidc.AccessTokenCredential; import io.quarkus.oidc.OidcTenantConfig; -import io.quarkus.oidc.common.runtime.OidcConstants; import io.quarkus.security.identity.IdentityProviderManager; import io.quarkus.security.identity.SecurityIdentity; import io.quarkus.vertx.http.runtime.security.ChallengeData; @@ -15,9 +16,6 @@ public class BearerAuthenticationMechanism extends AbstractOidcAuthenticationMechanism { - protected static final ChallengeData UNAUTHORIZED_CHALLENGE = new ChallengeData(HttpResponseStatus.UNAUTHORIZED.code(), - HttpHeaderNames.WWW_AUTHENTICATE, OidcConstants.BEARER_SCHEME); - public Uni authenticate(RoutingContext context, IdentityProviderManager identityProviderManager, OidcTenantConfig oidcTenantConfig) { String token = extractBearerToken(context, oidcTenantConfig); @@ -29,7 +27,14 @@ public Uni authenticate(RoutingContext context, } public Uni getChallenge(RoutingContext context) { - return Uni.createFrom().item(UNAUTHORIZED_CHALLENGE); + Uni tenantContext = resolver.resolveContext(context); + return tenantContext.onItem().transformToUni(new Function>() { + @Override + public Uni apply(TenantConfigContext tenantContext) { + return Uni.createFrom().item(new ChallengeData(HttpResponseStatus.UNAUTHORIZED.code(), + HttpHeaderNames.WWW_AUTHENTICATE, tenantContext.oidcConfig.token.authorizationScheme)); + } + }); } private String extractBearerToken(RoutingContext context, OidcTenantConfig oidcConfig) { @@ -49,7 +54,7 @@ private String extractBearerToken(RoutingContext context, OidcTenantConfig oidcC return headerValue; } - if (!OidcConstants.BEARER_SCHEME.equalsIgnoreCase(scheme)) { + if (!oidcConfig.token.authorizationScheme.equalsIgnoreCase(scheme)) { return null; } diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcAuthenticationMechanism.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcAuthenticationMechanism.java index edf944566678ba..bd5c8ab18aeea1 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcAuthenticationMechanism.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcAuthenticationMechanism.java @@ -23,8 +23,6 @@ @ApplicationScoped public class OidcAuthenticationMechanism implements HttpAuthenticationMechanism { - private static HttpCredentialTransport OIDC_SERVICE_TRANSPORT = new HttpCredentialTransport( - HttpCredentialTransport.Type.AUTHORIZATION, OidcConstants.BEARER_SCHEME); private static HttpCredentialTransport OIDC_WEB_APP_TRANSPORT = new HttpCredentialTransport( HttpCredentialTransport.Type.AUTHORIZATION_CODE, OidcConstants.CODE_FLOW_CODE); @@ -105,7 +103,8 @@ public HttpCredentialTransport apply(OidcTenantConfig oidcTenantConfig) { return null; } return isWebApp(context, oidcTenantConfig) ? OIDC_WEB_APP_TRANSPORT - : OIDC_SERVICE_TRANSPORT; + : new HttpCredentialTransport( + HttpCredentialTransport.Type.AUTHORIZATION, oidcTenantConfig.token.authorizationScheme); } }); } diff --git a/extensions/openshift-client/runtime/pom.xml b/extensions/openshift-client/runtime/pom.xml index 18fa13c77c2328..fd62cdfb36faac 100644 --- a/extensions/openshift-client/runtime/pom.xml +++ b/extensions/openshift-client/runtime/pom.xml @@ -41,6 +41,11 @@ + + org.graalvm.sdk + graal-sdk + provided + @@ -70,4 +75,4 @@ - \ No newline at end of file + diff --git a/extensions/openshift-client/runtime/src/main/java/io/quarkus/it/openshift/client/runtime/graal/MiscellaneousSubstitutions.java b/extensions/openshift-client/runtime/src/main/java/io/quarkus/it/openshift/client/runtime/graal/MiscellaneousSubstitutions.java new file mode 100644 index 00000000000000..3d1284aee3c8dc --- /dev/null +++ b/extensions/openshift-client/runtime/src/main/java/io/quarkus/it/openshift/client/runtime/graal/MiscellaneousSubstitutions.java @@ -0,0 +1,97 @@ +package io.quarkus.it.openshift.client.runtime.graal; + +import java.util.Arrays; +import java.util.function.BooleanSupplier; + +import com.oracle.svm.core.annotate.Substitute; +import com.oracle.svm.core.annotate.TargetClass; + +import io.fabric8.kubernetes.client.dsl.MixedOperation; +import io.fabric8.kubernetes.client.dsl.NonNamespaceOperation; +import io.fabric8.kubernetes.client.dsl.Resource; +import io.fabric8.openshift.api.model.miscellaneous.apiserver.v1.APIRequestCount; +import io.fabric8.openshift.api.model.miscellaneous.apiserver.v1.APIRequestCountList; +import io.fabric8.openshift.api.model.miscellaneous.cloudcredential.v1.CredentialsRequest; +import io.fabric8.openshift.api.model.miscellaneous.cloudcredential.v1.CredentialsRequestList; +import io.fabric8.openshift.api.model.miscellaneous.cncf.cni.v1.NetworkAttachmentDefinition; +import io.fabric8.openshift.api.model.miscellaneous.cncf.cni.v1.NetworkAttachmentDefinitionList; +import io.fabric8.openshift.api.model.miscellaneous.imageregistry.operator.v1.Config; +import io.fabric8.openshift.api.model.miscellaneous.imageregistry.operator.v1.ConfigList; +import io.fabric8.openshift.api.model.miscellaneous.metal3.v1alpha1.BareMetalHost; +import io.fabric8.openshift.api.model.miscellaneous.metal3.v1alpha1.BareMetalHostList; +import io.fabric8.openshift.api.model.miscellaneous.metal3.v1beta1.Metal3Remediation; +import io.fabric8.openshift.api.model.miscellaneous.metal3.v1beta1.Metal3RemediationList; +import io.fabric8.openshift.api.model.miscellaneous.metal3.v1beta1.Metal3RemediationTemplate; +import io.fabric8.openshift.api.model.miscellaneous.metal3.v1beta1.Metal3RemediationTemplateList; +import io.fabric8.openshift.api.model.miscellaneous.network.operator.v1.EgressRouter; +import io.fabric8.openshift.api.model.miscellaneous.network.operator.v1.EgressRouterList; +import io.fabric8.openshift.api.model.miscellaneous.network.operator.v1.OperatorPKI; +import io.fabric8.openshift.api.model.miscellaneous.network.operator.v1.OperatorPKIList; + +/** + * Allows the exclusion of the openshift-model-miscellaneous model without breaking the --link-at-build-time check. + */ +@TargetClass(className = "io.fabric8.openshift.client.impl.OpenShiftClientImpl", onlyWith = MiscellaneousSubstitutions.NoOpenShiftMiscellaneousModel.class) +public final class MiscellaneousSubstitutions { + + @Substitute + public NonNamespaceOperation> apiRequestCounts() { + throw new RuntimeException(Constants.ERROR_MESSAGE); + } + + @Substitute + public MixedOperation> bareMetalHosts() { + throw new RuntimeException(Constants.ERROR_MESSAGE); + } + + @Substitute + public MixedOperation> credentialsRequests() { + throw new RuntimeException(Constants.ERROR_MESSAGE); + } + + @Substitute + public MixedOperation> egressRouters() { + throw new RuntimeException(Constants.ERROR_MESSAGE); + } + + @Substitute + public NonNamespaceOperation> imageRegistryOperatorConfigs() { + throw new RuntimeException(Constants.ERROR_MESSAGE); + } + + @Substitute + public MixedOperation> metal3Remediations() { + throw new RuntimeException(Constants.ERROR_MESSAGE); + } + + @Substitute + public MixedOperation> metal3RemediationTemplates() { + throw new RuntimeException(Constants.ERROR_MESSAGE); + } + + @Substitute + public MixedOperation> networkAttachmentDefinitions() { + throw new RuntimeException(Constants.ERROR_MESSAGE); + } + + @Substitute + public MixedOperation> operatorPKIs() { + throw new RuntimeException(Constants.ERROR_MESSAGE); + } + + static final class Constants { + private static final String ERROR_MESSAGE = "OpenShift Miscellaneous API is not available, please add the openshift-model-miscellaneous module to your classpath"; + } + + static final class NoOpenShiftMiscellaneousModel implements BooleanSupplier { + + private static final String OPENSHIFT_MODEL_MISCELLANEOUS_PACKAGE = "io.fabric8.openshift.api.model.miscellaneous."; + static final Boolean OPENSHIFT_MODEL_MISCELLANEOUS_PRESENT = Arrays.stream(Package.getPackages()) + .map(Package::getName).anyMatch(p -> p.startsWith(OPENSHIFT_MODEL_MISCELLANEOUS_PACKAGE)); + + @Override + public boolean getAsBoolean() { + return !OPENSHIFT_MODEL_MISCELLANEOUS_PRESENT; + } + } +} diff --git a/extensions/openshift-client/runtime/src/main/java/io/quarkus/it/openshift/client/runtime/graal/OperatorSubstitutions.java b/extensions/openshift-client/runtime/src/main/java/io/quarkus/it/openshift/client/runtime/graal/OperatorSubstitutions.java new file mode 100644 index 00000000000000..3730608f29f249 --- /dev/null +++ b/extensions/openshift-client/runtime/src/main/java/io/quarkus/it/openshift/client/runtime/graal/OperatorSubstitutions.java @@ -0,0 +1,34 @@ +package io.quarkus.it.openshift.client.runtime.graal; + +import java.util.Arrays; +import java.util.function.BooleanSupplier; + +import com.oracle.svm.core.annotate.Substitute; +import com.oracle.svm.core.annotate.TargetClass; + +import io.fabric8.openshift.client.dsl.OpenShiftOperatorAPIGroupDSL; + +/** + * Allows the exclusion of the openshift-model-operator model without breaking the --link-at-build-time check. + */ +@TargetClass(className = "io.fabric8.openshift.client.impl.OpenShiftClientImpl", onlyWith = OperatorSubstitutions.NoOpenShiftOperatorModel.class) +public final class OperatorSubstitutions { + + @Substitute + public OpenShiftOperatorAPIGroupDSL operator() { + throw new RuntimeException( + "OpenShift Operator API is not available, please add the openshift-model-operator module to your classpath"); + } + + static final class NoOpenShiftOperatorModel implements BooleanSupplier { + + private static final String OPENSHIFT_MODEL_OPERATOR_PACKAGE = "io.fabric8.openshift.api.model.operator."; + static final Boolean OPENSHIFT_MODEL_OPERATOR_PRESENT = Arrays.stream(Package.getPackages()) + .map(Package::getName).anyMatch(p -> p.startsWith(OPENSHIFT_MODEL_OPERATOR_PACKAGE)); + + @Override + public boolean getAsBoolean() { + return !OPENSHIFT_MODEL_OPERATOR_PRESENT; + } + } +} 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 63337d717d8f0f..b07e5891fbd53c 100644 --- a/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/tracing/TracerProcessor.java +++ b/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/tracing/TracerProcessor.java @@ -1,5 +1,6 @@ package io.quarkus.opentelemetry.deployment.tracing; +import java.net.URL; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; @@ -118,7 +119,21 @@ void dropNames( // Drop framework paths List nonApplicationUris = new ArrayList<>(); frameworkEndpoints.ifPresent( - frameworkEndpointsBuildItem -> nonApplicationUris.addAll(frameworkEndpointsBuildItem.getEndpoints())); + frameworkEndpointsBuildItem -> { + for (String endpoint : frameworkEndpointsBuildItem.getEndpoints()) { + // Management routes are using full urls -> Extract the path. + if (endpoint.startsWith("http://") || endpoint.startsWith("https://")) { + try { + nonApplicationUris.add(new URL(endpoint).getPath()); + } catch (Exception ignored) { // Not an URL + nonApplicationUris.add(endpoint); + } + } else { + nonApplicationUris.add(endpoint); + } + } + }); + dropNonApplicationUris.produce(new DropNonApplicationUrisBuildItem(nonApplicationUris)); // Drop Static Resources diff --git a/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/tracing/instrumentation/InstrumentationProcessor.java b/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/tracing/instrumentation/InstrumentationProcessor.java index 1bac28f2b0fe32..83101515d740c1 100644 --- a/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/tracing/instrumentation/InstrumentationProcessor.java +++ b/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/tracing/instrumentation/InstrumentationProcessor.java @@ -23,7 +23,9 @@ import io.quarkus.opentelemetry.runtime.tracing.intrumentation.InstrumentationRecorder; import io.quarkus.opentelemetry.runtime.tracing.intrumentation.grpc.GrpcTracingClientInterceptor; import io.quarkus.opentelemetry.runtime.tracing.intrumentation.grpc.GrpcTracingServerInterceptor; -import io.quarkus.opentelemetry.runtime.tracing.intrumentation.reactivemessaging.ReactiveMessagingTracingDecorator; +import io.quarkus.opentelemetry.runtime.tracing.intrumentation.reactivemessaging.ReactiveMessagingTracingEmitterDecorator; +import io.quarkus.opentelemetry.runtime.tracing.intrumentation.reactivemessaging.ReactiveMessagingTracingIncomingDecorator; +import io.quarkus.opentelemetry.runtime.tracing.intrumentation.reactivemessaging.ReactiveMessagingTracingOutgoingDecorator; import io.quarkus.opentelemetry.runtime.tracing.intrumentation.restclient.OpenTelemetryClientFilter; import io.quarkus.opentelemetry.runtime.tracing.intrumentation.resteasy.AttachExceptionHandler; import io.quarkus.opentelemetry.runtime.tracing.intrumentation.resteasy.OpenTelemetryClassicServerFilter; @@ -90,7 +92,9 @@ void registerReactiveMessagingMessageDecorator( Capabilities capabilities, BuildProducer additionalBeans) { if (capabilities.isPresent(Capability.SMALLRYE_REACTIVE_MESSAGING)) { - additionalBeans.produce(new AdditionalBeanBuildItem(ReactiveMessagingTracingDecorator.class)); + additionalBeans.produce(new AdditionalBeanBuildItem(ReactiveMessagingTracingOutgoingDecorator.class)); + additionalBeans.produce(new AdditionalBeanBuildItem(ReactiveMessagingTracingIncomingDecorator.class)); + additionalBeans.produce(new AdditionalBeanBuildItem(ReactiveMessagingTracingEmitterDecorator.class)); } } diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryHttpCDILegacyTest.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryHttpCDILegacyTest.java index 5fc76afd35d2a5..dd03e834fa0a98 100644 --- a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryHttpCDILegacyTest.java +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryHttpCDILegacyTest.java @@ -24,7 +24,7 @@ import io.opentelemetry.extension.annotations.WithSpan; import io.opentelemetry.sdk.trace.data.SpanData; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.SemanticAttributes; import io.quarkus.opentelemetry.deployment.common.TestSpanExporter; import io.quarkus.opentelemetry.deployment.common.TestSpanExporterProvider; import io.quarkus.opentelemetry.deployment.common.TestUtil; diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryHttpCDITest.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryHttpCDITest.java index 92692bc32aaaad..19027b911c5688 100644 --- a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryHttpCDITest.java +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryHttpCDITest.java @@ -24,7 +24,7 @@ import io.opentelemetry.instrumentation.annotations.WithSpan; import io.opentelemetry.sdk.trace.data.SpanData; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.SemanticAttributes; import io.quarkus.opentelemetry.deployment.common.TestSpanExporter; import io.quarkus.opentelemetry.deployment.common.TestSpanExporterProvider; import io.quarkus.opentelemetry.deployment.common.TestUtil; diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryReactiveRoutesTest.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryReactiveRoutesTest.java index e127973ea23ccf..2af44b8e60a37b 100644 --- a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryReactiveRoutesTest.java +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryReactiveRoutesTest.java @@ -19,7 +19,7 @@ import org.junit.jupiter.api.extension.RegisterExtension; import io.opentelemetry.sdk.trace.data.SpanData; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.SemanticAttributes; import io.quarkus.opentelemetry.deployment.common.TestSpanExporter; import io.quarkus.opentelemetry.deployment.common.TestSpanExporterProvider; import io.quarkus.opentelemetry.deployment.common.TestUtil; diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/instrumentation/GraphQLOpenTelemetryTest.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/instrumentation/GraphQLOpenTelemetryTest.java index 765d5e5ca92840..0985ac324ac7fb 100644 --- a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/instrumentation/GraphQLOpenTelemetryTest.java +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/instrumentation/GraphQLOpenTelemetryTest.java @@ -1,9 +1,9 @@ package io.quarkus.opentelemetry.deployment.instrumentation; import static io.opentelemetry.api.common.AttributeKey.stringKey; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HTTP_METHOD; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HTTP_ROUTE; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HTTP_STATUS_CODE; +import static io.opentelemetry.semconv.SemanticAttributes.HTTP_METHOD; +import static io.opentelemetry.semconv.SemanticAttributes.HTTP_ROUTE; +import static io.opentelemetry.semconv.SemanticAttributes.HTTP_STATUS_CODE; import static io.quarkus.opentelemetry.deployment.common.TestSpanExporter.getSpanByKindAndParentId; import static java.net.HttpURLConnection.HTTP_OK; import static org.junit.jupiter.api.Assertions.assertEquals; diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/instrumentation/GrpcOpenTelemetryTest.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/instrumentation/GrpcOpenTelemetryTest.java index c4a7a624684137..edc1de34f739b6 100644 --- a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/instrumentation/GrpcOpenTelemetryTest.java +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/instrumentation/GrpcOpenTelemetryTest.java @@ -4,12 +4,12 @@ import static io.opentelemetry.api.trace.SpanKind.CLIENT; import static io.opentelemetry.api.trace.SpanKind.INTERNAL; import static io.opentelemetry.api.trace.SpanKind.SERVER; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NET_HOST_PORT; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NET_SOCK_HOST_ADDR; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.RPC_GRPC_STATUS_CODE; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.RPC_METHOD; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.RPC_SERVICE; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.RPC_SYSTEM; +import static io.opentelemetry.semconv.SemanticAttributes.NET_HOST_PORT; +import static io.opentelemetry.semconv.SemanticAttributes.NET_SOCK_HOST_ADDR; +import static io.opentelemetry.semconv.SemanticAttributes.RPC_GRPC_STATUS_CODE; +import static io.opentelemetry.semconv.SemanticAttributes.RPC_METHOD; +import static io.opentelemetry.semconv.SemanticAttributes.RPC_SERVICE; +import static io.opentelemetry.semconv.SemanticAttributes.RPC_SYSTEM; import static io.quarkus.opentelemetry.deployment.common.TestSpanExporter.getSpanByKindAndParentId; import static io.quarkus.opentelemetry.runtime.config.build.OTelBuildConfig.INSTRUMENTATION_NAME; import static org.assertj.core.api.Assertions.assertThat; diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/instrumentation/RestClientOpenTelemetryTest.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/instrumentation/RestClientOpenTelemetryTest.java index 110b7a0a340fbe..0ee590b103c22e 100644 --- a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/instrumentation/RestClientOpenTelemetryTest.java +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/instrumentation/RestClientOpenTelemetryTest.java @@ -2,13 +2,13 @@ import static io.opentelemetry.api.trace.SpanKind.CLIENT; import static io.opentelemetry.api.trace.SpanKind.SERVER; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HTTP_METHOD; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HTTP_ROUTE; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HTTP_STATUS_CODE; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HTTP_TARGET; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HTTP_URL; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NET_HOST_NAME; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NET_HOST_PORT; +import static io.opentelemetry.semconv.SemanticAttributes.HTTP_METHOD; +import static io.opentelemetry.semconv.SemanticAttributes.HTTP_ROUTE; +import static io.opentelemetry.semconv.SemanticAttributes.HTTP_STATUS_CODE; +import static io.opentelemetry.semconv.SemanticAttributes.HTTP_TARGET; +import static io.opentelemetry.semconv.SemanticAttributes.HTTP_URL; +import static io.opentelemetry.semconv.SemanticAttributes.NET_HOST_NAME; +import static io.opentelemetry.semconv.SemanticAttributes.NET_HOST_PORT; import static io.quarkus.opentelemetry.deployment.common.TestSpanExporter.getSpanByKindAndParentId; import static java.net.HttpURLConnection.HTTP_OK; import static org.junit.jupiter.api.Assertions.assertEquals; diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/instrumentation/VertxClientOpenTelemetryTest.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/instrumentation/VertxClientOpenTelemetryTest.java index c20222d95aca3d..e725a51525afae 100644 --- a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/instrumentation/VertxClientOpenTelemetryTest.java +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/instrumentation/VertxClientOpenTelemetryTest.java @@ -2,15 +2,15 @@ import static io.opentelemetry.api.trace.SpanKind.CLIENT; import static io.opentelemetry.api.trace.SpanKind.SERVER; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HTTP_METHOD; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HTTP_ROUTE; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HTTP_STATUS_CODE; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HTTP_TARGET; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HTTP_URL; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NET_HOST_NAME; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NET_HOST_PORT; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NET_PEER_NAME; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NET_PEER_PORT; +import static io.opentelemetry.semconv.SemanticAttributes.HTTP_METHOD; +import static io.opentelemetry.semconv.SemanticAttributes.HTTP_ROUTE; +import static io.opentelemetry.semconv.SemanticAttributes.HTTP_STATUS_CODE; +import static io.opentelemetry.semconv.SemanticAttributes.HTTP_TARGET; +import static io.opentelemetry.semconv.SemanticAttributes.HTTP_URL; +import static io.opentelemetry.semconv.SemanticAttributes.NET_HOST_NAME; +import static io.opentelemetry.semconv.SemanticAttributes.NET_HOST_PORT; +import static io.opentelemetry.semconv.SemanticAttributes.NET_PEER_NAME; +import static io.opentelemetry.semconv.SemanticAttributes.NET_PEER_PORT; import static io.quarkus.opentelemetry.deployment.common.TestSpanExporter.getSpanByKindAndParentId; import static java.net.HttpURLConnection.HTTP_OK; import static java.util.stream.Collectors.toSet; diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/instrumentation/VertxOpenTelemetryForwardedTest.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/instrumentation/VertxOpenTelemetryForwardedTest.java index 49bd40799d1af8..9556aa2bb13138 100644 --- a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/instrumentation/VertxOpenTelemetryForwardedTest.java +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/instrumentation/VertxOpenTelemetryForwardedTest.java @@ -1,7 +1,7 @@ package io.quarkus.opentelemetry.deployment.instrumentation; import static io.opentelemetry.api.trace.SpanKind.SERVER; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HTTP_CLIENT_IP; +import static io.opentelemetry.semconv.SemanticAttributes.HTTP_CLIENT_IP; import static io.quarkus.opentelemetry.deployment.common.TestSpanExporter.getSpanByKindAndParentId; import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.Assertions.assertEquals; diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/instrumentation/VertxOpenTelemetryTest.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/instrumentation/VertxOpenTelemetryTest.java index 34425de9026145..79d8cdae41227c 100644 --- a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/instrumentation/VertxOpenTelemetryTest.java +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/instrumentation/VertxOpenTelemetryTest.java @@ -3,15 +3,15 @@ import static io.opentelemetry.api.common.AttributeKey.stringKey; import static io.opentelemetry.api.trace.SpanKind.INTERNAL; import static io.opentelemetry.api.trace.SpanKind.SERVER; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HTTP_CLIENT_IP; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HTTP_METHOD; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HTTP_ROUTE; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HTTP_SCHEME; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HTTP_STATUS_CODE; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HTTP_TARGET; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NET_HOST_NAME; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NET_HOST_PORT; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.USER_AGENT_ORIGINAL; +import static io.opentelemetry.semconv.SemanticAttributes.HTTP_CLIENT_IP; +import static io.opentelemetry.semconv.SemanticAttributes.HTTP_METHOD; +import static io.opentelemetry.semconv.SemanticAttributes.HTTP_ROUTE; +import static io.opentelemetry.semconv.SemanticAttributes.HTTP_SCHEME; +import static io.opentelemetry.semconv.SemanticAttributes.HTTP_STATUS_CODE; +import static io.opentelemetry.semconv.SemanticAttributes.HTTP_TARGET; +import static io.opentelemetry.semconv.SemanticAttributes.NET_HOST_NAME; +import static io.opentelemetry.semconv.SemanticAttributes.NET_HOST_PORT; +import static io.opentelemetry.semconv.SemanticAttributes.USER_AGENT_ORIGINAL; import static io.quarkus.opentelemetry.deployment.common.TestSpanExporter.getSpanByKindAndParentId; import static io.restassured.RestAssured.given; import static io.vertx.core.http.HttpMethod.GET; diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/instrumentation/VertxOpenTelemetryXForwardedTest.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/instrumentation/VertxOpenTelemetryXForwardedTest.java index 829270e86d015f..e58f286f9df68e 100644 --- a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/instrumentation/VertxOpenTelemetryXForwardedTest.java +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/instrumentation/VertxOpenTelemetryXForwardedTest.java @@ -1,7 +1,7 @@ package io.quarkus.opentelemetry.deployment.instrumentation; import static io.opentelemetry.api.trace.SpanKind.SERVER; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HTTP_CLIENT_IP; +import static io.opentelemetry.semconv.SemanticAttributes.HTTP_CLIENT_IP; import static io.quarkus.opentelemetry.deployment.common.TestSpanExporter.getSpanByKindAndParentId; import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.Assertions.assertEquals; diff --git a/extensions/opentelemetry/runtime/pom.xml b/extensions/opentelemetry/runtime/pom.xml index 7befd1be1b261e..dd7d2fda4acde4 100644 --- a/extensions/opentelemetry/runtime/pom.xml +++ b/extensions/opentelemetry/runtime/pom.xml @@ -80,7 +80,7 @@ opentelemetry-sdk-extension-autoconfigure-spi - io.opentelemetry + io.opentelemetry.semconv opentelemetry-semconv @@ -102,6 +102,12 @@ io.opentelemetry.instrumentation opentelemetry-instrumentation-api-semconv + + + io.opentelemetry.semconv + opentelemetry-semconv + + io.opentelemetry diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/build/EndUserSpanProcessorConfig.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/build/EndUserSpanProcessorConfig.java index 3a9d988406c916..2e261c0d3e89bf 100644 --- a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/build/EndUserSpanProcessorConfig.java +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/build/EndUserSpanProcessorConfig.java @@ -15,8 +15,8 @@ public interface EndUserSpanProcessorConfig { * Enable the {@link io.quarkus.opentelemetry.runtime.exporter.otlp.EndUserSpanProcessor}. *

* The {@link io.quarkus.opentelemetry.runtime.exporter.otlp.EndUserSpanProcessor} adds - * the {@link io.opentelemetry.semconv.trace.attributes.SemanticAttributes.ENDUSER_ID} - * and {@link io.opentelemetry.semconv.trace.attributes.SemanticAttributes.ENDUSER_ROLE} to the Span. + * the {@link io.opentelemetry.semconv.SemanticAttributes.ENDUSER_ID} + * and {@link io.opentelemetry.semconv.SemanticAttributes.ENDUSER_ROLE} to the Span. */ @WithDefault("false") Optional enabled(); diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/EndUserSpanProcessor.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/EndUserSpanProcessor.java index c30223f8a737b8..a17cff566afd39 100644 --- a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/EndUserSpanProcessor.java +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/EndUserSpanProcessor.java @@ -11,7 +11,7 @@ import io.opentelemetry.sdk.trace.ReadWriteSpan; import io.opentelemetry.sdk.trace.ReadableSpan; import io.opentelemetry.sdk.trace.SpanProcessor; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.SemanticAttributes; import io.quarkus.security.identity.SecurityIdentity; @ApplicationScoped diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/DropTargetsSampler.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/DropTargetsSampler.java index 06aa5ecf484389..8222d2c99a74d2 100644 --- a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/DropTargetsSampler.java +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/DropTargetsSampler.java @@ -8,7 +8,7 @@ import io.opentelemetry.sdk.trace.data.LinkData; import io.opentelemetry.sdk.trace.samplers.Sampler; import io.opentelemetry.sdk.trace.samplers.SamplingResult; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.SemanticAttributes; public class DropTargetsSampler implements Sampler { private final Sampler sampler; diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/TracerRecorder.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/TracerRecorder.java index 163ab17705b613..a0d5486ba0a3da 100644 --- a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/TracerRecorder.java +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/TracerRecorder.java @@ -6,7 +6,7 @@ import io.opentelemetry.api.common.Attributes; import io.opentelemetry.sdk.resources.Resource; -import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; +import io.opentelemetry.semconv.ResourceAttributes; import io.quarkus.arc.runtime.BeanContainer; import io.quarkus.runtime.annotations.Recorder; diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/TracerUtil.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/TracerUtil.java index 9c8e915904a04d..2bcb46ecf6a8ee 100644 --- a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/TracerUtil.java +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/TracerUtil.java @@ -1,7 +1,7 @@ package io.quarkus.opentelemetry.runtime.tracing; -import static io.opentelemetry.semconv.resource.attributes.ResourceAttributes.HOST_NAME; -import static io.opentelemetry.semconv.resource.attributes.ResourceAttributes.SERVICE_NAME; +import static io.opentelemetry.semconv.ResourceAttributes.HOST_NAME; +import static io.opentelemetry.semconv.ResourceAttributes.SERVICE_NAME; import java.util.List; diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/grpc/GrpcStatusCodeExtractor.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/grpc/GrpcStatusCodeExtractor.java index f5e5cae9aa8de3..73bb2c515e5f6b 100644 --- a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/grpc/GrpcStatusCodeExtractor.java +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/grpc/GrpcStatusCodeExtractor.java @@ -4,7 +4,7 @@ import io.opentelemetry.api.common.AttributesBuilder; import io.opentelemetry.context.Context; import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.SemanticAttributes; class GrpcStatusCodeExtractor implements AttributesExtractor { @Override diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/reactivemessaging/ReactiveMessagingTracingEmitterDecorator.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/reactivemessaging/ReactiveMessagingTracingEmitterDecorator.java new file mode 100644 index 00000000000000..17f6541114ecdb --- /dev/null +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/reactivemessaging/ReactiveMessagingTracingEmitterDecorator.java @@ -0,0 +1,50 @@ +package io.quarkus.opentelemetry.runtime.tracing.intrumentation.reactivemessaging; + +import static io.quarkus.opentelemetry.runtime.tracing.intrumentation.reactivemessaging.ReactiveMessagingTracingOutgoingDecorator.decorateOutgoing; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.mutiny.Multi; +import io.smallrye.reactive.messaging.ChannelRegistry; +import io.smallrye.reactive.messaging.PublisherDecorator; + +/** + * Intercepts outgoing messages from emitters from Reactive Messaging connectors. + *

+ * For outgoing messages from emitters, if the message doesn't already contain a tracing metadata, it attaches one with the + * current + * OpenTelemetry context. + * Reactive messaging outbound connectors, if tracing is supported, will use that context as parent span to trace outbound + * message transmission. + */ +@ApplicationScoped +public class ReactiveMessagingTracingEmitterDecorator implements PublisherDecorator { + + @Override + public int getPriority() { + // Place the decorator before all others including the ContextDecorator which is priority 0 + // This is only important for the emitter case + return -1000; + } + + @Inject + ChannelRegistry registry; + + /** + * Incoming messages + */ + @Override + public Multi> decorate(Multi> publisher, + String channelName, boolean isConnector) { + Multi> multi = publisher; + if (!isConnector && registry.getEmitterNames().contains(channelName)) { + // Emitter is a special case for the emitter publisher + multi = decorateOutgoing(multi); + } + return multi; + } + +} diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/reactivemessaging/ReactiveMessagingTracingDecorator.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/reactivemessaging/ReactiveMessagingTracingIncomingDecorator.java similarity index 56% rename from extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/reactivemessaging/ReactiveMessagingTracingDecorator.java rename to extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/reactivemessaging/ReactiveMessagingTracingIncomingDecorator.java index cba4e77aba4048..229074e81dc20c 100644 --- a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/reactivemessaging/ReactiveMessagingTracingDecorator.java +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/reactivemessaging/ReactiveMessagingTracingIncomingDecorator.java @@ -1,7 +1,5 @@ package io.quarkus.opentelemetry.runtime.tracing.intrumentation.reactivemessaging; -import java.util.List; - import jakarta.enterprise.context.ApplicationScoped; import org.eclipse.microprofile.reactive.messaging.Message; @@ -10,30 +8,19 @@ import io.quarkus.opentelemetry.runtime.QuarkusContextStorage; import io.smallrye.mutiny.Multi; import io.smallrye.reactive.messaging.PublisherDecorator; -import io.smallrye.reactive.messaging.SubscriberDecorator; import io.smallrye.reactive.messaging.TracingMetadata; import io.smallrye.reactive.messaging.providers.locals.LocalContextMetadata; /** - * Intercepts incoming and outgoing messages from Reactive Messaging connectors. + * Intercepts incoming messages from Reactive Messaging connectors. *

* For incoming messages, it fetches OpenTelemetry context from the message and attaches to the duplicated context of the * message. * Consumer methods will be called on this duplicated context, so the OpenTelemetry context associated with the incoming message * will be propagated. - *

- * For outgoing messages, if the message doesn't already contain a tracing metadata, it attaches one with the current - * OpenTelemetry context. - * Reactive messaging outbound connectors, if tracing is supported, will use that context as parent span to trace outbound - * message transmission. */ @ApplicationScoped -public class ReactiveMessagingTracingDecorator implements PublisherDecorator, SubscriberDecorator { - - @Override - public int getPriority() { - return 1000; - } +public class ReactiveMessagingTracingIncomingDecorator implements PublisherDecorator { /** * Incoming messages @@ -59,25 +46,4 @@ public Multi> decorate(Multi> publishe return multi; } - /** - * Outgoing messages - */ - @Override - public Multi> decorate(Multi> toBeSubscribed, - List channelName, boolean isConnector) { - Multi> multi = toBeSubscribed; - if (isConnector) { - // add TracingMetadata to the outgoing message if it doesn't exist already - multi = multi.map(m -> { - Message message = m; - if (m.getMetadata(TracingMetadata.class).isEmpty()) { - var otelContext = QuarkusContextStorage.INSTANCE.current(); - message = m.addMetadata(TracingMetadata.withCurrent(otelContext)); - } - return message; - }); - } - return multi; - } - } diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/reactivemessaging/ReactiveMessagingTracingOutgoingDecorator.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/reactivemessaging/ReactiveMessagingTracingOutgoingDecorator.java new file mode 100644 index 00000000000000..00531430dfcd32 --- /dev/null +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/reactivemessaging/ReactiveMessagingTracingOutgoingDecorator.java @@ -0,0 +1,50 @@ +package io.quarkus.opentelemetry.runtime.tracing.intrumentation.reactivemessaging; + +import java.util.List; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.quarkus.opentelemetry.runtime.QuarkusContextStorage; +import io.smallrye.mutiny.Multi; +import io.smallrye.reactive.messaging.SubscriberDecorator; +import io.smallrye.reactive.messaging.TracingMetadata; + +/** + * Intercepts outgoing messages from Reactive Messaging connectors. + *

+ * For outgoing messages, if the message doesn't already contain a tracing metadata, it attaches one with the current + * OpenTelemetry context. + * Reactive messaging outbound connectors, if tracing is supported, will use that context as parent span to trace outbound + * message transmission. + */ +@ApplicationScoped +public class ReactiveMessagingTracingOutgoingDecorator implements SubscriberDecorator { + + /** + * Outgoing messages + */ + @Override + public Multi> decorate(Multi> toBeSubscribed, + List channelName, boolean isConnector) { + Multi> multi = toBeSubscribed; + if (isConnector) { + // add TracingMetadata to the outgoing message if it doesn't exist already + multi = decorateOutgoing(multi); + } + return multi; + } + + static Multi> decorateOutgoing(Multi> multi) { + return multi.map(m -> { + Message message = m; + if (m.getMetadata(TracingMetadata.class).isEmpty()) { + var otelContext = QuarkusContextStorage.INSTANCE.current(); + message = m.addMetadata(TracingMetadata.withCurrent(otelContext)); + } + return message; + }); + } + +} diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/restclient/OpenTelemetryClientFilter.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/restclient/OpenTelemetryClientFilter.java index 993c391afa4891..d94ae7032e5c2c 100644 --- a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/restclient/OpenTelemetryClientFilter.java +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/restclient/OpenTelemetryClientFilter.java @@ -26,7 +26,7 @@ import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientAttributesGetter; import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanNameExtractor; import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanStatusExtractor; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.SemanticAttributes; import io.quarkus.arc.Unremovable; import io.quarkus.opentelemetry.runtime.QuarkusContextStorage; diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/resteasy/OpenTelemetryClassicServerFilter.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/resteasy/OpenTelemetryClassicServerFilter.java index 1b57e66e173459..c3251cf02738c4 100644 --- a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/resteasy/OpenTelemetryClassicServerFilter.java +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/resteasy/OpenTelemetryClassicServerFilter.java @@ -10,7 +10,7 @@ import io.opentelemetry.api.trace.Span; import io.opentelemetry.instrumentation.api.instrumenter.LocalRootSpan; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.SemanticAttributes; /** * Handles RESTEasy Classic diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/resteasy/OpenTelemetryReactiveServerFilter.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/resteasy/OpenTelemetryReactiveServerFilter.java index ddf382a32f6483..f000f2e44069d2 100644 --- a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/resteasy/OpenTelemetryReactiveServerFilter.java +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/resteasy/OpenTelemetryReactiveServerFilter.java @@ -7,7 +7,7 @@ import io.opentelemetry.api.trace.Span; import io.opentelemetry.instrumentation.api.instrumenter.LocalRootSpan; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.SemanticAttributes; /** * Handles RESTEasy Reactive (via Vert.x) diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/vertx/HttpInstrumenterVertxTracer.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/vertx/HttpInstrumenterVertxTracer.java index eb7b88bd4c4eeb..bb4f44309a4bad 100644 --- a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/vertx/HttpInstrumenterVertxTracer.java +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/vertx/HttpInstrumenterVertxTracer.java @@ -1,7 +1,7 @@ package io.quarkus.opentelemetry.runtime.tracing.intrumentation.vertx; import static io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteSource.FILTER; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HTTP_CLIENT_IP; +import static io.opentelemetry.semconv.SemanticAttributes.HTTP_CLIENT_IP; import static io.quarkus.opentelemetry.runtime.config.build.OTelBuildConfig.INSTRUMENTATION_NAME; import java.net.URI; @@ -27,7 +27,7 @@ import io.opentelemetry.instrumentation.api.instrumenter.http.HttpServerAttributesGetter; import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanNameExtractor; import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanStatusExtractor; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.SemanticAttributes; import io.vertx.core.Context; import io.vertx.core.MultiMap; import io.vertx.core.http.HttpHeaders; diff --git a/extensions/opentelemetry/runtime/src/test/java/io/quarkus/opentelemetry/runtime/tracing/DropTargetsSamplerTest.java b/extensions/opentelemetry/runtime/src/test/java/io/quarkus/opentelemetry/runtime/tracing/DropTargetsSamplerTest.java index 662c0538b2ab47..43de8f26064e7e 100644 --- a/extensions/opentelemetry/runtime/src/test/java/io/quarkus/opentelemetry/runtime/tracing/DropTargetsSamplerTest.java +++ b/extensions/opentelemetry/runtime/src/test/java/io/quarkus/opentelemetry/runtime/tracing/DropTargetsSamplerTest.java @@ -13,7 +13,7 @@ import io.opentelemetry.sdk.trace.data.LinkData; import io.opentelemetry.sdk.trace.samplers.Sampler; import io.opentelemetry.sdk.trace.samplers.SamplingResult; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.SemanticAttributes; class DropTargetsSamplerTest { diff --git a/extensions/opentelemetry/runtime/src/test/java/io/quarkus/opentelemetry/runtime/tracing/TracerUtilTest.java b/extensions/opentelemetry/runtime/src/test/java/io/quarkus/opentelemetry/runtime/tracing/TracerUtilTest.java index 99c005ce5ed91a..9383d1824d3fb1 100644 --- a/extensions/opentelemetry/runtime/src/test/java/io/quarkus/opentelemetry/runtime/tracing/TracerUtilTest.java +++ b/extensions/opentelemetry/runtime/src/test/java/io/quarkus/opentelemetry/runtime/tracing/TracerUtilTest.java @@ -8,7 +8,7 @@ import io.opentelemetry.api.common.Attributes; import io.opentelemetry.sdk.resources.Resource; -import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; +import io.opentelemetry.semconv.ResourceAttributes; public class TracerUtilTest { diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleProcessor.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleProcessor.java index 2a67ca69321376..12477cf5171148 100644 --- a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleProcessor.java +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleProcessor.java @@ -435,7 +435,8 @@ void validateMessageBundleMethodsInTemplates(TemplatesAnalysisBuildItem analysis List checkedTemplates, BeanDiscoveryFinishedBuildItem beanDiscovery, List templateData, - QuteConfig config) { + QuteConfig config, + List globals) { IndexView index = beanArchiveIndex.getIndex(); Function templateIdToPathFun = new Function() { @@ -585,7 +586,7 @@ public String apply(String id) { implicitClassToMembersUsed, templateIdToPathFun, generatedIdsToMatches, extensionMethodExcludes, checkedTemplate, lookupConfig, namedBeans, namespaceTemplateData, regularExtensionMethods, namespaceExtensionMethods, - assignabilityCheck); + assignabilityCheck, globals); MatchResult match = results.get(param.toOriginalString()); if (match != null && !match.isEmpty() && !assignabilityCheck.isAssignableFrom(match.type(), methodParams.get(idx))) { diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java index adc3c78166a5c9..c2fc7b73931673 100644 --- a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java @@ -153,6 +153,7 @@ public class QuteProcessor { public static final DotName LOCATION = Names.LOCATION; + public static final String GLOBAL_NAMESPACE = "global"; private static final Logger LOGGER = Logger.getLogger(QuteProcessor.class); @@ -922,7 +923,8 @@ void validateExpressions(TemplatesAnalysisBuildItem templatesAnalysis, List checkedTemplates, List templateData, QuteConfig config, - PackageConfig packageConfig) { + PackageConfig packageConfig, + List globals) { long start = System.nanoTime(); @@ -996,7 +998,7 @@ public String apply(String id) { incorrectExpressions, expression, index, implicitClassToMembersUsed, templateIdToPathFun, generatedIdsToMatches, extensionMethodExcludes, checkedTemplate, lookupConfig, namedBeans, namespaceTemplateData, regularExtensionMethods, - namespaceExtensionMethods, assignabilityCheck); + namespaceExtensionMethods, assignabilityCheck, globals); generatedIdsToMatches.put(expression.getGeneratedId(), match); } @@ -1130,7 +1132,8 @@ static MatchResult validateNestedExpressions(QuteConfig config, TemplateAnalysis Map namespaceTemplateData, List regularExtensionMethods, Map> namespaceToExtensionMethods, - AssignabilityCheck assignabilityCheck) { + AssignabilityCheck assignabilityCheck, + List globals) { LOGGER.debugf("Validate %s from %s", expression, expression.getOrigin()); @@ -1140,7 +1143,7 @@ static MatchResult validateNestedExpressions(QuteConfig config, TemplateAnalysis validateParametersOfNestedVirtualMethods(config, templateAnalysis, results, excludes, incorrectExpressions, expression, index, implicitClassToMembersUsed, templateIdToPathFun, generatedIdsToMatches, extensionMethodExcludes, checkedTemplate, lookupConfig, namedBeans, namespaceTemplateData, regularExtensionMethods, - namespaceToExtensionMethods, assignabilityCheck); + namespaceToExtensionMethods, assignabilityCheck, globals); MatchResult match = new MatchResult(assignabilityCheck); @@ -1148,7 +1151,8 @@ static MatchResult validateNestedExpressions(QuteConfig config, TemplateAnalysis // Process the namespace // ====================== NamespaceResult namespaceResult = processNamespace(expression, match, index, incorrectExpressions, namedBeans, results, - templateAnalysis, namespaceTemplateData, lookupConfig, namespaceToExtensionMethods, templateIdToPathFun); + templateAnalysis, namespaceTemplateData, lookupConfig, namespaceToExtensionMethods, templateIdToPathFun, + globals); if (namespaceResult.ignoring) { return match; } @@ -1326,19 +1330,44 @@ private static RootResult processRoot(Expression expression, MatchResult match, ignoring = true; } } else { - // No namespace extension method found - incorrect expression - incorrectExpressions.produce(new IncorrectExpressionBuildItem(expression.toOriginalString(), - String.format("No matching namespace [%s] extension method found", namespace.namespace), - expression.getOrigin())); - match.clearValues(); - putResult(match, results, expression); - ignoring = true; + if (namespace.hasGlobal()) { + ClassInfo variableClass = index.getClassByName(namespace.global.getVariableType().name()); + if (variableClass != null) { + match.setValues(variableClass, namespace.global.getVariableType()); + iterator = processHintsIfNeeded(root, iterator, parts, templateAnalysis, root.asHintInfo().hints, match, + index, expression, generatedIdsToMatches, incorrectExpressions); + } else { + // Global variable type not available + putResult(match, results, expression); + ignoring = true; + } + } else { + // No global and no namespace extension method found - incorrect expression + incorrectExpressions.produce(new IncorrectExpressionBuildItem(expression.toOriginalString(), + String.format("No matching namespace [%s] extension method found", namespace.namespace), + expression.getOrigin())); + match.clearValues(); + putResult(match, results, expression); + ignoring = true; + } } } else if (namespace.hasDataNamespaceInfo()) { // Validate as Data namespace expression has parameter declaration bound to the variable // Skip the first part, e.g. for {data:item.name} we start validation with "name" match.setValues(namespace.dataNamespaceExpTypeInfo.rawClass, namespace.dataNamespaceExpTypeInfo.resolvedType); + } else if (namespace.hasGlobal()) { + // "global:" namespace is used and no namespace extension methods exist + ClassInfo variableClass = index.getClassByName(namespace.global.getVariableType().name()); + if (variableClass != null) { + match.setValues(variableClass, namespace.global.getVariableType()); + iterator = processHintsIfNeeded(root, iterator, parts, templateAnalysis, root.asHintInfo().hints, match, + index, expression, generatedIdsToMatches, incorrectExpressions); + } else { + // Global variable type not available + putResult(match, results, expression); + ignoring = true; + } } else if (rootClazz == null) { // No namespace is used or no declarative resolver (extension methods, @TemplateData, etc.) if (root.isTypeInfo()) { @@ -1416,7 +1445,8 @@ private static NamespaceResult processNamespace(Expression expression, MatchResu Map results, TemplateAnalysis templateAnalysis, Map namespaceTemplateData, JavaMemberLookupConfig lookupConfig, Map> namespaceToExtensionMethods, - Function templateIdToPathFun) { + Function templateIdToPathFun, + List globals) { String namespace = expression.getNamespace(); if (namespace == null) { return NamespaceResult.EMPTY; @@ -1426,6 +1456,10 @@ private static NamespaceResult processNamespace(Expression expression, MatchResu TemplateDataBuildItem templateData = null; List namespaceExtensionMethods = null; boolean ignored = false; + TemplateGlobalBuildItem global = namespace.equals(GLOBAL_NAMESPACE) + ? globals.stream().filter(g -> g.getName().equals(expression.getParts().get(0).getName())).findFirst() + .orElse(null) + : null; if (namespace.equals(INJECT_NAMESPACE) || namespace.equals(CDI_NAMESPACE)) { // cdi:, inject: @@ -1475,22 +1509,26 @@ private static NamespaceResult processNamespace(Expression expression, MatchResu filter = filter.and(templateData::filter); lookupConfig = new FirstPassJavaMemberLookupConfig(lookupConfig, filter, true); } else { - // Extension methods exist for the given namespace + // Extension methods may exist for the given namespace namespaceExtensionMethods = namespaceToExtensionMethods.get(namespace); + if (namespaceExtensionMethods == null) { - // All other namespaces are ignored - putResult(match, results, expression); - ignored = true; + if (!namespace.equals(GLOBAL_NAMESPACE) || global == null) { + // Not "global:" with a matching global variable + // All other namespaces are ignored + putResult(match, results, expression); + ignored = true; + } } } } return new NamespaceResult(namespace, rootClazz, dataNamespaceTypeInfo, templateData, namespaceExtensionMethods, - ignored, lookupConfig); + ignored, lookupConfig, global); } private static class NamespaceResult { - static final NamespaceResult EMPTY = new NamespaceResult(null, null, null, null, null, false, null); + static final NamespaceResult EMPTY = new NamespaceResult(null, null, null, null, null, false, null, null); private final String namespace; private final ClassInfo rootClazz; @@ -1499,11 +1537,12 @@ private static class NamespaceResult { private final List extensionMethods; private final boolean ignoring; private final JavaMemberLookupConfig lookupConfig; + private final TemplateGlobalBuildItem global; NamespaceResult(String namespace, ClassInfo rootClazz, TypeInfo dataNamespaceExpTypeInfo, - TemplateDataBuildItem templateData, - List namespaceExtensionMethods, boolean ignoring, - JavaMemberLookupConfig lookupConfig) { + TemplateDataBuildItem templateData, List namespaceExtensionMethods, + boolean ignoring, + JavaMemberLookupConfig lookupConfig, TemplateGlobalBuildItem global) { this.namespace = namespace; this.rootClazz = rootClazz; this.dataNamespaceExpTypeInfo = dataNamespaceExpTypeInfo; @@ -1511,6 +1550,7 @@ private static class NamespaceResult { this.extensionMethods = namespaceExtensionMethods; this.ignoring = ignoring; this.lookupConfig = lookupConfig; + this.global = global; } boolean hasExtensionMethods() { @@ -1529,6 +1569,10 @@ boolean hasLookupConfig() { return lookupConfig != null; } + boolean hasGlobal() { + return global != null; + } + boolean isIn(String... values) { for (String value : values) { if (value.equals(namespace)) { @@ -1595,7 +1639,8 @@ private static void validateParametersOfNestedVirtualMethods(QuteConfig config, Map namespaceTemplateData, List regularExtensionMethods, Map> namespaceExtensionMethods, - AssignabilityCheck assignabilityCheck) { + AssignabilityCheck assignabilityCheck, + List globals) { for (Expression.Part part : expression.getParts()) { if (part.isVirtualMethod()) { for (Expression param : part.asVirtualMethod().getParameters()) { @@ -1607,7 +1652,8 @@ private static void validateParametersOfNestedVirtualMethods(QuteConfig config, validateNestedExpressions(config, templateAnalysis, null, results, excludes, incorrectExpressions, param, index, implicitClassToMembersUsed, templateIdToPathFun, generatedIdsToMatches, extensionMethodExcludes, checkedTemplate, lookupConfig, namedBeans, - namespaceTemplateData, regularExtensionMethods, namespaceExtensionMethods, assignabilityCheck); + namespaceTemplateData, regularExtensionMethods, namespaceExtensionMethods, assignabilityCheck, + globals); } } } @@ -1834,7 +1880,7 @@ void generateValueResolvers(QuteConfig config, BuildProducer generatedResolvers, BuildProducer reflectiveClass, - BuildProducer generatedInitializers) { + BuildProducer globalProviders) { if (!incorrectExpressions.isEmpty()) { // Skip generation if a validation error occurs @@ -2004,7 +2050,7 @@ public Function apply(ClassInfo clazz) { } if (!templateGlobals.isEmpty()) { - TemplateGlobalGenerator globalGenerator = new TemplateGlobalGenerator(classOutput); + TemplateGlobalGenerator globalGenerator = new TemplateGlobalGenerator(classOutput, GLOBAL_NAMESPACE, -1000, index); Map> classToTargets = new HashMap<>(); Map> classToGlobals = templateGlobals.stream() @@ -2019,7 +2065,7 @@ public Function apply(ClassInfo clazz) { } for (String generatedType : globalGenerator.getGeneratedTypes()) { - generatedInitializers.produce(new GeneratedTemplateInitializerBuildItem(generatedType)); + globalProviders.produce(new TemplateGlobalProviderBuildItem(generatedType)); reflectiveClass.produce(ReflectiveClassBuildItem.builder(generatedType).build()); } } @@ -2399,7 +2445,7 @@ public boolean test(TypeCheck check) { void initialize(BuildProducer syntheticBeans, QuteRecorder recorder, List generatedValueResolvers, List templatePaths, Optional templateVariants, - List templateInitializers, + List templateInitializers, TemplateRootsBuildItem templateRoots) { List templates = new ArrayList<>(); @@ -2424,7 +2470,7 @@ void initialize(BuildProducer syntheticBeans, QuteRecord .supplier(recorder.createContext(generatedValueResolvers.stream() .map(GeneratedValueResolverBuildItem::getClassName).collect(Collectors.toList()), templates, tags, variants, templateInitializers.stream() - .map(GeneratedTemplateInitializerBuildItem::getClassName).collect(Collectors.toList()), + .map(TemplateGlobalProviderBuildItem::getClassName).collect(Collectors.toList()), templateRoots.getPaths().stream().map(p -> p + "/").collect(Collectors.toSet()))) .done()); } diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/GeneratedTemplateInitializerBuildItem.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplateGlobalProviderBuildItem.java similarity index 60% rename from extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/GeneratedTemplateInitializerBuildItem.java rename to extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplateGlobalProviderBuildItem.java index 3101fa3a59c345..d02a84ee06beff 100644 --- a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/GeneratedTemplateInitializerBuildItem.java +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplateGlobalProviderBuildItem.java @@ -2,11 +2,11 @@ import io.quarkus.builder.item.MultiBuildItem; -public final class GeneratedTemplateInitializerBuildItem extends MultiBuildItem { +public final class TemplateGlobalProviderBuildItem extends MultiBuildItem { private final String className; - public GeneratedTemplateInitializerBuildItem(String className) { + public TemplateGlobalProviderBuildItem(String className) { this.className = className; } diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/devmode/NoRestartRoute.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/devmode/NoRestartRoute.java index 454868cd93d710..43f088655cfda7 100644 --- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/devmode/NoRestartRoute.java +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/devmode/NoRestartRoute.java @@ -6,6 +6,7 @@ import jakarta.inject.Inject; import jakarta.inject.Singleton; +import io.quarkus.qute.Location; import io.quarkus.qute.Template; import io.quarkus.vertx.web.Route; import io.vertx.ext.web.RoutingContext; @@ -15,12 +16,20 @@ public class NoRestartRoute { private String id; - @Inject + @Location("foo/norestart") Template norestart; + @Inject + Template bar; + @Route(path = "norestart") public void test(RoutingContext ctx) { - ctx.end(norestart.data("foo", id).render()); + ctx.end(norestart.data("id", id).render()); + } + + @Route(path = "bar") + public void testBar(RoutingContext ctx) { + ctx.end(bar.data("id", id).render()); } @PostConstruct diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/devmode/NoRestartTemplatesDevModeTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/devmode/NoRestartTemplatesDevModeTest.java index c7d9dca8e67172..5636fa2b777827 100644 --- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/devmode/NoRestartTemplatesDevModeTest.java +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/devmode/NoRestartTemplatesDevModeTest.java @@ -18,10 +18,13 @@ public class NoRestartTemplatesDevModeTest { .withApplicationRoot(root -> root .addClass(NoRestartRoute.class) .addAsResource(new StringAsset( - "Hello {foo}!"), - "templates/norestart.html") + "Hello {id}!"), + "templates/foo/norestart.html") .addAsResource(new StringAsset( - "quarkus.qute.dev-mode.no-restart-templates=templates/norestart.html"), + "Hi {id}!"), + "templates/bar.html") + .addAsResource(new StringAsset( + "quarkus.qute.dev-mode.no-restart-templates=templates/.+"), "application.properties")); @Test @@ -29,14 +32,25 @@ public void testNoRestartTemplates() { Response resp = given().get("norestart"); resp.then() .statusCode(200); - String val = resp.getBody().asString(); - assertTrue(val.startsWith("Hello ")); + String val1 = resp.getBody().asString(); + assertTrue(val1.startsWith("Hello ")); + + resp = given().get("bar"); + resp.then() + .statusCode(200); + String val2 = resp.getBody().asString(); + assertTrue(val2.startsWith("Hi ")); - config.modifyResourceFile("templates/norestart.html", t -> t.concat("!!")); + config.modifyResourceFile("templates/foo/norestart.html", t -> t.concat("!!")); + config.modifyResourceFile("templates/bar.html", t -> t.concat("!!")); resp = given().get("norestart"); resp.then().statusCode(200); - assertEquals(val + "!!", resp.getBody().asString()); + assertEquals(val1 + "!!", resp.getBody().asString()); + + resp = given().get("bar"); + resp.then().statusCode(200); + assertEquals(val2 + "!!", resp.getBody().asString()); } } diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/globals/TemplateGlobalNamespaceValidationFailureTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/globals/TemplateGlobalNamespaceValidationFailureTest.java new file mode 100644 index 00000000000000..5e07de37c89455 --- /dev/null +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/globals/TemplateGlobalNamespaceValidationFailureTest.java @@ -0,0 +1,55 @@ +package io.quarkus.qute.deployment.globals; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.qute.TemplateException; +import io.quarkus.qute.TemplateGlobal; +import io.quarkus.test.QuarkusUnitTest; + +public class TemplateGlobalNamespaceValidationFailureTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot(root -> root + .addClasses(Globals.class) + .addAsResource(new StringAsset( + "Hello {global:user.name}!"), + "templates/hello.txt")) + .assertException(t -> { + Throwable e = t; + TemplateException te = null; + while (e != null) { + if (e instanceof TemplateException) { + te = (TemplateException) e; + break; + } + e = e.getCause(); + } + assertNotNull(te); + assertTrue( + te.getMessage().contains("Found incorrect expressions (1)"), te.getMessage()); + assertTrue( + te.getMessage().contains( + "Property/method [name] not found on class [java.lang.String] nor handled by an extension method"), + te.getMessage()); + }); + + @Test + public void test() { + fail(); + } + + public static class Globals { + + @TemplateGlobal + static String user = "Fu"; + + } + +} diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/globals/TemplateGlobalTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/globals/TemplateGlobalTest.java index de372ee5c691a0..b5b0388ecca852 100644 --- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/globals/TemplateGlobalTest.java +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/globals/TemplateGlobalTest.java @@ -20,7 +20,7 @@ public class TemplateGlobalTest { .withApplicationRoot(root -> root .addClasses(Globals.class, NextGlobals.class) .addAsResource(new StringAsset( - "Hello {currentUser}! Your name is {_name}. You're {age} years old."), + "Hello {currentUser}|{global:currentUser}! Your name is {_name}|{global:_name}. You're {age}|{global:age} years old."), "templates/hello.txt")); @Inject @@ -28,15 +28,19 @@ public class TemplateGlobalTest { @Test public void testTemplateData() { - assertEquals("Hello Fu! Your name is Lu. You're 40 years old.", hello.render()); - assertEquals("Hello Fu! Your name is Lu. You're 40 years old.", - Qute.fmt("Hello {currentUser}! Your name is {_name}. You're {age} years old.").render()); + assertEquals("Hello Fu|Fu! Your name is Lu|Lu. You're 40|40 years old.", hello.render()); + assertEquals("Hello Fu|Fu! Your name is Lu|Lu. You're 40|40 years old.", + Qute.fmt( + "Hello {currentUser}|{global:currentUser}! Your name is {_name}|{global:_name}. You're {age}|{global:age} years old.") + .render()); Globals.user = "Hu"; - assertEquals("Hello Hu! Your name is Lu. You're 20 years old.", hello.render()); - assertEquals("Hello Hu! Your name is Lu. You're 20 years old.", - Qute.fmt("Hello {currentUser}! Your name is {_name}. You're {age} years old.").render()); + assertEquals("Hello Hu|Hu! Your name is Lu|Lu. You're 20|20 years old.", hello.render()); + assertEquals("Hello Hu|Hu! Your name is Lu|Lu. You're 20|20 years old.", + Qute.fmt( + "Hello {currentUser}|{global:currentUser}! Your name is {_name}|{global:_name}. You're {age}|{global:age} years old.") + .render()); - assertEquals("First color is: RED", Qute.fmt("First color is: {colors[0]}").render()); + assertEquals("First color is: RED|RED", Qute.fmt("First color is: {colors[0]}|{global:colors[0]}").render()); } public static class Globals { diff --git a/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/EngineProducer.java b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/EngineProducer.java index 9bcf522449dc66..48a5360c3e92eb 100644 --- a/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/EngineProducer.java +++ b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/EngineProducer.java @@ -46,6 +46,7 @@ import io.quarkus.qute.Results; import io.quarkus.qute.SectionHelperFactory; import io.quarkus.qute.Template; +import io.quarkus.qute.TemplateGlobalProvider; import io.quarkus.qute.TemplateInstance; import io.quarkus.qute.TemplateInstance.Initializer; import io.quarkus.qute.TemplateLocator; @@ -204,9 +205,11 @@ public EngineProducer(QuteContext context, QuteConfig config, QuteRuntimeConfig // Add a special parser hook for Qute.fmt() methods builder.addParserHook(new Qute.IndexedArgumentsParserHook()); - // Add template initializers - for (String initializerClass : context.getTemplateInstanceInitializerClasses()) { - builder.addTemplateInstanceInitializer(createInitializer(initializerClass)); + // Add global providers + for (String globalProviderClass : context.getTemplateGlobalProviderClasses()) { + TemplateGlobalProvider provider = createGlobalProvider(globalProviderClass); + builder.addTemplateInstanceInitializer(provider); + builder.addNamespaceResolver(provider); } // Add a special initializer for templates that contain an inject/cdi namespace expressions @@ -313,17 +316,17 @@ private Resolver createResolver(String resolverClassName) { } } - private TemplateInstance.Initializer createInitializer(String initializerClassName) { + private TemplateGlobalProvider createGlobalProvider(String initializerClassName) { try { Class initializerClazz = Thread.currentThread() .getContextClassLoader().loadClass(initializerClassName); - if (TemplateInstance.Initializer.class.isAssignableFrom(initializerClazz)) { - return (TemplateInstance.Initializer) initializerClazz.getDeclaredConstructor().newInstance(); + if (TemplateGlobalProvider.class.isAssignableFrom(initializerClazz)) { + return (TemplateGlobalProvider) initializerClazz.getDeclaredConstructor().newInstance(); } - throw new IllegalStateException("Not an initializer: " + initializerClazz); + throw new IllegalStateException("Not a global provider: " + initializerClazz); } catch (InstantiationException | IllegalAccessException | ClassNotFoundException | IllegalArgumentException | InvocationTargetException | NoSuchMethodException | SecurityException e) { - throw new IllegalStateException("Unable to create initializer: " + initializerClassName, e); + throw new IllegalStateException("Unable to create global provider: " + initializerClassName, e); } } diff --git a/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/QuteDevModeConfig.java b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/QuteDevModeConfig.java index be0787fc88113f..843a08b3eadcfe 100644 --- a/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/QuteDevModeConfig.java +++ b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/QuteDevModeConfig.java @@ -15,8 +15,8 @@ public class QuteDevModeConfig { * This regular expression can be used to specify the templates for which the application is not restarted. * I.e. the templates are reloaded and only runtime validations are performed. *

- * The matched input is the template path relative from the {@code templates} directory and the - * {@code /} is used as a path separator. For example, {@code templates/foo.html}. + * The matched input is the template path that starts with a template root, and the {@code /} is used as a path separator. + * For example, {@code templates/foo.html}. */ @ConfigItem public Optional noRestartTemplates; diff --git a/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/QuteRecorder.java b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/QuteRecorder.java index 17a2caee2ddba9..0fff3270c57354 100644 --- a/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/QuteRecorder.java +++ b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/QuteRecorder.java @@ -12,7 +12,7 @@ public class QuteRecorder { public Supplier createContext(List resolverClasses, List templatePaths, List tags, Map> variants, - List templateInstanceInitializerClasses, Set templateRoots) { + List templateGlobalProviderClasses, Set templateRoots) { return new Supplier() { @Override @@ -40,8 +40,8 @@ public Map> getVariants() { } @Override - public List getTemplateInstanceInitializerClasses() { - return templateInstanceInitializerClasses; + public List getTemplateGlobalProviderClasses() { + return templateGlobalProviderClasses; } @Override @@ -63,7 +63,7 @@ public interface QuteContext { Map> getVariants(); - List getTemplateInstanceInitializerClasses(); + List getTemplateGlobalProviderClasses(); Set getTemplateRoots(); diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/pom.xml b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/pom.xml index 87da39c8b69fc5..f357b700c7e4cf 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/pom.xml +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/pom.xml @@ -50,7 +50,11 @@ quarkus-jaxrs-client-reactive-deployment test - + + io.quarkus + quarkus-security-test-utils + test + diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/main/java/io/quarkus/resteasy/reactive/jackson/deployment/processor/ResteasyReactiveJacksonProcessor.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/main/java/io/quarkus/resteasy/reactive/jackson/deployment/processor/ResteasyReactiveJacksonProcessor.java index 74f4cc444bc450..f40508ca116653 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/main/java/io/quarkus/resteasy/reactive/jackson/deployment/processor/ResteasyReactiveJacksonProcessor.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/main/java/io/quarkus/resteasy/reactive/jackson/deployment/processor/ResteasyReactiveJacksonProcessor.java @@ -1,5 +1,6 @@ package io.quarkus.resteasy.reactive.jackson.deployment.processor; +import static io.quarkus.security.spi.RolesAllowedConfigExpResolverBuildItem.isSecurityConfigExpressionCandidate; import static org.jboss.resteasy.reactive.common.util.RestMediaType.APPLICATION_NDJSON; import static org.jboss.resteasy.reactive.common.util.RestMediaType.APPLICATION_STREAM_JSON; @@ -10,7 +11,10 @@ import java.util.Locale; import java.util.Optional; import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.Supplier; +import jakarta.inject.Singleton; import jakarta.ws.rs.Priorities; import jakarta.ws.rs.RuntimeType; import jakarta.ws.rs.core.Cookie; @@ -34,13 +38,20 @@ import com.fasterxml.jackson.databind.exc.MismatchedInputException; import io.quarkus.arc.deployment.AdditionalBeanBuildItem; +import io.quarkus.arc.deployment.SynthesisFinishedBuildItem; +import io.quarkus.arc.deployment.SyntheticBeanBuildItem; +import io.quarkus.builder.item.SimpleBuildItem; +import io.quarkus.deployment.Capabilities; +import io.quarkus.deployment.Capability; import io.quarkus.deployment.Feature; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.Consume; import io.quarkus.deployment.annotations.ExecutionTime; import io.quarkus.deployment.annotations.Record; import io.quarkus.deployment.builditem.CombinedIndexBuildItem; import io.quarkus.deployment.builditem.FeatureBuildItem; +import io.quarkus.deployment.builditem.RuntimeConfigSetupCompleteBuildItem; import io.quarkus.deployment.builditem.ShutdownContextBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; import io.quarkus.resteasy.reactive.common.deployment.JaxRsResourceIndexBuildItem; @@ -54,6 +65,7 @@ import io.quarkus.resteasy.reactive.jackson.runtime.ResteasyReactiveServerJacksonRecorder; import io.quarkus.resteasy.reactive.jackson.runtime.mappers.DefaultMismatchedInputException; import io.quarkus.resteasy.reactive.jackson.runtime.mappers.NativeInvalidDefinitionExceptionMapper; +import io.quarkus.resteasy.reactive.jackson.runtime.security.RolesAllowedConfigExpStorage; import io.quarkus.resteasy.reactive.jackson.runtime.security.SecurityCustomSerialization; import io.quarkus.resteasy.reactive.jackson.runtime.serialisers.BasicServerJacksonMessageBodyWriter; import io.quarkus.resteasy.reactive.jackson.runtime.serialisers.FullyFeaturedServerJacksonMessageBodyReader; @@ -69,6 +81,7 @@ import io.quarkus.resteasy.reactive.spi.ExceptionMapperBuildItem; import io.quarkus.resteasy.reactive.spi.MessageBodyReaderBuildItem; import io.quarkus.resteasy.reactive.spi.MessageBodyWriterBuildItem; +import io.quarkus.security.spi.RolesAllowedConfigExpResolverBuildItem; import io.quarkus.vertx.deployment.ReinitializeVertxJsonBuildItem; import io.vertx.core.json.JsonArray; import io.vertx.core.json.JsonObject; @@ -310,6 +323,46 @@ void handleJsonAnnotations(Optional resourceSca } } + @Record(ExecutionTime.STATIC_INIT) + @BuildStep + public void resolveRolesAllowedConfigExpressions(BuildProducer resolverProducer, + Capabilities capabilities, ResteasyReactiveServerJacksonRecorder recorder, CombinedIndexBuildItem indexBuildItem, + BuildProducer syntheticBeanProducer, + BuildProducer initAndValidateItemProducer) { + if (capabilities.isPresent(Capability.SECURITY)) { + BiConsumer> configValRecorder = null; + for (AnnotationInstance instance : indexBuildItem.getIndex().getAnnotations(SECURE_FIELD)) { + for (String role : instance.value("rolesAllowed").asStringArray()) { + if (isSecurityConfigExpressionCandidate(role)) { + if (configValRecorder == null) { + var storage = recorder.createConfigExpToAllowedRoles(); + configValRecorder = recorder.recordRolesAllowedConfigExpression(storage); + syntheticBeanProducer.produce(SyntheticBeanBuildItem + .configure(RolesAllowedConfigExpStorage.class) + .scope(Singleton.class) + .supplier(recorder.createRolesAllowedConfigExpStorage(storage)) + .unremovable() + .done()); + initAndValidateItemProducer.produce(new InitAndValidateRolesAllowedConfigExp()); + } + resolverProducer.produce(new RolesAllowedConfigExpResolverBuildItem(role, configValRecorder)); + } + } + } + } + } + + @Record(ExecutionTime.RUNTIME_INIT) + @BuildStep + @Consume(RuntimeConfigSetupCompleteBuildItem.class) + @Consume(SynthesisFinishedBuildItem.class) + public void initializeRolesAllowedConfigExp(ResteasyReactiveServerJacksonRecorder recorder, + Optional initAndValidateItem) { + if (initAndValidateItem.isPresent()) { + recorder.initAndValidateRolesAllowedConfigExp(); + } + } + @BuildStep public void handleFieldSecurity(ResteasyReactiveResourceMethodEntriesBuildItem resourceMethodEntries, JaxRsResourceIndexBuildItem index, @@ -432,4 +485,13 @@ private String getMethodId(MethodInfo methodInfo, ClassInfo declaringClassInfo) return MethodId.get(methodInfo.name(), declaringClassInfo.name().toString(), parameterClassNames.toArray(EMPTY_STRING_ARRAY)); } + + /** + * Purely marker build item so that we know at least one allowed role with configuration + * expressions has been detected. + */ + public static final class InitAndValidateRolesAllowedConfigExp extends SimpleBuildItem { + private InitAndValidateRolesAllowedConfigExp() { + } + } } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/AbstractPersonResource.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/AbstractPersonResource.java index 00bb0ab2d3d512..5c9c00326443e0 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/AbstractPersonResource.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/AbstractPersonResource.java @@ -11,6 +11,8 @@ public Person abstractPerson() { Person person = new Person(); person.setFirst("Bob"); person.setLast("Builder"); + person.setAddress("10 Downing St"); + person.setBirthDate("November 30, 1874"); return person; } } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/CustomSerializationResource.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/CustomSerializationResource.java index 6bbb582914d7a5..becd3f13bed801 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/CustomSerializationResource.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/CustomSerializationResource.java @@ -44,6 +44,8 @@ public Person getPerson() { Person person = new Person(); person.setFirst("Bob"); person.setLast("Builder"); + person.setAddress("10 Downing St"); + person.setBirthDate("November 30, 1874"); return person; } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/Person.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/Person.java index 7f5abadd4cc69c..133e3a9f619e4e 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/Person.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/Person.java @@ -17,6 +17,12 @@ public class Person { @JsonView(Views.Private.class) public int id = 0; + @SecureField(rolesAllowed = { "${admin-expression:disabled}", "${user-expression:disabled}" }) + private String address; + + @SecureField(rolesAllowed = "${birth-date-roles:disabled}") + private String birthDate; + public String getFirst() { return first; } @@ -40,4 +46,20 @@ public int getId() { public void setId(int id) { this.id = id; } + + public String getAddress() { + return address; + } + + public void setAddress(String address) { + this.address = address; + } + + public String getBirthDate() { + return birthDate; + } + + public void setBirthDate(String birthDate) { + this.birthDate = birthDate; + } } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonResource.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonResource.java index b538997b62f25a..9621a6fdde84a2 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonResource.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonResource.java @@ -56,6 +56,8 @@ public Person getPerson() { Person person = new Person(); person.setFirst("Bob"); person.setLast("Builder"); + person.setAddress("10 Downing St"); + person.setBirthDate("November 30, 1874"); return person; } @@ -247,6 +249,8 @@ public void run() { Person person = new Person(); person.setFirst("Bob"); person.setLast("Builder"); + person.setAddress("10 Downing St"); + person.setBirthDate("November 30, 1874"); response.resume(person); } }).start(); @@ -285,6 +289,8 @@ public Multi getMulti1() { Person person = new Person(); person.setFirst("Bob"); person.setLast("Builder"); + person.setAddress("10 Downing St"); + person.setBirthDate("November 30, 1874"); return Multi.createFrom().items(person); } @@ -294,6 +300,8 @@ public Multi getMulti2() { Person person = new Person(); person.setFirst("Bob"); person.setLast("Builder"); + person.setAddress("10 Downing St"); + person.setBirthDate("November 30, 1874"); Person person2 = new Person(); person2.setFirst("Bob2"); person2.setLast("Builder2"); diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonTest.java index 9ebb1b30d6d5ea..a36cd7a2aef681 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonTest.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonTest.java @@ -11,10 +11,13 @@ import org.hamcrest.Matchers; import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; import org.jboss.shrinkwrap.api.spec.JavaArchive; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; +import io.quarkus.security.test.utils.TestIdentityController; +import io.quarkus.security.test.utils.TestIdentityProvider; import io.quarkus.test.QuarkusUnitTest; import io.restassured.RestAssured; @@ -28,7 +31,10 @@ public JavaArchive get() { return ShrinkWrap.create(JavaArchive.class) .addClasses(Person.class, SimpleJsonResource.class, User.class, Views.class, SuperClass.class, OtherPersonResource.class, AbstractPersonResource.class, DataItem.class, Item.class, - NoopReaderInterceptor.class); + NoopReaderInterceptor.class, TestIdentityProvider.class, TestIdentityController.class) + .addAsResource(new StringAsset("admin-expression=admin\n" + + "user-expression=user\n" + + "birth-date-roles=alice,bob\n"), "application.properties"); } }); @@ -389,6 +395,52 @@ public void testSecureRestResponsePerson() { doTestSecurePerson("/simple", "/secure-rest-response-person"); } + @Test + public void testSecureFieldRolesAllowedConfigExp() { + TestIdentityController.resetRoles().add("max", "max", "admin"); + RestAssured.given() + .auth().preemptive().basic("max", "max") + .get("/simple/secure-person") + .then() + .statusCode(200) + .contentType("application/json") + .header("transfer-encoding", nullValue()) + .header("content-length", notNullValue()) + .body(containsString("Bob")) + .body(containsString("0")) + .body(containsString("10 Downing St")) + .body(not(containsString("November 30, 1874"))) + .body(containsString("Builder")); + TestIdentityController.resetRoles().add("max", "max", "user"); + RestAssured.given() + .auth().preemptive().basic("max", "max") + .get("/simple/secure-person") + .then() + .statusCode(200) + .contentType("application/json") + .header("transfer-encoding", nullValue()) + .header("content-length", notNullValue()) + .body(containsString("Bob")) + .body(containsString("0")) + .body(containsString("10 Downing St")) + .body(not(containsString("November 30, 1874"))) + .body(not(containsString("Builder"))); + TestIdentityController.resetRoles().add("max", "max", "alice"); + RestAssured.given() + .auth().preemptive().basic("max", "max") + .get("/simple/secure-person") + .then() + .statusCode(200) + .contentType("application/json") + .header("transfer-encoding", nullValue()) + .header("content-length", notNullValue()) + .body(containsString("Bob")) + .body(containsString("0")) + .body(not(containsString("10 Downing St"))) + .body(containsString("November 30, 1874")) + .body(not(containsString("Builder"))); + } + private void doTestSecurePerson(String basePath, final String path) { RestAssured.get(basePath + path) .then() @@ -398,6 +450,8 @@ private void doTestSecurePerson(String basePath, final String path) { .header("content-length", notNullValue()) .body(containsString("Bob")) .body(containsString("0")) + .body(not(containsString("10 Downing St"))) + .body(not(containsString("November 30, 1874"))) .body(not(containsString("Builder"))); } @@ -410,6 +464,8 @@ private void doTestSecurePersonWithPublicView(String basePath, final String path .header("content-length", notNullValue()) .body(containsString("Bob")) .body(not(containsString("0"))) + .body(not(containsString("10 Downing St"))) + .body(not(containsString("November 30, 1874"))) .body(not(containsString("Builder"))); } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/runtime/src/main/java/io/quarkus/resteasy/reactive/jackson/runtime/ResteasyReactiveServerJacksonRecorder.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/runtime/src/main/java/io/quarkus/resteasy/reactive/jackson/runtime/ResteasyReactiveServerJacksonRecorder.java index 2e86508b9f498f..a55ddadc4e3cef 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/runtime/src/main/java/io/quarkus/resteasy/reactive/jackson/runtime/ResteasyReactiveServerJacksonRecorder.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/runtime/src/main/java/io/quarkus/resteasy/reactive/jackson/runtime/ResteasyReactiveServerJacksonRecorder.java @@ -3,12 +3,18 @@ import java.lang.reflect.Type; import java.util.HashMap; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.BiConsumer; import java.util.function.BiFunction; +import java.util.function.Supplier; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectReader; import com.fasterxml.jackson.databind.ObjectWriter; +import io.quarkus.arc.Arc; +import io.quarkus.resteasy.reactive.jackson.runtime.security.RolesAllowedConfigExpStorage; +import io.quarkus.runtime.RuntimeValue; import io.quarkus.runtime.ShutdownContext; import io.quarkus.runtime.annotations.Recorder; @@ -19,6 +25,45 @@ public class ResteasyReactiveServerJacksonRecorder { private static final Map> customSerializationMap = new HashMap<>(); private static final Map> customDeserializationMap = new HashMap<>(); + /* STATIC INIT */ + public RuntimeValue>> createConfigExpToAllowedRoles() { + return new RuntimeValue<>(new ConcurrentHashMap<>()); + } + + /* STATIC INIT */ + public BiConsumer> recordRolesAllowedConfigExpression( + RuntimeValue>> configExpToAllowedRoles) { + return new BiConsumer>() { + @Override + public void accept(String configKey, Supplier configValueSupplier) { + configExpToAllowedRoles.getValue().put(configKey, configValueSupplier); + } + }; + } + + /* STATIC INIT */ + public Supplier createRolesAllowedConfigExpStorage( + RuntimeValue>> configExpToAllowedRoles) { + return new Supplier() { + @Override + public RolesAllowedConfigExpStorage get() { + Map> map = configExpToAllowedRoles.getValue(); + if (map.isEmpty()) { + // there is no reason why this should happen, because we initialize the bean ourselves + // when runtime configuration is ready + throw new IllegalStateException( + "The 'RolesAllowedConfigExpStorage' bean is created before runtime configuration is ready"); + } + return new RolesAllowedConfigExpStorage(configExpToAllowedRoles.getValue()); + } + }; + } + + /* RUNTIME INIT */ + public void initAndValidateRolesAllowedConfigExp() { + Arc.container().instance(RolesAllowedConfigExpStorage.class).get().resolveRolesAllowedConfigExp(); + } + public void recordJsonView(String targetId, String className) { jsonViewMap.put(targetId, loadClass(className)); } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/runtime/src/main/java/io/quarkus/resteasy/reactive/jackson/runtime/security/RolesAllowedConfigExpStorage.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/runtime/src/main/java/io/quarkus/resteasy/reactive/jackson/runtime/security/RolesAllowedConfigExpStorage.java new file mode 100644 index 00000000000000..e1ef44af6bed1e --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/runtime/src/main/java/io/quarkus/resteasy/reactive/jackson/runtime/security/RolesAllowedConfigExpStorage.java @@ -0,0 +1,34 @@ +package io.quarkus.resteasy.reactive.jackson.runtime.security; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Supplier; + +public class RolesAllowedConfigExpStorage { + + private final Map> configExpToAllowedRoles; + private final Map rolesAllowedExpCache; + + public RolesAllowedConfigExpStorage(Map> configExpToAllowedRoles) { + this.configExpToAllowedRoles = Map.copyOf(configExpToAllowedRoles); + this.rolesAllowedExpCache = new HashMap<>(); + } + + /** + * Transforms configuration expressions to configuration values. + * Should be called on startup once runtime config is ready. + */ + public synchronized void resolveRolesAllowedConfigExp() { + if (rolesAllowedExpCache.isEmpty()) { + for (Map.Entry> e : configExpToAllowedRoles.entrySet()) { + String roleConfigExp = e.getKey(); + Supplier rolesSupplier = e.getValue(); + rolesAllowedExpCache.put(roleConfigExp, rolesSupplier.get()); + } + } + } + + String[] getRoles(String configExpression) { + return rolesAllowedExpCache.get(configExpression); + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/runtime/src/main/java/io/quarkus/resteasy/reactive/jackson/runtime/security/SecurityPropertyFilter.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/runtime/src/main/java/io/quarkus/resteasy/reactive/jackson/runtime/security/SecurityPropertyFilter.java index e361614fb7f3b9..7d08f8b0fce5c5 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/runtime/src/main/java/io/quarkus/resteasy/reactive/jackson/runtime/security/SecurityPropertyFilter.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/runtime/src/main/java/io/quarkus/resteasy/reactive/jackson/runtime/security/SecurityPropertyFilter.java @@ -12,6 +12,23 @@ public class SecurityPropertyFilter extends SimpleBeanPropertyFilter { static final String FILTER_ID = "securityFilter"; + private volatile InstanceHandle rolesAllowedConfigExpStorage; + + private RolesAllowedConfigExpStorage getRolesAllowedConfigExpStorage(ArcContainer container) { + if (rolesAllowedConfigExpStorage == null) { + synchronized (this) { + if (rolesAllowedConfigExpStorage == null) { + rolesAllowedConfigExpStorage = container.instance(RolesAllowedConfigExpStorage.class); + } + } + } + + if (rolesAllowedConfigExpStorage.isAvailable()) { + return rolesAllowedConfigExpStorage.get(); + } else { + return null; + } + } @Override protected boolean include(PropertyWriter writer) { @@ -31,7 +48,21 @@ protected boolean include(PropertyWriter writer) { } SecurityIdentity securityIdentity = instance.get(); + RolesAllowedConfigExpStorage rolesConfigExpStorage = getRolesAllowedConfigExpStorage(container); for (String role : secureField.rolesAllowed()) { + if (rolesConfigExpStorage != null) { + // role config expression => resolved roles + String[] roles = rolesConfigExpStorage.getRoles(role); + if (roles != null) { + for (String r : roles) { + if (securityIdentity.hasRole(r)) { + return true; + } + } + continue; + } + // at this point, we know 'role' is not a configuration expression + } if (securityIdentity.hasRole(role)) { return true; } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/providers/FileTestCase.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/providers/FileTestCase.java index c9adc3def2c488..80f99ddf1105dd 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/providers/FileTestCase.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/providers/FileTestCase.java @@ -41,6 +41,21 @@ public void testFiles() throws Exception { .statusCode(200) .header(HttpHeaders.CONTENT_LENGTH, contentLength) .body(Matchers.equalTo(content)); + RestAssured.given().header("Range", "bytes=0-9").get("/providers/file/file") + .then() + .statusCode(206) + .header(HttpHeaders.CONTENT_LENGTH, "10") + .body(Matchers.equalTo(content.substring(0, 10))); + RestAssured.given().header("Range", "bytes=10-19").get("/providers/file/file") + .then() + .statusCode(206) + .header(HttpHeaders.CONTENT_LENGTH, "10") + .body(Matchers.equalTo(content.substring(10, 20))); + RestAssured.given().header("Range", "bytes=10-").get("/providers/file/file") + .then() + .statusCode(206) + .header(HttpHeaders.CONTENT_LENGTH, String.valueOf(content.length() - 10)) + .body(Matchers.equalTo(content.substring(10))); RestAssured.get("/providers/file/file-partial") .then() .statusCode(200) diff --git a/extensions/resteasy-reactive/rest-client-reactive-jackson/deployment/src/test/java/io/quarkus/rest/client/reactive/jackson/test/MultiSseTest.java b/extensions/resteasy-reactive/rest-client-reactive-jackson/deployment/src/test/java/io/quarkus/rest/client/reactive/jackson/test/MultiSseTest.java index aa715e04fb9482..629b881a93bec0 100644 --- a/extensions/resteasy-reactive/rest-client-reactive-jackson/deployment/src/test/java/io/quarkus/rest/client/reactive/jackson/test/MultiSseTest.java +++ b/extensions/resteasy-reactive/rest-client-reactive-jackson/deployment/src/test/java/io/quarkus/rest/client/reactive/jackson/test/MultiSseTest.java @@ -8,15 +8,23 @@ import java.util.Objects; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; +import java.util.function.Predicate; import jakarta.ws.rs.GET; import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.Context; import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.sse.OutboundSseEvent; +import jakarta.ws.rs.sse.Sse; +import jakarta.ws.rs.sse.SseEventSink; import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; import org.jboss.resteasy.reactive.RestStreamElementType; +import org.jboss.resteasy.reactive.client.SseEvent; +import org.jboss.resteasy.reactive.client.SseEventFilter; import org.jboss.resteasy.reactive.server.jackson.JacksonBasicMessageBodyReader; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -112,6 +120,82 @@ void shouldRestStreamElementTypeOverwriteProducesAtClassLevel() { .containsExactly(new Dto("foo", "bar"), new Dto("chocolate", "bar"))); } + @Test + void shouldBeAbleReadEntireEvent() { + var resultList = new CopyOnWriteArrayList<>(); + createClient() + .event() + .subscribe().with(new Consumer<>() { + @Override + public void accept(SseEvent event) { + resultList.add(new EventContainer(event.id(), event.name(), event.data())); + } + }); + await().atMost(5, TimeUnit.SECONDS) + .untilAsserted( + () -> assertThat(resultList).containsExactly( + new EventContainer("id0", "name0", new Dto("name0", "0")), + new EventContainer("id1", "name1", new Dto("name1", "1")))); + } + + @Test + void shouldBeAbleReadEntireEventWhileAlsoBeingAbleToFilterEvents() { + var resultList = new CopyOnWriteArrayList<>(); + createClient() + .eventWithFilter() + .subscribe().with(new Consumer<>() { + @Override + public void accept(SseEvent event) { + resultList.add(new EventContainer(event.id(), event.name(), event.data())); + } + }); + await().atMost(5, TimeUnit.SECONDS) + .untilAsserted( + () -> assertThat(resultList).containsExactly( + new EventContainer("id", "n0", new Dto("name0", "0")), + new EventContainer("id", "n1", new Dto("name1", "1")), + new EventContainer("id", "n2", new Dto("name2", "2")))); + } + + static class EventContainer { + final String id; + final String name; + final Dto dto; + + EventContainer(String id, String name, Dto dto) { + this.id = id; + this.name = name; + this.dto = dto; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + EventContainer that = (EventContainer) o; + return Objects.equals(id, that.id) && Objects.equals(name, that.name) + && Objects.equals(dto, that.dto); + } + + @Override + public int hashCode() { + return Objects.hash(id, name, dto); + } + + @Override + public String toString() { + return "EventContainer{" + + "id='" + id + '\'' + + ", name='" + name + '\'' + + ", dto=" + dto + + '}'; + } + } + private SseClient createClient() { return QuarkusRestClientBuilder.newBuilder() .baseUri(uri) @@ -144,6 +228,31 @@ public interface SseClient { @Produces(MediaType.SERVER_SENT_EVENTS) @Path("/with-entity-json") Multi> postAndReadAsMap(String entity); + + @GET + @Path("/event") + @Produces(MediaType.SERVER_SENT_EVENTS) + Multi> event(); + + @GET + @Path("/event-with-filter") + @Produces(MediaType.SERVER_SENT_EVENTS) + @SseEventFilter(CustomFilter.class) + Multi> eventWithFilter(); + } + + public static class CustomFilter implements Predicate> { + + @Override + public boolean test(SseEvent event) { + if ("heartbeat".equals(event.id())) { + return false; + } + if ("END".equals(event.data())) { + return false; + } + return true; + } } @Path("/sse") @@ -175,6 +284,68 @@ public Multi post(String entity) { public Multi postAndReadAsMap(String entity) { return Multi.createBy().repeating().supplier(() -> new Dto("foo", entity)).atMost(3); } + + @GET + @Path("/event") + @Produces(MediaType.SERVER_SENT_EVENTS) + public void event(@Context SseEventSink sink, @Context Sse sse) { + // send a stream of few events + try (sink) { + for (int i = 0; i < 2; i++) { + final OutboundSseEvent.Builder builder = sse.newEventBuilder(); + builder.id("id" + i) + .mediaType(MediaType.APPLICATION_JSON_TYPE) + .data(Dto.class, new Dto("name" + i, String.valueOf(i))) + .name("name" + i); + + sink.send(builder.build()); + } + } + } + + @GET + @Path("/event-with-filter") + @Produces(MediaType.SERVER_SENT_EVENTS) + public void eventWithFilter(@Context SseEventSink sink, @Context Sse sse) { + try (sink) { + sink.send(sse.newEventBuilder() + .id("id") + .mediaType(MediaType.APPLICATION_JSON_TYPE) + .data(Dto.class, new Dto("name0", "0")) + .name("n0") + .build()); + + sink.send(sse.newEventBuilder() + .id("heartbeat") + .comment("heartbeat") + .mediaType(MediaType.APPLICATION_JSON_TYPE) + .build()); + + sink.send(sse.newEventBuilder() + .id("id") + .mediaType(MediaType.APPLICATION_JSON_TYPE) + .data(Dto.class, new Dto("name1", "1")) + .name("n1") + .build()); + + sink.send(sse.newEventBuilder() + .id("heartbeat") + .comment("heartbeat") + .build()); + + sink.send(sse.newEventBuilder() + .id("id") + .mediaType(MediaType.APPLICATION_JSON_TYPE) + .data(Dto.class, new Dto("name2", "2")) + .name("n2") + .build()); + + sink.send(sse.newEventBuilder() + .id("end") + .data("END") + .build()); + } + } } @Path("/sse-rest-stream-element-type") @@ -226,5 +397,13 @@ public boolean equals(Object o) { public int hashCode() { return Objects.hash(name, value); } + + @Override + public String toString() { + return "Dto{" + + "name='" + name + '\'' + + ", value='" + value + '\'' + + '}'; + } } } diff --git a/extensions/resteasy-reactive/rest-client-reactive/deployment/src/main/java/io/quarkus/rest/client/reactive/deployment/DotNames.java b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/main/java/io/quarkus/rest/client/reactive/deployment/DotNames.java index add3e44795d658..f635e470595a4e 100644 --- a/extensions/resteasy-reactive/rest-client-reactive/deployment/src/main/java/io/quarkus/rest/client/reactive/deployment/DotNames.java +++ b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/main/java/io/quarkus/rest/client/reactive/deployment/DotNames.java @@ -12,6 +12,7 @@ import org.eclipse.microprofile.rest.client.annotation.RegisterProviders; import org.eclipse.microprofile.rest.client.ext.ResponseExceptionMapper; import org.jboss.jandex.DotName; +import org.jboss.resteasy.reactive.client.SseEventFilter; import io.quarkus.rest.client.reactive.ClientExceptionMapper; import io.quarkus.rest.client.reactive.ClientFormParam; @@ -41,6 +42,8 @@ public class DotNames { static final DotName METHOD = DotName.createSimple(Method.class.getName()); + public static final DotName SSE_EVENT_FILTER = DotName.createSimple(SseEventFilter.class); + private DotNames() { } } diff --git a/extensions/resteasy-reactive/rest-client-reactive/deployment/src/main/java/io/quarkus/rest/client/reactive/deployment/RestClientReactiveProcessor.java b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/main/java/io/quarkus/rest/client/reactive/deployment/RestClientReactiveProcessor.java index 49ee3402e3daff..22a4b76f9b69ea 100644 --- a/extensions/resteasy-reactive/rest-client-reactive/deployment/src/main/java/io/quarkus/rest/client/reactive/deployment/RestClientReactiveProcessor.java +++ b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/main/java/io/quarkus/rest/client/reactive/deployment/RestClientReactiveProcessor.java @@ -64,6 +64,7 @@ import org.jboss.resteasy.reactive.common.util.QuarkusMultivaluedHashMap; import io.quarkus.arc.deployment.AdditionalBeanBuildItem; +import io.quarkus.arc.deployment.BeanArchiveIndexBuildItem; import io.quarkus.arc.deployment.CustomScopeAnnotationsBuildItem; import io.quarkus.arc.deployment.GeneratedBeanBuildItem; import io.quarkus.arc.deployment.GeneratedBeanGizmoAdaptor; @@ -371,6 +372,42 @@ void registerCompressionInterceptors(BuildProducer ref } } + @BuildStep + void handleSseEventFilter(BuildProducer reflectiveClasses, + BeanArchiveIndexBuildItem beanArchiveIndexBuildItem) { + var index = beanArchiveIndexBuildItem.getIndex(); + Collection instances = index.getAnnotations(DotNames.SSE_EVENT_FILTER); + if (instances.isEmpty()) { + return; + } + + List filterClassNames = new ArrayList<>(instances.size()); + for (AnnotationInstance instance : instances) { + if (instance.target().kind() != AnnotationTarget.Kind.METHOD) { + continue; + } + if (instance.value() == null) { + continue; // can't happen + } + Type filterType = instance.value().asClass(); + DotName filterClassName = filterType.name(); + ClassInfo filterClassInfo = index.getClassByName(filterClassName.toString()); + if (filterClassInfo == null) { + log.warn("Unable to find class '" + filterType.name() + "' in index"); + } else if (!filterClassInfo.hasNoArgsConstructor()) { + throw new RestClientDefinitionException( + "Classes used in @SseEventFilter must have a no-args constructor. Offending class is '" + + filterClassName + "'"); + } else { + filterClassNames.add(filterClassName.toString()); + } + } + reflectiveClasses.produce(ReflectiveClassBuildItem + .builder(filterClassNames.toArray(new String[0])) + .constructors(true) + .build()); + } + @BuildStep @Record(ExecutionTime.STATIC_INIT) void addRestClientBeans(Capabilities capabilities, diff --git a/extensions/security-jpa-common/deployment/src/main/java/io/quarkus/security/jpa/common/deployment/JpaSecurityIdentityUtil.java b/extensions/security-jpa-common/deployment/src/main/java/io/quarkus/security/jpa/common/deployment/JpaSecurityIdentityUtil.java index 5501ea44443538..054fcb33d6e69f 100644 --- a/extensions/security-jpa-common/deployment/src/main/java/io/quarkus/security/jpa/common/deployment/JpaSecurityIdentityUtil.java +++ b/extensions/security-jpa-common/deployment/src/main/java/io/quarkus/security/jpa/common/deployment/JpaSecurityIdentityUtil.java @@ -45,18 +45,21 @@ public static void buildIdentity(Index index, JpaSecurityDefinition jpaSecurityD PanacheEntityPredicateBuildItem panacheEntityPredicate, FieldDescriptor passwordProviderField, MethodCreator outerMethod, ResultHandle userVar, BytecodeCreator innerMethod) { // if(user == null) throw new AuthenticationFailedException(); + + PasswordType passwordType = passwordTypeValue != null ? PasswordType.valueOf(passwordTypeValue.asEnum()) + : PasswordType.MCF; + try (BytecodeCreator trueBranch = innerMethod.ifNull(userVar).trueBranch()) { + ResultHandle exceptionInstance = trueBranch .newInstance(MethodDescriptor.ofConstructor(AuthenticationFailedException.class)); + trueBranch.invokeStaticMethod(passwordActionMethod(), trueBranch.load(passwordType)); trueBranch.throwException(exceptionInstance); } // :pass = user.pass | user.getPass() ResultHandle pass = jpaSecurityDefinition.password.readValue(innerMethod, userVar); - PasswordType passwordType = passwordTypeValue != null ? PasswordType.valueOf(passwordTypeValue.asEnum()) - : PasswordType.MCF; - if (passwordType == PasswordType.CUSTOM && passwordProviderValue == null) { throw new RuntimeException("Missing password provider for password type: " + passwordType); } @@ -245,4 +248,8 @@ private static MethodDescriptor getUtilMethod(String passwordProviderMethod) { return MethodDescriptor.ofMethod(JpaIdentityProviderUtil.class, passwordProviderMethod, org.wildfly.security.password.Password.class, String.class); } + + private static MethodDescriptor passwordActionMethod() { + return MethodDescriptor.ofMethod(JpaIdentityProviderUtil.class, "passwordAction", void.class, PasswordType.class); + } } diff --git a/extensions/security-jpa-common/runtime/src/main/java/io/quarkus/security/jpa/common/runtime/JpaIdentityProviderUtil.java b/extensions/security-jpa-common/runtime/src/main/java/io/quarkus/security/jpa/common/runtime/JpaIdentityProviderUtil.java index a65f771596a5dc..15a3c4710d1c89 100644 --- a/extensions/security-jpa-common/runtime/src/main/java/io/quarkus/security/jpa/common/runtime/JpaIdentityProviderUtil.java +++ b/extensions/security-jpa-common/runtime/src/main/java/io/quarkus/security/jpa/common/runtime/JpaIdentityProviderUtil.java @@ -2,6 +2,7 @@ import java.security.spec.InvalidKeySpecException; import java.util.List; +import java.util.UUID; import org.wildfly.security.credential.PasswordCredential; import org.wildfly.security.evidence.PasswordGuessEvidence; @@ -10,9 +11,11 @@ import org.wildfly.security.password.util.ModularCrypt; import org.wildfly.security.provider.util.ProviderUtil; +import io.quarkus.elytron.security.common.BcryptUtil; import io.quarkus.security.AuthenticationFailedException; import io.quarkus.security.identity.request.TrustedAuthenticationRequest; import io.quarkus.security.identity.request.UsernamePasswordAuthenticationRequest; +import io.quarkus.security.jpa.PasswordType; import io.quarkus.security.runtime.QuarkusPrincipal; import io.quarkus.security.runtime.QuarkusSecurityIdentity; @@ -70,4 +73,13 @@ public static Password getMcfPassword(String pass) { throw new RuntimeException(e); } } + + public static void passwordAction(PasswordType type) { + String uuid = UUID.randomUUID().toString(); + if (type == PasswordType.CLEAR) { + ClearPassword.createRaw(ClearPassword.ALGORITHM_CLEAR, uuid.toCharArray()); + } else { + BcryptUtil.bcryptHash(uuid); + } + } } diff --git a/extensions/security-webauthn/runtime/src/main/java/io/quarkus/security/webauthn/WebAuthnRunTimeConfig.java b/extensions/security-webauthn/runtime/src/main/java/io/quarkus/security/webauthn/WebAuthnRunTimeConfig.java index 6433f257420063..54c2a9737e0eff 100644 --- a/extensions/security-webauthn/runtime/src/main/java/io/quarkus/security/webauthn/WebAuthnRunTimeConfig.java +++ b/extensions/security-webauthn/runtime/src/main/java/io/quarkus/security/webauthn/WebAuthnRunTimeConfig.java @@ -213,14 +213,14 @@ public static class RelyingPartyConfig { * referred to as "renewal-timeout". * * Note that smaller values will result in slightly more server load (as new encrypted cookies will be - * generated more often), however larger values affect the inactivity timeout as the timeout is set + * generated more often); however, larger values affect the inactivity timeout because the timeout is set * when a cookie is generated. * - * For example if this is set to 10 minutes, and the inactivity timeout is 30m, if a users last request - * is when the cookie is 9m old then the actual timeout will happen 21m after the last request, as the timeout + * For example if this is set to 10 minutes, and the inactivity timeout is 30m, if a user's last request + * is when the cookie is 9m old then the actual timeout will happen 21m after the last request because the timeout * is only refreshed when a new cookie is generated. * - * In other words, no timeout is tracked on the server side; the timestamp is encoded and encrypted in the cookie + * That is, no timeout is tracked on the server side; the timestamp is encoded and encrypted in the cookie * itself, and it is decrypted and parsed with each request. */ @ConfigItem(defaultValue = "PT1M") 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 6c4e61670bd02d..d781085f835ac3 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 @@ -105,6 +105,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.RolesAllowedConfigExpResolverBuildItem; import io.quarkus.security.spi.runtime.AuthorizationController; import io.quarkus.security.spi.runtime.DevModeDisabledAuthorizationController; import io.quarkus.security.spi.runtime.MethodDescription; @@ -520,6 +521,7 @@ void transformSecurityAnnotations(BuildProducer @Record(ExecutionTime.STATIC_INIT) void gatherSecurityChecks(BuildProducer syntheticBeans, BuildProducer configExpSecurityCheckProducer, + List rolesAllowedConfigExpResolverBuildItems, BeanArchiveIndexBuildItem beanArchiveBuildItem, BuildProducer classPredicate, BuildProducer configBuilderProducer, @@ -540,7 +542,7 @@ void gatherSecurityChecks(BuildProducer syntheticBeans, IndexView index = beanArchiveBuildItem.getIndex(); Map securityChecks = gatherSecurityAnnotations(index, configExpSecurityCheckProducer, additionalSecured.values(), config.denyUnannotated, recorder, configBuilderProducer, - reflectiveClassBuildItemBuildProducer); + reflectiveClassBuildItemBuildProducer, rolesAllowedConfigExpResolverBuildItems); for (AdditionalSecurityCheckBuildItem additionalSecurityCheck : additionalSecurityChecks) { securityChecks.put(additionalSecurityCheck.getMethodInfo(), additionalSecurityCheck.getSecurityCheck()); @@ -587,7 +589,8 @@ private Map gatherSecurityAnnotations(IndexView index BuildProducer configExpSecurityCheckProducer, Collection additionalSecuredMethods, boolean denyUnannotated, SecurityCheckRecorder recorder, BuildProducer configBuilderProducer, - BuildProducer reflectiveClassBuildItemBuildProducer) { + BuildProducer reflectiveClassBuildItemBuildProducer, + List rolesAllowedConfigExpResolverBuildItems) { Map methodToInstanceCollector = new HashMap<>(); Map classAnnotations = new HashMap<>(); @@ -670,11 +673,24 @@ public SecurityCheck apply(Set allowedRolesSet) { })); } + final boolean registerRolesAllowedConfigSource; + // way to resolve roles allowed configuration expressions specified via annotations to configuration values + if (!rolesAllowedConfigExpResolverBuildItems.isEmpty()) { + registerRolesAllowedConfigSource = true; + for (RolesAllowedConfigExpResolverBuildItem item : rolesAllowedConfigExpResolverBuildItems) { + recorder.recordRolesAllowedConfigExpression(item.getRoleConfigExpr(), keyIndex.getAndIncrement(), + item.getConfigValueRecorder()); + } + } else { + registerRolesAllowedConfigSource = hasRolesAllowedCheckWithConfigExp.get(); + } + if (hasRolesAllowedCheckWithConfigExp.get()) { - // make sure config expressions are resolved when app starts + // make sure config expressions are eagerly resolved inside security checks when app starts configExpSecurityCheckProducer .produce(new ConfigExpRolesAllowedSecurityCheckBuildItem()); - + } + if (registerRolesAllowedConfigSource) { // register config source with the Config system configBuilderProducer .produce(new RunTimeConfigBuilderBuildItem(QuarkusSecurityRolesAllowedConfigBuilder.class.getName())); 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 8ffc983e7e057c..ee6639d2ef4951 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 @@ -10,6 +10,7 @@ import java.util.Objects; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.function.BiConsumer; import java.util.function.Function; import java.util.function.Supplier; @@ -68,6 +69,16 @@ public SecurityCheck rolesAllowedSupplier(String[] allowedRoles, int[] configExp return check; } + /* STATIC INIT */ + public void recordRolesAllowedConfigExpression(String configExpression, int configKeyIndex, + BiConsumer> configValueRecorder) { + QuarkusSecurityRolesAllowedConfigBuilder.addProperty(configKeyIndex, configExpression); + // one configuration expression resolves to string array because the expression can be list treated as list + Supplier configValSupplier = resolveRolesAllowedConfigExp(new String[] { configExpression }, + new int[] { 0 }, new int[] { configKeyIndex }); + configValueRecorder.accept(configExpression, configValSupplier); + } + private static Supplier resolveRolesAllowedConfigExp(String[] allowedRoles, int[] configExpIndexes, int[] configKeys) { diff --git a/extensions/security/spi/src/main/java/io/quarkus/security/spi/RolesAllowedConfigExpResolverBuildItem.java b/extensions/security/spi/src/main/java/io/quarkus/security/spi/RolesAllowedConfigExpResolverBuildItem.java new file mode 100644 index 00000000000000..95649dd6ca6ed2 --- /dev/null +++ b/extensions/security/spi/src/main/java/io/quarkus/security/spi/RolesAllowedConfigExpResolverBuildItem.java @@ -0,0 +1,43 @@ +package io.quarkus.security.spi; + +import java.util.Objects; +import java.util.function.BiConsumer; +import java.util.function.Supplier; + +import io.quarkus.builder.item.MultiBuildItem; + +/** + * Provides a way to transform roles allowed specified as configuration expressions in annotations to runtime + * configuration values. + */ +public final class RolesAllowedConfigExpResolverBuildItem extends MultiBuildItem { + private final String roleConfigExpr; + private final BiConsumer> configValueRecorder; + + /** + * @param roleConfigExpr roles allowed configuration expression + * @param configValueRecorder roles allowed supplier will be recorded to this consumer created during static-init; + * runtime roles allowed expressions are supplied correctly only when runtime config is ready + */ + public RolesAllowedConfigExpResolverBuildItem(String roleConfigExpr, + BiConsumer> configValueRecorder) { + this.roleConfigExpr = Objects.requireNonNull(roleConfigExpr); + this.configValueRecorder = Objects.requireNonNull(configValueRecorder); + } + + public String getRoleConfigExpr() { + return roleConfigExpr; + } + + public BiConsumer> getConfigValueRecorder() { + return configValueRecorder; + } + + public static boolean isSecurityConfigExpressionCandidate(String configExpression) { + if (configExpression == null || configExpression.length() < 4) { + return false; + } + final int exprStart = configExpression.indexOf("${"); + return exprStart >= 0 && configExpression.indexOf('}', exprStart + 2) > 0; + } +} diff --git a/extensions/smallrye-graphql-client/deployment/pom.xml b/extensions/smallrye-graphql-client/deployment/pom.xml index e2416a9795720f..c955fb72b1a02a 100644 --- a/extensions/smallrye-graphql-client/deployment/pom.xml +++ b/extensions/smallrye-graphql-client/deployment/pom.xml @@ -63,6 +63,16 @@ stork-service-discovery-static-list test + + io.quarkus + quarkus-elytron-security-deployment + test + + + io.quarkus + quarkus-elytron-security-properties-file-deployment + test + diff --git a/extensions/smallrye-graphql-client/deployment/src/test/java/io/quarkus/smallrye/graphql/client/deployment/DynamicGraphQLClientWebSocketAuthenticationHttpPermissionsTest.java b/extensions/smallrye-graphql-client/deployment/src/test/java/io/quarkus/smallrye/graphql/client/deployment/DynamicGraphQLClientWebSocketAuthenticationHttpPermissionsTest.java new file mode 100644 index 00000000000000..f2a72d45f82314 --- /dev/null +++ b/extensions/smallrye-graphql-client/deployment/src/test/java/io/quarkus/smallrye/graphql/client/deployment/DynamicGraphQLClientWebSocketAuthenticationHttpPermissionsTest.java @@ -0,0 +1,137 @@ +package io.quarkus.smallrye.graphql.client.deployment; + +import jakarta.annotation.security.RolesAllowed; + +import org.eclipse.microprofile.graphql.GraphQLApi; +import org.eclipse.microprofile.graphql.Query; +import org.jboss.shrinkwrap.api.asset.EmptyAsset; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.smallrye.common.annotation.NonBlocking; +import io.smallrye.graphql.api.Subscription; +import io.smallrye.graphql.client.Response; +import io.smallrye.graphql.client.dynamic.api.DynamicGraphQLClient; +import io.smallrye.graphql.client.dynamic.api.DynamicGraphQLClientBuilder; +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.helpers.test.AssertSubscriber; +import io.vertx.core.http.UpgradeRejectedException; + +/** + * Due to the complexity of establishing a WebSocket, WebSocket/Subscription testing of the GraphQL server is done here, + * as the client framework comes in very useful for establishing the connection to the server. + *
+ * This test establishes connections to the server, and ensures that the connected user has the necessary permissions to + * execute the operation. + */ +public class DynamicGraphQLClientWebSocketAuthenticationHttpPermissionsTest { + + static String url = "http://" + System.getProperty("quarkus.http.host", "localhost") + ":" + + System.getProperty("quarkus.http.test-port", "8081") + "/graphql"; + + @RegisterExtension + static QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(SecuredApi.class, Foo.class) + .addAsResource("application-secured-http-permissions.properties", "application.properties") + .addAsResource("users.properties") + .addAsResource("roles.properties") + .addAsManifestResource(EmptyAsset.INSTANCE, "beans.xml")); + + @Disabled("TODO: enable after upgrade to smallrye-graphql 1.6.1, with 1.6.0 a websocket upgrade failure causes a hang here") + @Test + public void testUnauthenticatedForQueryWebSocket() throws Exception { + DynamicGraphQLClientBuilder clientBuilder = DynamicGraphQLClientBuilder.newBuilder() + .url(url) + .executeSingleOperationsOverWebsocket(true); + try (DynamicGraphQLClient client = clientBuilder.build()) { + try { + client.executeSync("{ baz { message} }"); + Assertions.fail("WebSocket upgrade should fail"); + } catch (UpgradeRejectedException e) { + // ok + } + } + } + + @Test + public void testUnauthenticatedForSubscriptionWebSocket() throws Exception { + DynamicGraphQLClientBuilder clientBuilder = DynamicGraphQLClientBuilder.newBuilder() + .url(url); + try (DynamicGraphQLClient client = clientBuilder.build()) { + AssertSubscriber subscriber = new AssertSubscriber<>(); + client.subscription("{ bazSub { message} }").subscribe().withSubscriber(subscriber); + subscriber.awaitFailure().assertFailedWith(UpgradeRejectedException.class); + } + } + + public static class Foo { + + private String message; + + public Foo(String foo) { + this.message = foo; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + } + + @GraphQLApi + public static class SecuredApi { + + @Query + @RolesAllowed("fooRole") + @NonBlocking + public Foo foo() { + return new Foo("foo"); + } + + @Query + @RolesAllowed("barRole") + public Foo bar() { + return new Foo("bar"); + } + + @Query + public Foo baz() { + return new Foo("baz"); + } + + @Subscription + @RolesAllowed("fooRole") + public Multi fooSub() { + return Multi.createFrom().emitter(emitter -> { + emitter.emit(new Foo("foo")); + emitter.complete(); + }); + } + + @Subscription + @RolesAllowed("barRole") + public Multi barSub() { + return Multi.createFrom().emitter(emitter -> { + emitter.emit(new Foo("bar")); + emitter.complete(); + }); + } + + @Subscription + public Multi bazSub() { + return Multi.createFrom().emitter(emitter -> { + emitter.emit(new Foo("baz")); + emitter.complete(); + }); + } + + } +} diff --git a/extensions/smallrye-graphql-client/deployment/src/test/java/io/quarkus/smallrye/graphql/client/deployment/DynamicGraphQLClientWebSocketAuthenticationTest.java b/extensions/smallrye-graphql-client/deployment/src/test/java/io/quarkus/smallrye/graphql/client/deployment/DynamicGraphQLClientWebSocketAuthenticationTest.java new file mode 100644 index 00000000000000..b04b74a6b3a24c --- /dev/null +++ b/extensions/smallrye-graphql-client/deployment/src/test/java/io/quarkus/smallrye/graphql/client/deployment/DynamicGraphQLClientWebSocketAuthenticationTest.java @@ -0,0 +1,207 @@ +package io.quarkus.smallrye.graphql.client.deployment; + +import static org.awaitility.Awaitility.await; +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; + +import java.util.concurrent.atomic.AtomicBoolean; + +import jakarta.annotation.security.RolesAllowed; +import jakarta.json.JsonValue; + +import org.eclipse.microprofile.graphql.GraphQLApi; +import org.eclipse.microprofile.graphql.Query; +import org.jboss.shrinkwrap.api.asset.EmptyAsset; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.smallrye.common.annotation.NonBlocking; +import io.smallrye.graphql.api.Subscription; +import io.smallrye.graphql.client.Response; +import io.smallrye.graphql.client.dynamic.api.DynamicGraphQLClient; +import io.smallrye.graphql.client.dynamic.api.DynamicGraphQLClientBuilder; +import io.smallrye.mutiny.Multi; + +/** + * Due to the complexity of establishing a WebSocket, WebSocket/Subscription testing of the GraphQL server is done here, + * as the client framework comes in very useful for establishing the connection to the server. + *
+ * This test establishes connections to the server, and ensures that the connected user has the necessary permissions to + * execute the operation. + */ +public class DynamicGraphQLClientWebSocketAuthenticationTest { + + static String url = "http://" + System.getProperty("quarkus.http.host", "localhost") + ":" + + System.getProperty("quarkus.http.test-port", "8081") + "/graphql"; + + @RegisterExtension + static QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(SecuredApi.class, Foo.class) + .addAsResource("application-secured.properties", "application.properties") + .addAsResource("users.properties") + .addAsResource("roles.properties") + .addAsManifestResource(EmptyAsset.INSTANCE, "beans.xml")); + + @Test + public void testAuthenticatedUserForSubscription() throws Exception { + DynamicGraphQLClientBuilder clientBuilder = DynamicGraphQLClientBuilder.newBuilder() + .url(url) + .header("Authorization", "Basic ZGF2aWQ6cXdlcnR5MTIz"); + try (DynamicGraphQLClient client = clientBuilder.build()) { + Multi subscription = client + .subscription("subscription fooSub { fooSub { message } }"); + + assertNotNull(subscription); + + AtomicBoolean hasData = new AtomicBoolean(false); + AtomicBoolean hasCompleted = new AtomicBoolean(false); + + subscription.subscribe().with(item -> { + assertFalse(hasData.get()); + assertTrue(item.hasData()); + assertEquals(JsonValue.ValueType.OBJECT, item.getData().get("fooSub").getValueType()); + assertEquals("foo", item.getData().getJsonObject("fooSub").getString("message")); + hasData.set(true); + }, Assertions::fail, () -> { + hasCompleted.set(true); + }); + + await().untilTrue(hasCompleted); + assertTrue(hasData.get()); + } + } + + @Test + public void testAuthenticatedUserForQueryWebSocket() throws Exception { + DynamicGraphQLClientBuilder clientBuilder = DynamicGraphQLClientBuilder.newBuilder() + .url(url) + .header("Authorization", "Basic ZGF2aWQ6cXdlcnR5MTIz") + .executeSingleOperationsOverWebsocket(true); + try (DynamicGraphQLClient client = clientBuilder.build()) { + Response response = client.executeSync("{ foo { message} }"); + assertTrue(response.hasData()); + assertEquals("foo", response.getData().getJsonObject("foo").getString("message")); + } + } + + @Test + public void testAuthorizedAndUnauthorizedForQueryWebSocket() throws Exception { + DynamicGraphQLClientBuilder clientBuilder = DynamicGraphQLClientBuilder.newBuilder() + .url(url) + .header("Authorization", "Basic ZGF2aWQ6cXdlcnR5MTIz") + .executeSingleOperationsOverWebsocket(true); + try (DynamicGraphQLClient client = clientBuilder.build()) { + Response response = client.executeSync("{ foo { message} }"); + assertTrue(response.hasData()); + assertEquals("foo", response.getData().getJsonObject("foo").getString("message")); + + // Run a second query with a different result to validate that the result of the first query isn't being cached at all. + response = client.executeSync("{ bar { message} }"); + assertEquals(JsonValue.ValueType.NULL, response.getData().get("bar").getValueType()); + } + } + + @Test + public void testUnauthorizedUserForSubscription() throws Exception { + DynamicGraphQLClientBuilder clientBuilder = DynamicGraphQLClientBuilder.newBuilder() + .url(url) + .header("Authorization", "Basic ZGF2aWQ6cXdlcnR5MTIz"); + try (DynamicGraphQLClient client = clientBuilder.build()) { + Multi subscription = client + .subscription("subscription barSub { barSub { message } }"); + + assertNotNull(subscription); + + AtomicBoolean returned = new AtomicBoolean(false); + + subscription.subscribe().with(item -> { + assertEquals(JsonValue.ValueType.NULL, item.getData().get("barSub").getValueType()); + returned.set(true); + }, throwable -> Assertions.fail(throwable)); + + await().untilTrue(returned); + } + } + + @Test + public void testUnauthorizedUserForQueryWebSocket() throws Exception { + DynamicGraphQLClientBuilder clientBuilder = DynamicGraphQLClientBuilder.newBuilder() + .url(url) + .header("Authorization", "Basic ZGF2aWQ6cXdlcnR5MTIz") + .executeSingleOperationsOverWebsocket(true); + try (DynamicGraphQLClient client = clientBuilder.build()) { + Response response = client.executeSync("{ bar { message } }"); + assertEquals(JsonValue.ValueType.NULL, response.getData().get("bar").getValueType()); + } + } + + @Test + public void testUnauthenticatedForQueryWebSocket() throws Exception { + DynamicGraphQLClientBuilder clientBuilder = DynamicGraphQLClientBuilder.newBuilder() + .url(url) + .executeSingleOperationsOverWebsocket(true); + try (DynamicGraphQLClient client = clientBuilder.build()) { + Response response = client.executeSync("{ foo { message} }"); + assertEquals(JsonValue.ValueType.NULL, response.getData().get("foo").getValueType()); + } + } + + public static class Foo { + + private String message; + + public Foo(String foo) { + this.message = foo; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + } + + @GraphQLApi + public static class SecuredApi { + + @Query + @RolesAllowed("fooRole") + @NonBlocking + public Foo foo() { + return new Foo("foo"); + } + + @Query + @RolesAllowed("barRole") + public Foo bar() { + return new Foo("bar"); + } + + @Subscription + @RolesAllowed("fooRole") + public Multi fooSub() { + return Multi.createFrom().emitter(emitter -> { + emitter.emit(new Foo("foo")); + emitter.complete(); + }); + } + + @Subscription + @RolesAllowed("barRole") + public Multi barSub() { + return Multi.createFrom().emitter(emitter -> { + emitter.emit(new Foo("bar")); + emitter.complete(); + }); + } + + } +} diff --git a/extensions/smallrye-graphql-client/deployment/src/test/resources/application-secured-http-permissions.properties b/extensions/smallrye-graphql-client/deployment/src/test/resources/application-secured-http-permissions.properties new file mode 100644 index 00000000000000..770567e9e35652 --- /dev/null +++ b/extensions/smallrye-graphql-client/deployment/src/test/resources/application-secured-http-permissions.properties @@ -0,0 +1,13 @@ +quarkus.security.users.file.enabled=true +quarkus.security.users.file.plain-text=true +quarkus.security.users.file.users=users.properties +quarkus.security.users.file.roles=roles.properties +quarkus.http.auth.basic=true + +quarkus.smallrye-graphql.log-payload=queryAndVariables +quarkus.smallrye-graphql.print-data-fetcher-exception=true +quarkus.smallrye-graphql.error-extension-fields=exception,classification,code,description,validationErrorType,queryPath + +quarkus.http.auth.permission.authenticated.paths=/graphql +quarkus.http.auth.permission.authenticated.methods=GET,POST +quarkus.http.auth.permission.authenticated.policy=authenticated \ No newline at end of file diff --git a/extensions/smallrye-graphql-client/deployment/src/test/resources/application-secured.properties b/extensions/smallrye-graphql-client/deployment/src/test/resources/application-secured.properties new file mode 100644 index 00000000000000..eb7d901e0c93fc --- /dev/null +++ b/extensions/smallrye-graphql-client/deployment/src/test/resources/application-secured.properties @@ -0,0 +1,9 @@ +quarkus.security.users.file.enabled=true +quarkus.security.users.file.plain-text=true +quarkus.security.users.file.users=users.properties +quarkus.security.users.file.roles=roles.properties +quarkus.http.auth.basic=true + +quarkus.smallrye-graphql.log-payload=queryAndVariables +quarkus.smallrye-graphql.print-data-fetcher-exception=true +quarkus.smallrye-graphql.error-extension-fields=exception,classification,code,description,validationErrorType,queryPath diff --git a/extensions/smallrye-graphql-client/deployment/src/test/resources/roles.properties b/extensions/smallrye-graphql-client/deployment/src/test/resources/roles.properties new file mode 100644 index 00000000000000..ef2a67ac7e9e64 --- /dev/null +++ b/extensions/smallrye-graphql-client/deployment/src/test/resources/roles.properties @@ -0,0 +1 @@ +david=fooRole \ No newline at end of file diff --git a/extensions/smallrye-graphql-client/deployment/src/test/resources/users.properties b/extensions/smallrye-graphql-client/deployment/src/test/resources/users.properties new file mode 100644 index 00000000000000..0f1cc7592d0553 --- /dev/null +++ b/extensions/smallrye-graphql-client/deployment/src/test/resources/users.properties @@ -0,0 +1 @@ +david=qwerty123 \ No newline at end of file diff --git a/extensions/smallrye-graphql/deployment/src/main/java/io/quarkus/smallrye/graphql/deployment/SmallRyeGraphQLProcessor.java b/extensions/smallrye-graphql/deployment/src/main/java/io/quarkus/smallrye/graphql/deployment/SmallRyeGraphQLProcessor.java index f10de933cc1da4..4ead0d35da8f18 100644 --- a/extensions/smallrye-graphql/deployment/src/main/java/io/quarkus/smallrye/graphql/deployment/SmallRyeGraphQLProcessor.java +++ b/extensions/smallrye-graphql/deployment/src/main/java/io/quarkus/smallrye/graphql/deployment/SmallRyeGraphQLProcessor.java @@ -65,6 +65,7 @@ import io.quarkus.smallrye.graphql.runtime.SmallRyeGraphQLRecorder; import io.quarkus.smallrye.graphql.runtime.SmallRyeGraphQLRuntimeConfig; import io.quarkus.vertx.http.deployment.BodyHandlerBuildItem; +import io.quarkus.vertx.http.deployment.FilterBuildItem; import io.quarkus.vertx.http.deployment.HttpRootPathBuildItem; import io.quarkus.vertx.http.deployment.NonApplicationRootPathBuildItem; import io.quarkus.vertx.http.deployment.RouteBuildItem; @@ -149,7 +150,7 @@ public class SmallRyeGraphQLProcessor { private static final List SUPPORTED_WEBSOCKET_SUBPROTOCOLS = List.of(SUBPROTOCOL_GRAPHQL_WS, SUBPROTOCOL_GRAPHQL_TRANSPORT_WS); - private static final int GRAPHQL_WEBSOCKET_HANDLER_ORDER = -10000; + private static final int GRAPHQL_WEBSOCKET_HANDLER_ORDER = (-1 * FilterBuildItem.AUTHORIZATION) + 1; private static final String GRAPHQL_MEDIA_TYPE = "application/graphql+json"; diff --git a/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/SmallRyeGraphQLOverWebSocketHandler.java b/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/SmallRyeGraphQLOverWebSocketHandler.java index 2cae3cd455bb69..c309a4f09eaf79 100644 --- a/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/SmallRyeGraphQLOverWebSocketHandler.java +++ b/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/SmallRyeGraphQLOverWebSocketHandler.java @@ -6,10 +6,12 @@ import io.quarkus.security.identity.CurrentIdentityAssociation; import io.quarkus.vertx.http.runtime.CurrentVertxRequest; +import io.quarkus.vertx.http.runtime.security.QuarkusHttpUser; import io.smallrye.graphql.websocket.GraphQLWebSocketSession; import io.smallrye.graphql.websocket.GraphQLWebsocketHandler; import io.smallrye.graphql.websocket.graphqltransportws.GraphQLTransportWSSubprotocolHandler; import io.smallrye.graphql.websocket.graphqlws.GraphQLWSSubprotocolHandler; +import io.vertx.core.Handler; import io.vertx.core.http.HttpHeaders; import io.vertx.core.http.ServerWebSocket; import io.vertx.ext.web.RoutingContext; @@ -54,9 +56,34 @@ protected void doHandle(final RoutingContext ctx) { serverWebSocket.close(); return; } + + QuarkusHttpUser user = (QuarkusHttpUser) ctx.user(); + long cancellation = -1L; // Do not use 0, as you won't be able to distinguish between not set, and the first task Id + if (user != null) { + //close the connection when the identity expires + Long expire = user.getSecurityIdentity().getAttribute("quarkus.identity.expire-time"); + if (expire != null) { + cancellation = ctx.vertx().setTimer((expire * 1000) - System.currentTimeMillis(), + new Handler() { + @Override + public void handle(Long event) { + if (!serverWebSocket.isClosed()) { + serverWebSocket.close((short) 1008, "Authentication expired"); + } + } + }); + } + } + log.debugf("Starting websocket with subprotocol = %s", subprotocol); GraphQLWebsocketHandler finalHandler = handler; - serverWebSocket.closeHandler(v -> finalHandler.onClose()); + long finalCancellation = cancellation; + serverWebSocket.closeHandler(v -> { + finalHandler.onClose(); + if (finalCancellation != -1) { + ctx.vertx().cancelTimer(finalCancellation); + } + }); serverWebSocket.endHandler(v -> finalHandler.onEnd()); serverWebSocket.exceptionHandler(finalHandler::onThrowable); serverWebSocket.textMessageHandler(finalHandler::onMessage); diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/VertxHttpProcessor.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/VertxHttpProcessor.java index fd8ec2b3ce36e4..0d6835565655e1 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/VertxHttpProcessor.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/VertxHttpProcessor.java @@ -114,16 +114,24 @@ FrameworkEndpointsBuildItem frameworkEndpoints(NonApplicationRootPathBuildItem n for (RouteBuildItem route : routes) { if (FRAMEWORK_ROUTE.equals(route.getRouteType())) { if (route.getConfiguredPathInfo() != null) { - frameworkEndpoints.add(route.getConfiguredPathInfo().getEndpointPath(nonApplicationRootPath, - managementInterfaceBuildTimeConfig, launchModeBuildItem)); + String endpointPath = route.getConfiguredPathInfo().getEndpointPath(nonApplicationRootPath, + managementInterfaceBuildTimeConfig, launchModeBuildItem); + frameworkEndpoints.add(endpointPath); continue; } if (route.getRouteFunction() != null && route.getRouteFunction() instanceof BasicRoute) { BasicRoute basicRoute = (BasicRoute) route.getRouteFunction(); if (basicRoute.getPath() != null) { - // Calling TemplateHtmlBuilder does not see very correct here, but it is the underlying API for ConfiguredPathInfo - frameworkEndpoints - .add(adjustRoot(nonApplicationRootPath.getNonApplicationRootPath(), basicRoute.getPath())); + if (basicRoute.getPath().startsWith(nonApplicationRootPath.getNonApplicationRootPath())) { + // Do not repeat the non application root path. + frameworkEndpoints.add(basicRoute.getPath()); + } else { + // Calling TemplateHtmlBuilder does not see very correct here, but it is the underlying API for ConfiguredPathInfo + String adjustRoot = adjustRoot(nonApplicationRootPath.getNonApplicationRootPath(), + basicRoute.getPath()); + frameworkEndpoints.add(adjustRoot); + } + } } } diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/PathMatchingHttpSecurityPolicyTest.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/PathMatchingHttpSecurityPolicyTest.java index 08679d345bcaa2..3169e6bb6067dc 100644 --- a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/PathMatchingHttpSecurityPolicyTest.java +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/PathMatchingHttpSecurityPolicyTest.java @@ -41,6 +41,18 @@ public class PathMatchingHttpSecurityPolicyTest { "quarkus.http.auth.permission.public.policy=permit\n" + "quarkus.http.auth.permission.foo.paths=/api/foo/bar\n" + "quarkus.http.auth.permission.foo.policy=authenticated\n" + + "quarkus.http.auth.permission.inner-wildcard.paths=/api/*/bar\n" + + "quarkus.http.auth.permission.inner-wildcard.policy=authenticated\n" + + "quarkus.http.auth.permission.inner-wildcard2.paths=/api/next/*/prev\n" + + "quarkus.http.auth.permission.inner-wildcard2.policy=authenticated\n" + + "quarkus.http.auth.permission.inner-wildcard3.paths=/api/one/*/three/*\n" + + "quarkus.http.auth.permission.inner-wildcard3.policy=authenticated\n" + + "quarkus.http.auth.permission.inner-wildcard4.paths=/api/one/*/*/five\n" + + "quarkus.http.auth.permission.inner-wildcard4.policy=authenticated\n" + + "quarkus.http.auth.permission.inner-wildcard5.paths=/api/one/*/jamaica/*\n" + + "quarkus.http.auth.permission.inner-wildcard5.policy=permit\n" + + "quarkus.http.auth.permission.inner-wildcard6.paths=/api/*/sadly/*/dont-know\n" + + "quarkus.http.auth.permission.inner-wildcard6.policy=deny\n" + "quarkus.http.auth.permission.baz.paths=/api/baz\n" + "quarkus.http.auth.permission.baz.policy=authenticated\n" + "quarkus.http.auth.permission.static-resource.paths=/static-file.html\n" + @@ -85,6 +97,25 @@ private WebClient getClient() { return client; } + @Test + public void testInnerWildcardPath() { + assurePath("/api/any-value/bar", 401); + assurePath("/api/any-value/bar", 401); + assurePath("/api/next/any-value/prev", 401); + assurePath("/api/one/two/three/four", 401); + assurePath("/api////any-value//////bar", 401); + assurePath("/api/next///////any-value////prev", 401); + assurePath("////api//one/two//three////four?door=wood", 401); + assurePath("/api/one/three/four/five", 401); + assurePath("/api/one/3/4/five", 401); + assurePath("////api/one///3/4/five", 401); + assurePath("/api/now/sadly/i/dont-know", 401); + assurePath("/api/now/sadly///i/dont-know", 401); + assurePath("/api/one/three/jamaica/five", 200); + assurePath("/api/one/three/jamaica/football", 200); + assurePath("/api/now/sally/i/dont-know", 200); + } + @ParameterizedTest @ValueSource(strings = { // path policy without wildcard 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 84670fc933c50f..917c076e6e6555 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 @@ -13,6 +13,7 @@ import '@vaadin/integer-field'; import '@vaadin/text-field'; import '@vaadin/select'; import '@vaadin/details'; +import '@vaadin/combo-box'; import { notifier } from 'notifier'; import { unsafeHTML } from 'lit/directives/unsafe-html.js'; import { gridRowDetailsRenderer } from '@vaadin/grid/lit.js'; @@ -35,7 +36,7 @@ export class QwcConfiguration extends observeState(LitElement) { flex-direction: column; overflow: hidden; } - + .confTopBar { display: flex; justify-content: space-between; @@ -50,7 +51,7 @@ export class QwcConfiguration extends observeState(LitElement) { vertical-align: top; width: 100%; } - + .description { padding: 1em; } @@ -70,7 +71,7 @@ export class QwcConfiguration extends observeState(LitElement) { cursor: pointer; color: var(--lumo-primary-color); } - + .lock-icon { color: var(--lumo-contrast-60pct); font-size: small; @@ -84,6 +85,10 @@ export class QwcConfiguration extends observeState(LitElement) { pointer-events: none; opacity: 0.4; } + .config-source-dropdown { + padding-left: 5px; + width: 300px; + } `; static properties = { @@ -93,13 +98,23 @@ export class QwcConfiguration extends observeState(LitElement) { _values: {state: true}, _detailsOpenedItem: {state: true, type: Array}, _busy: {state: true}, - _showOnlyOwnProperties: {state: true}, - _searchTerm: {state: true} + _showOnlyConfigSource: {state: true}, + _searchTerm: {state: true}, + _configSourceSet: {state: true} }; constructor() { super(); + this._configSourceSet = new Map(); + this._detailsOpenedItem = []; + this._busy = null; + + this._showOnlyConfigSource = null; + this._searchTerm = ''; + } + connectedCallback() { + super.connectedCallback(); this._filteredValue = this.routerController.getQueryParameter("filter"); if(this._filteredValue){ @@ -109,15 +124,37 @@ export class QwcConfiguration extends observeState(LitElement) { this._allConfiguration = e.result; this._visibleConfiguration = e.result; this._filtered = e.result; - }) + + for (const configItem of this._allConfiguration) { + let configSourceName = this._getConfigSourceName(configItem.configValue); + if(configSourceName && !this._configSourceSet.has(configSourceName)){ + this._configSourceSet.set(configSourceName, this._createConfigSourceObject(configSourceName, configItem.configValue)); + } + } + }); this.jsonRpc.getAllValues().then(e => { this._values = e.result; }); - this._detailsOpenedItem = []; - this._busy = null; + } - this._showOnlyOwnProperties = false; - this._searchTerm = ''; + _getConfigSourceName(configValue){ + if(configValue.sourceName){ + return configValue.configSourceName; + } + return null; + } + + _createConfigSourceObject(configSourceName,configValue){ + + let displayName = configSourceName; + + if(configSourceName.startsWith("PropertiesConfigSource[source") + && configSourceName.endsWith("/application.properties]")){ + displayName = "My properties"; + } + + let configSourceObject = {name:configSourceName, display: displayName, position:configValue.configSourcePosition, ordinal:configValue.configSourceOrdinal}; + return configSourceObject; } render() { @@ -173,25 +210,31 @@ export class QwcConfiguration extends observeState(LitElement) { ${this._filtered.length} - - + + + ${this._renderGrid()} `; } - _toggleShowOnlyOwnProperties(onlyMine){ - this._showOnlyOwnProperties = onlyMine; - if(this._showOnlyOwnProperties){ + _toggleFilterByConfigSource(event){ + if(event.target.value){ + this._showOnlyConfigSource = event.target.value; this._visibleConfiguration = this._allConfiguration.filter((prop) => { - return (prop.configValue.sourceName && prop.configValue.sourceName.startsWith("PropertiesConfigSource[source") - && prop.configValue.sourceName.endsWith("/application.properties]")); + return prop.configValue.sourceName && prop.configValue.sourceName === this._showOnlyConfigSource; }); - }else { + }else{ + this._showOnlyConfigSource = null; this._visibleConfiguration = this._allConfiguration; } return this._filterGrid(); @@ -219,7 +262,7 @@ export class QwcConfiguration extends observeState(LitElement) { @@ -306,11 +349,11 @@ export class QwcConfiguration extends observeState(LitElement) { `; } else if (prop.typeName === "java.lang.Float" || prop.typeName === "java.lang.Double") { return html` - - + `; } else { return html` - - @@ -374,12 +417,16 @@ export class QwcConfiguration extends observeState(LitElement) { } } res = res.toUpperCase(); - + let def = "Default value: None"; if (prop.defaultValue) { def = "Default value: " + prop.defaultValue; } - let src = "Config source: " + prop.configValue.sourceName; + let configSourceName = "Unknown"; + if(prop.configValue.sourceName){ + configSourceName = prop.configValue.sourceName; + } + let src = "Config source: " + configSourceName; return html`

${unsafeHTML(prop.description)}

diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/FormAuthRuntimeConfig.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/FormAuthRuntimeConfig.java index d4152fe4a9b133..d4d3db76f3d2ac 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/FormAuthRuntimeConfig.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/FormAuthRuntimeConfig.java @@ -65,7 +65,7 @@ public enum CookieSameSite { /** * Option to control the name of the cookie used to redirect the user back - * to where he wants to get access to. + * to the location they want to access. */ @ConfigItem(defaultValue = "quarkus-redirect-location") public String locationCookie; @@ -83,15 +83,15 @@ public enum CookieSameSite { * referred to as "renewal-timeout". * * Note that smaller values will result in slightly more server load (as new encrypted cookies will be - * generated more often), however larger values affect the inactivity timeout as the timeout is set + * generated more often); however, larger values affect the inactivity timeout because the timeout is set * when a cookie is generated. * - * For example if this is set to 10 minutes, and the inactivity timeout is 30m, if a users last request - * is when the cookie is 9m old then the actual timeout will happen 21m after the last request, as the timeout + * For example if this is set to 10 minutes, and the inactivity timeout is 30m, if a user's last request + * is when the cookie is 9m old then the actual timeout will happen 21m after the last request because the timeout * is only refreshed when a new cookie is generated. * - * In other words no timeout is tracked on the server side; the timestamp is encoded and encrypted in the cookie itself, - * and it is decrypted and parsed with each request. + * That is, no timeout is tracked on the server side; the timestamp is encoded and encrypted in the cookie + * itself, and it is decrypted and parsed with each request. */ @ConfigItem(defaultValue = "PT1M") public Duration newCookieInterval; diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ForwardedParser.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ForwardedParser.java index 0bdee036fbf3b5..1b11431a81c7fc 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ForwardedParser.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ForwardedParser.java @@ -121,7 +121,7 @@ private void calculate() { calculated = true; remoteAddress = delegate.remoteAddress(); scheme = delegate.scheme(); - setHostAndPort(delegate.getHeader(HttpHeaders.HOST), port); + setHostAndPort(delegate.host(), port); uri = delegate.uri(); if (trustedProxyCheck.isProxyAllowed()) { diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/AbstractPathMatchingHttpSecurityPolicy.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/AbstractPathMatchingHttpSecurityPolicy.java index 304688d56f51d8..3371e6c3651625 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/AbstractPathMatchingHttpSecurityPolicy.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/AbstractPathMatchingHttpSecurityPolicy.java @@ -23,6 +23,7 @@ import io.quarkus.vertx.http.runtime.PolicyMappingConfig; import io.quarkus.vertx.http.runtime.security.HttpSecurityPolicy.AuthorizationRequestContext; import io.quarkus.vertx.http.runtime.security.HttpSecurityPolicy.CheckResult; +import io.quarkus.vertx.http.runtime.security.ImmutablePathMatcher.PathMatch; import io.smallrye.mutiny.Uni; import io.vertx.ext.web.RoutingContext; @@ -33,15 +34,15 @@ */ public class AbstractPathMatchingHttpSecurityPolicy { - private final PathMatcher> pathMatcher = new PathMatcher<>(); + private final ImmutablePathMatcher> pathMatcher; AbstractPathMatchingHttpSecurityPolicy(Map permissions, Map rolePolicy, String rootPath, Instance installedPolicies) { - init(permissions, toNamedHttpSecPolicies(rolePolicy, installedPolicies), rootPath); + pathMatcher = init(permissions, toNamedHttpSecPolicies(rolePolicy, installedPolicies), rootPath); } public String getAuthMechanismName(RoutingContext routingContext) { - PathMatcher.PathMatch> toCheck = pathMatcher.match(routingContext.normalizedPath()); + PathMatch> toCheck = pathMatcher.match(routingContext.normalizedPath()); if (toCheck.getValue() == null || toCheck.getValue().isEmpty()) { return null; } @@ -93,9 +94,9 @@ public Uni apply(CheckResult checkResult) { }); } - private void init(Map permissions, + private static ImmutablePathMatcher> init(Map permissions, Map permissionCheckers, String rootPath) { - Map> tempMap = new HashMap<>(); + final var builder = ImmutablePathMatcher.> builder().handlerAccumulator(List::addAll); for (Map.Entry entry : permissions.entrySet()) { HttpSecurityPolicy checker = permissionCheckers.get(entry.getValue().policy); if (checker == null) { @@ -108,34 +109,19 @@ private void init(Map permissions, if (!path.startsWith("/")) { path = rootPath + path; } - if (tempMap.containsKey(path)) { - HttpMatcher m = new HttpMatcher(entry.getValue().authMechanism.orElse(null), - new HashSet<>(entry.getValue().methods.orElse(Collections.emptyList())), - checker); - tempMap.get(path).add(m); - } else { - HttpMatcher m = new HttpMatcher(entry.getValue().authMechanism.orElse(null), - new HashSet<>(entry.getValue().methods.orElse(Collections.emptyList())), - checker); - List perms = new ArrayList<>(); - tempMap.put(path, perms); - perms.add(m); - if (path.endsWith("/*")) { - String stripped = path.substring(0, path.length() - 2); - pathMatcher.addPrefixPath(stripped.isEmpty() ? "/" : stripped, perms); - } else if (path.endsWith("*")) { - pathMatcher.addPrefixPath(path.substring(0, path.length() - 1), perms); - } else { - pathMatcher.addExactPath(path, perms); - } - } + HttpMatcher m = new HttpMatcher(entry.getValue().authMechanism.orElse(null), + new HashSet<>(entry.getValue().methods.orElse(Collections.emptyList())), checker); + List perms = new ArrayList<>(); + perms.add(m); + builder.addPath(path, perms); } } } + return builder.build(); } public List findPermissionCheckers(RoutingContext context) { - PathMatcher.PathMatch> toCheck = pathMatcher.match(context.normalizedPath()); + PathMatch> toCheck = pathMatcher.match(context.normalizedPath()); if (toCheck.getValue() == null || toCheck.getValue().isEmpty()) { return Collections.emptyList(); } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/ImmutablePathMatcher.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/ImmutablePathMatcher.java new file mode 100644 index 00000000000000..1778c24d81a947 --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/ImmutablePathMatcher.java @@ -0,0 +1,346 @@ +package io.quarkus.vertx.http.runtime.security; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.TreeSet; +import java.util.function.BiConsumer; + +import io.quarkus.runtime.configuration.ConfigurationException; +import io.quarkus.vertx.http.runtime.security.ImmutableSubstringMap.SubstringMatch; + +/** + * Handler that dispatches to a given handler based on a match of the path. + */ +public class ImmutablePathMatcher { + + private final ImmutableSubstringMap paths; + private final Map exactPathMatches; + + /** + * lengths of all registered paths + */ + private final int[] lengths; + private final T defaultHandler; + private final boolean hasPathWithInnerWildcard; + private final boolean hasExactPathMatches; + + private ImmutablePathMatcher(T defaultHandler, ImmutableSubstringMap paths, Map exactPathMatches, + int[] lengths, boolean hasPathWithInnerWildcard) { + this.defaultHandler = defaultHandler; + this.paths = paths; + this.lengths = Arrays.copyOf(lengths, lengths.length); + this.hasPathWithInnerWildcard = hasPathWithInnerWildcard; + if (exactPathMatches.isEmpty()) { + this.exactPathMatches = null; + this.hasExactPathMatches = false; + } else { + this.exactPathMatches = Map.copyOf(exactPathMatches); + this.hasExactPathMatches = true; + } + } + + /** + * Matches a path against the registered handlers. + * + * @param path The relative path to match + * @return The match. This will never be null, however if none matched its value field will be + */ + public PathMatch match(String path) { + if (hasExactPathMatches) { + T match = exactPathMatches.get(path); + if (match != null) { + return new PathMatch<>(path, "", match); + } + } + + int length = path.length(); + for (int pathLength : lengths) { + if (pathLength == length) { + SubstringMatch next = paths.get(path, length); + if (next != null) { + return new PathMatch<>(path, "", next.getValue()); + } + } else if (pathLength < length) { + char c = path.charAt(pathLength); + // pathLength == 1 means prefix path is / because prefix path always starts with / + // which means it's default handler match, but if there is at least + // one path with inner wildcard, we need to check for paths like /*/one + if (c == '/' || (hasPathWithInnerWildcard && pathLength == 1)) { + + //String part = path.substring(0, pathLength); + SubstringMatch next = paths.get(path, pathLength); + if (next != null) { + return new PathMatch<>(next.getKey(), path.substring(pathLength), next.getValue()); + } + } + } + } + return new PathMatch<>("", path, defaultHandler); + } + + public static ImmutablePathMatcherBuilder builder() { + return new ImmutablePathMatcherBuilder<>(); + } + + public static final class PathMatch { + private final String matched; + private final String remaining; + private final T value; + + public PathMatch(String matched, String remaining, T value) { + this.matched = matched; + this.remaining = remaining; + this.value = value; + } + + /** + * @deprecated because it can't be supported with inner wildcard without cost. It's unlikely this method is + * used by anyone as users don't get in touch with this class. If there is legit use case, please + * open Quarkus issue. + */ + @Deprecated + public String getRemaining() { + return remaining; + } + + public String getMatched() { + return matched; + } + + public T getValue() { + return value; + } + } + + public static class ImmutablePathMatcherBuilder { + + private static final String STRING_PATH_SEPARATOR = "/"; + private final Map exactPathMatches = new HashMap<>(); + private final Map> pathsWithWildcard = new HashMap<>(); + private BiConsumer handlerAccumulator; + + private ImmutablePathMatcherBuilder() { + } + + /** + * @param handlerAccumulator policies defined with same path are accumulated, this way, you can define + * more than one policy of one path (e.g. one for POST method, one for GET method) + * @return ImmutablePathMatcherBuilder + */ + public ImmutablePathMatcherBuilder handlerAccumulator(BiConsumer handlerAccumulator) { + this.handlerAccumulator = handlerAccumulator; + return this; + } + + public ImmutablePathMatcher build() { + T defaultHandler = null; + SubstringMap paths = new SubstringMap<>(); + boolean hasPathWithInnerWildcard = false; + // process paths with a wildcard first, that way we only create inner path matcher when really needed + for (Path p : pathsWithWildcard.values()) { + T handler = null; + ImmutablePathMatcher> subPathMatcher = null; + + if (p.prefixPathHandler != null) { + handler = p.prefixPathHandler; + if (STRING_PATH_SEPARATOR.equals(p.path)) { + defaultHandler = p.prefixPathHandler; + } + } + + if (p.pathsWithInnerWildcard != null) { + if (!hasPathWithInnerWildcard) { + hasPathWithInnerWildcard = true; + } + // create path matcher for sub-path after inner wildcard: /one/*/three/four => /three/four + var builder = new ImmutablePathMatcherBuilder>(); + if (handlerAccumulator != null) { + builder.handlerAccumulator( + new BiConsumer, SubstringMatch>() { + @Override + public void accept(SubstringMatch match1, SubstringMatch match2) { + if (match2.hasSubPathMatcher()) { + // this should be impossible to happen since these matches are created + // right in this 'build()' method, but let's make sure of that + throw new IllegalStateException( + String.format("Failed to merge sub-matches with key '%s' for path '%s'", + match1.getKey(), p.originalPath)); + } + handlerAccumulator.accept(match1.getValue(), match2.getValue()); + } + }); + } + for (PathWithInnerWildcard p1 : p.pathsWithInnerWildcard) { + builder.addPath(p.originalPath, p1.remaining, new SubstringMatch<>(p1.remaining, p1.handler)); + } + subPathMatcher = builder.build(); + } + + paths.put(p.path, handler, subPathMatcher); + } + int[] lengths = buildLengths(paths.keys()); + return new ImmutablePathMatcher<>(defaultHandler, paths.asImmutableMap(), exactPathMatches, lengths, + hasPathWithInnerWildcard); + } + + /** + * Two sorts of paths are accepted: + * - exact path matches (without wildcard); these are matched first and Quarkus does no magic, + * request path must exactly match + * - paths with one or more wildcard: + * - ending wildcard matches zero or more path segment + * - inner wildcard matches exactly one path segment + * few notes: + * - it's key to understand only segments are matched, for example '/one*' will not match request path '/ones' + * - path patterns '/one*' and '/one/*' are one and the same thing as we only match path segments and '/one*' + * in fact means 'either /one or /one/any-number-of-path-segments' + * - paths are matched on longer-prefix-wins basis + * - what we call 'prefix' is in fact path to the first wildcard + * - if there is a path after first wildcard like in the '/one/*\/three' pattern ('/three' is remainder) + * path pattern is considered longer than the '/one/*' pattern and wins for request path '/one/two/three' + * - more specific pattern wins and wildcard is always less specific than any other path segment character, + * therefore path '/one/two/three*' will win over '/one/*\/three*' for request path '/one/two/three/four' + * + * @param path normalized path + * @param handler prefix path handler + * @return self + */ + public ImmutablePathMatcherBuilder addPath(String path, T handler) { + return addPath(path, path, handler); + } + + private ImmutablePathMatcherBuilder addPath(String originalPath, String path, T handler) { + if (!path.startsWith("/")) { + String errMsg = "Path must always start with a path separator, but was '" + path + "'"; + if (!originalPath.equals(path)) { + errMsg += " created from original path pattern '" + originalPath + "'"; + } + throw new IllegalArgumentException(errMsg); + } + final int wildcardIdx = path.indexOf('*'); + if (wildcardIdx == -1) { + addExactPath(path, handler); + } else { + addWildcardPath(path, handler, wildcardIdx, originalPath); + } + return this; + } + + private void addWildcardPath(String path, T handler, int wildcardIdx, String originalPath) { + final int lastIdx = path.length() - 1; + final String pathWithWildcard; + final String pathAfter1stWildcard; + + if (lastIdx == wildcardIdx) { + // ends with a wildcard => it's a prefix path + pathWithWildcard = path; + pathAfter1stWildcard = null; + } else { + // contains at least one inner wildcard: /one/*/three, /one/two/*/four/*, ... + // the inner wildcard represents exactly one path segment + pathWithWildcard = path.substring(0, wildcardIdx + 1); + pathAfter1stWildcard = path.substring(wildcardIdx + 1); + + // validate that inner wildcard is enclosed with path separators like: /one/*/two + // anything like: /one*/two, /one/*two/, /one/tw*o/ is not allowed + if (!pathWithWildcard.endsWith("/*") || !pathAfter1stWildcard.startsWith("/")) { + throw new ConfigurationException("HTTP permission path '" + originalPath + "' contains inner " + + "wildcard enclosed with a path character other than a separator. The inner wildcard " + + "must represent exactly one path segment. Please see this Quarkus guide for more " + + "information: https://quarkus.io/guides/security-authorize-web-endpoints-reference"); + } + } + + final String pathWithoutWildcard; + if (pathWithWildcard.endsWith("/*")) { + // remove /* + String stripped = pathWithWildcard.substring(0, pathWithWildcard.length() - 2); + pathWithoutWildcard = stripped.isEmpty() ? "/" : stripped; + } else { + // remove * + pathWithoutWildcard = pathWithWildcard.substring(0, pathWithWildcard.length() - 1); + } + + Path p = pathsWithWildcard.computeIfAbsent(pathWithoutWildcard, Path::new); + p.originalPath = originalPath; + if (pathAfter1stWildcard == null) { + p.addPrefixPath(handler, handlerAccumulator); + } else { + p.addPathWithInnerWildcard(pathAfter1stWildcard, handler); + } + } + + private void addExactPath(final String path, final T handler) { + if (path.isEmpty()) { + throw new IllegalArgumentException("Path not specified"); + } + if (exactPathMatches.containsKey(path) && handlerAccumulator != null) { + handlerAccumulator.accept(exactPathMatches.get(path), handler); + } else { + exactPathMatches.put(path, handler); + } + } + + private static int[] buildLengths(Iterable keys) { + final Set lengths = new TreeSet<>(new Comparator() { + @Override + public int compare(Integer o1, Integer o2) { + return -o1.compareTo(o2); + } + }); + for (String p : keys) { + lengths.add(p.length()); + } + + int[] lengthArray = new int[lengths.size()]; + int pos = 0; + for (int i : lengths) { + lengthArray[pos++] = i; + } + return lengthArray; + } + } + + private static class Path { + private final String path; + private String originalPath = null; + private T prefixPathHandler = null; + private List> pathsWithInnerWildcard = null; + + private Path(String path) { + this.path = path; + } + + private void addPathWithInnerWildcard(String remaining, T handler) { + if (pathsWithInnerWildcard == null) { + pathsWithInnerWildcard = new ArrayList<>(); + } + pathsWithInnerWildcard.add(new PathWithInnerWildcard<>(remaining, handler)); + } + + public void addPrefixPath(T prefixPathHandler, BiConsumer handlerAccumulator) { + Objects.requireNonNull(prefixPathHandler); + if (this.prefixPathHandler != null && handlerAccumulator != null) { + handlerAccumulator.accept(this.prefixPathHandler, prefixPathHandler); + } else { + this.prefixPathHandler = prefixPathHandler; + } + } + } + + private static class PathWithInnerWildcard { + private final String remaining; + private final T handler; + + private PathWithInnerWildcard(String remaining, T handler) { + this.remaining = remaining; + this.handler = handler; + } + } +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/ImmutableSubstringMap.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/ImmutableSubstringMap.java new file mode 100644 index 00000000000000..fd0e572b83cfd6 --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/ImmutableSubstringMap.java @@ -0,0 +1,135 @@ +package io.quarkus.vertx.http.runtime.security; + +import java.util.Arrays; + +import io.quarkus.vertx.http.runtime.security.ImmutablePathMatcher.PathMatch; + +/** + * A string keyed map that can be accessed as a substring, eliminating the need to allocate a new string + * to do a key comparison against. + */ +public class ImmutableSubstringMap { + + private static final int ALL_BUT_LAST_BIT = ~1; + private final Object[] table; + + ImmutableSubstringMap(Object[] table) { + this.table = Arrays.copyOf(table, table.length); + } + + @SuppressWarnings("unchecked") + public SubstringMatch get(String key, int length) { + if (key.length() < length) { + throw new IllegalArgumentException(); + } + int hash = hash(key, length); + int pos = tablePos(table, hash); + int start = pos; + while (table[pos] != null) { + if (doEquals((String) table[pos], key, length)) { + SubstringMatch match = (SubstringMatch) table[pos + 1]; + if (match == null) { + return null; + } + if (match.hasSubPathMatcher) { + // consider request path '/one/two/three/four/five' + // 'match.key' (which is prefix path) never ends with a slash, e.g. 'match.key=/one/two' + // which means index 'match.key.length()' is index of the last char of the '/one/two/' sub-path + // considering we are looking for a path segment after '/one/two/*', that is the first char + // of the '/four/five' sub-path, the separator index must be greater than 'match.key.length() + 1' + if (key.length() > (match.key.length() + 1)) { + // let say match key is '/one/two' + // then next path segment is '/four' and '/three' is skipped + // for path pattern was like: '/one/two/*/four/five' + int nextPathSegmentIdx = key.indexOf('/', match.key.length() + 1); + if (nextPathSegmentIdx != -1) { + // following the example above, 'nextPath' would be '/four/five' + // and * matched 'three' path segment characters + String nextPath = key.substring(nextPathSegmentIdx); + PathMatch> subMatch = match.subPathMatcher.match(nextPath); + if (subMatch.getValue() != null) { + return subMatch.getValue(); + } + } + } + + if (match.value == null) { + // paths with inner wildcard didn't match + // and there is no prefix path with ending wildcard either + return null; + } + } + // prefix path with ending wildcard: /one/two* + return match; + } + pos += 2; + if (pos >= table.length) { + pos = 0; + } + if (pos == start) { + return null; + } + } + return null; + } + + static int tablePos(Object[] table, int hash) { + return (hash & (table.length - 1)) & ALL_BUT_LAST_BIT; + } + + static boolean doEquals(String s1, String s2, int length) { + if (s1.length() != length || s2.length() < length) { + return false; + } + for (int i = 0; i < length; ++i) { + if (s1.charAt(i) != s2.charAt(i)) { + return false; + } + } + return true; + } + + static int hash(String value, int length) { + if (length == 0) { + return 0; + } + int h = 0; + for (int i = 0; i < length; i++) { + h = 31 * h + value.charAt(i); + } + return h; + } + + public static final class SubstringMatch { + private final String key; + private final V value; + private final boolean hasSubPathMatcher; + private final ImmutablePathMatcher> subPathMatcher; + + SubstringMatch(String key, V value) { + this.key = key; + this.value = value; + this.subPathMatcher = null; + this.hasSubPathMatcher = false; + } + + SubstringMatch(String key, V value, ImmutablePathMatcher> subPathMatcher) { + this.key = key; + this.value = value; + this.subPathMatcher = subPathMatcher; + this.hasSubPathMatcher = subPathMatcher != null; + } + + public String getKey() { + return key; + } + + public V getValue() { + return value; + } + + boolean hasSubPathMatcher() { + return hasSubPathMatcher; + } + } +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/PathMatcher.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/PathMatcher.java index 032a9f91fe1185..c069fe2645a0cd 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/PathMatcher.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/PathMatcher.java @@ -7,6 +7,8 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import io.quarkus.vertx.http.runtime.security.ImmutableSubstringMap.SubstringMatch; + /** * Handler that dispatches to a given handler based of a prefix match of the path. *

@@ -16,7 +18,10 @@ *

* * @author Stuart Douglas + * + * @deprecated use {@link ImmutablePathMatcher} instead */ +@Deprecated public class PathMatcher { private static final String STRING_PATH_SEPARATOR = "/"; @@ -55,7 +60,7 @@ public PathMatch match(String path) { final int[] lengths = this.lengths; for (int pathLength : lengths) { if (pathLength == length) { - SubstringMap.SubstringMatch next = paths.get(path, length); + SubstringMatch next = paths.get(path, length); if (next != null) { return new PathMatch<>(path, "", next.getValue()); } @@ -64,7 +69,7 @@ public PathMatch match(String path) { if (c == '/') { //String part = path.substring(0, pathLength); - SubstringMap.SubstringMatch next = paths.get(path, pathLength); + SubstringMatch next = paths.get(path, pathLength); if (next != null) { return new PathMatch<>(next.getKey(), path.substring(pathLength), next.getValue()); } @@ -117,7 +122,7 @@ public T getExactPath(final String path) { public T getPrefixPath(final String path) { // enable the prefix path mechanism to return the default handler - SubstringMap.SubstringMatch match = paths.get(path); + SubstringMatch match = paths.get(path); if (PathMatcher.STRING_PATH_SEPARATOR.equals(path) && match == null) { return this.defaultHandler; } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/SubstringMap.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/SubstringMap.java index 8ee402e48e220b..75867de490fe0f 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/SubstringMap.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/SubstringMap.java @@ -1,10 +1,16 @@ package io.quarkus.vertx.http.runtime.security; +import static io.quarkus.vertx.http.runtime.security.ImmutableSubstringMap.doEquals; +import static io.quarkus.vertx.http.runtime.security.ImmutableSubstringMap.hash; +import static io.quarkus.vertx.http.runtime.security.ImmutableSubstringMap.tablePos; + import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.NoSuchElementException; +import io.quarkus.vertx.http.runtime.security.ImmutableSubstringMap.SubstringMatch; + /** * A string keyed map that can be accessed as a substring, eliminating the need to allocate a new string * to do a key comparison against. @@ -17,19 +23,27 @@ * @author Stuart Douglas */ public class SubstringMap { - private static final int ALL_BUT_LAST_BIT = ~1; private volatile Object[] table = new Object[16]; private int size; + /** + * @deprecated use {@link ImmutablePathMatcher} + */ + @Deprecated public SubstringMatch get(String key, int length) { return get(key, length, false); } + /** + * @deprecated use {@link ImmutablePathMatcher} + */ + @Deprecated public SubstringMatch get(String key) { return get(key, key.length(), false); } + @SuppressWarnings("unchecked") private SubstringMatch get(String key, int length, boolean exact) { if (key.length() < length) { throw new IllegalArgumentException(); @@ -59,26 +73,19 @@ private SubstringMatch get(String key, int length, boolean exact) { return null; } - private int tablePos(Object[] table, int hash) { - return (hash & (table.length - 1)) & ALL_BUT_LAST_BIT; - } - - private boolean doEquals(String s1, String s2, int length) { - if (s1.length() != length || s2.length() < length) { - return false; - } - for (int i = 0; i < length; ++i) { - if (s1.charAt(i) != s2.charAt(i)) { - return false; - } - } - return true; + /** + * @deprecated use {@link ImmutablePathMatcher} + */ + @Deprecated + public synchronized void put(String key, V value) { + put(key, value, null); } - public synchronized void put(String key, V value) { + void put(String key, V value, ImmutablePathMatcher> subPathMatcher) { if (key == null) { throw new NullPointerException(); } + Object[] newTable; if (table.length / (double) size < 4 && table.length != Integer.MAX_VALUE) { newTable = new Object[table.length << 1]; @@ -91,11 +98,15 @@ public synchronized void put(String key, V value) { newTable = new Object[table.length]; System.arraycopy(table, 0, newTable, 0, table.length); } - doPut(newTable, key, new SubstringMap.SubstringMatch<>(key, value)); + doPut(newTable, key, new SubstringMatch<>(key, value, subPathMatcher)); this.table = newTable; size++; } + /** + * @deprecated use {@link ImmutablePathMatcher} + */ + @Deprecated public synchronized V remove(String key) { if (key == null) { throw new NullPointerException(); @@ -133,33 +144,30 @@ private void doPut(Object[] newTable, String key, Object value) { newTable[pos + 1] = value; } + /** + * @deprecated use {@link ImmutablePathMatcher} + */ + @Deprecated public Map toMap() { Map map = new HashMap<>(); Object[] t = this.table; for (int i = 0; i < t.length; i += 2) { if (t[i] != null) { - map.put((String) t[i], ((SubstringMatch) t[i + 1]).value); + map.put((String) t[i], ((SubstringMatch) t[i + 1]).getValue()); } } return map; } + /** + * @deprecated use {@link ImmutablePathMatcher} + */ + @Deprecated public synchronized void clear() { size = 0; table = new Object[16]; } - private static int hash(String value, int length) { - if (length == 0) { - return 0; - } - int h = 0; - for (int i = 0; i < length; i++) { - h = 31 * h + value.charAt(i); - } - return h; - } - public Iterable keys() { return new Iterable() { @Override @@ -206,21 +214,8 @@ public void remove() { } - public static final class SubstringMatch { - private final String key; - private final V value; - - public SubstringMatch(String key, V value) { - this.key = key; - this.value = value; - } - - public String getKey() { - return key; - } - - public V getValue() { - return value; - } + ImmutableSubstringMap asImmutableMap() { + return new ImmutableSubstringMap<>(table); } + } diff --git a/extensions/vertx-http/runtime/src/test/java/io/quarkus/vertx/http/runtime/PathMatcherTest.java b/extensions/vertx-http/runtime/src/test/java/io/quarkus/vertx/http/runtime/PathMatcherTest.java new file mode 100644 index 00000000000000..9cc89a3e3bd324 --- /dev/null +++ b/extensions/vertx-http/runtime/src/test/java/io/quarkus/vertx/http/runtime/PathMatcherTest.java @@ -0,0 +1,511 @@ +package io.quarkus.vertx.http.runtime; + +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 static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import io.quarkus.runtime.configuration.ConfigurationException; +import io.quarkus.vertx.http.runtime.security.ImmutablePathMatcher; + +public class PathMatcherTest { + + private static final Object HANDLER = new Object(); + + @Test + public void testPrefixPathWithEndingWildcard() { + ImmutablePathMatcher matcher = ImmutablePathMatcher.builder().addPath("/one/two/*", HANDLER).build(); + assertMatched(matcher, "/one/two"); + assertMatched(matcher, "/one/two/"); + assertMatched(matcher, "/one/two/three"); + assertNotMatched(matcher, "/one/twothree"); + assertNotMatched(matcher, "/one/tw"); + assertNotMatched(matcher, "/one"); + assertNotMatched(matcher, "/"); + assertNotMatched(matcher, ""); + final Object exactPathMatcher1 = new Object(); + final Object exactPathMatcher2 = new Object(); + final Object exactPathMatcher3 = new Object(); + final Object prefixPathMatcher1 = new Object(); + final Object prefixPathMatcher2 = new Object(); + matcher = ImmutablePathMatcher.builder().addPath("/one/two/*", prefixPathMatcher1) + .addPath("/one/two/three", exactPathMatcher1).addPath("/one/two", exactPathMatcher2) + .addPath("/one/two/three*", prefixPathMatcher2).addPath("/one/two/three/four", exactPathMatcher3).build(); + assertMatched(matcher, "/one/two/three", exactPathMatcher1); + assertMatched(matcher, "/one/two", exactPathMatcher2); + assertMatched(matcher, "/one/two/three/four", exactPathMatcher3); + assertMatched(matcher, "/one/two/three/fou", prefixPathMatcher2); + assertMatched(matcher, "/one/two/three/four/", prefixPathMatcher2); + assertMatched(matcher, "/one/two/three/five", prefixPathMatcher2); + assertMatched(matcher, "/one/two/three/", prefixPathMatcher2); + assertMatched(matcher, "/one/two/thre", prefixPathMatcher1); + assertMatched(matcher, "/one/two/", prefixPathMatcher1); + assertNotMatched(matcher, "/one/tw"); + assertNotMatched(matcher, "/one/"); + assertNotMatched(matcher, "/"); + assertNotMatched(matcher, ""); + } + + @Test + public void testPrefixPathDefaultHandler() { + final Object defaultHandler = new Object(); + ImmutablePathMatcher matcher = ImmutablePathMatcher.builder().addPath("/one/two*", HANDLER) + .addPath("/*", defaultHandler).addPath("/q*", HANDLER).build(); + assertMatched(matcher, "/", defaultHandler); + assertMatched(matcher, "", defaultHandler); + assertMatched(matcher, "0", defaultHandler); + assertMatched(matcher, "/q"); + assertMatched(matcher, "/q/dev-ui"); + assertMatched(matcher, "/qE", defaultHandler); + assertMatched(matcher, "/one/two"); + assertMatched(matcher, "/one/two/three"); + assertMatched(matcher, "/one/twothree", defaultHandler); + final Object exactPathMatcher1 = new Object(); + final Object exactPathMatcher2 = new Object(); + final Object exactPathMatcher3 = new Object(); + final Object prefixPathMatcher1 = new Object(); + final Object prefixPathMatcher2 = new Object(); + matcher = ImmutablePathMatcher.builder().addPath("/one/two/*", prefixPathMatcher1).addPath("/*", defaultHandler) + .addPath("/one/two/three", exactPathMatcher1).addPath("/one/two", exactPathMatcher2) + .addPath("/one/two/three*", prefixPathMatcher2).addPath("/one/two/three/four", exactPathMatcher3).build(); + assertMatched(matcher, "/one/two/three", exactPathMatcher1); + assertMatched(matcher, "/one/two", exactPathMatcher2); + assertMatched(matcher, "/one/two/three/four", exactPathMatcher3); + assertMatched(matcher, "/one/two/three/fou", prefixPathMatcher2); + assertMatched(matcher, "/one/two/three/four/", prefixPathMatcher2); + assertMatched(matcher, "/one/two/three/five", prefixPathMatcher2); + assertMatched(matcher, "/one/two/three/", prefixPathMatcher2); + assertMatched(matcher, "/one/two/thre", prefixPathMatcher1); + assertMatched(matcher, "/one/two/", prefixPathMatcher1); + assertMatched(matcher, "/one/tw", defaultHandler); + assertMatched(matcher, "/one/", defaultHandler); + assertMatched(matcher, "/", defaultHandler); + assertMatched(matcher, "", defaultHandler); + } + + @Test + public void testPrefixPathsNoDefaultHandlerNoExactPath() { + final Object handler1 = new Object(); + final Object handler2 = new Object(); + final ImmutablePathMatcher matcher = ImmutablePathMatcher.builder().addPath("/one/two*", handler1) + .addPath("/q*", handler2).build(); + assertNotMatched(matcher, "/"); + assertNotMatched(matcher, ""); + assertNotMatched(matcher, "0"); + assertMatched(matcher, "/q", handler2); + assertMatched(matcher, "/q/dev-ui", handler2); + assertNotMatched(matcher, "/qE"); + assertMatched(matcher, "/one/two", handler1); + assertMatched(matcher, "/one/two/three", handler1); + assertMatched(matcher, "/one/two/", handler1); + assertNotMatched(matcher, "/one/twothree"); + } + + @Test + public void testSpecialChars() { + // strictly speaking query params are not part of request path passed to the matcher + // but here they are treated like any other character different from path separator + final Object handler1 = new Object(); + final Object handler2 = new Object(); + final Object handler3 = new Object(); + final Object handler4 = new Object(); + final Object handler5 = new Object(); + // with default handler + ImmutablePathMatcher matcher = ImmutablePathMatcher.builder().addPath("/one/two#three", handler2) + .addPath("/one/two?three=four", handler1).addPath("/one/*/three?one\\\\\\=two", handler3) + .addPath("/one/two#three*", handler4).addPath("/*/two#three*", handler5).addPath("/*", HANDLER) + .build(); + assertMatched(matcher, "/one/two#three", handler2); + assertMatched(matcher, "/one/two?three=four", handler1); + assertMatched(matcher, "/one/any-value/three?one\\\\\\=two", handler3); + assertMatched(matcher, "/one/two/three?one\\\\\\=two", handler3); + assertMatched(matcher, "/one/two/three?one\\=two"); + assertMatched(matcher, "/one/two/three?one\\\\\\=two-three"); + assertMatched(matcher, "/one/two/three?one"); + assertMatched(matcher, "/one/two/three?"); + assertMatched(matcher, "/one/two#three?"); + assertMatched(matcher, "/one/two#thre"); + assertMatched(matcher, "/one/two"); + assertMatched(matcher, "/one/two?three=four#"); + assertMatched(matcher, "/one/two?three=fou"); + assertMatched(matcher, "/one/two#three/", handler4); + assertMatched(matcher, "/one/two#three/christmas!", handler4); + assertMatched(matcher, "/one/two#thre"); + assertMatched(matcher, "/one1/two#three", handler5); + assertMatched(matcher, "/one1/two#three/", handler5); + assertMatched(matcher, "/one1/two#three/christmas!", handler5); + assertMatched(matcher, "/one1/two#thre"); + // no default handler + matcher = ImmutablePathMatcher.builder().addPath("/one/two#three", handler2) + .addPath("/one/two?three=four", handler1).addPath("/one/*/three?one\\\\\\=two", handler3) + .addPath("/one/two#three*", handler4).addPath("/*/two#three*", handler5).build(); + assertMatched(matcher, "/one/two#three", handler2); + assertMatched(matcher, "/one/two?three=four", handler1); + assertMatched(matcher, "/one/any-value/three?one\\\\\\=two", handler3); + assertMatched(matcher, "/one/two/three?one\\\\\\=two", handler3); + assertNotMatched(matcher, "/one/two/three?one\\=two"); + assertNotMatched(matcher, "/one/two/three?one\\\\\\=two-three"); + assertNotMatched(matcher, "/one/two/three?one"); + assertNotMatched(matcher, "/one/two/three?"); + assertNotMatched(matcher, "/one/two#three?"); + assertNotMatched(matcher, "/one/two#thre"); + assertNotMatched(matcher, "/one/two"); + assertNotMatched(matcher, "/one/two?three=four#"); + assertNotMatched(matcher, "/one/two?three=fou"); + assertMatched(matcher, "/one/two#three/", handler4); + assertMatched(matcher, "/one/two#three/christmas!", handler4); + assertNotMatched(matcher, "/one/two#thre"); + assertMatched(matcher, "/one1/two#three", handler5); + assertMatched(matcher, "/one1/two#three/", handler5); + assertMatched(matcher, "/one1/two#three/christmas!", handler5); + assertNotMatched(matcher, "/one1/two#thre"); + } + + @Test + public void testInnerWildcardsWithExactMatches() { + final Object handler1 = new Object(); + final Object handler2 = new Object(); + final Object handler3 = new Object(); + final Object handler4 = new Object(); + final Object handler5 = new Object(); + final Object handler6 = new Object(); + final Object handler7 = new Object(); + final Object handler8 = new Object(); + final ImmutablePathMatcher matcher = ImmutablePathMatcher.builder().addPath("/one/two", handler1) + .addPath("/one/two/three", handler2).addPath("/one/two/three/four", handler3) + .addPath("/", handler4).addPath("/*", HANDLER).addPath("/one/two/*/four", handler5) + .addPath("/one/*/three/four", handler6).addPath("/*/two/three/four", handler7) + .addPath("/*/two", handler8).build(); + assertMatched(matcher, "/one/two", handler1); + assertMatched(matcher, "/one/two/three", handler2); + assertMatched(matcher, "/one/two/three/four", handler3); + assertMatched(matcher, "/", handler4); + assertMatched(matcher, ""); + assertMatched(matcher, "no-one-likes-us"); + assertMatched(matcher, "/one/two/we-do-not-care/four", handler5); + assertMatched(matcher, "/one/two/we-do-not-care/four/4"); + assertMatched(matcher, "/one/we-are-millwall/three/four", handler6); + assertMatched(matcher, "/1-one/we-are-millwall/three/four"); + assertMatched(matcher, "/super-millwall/two/three/four", handler7); + assertMatched(matcher, "/super-millwall/two/three/four/"); + assertMatched(matcher, "/super-millwall/two/three/four/1"); + assertMatched(matcher, "/from-the-den/two", handler8); + assertMatched(matcher, "/from-the-den/two2"); + } + + @Test + public void testInnerWildcardsOnly() { + final Object handler1 = new Object(); + final Object handler2 = new Object(); + final Object handler3 = new Object(); + final Object handler4 = new Object(); + final Object handler5 = new Object(); + // with default path handler + ImmutablePathMatcher matcher = ImmutablePathMatcher.builder().addPath("/*/two", handler2) + .addPath("/*/*/three", handler1).addPath("/one/*/three", handler3) + .addPath("/one/two/*/four", handler4).addPath("/one/two/three/*/five", handler5) + .addPath("/*", HANDLER).build(); + assertMatched(matcher, "/any-value"); + assertMatched(matcher, "/one/two/three/four/five", handler5); + assertMatched(matcher, "/one/two/three/4/five", handler5); + assertMatched(matcher, "/one/two/three/sergey/five", handler5); + assertMatched(matcher, "/one/two/three/sergey/five-ish"); + assertMatched(matcher, "/one/two/three/sergey/five/"); + assertMatched(matcher, "/one/two/three/four", handler4); + assertMatched(matcher, "/one/two/3/four", handler4); + assertMatched(matcher, "/one/two/three", handler3); + assertMatched(matcher, "/one/2/three", handler3); + assertMatched(matcher, "/one/some-very-long-text/three", handler3); + assertMatched(matcher, "/two"); + assertMatched(matcher, "/two/two", handler2); + assertMatched(matcher, "/2/two", handler2); + assertMatched(matcher, "/ho-hey/two", handler2); + assertMatched(matcher, "/ho-hey/two2"); + assertMatched(matcher, "/ho-hey/two2/"); + assertMatched(matcher, "/ho-hey/two/"); + assertMatched(matcher, "/ho-hey/hey-ho/three", handler1); + assertMatched(matcher, "/1/2/three", handler1); + assertMatched(matcher, "/1/two/three", handler1); + assertMatched(matcher, "/1/two/three/"); + assertMatched(matcher, "/1/two/three/f"); + // no default path handler + matcher = ImmutablePathMatcher.builder().addPath("/*/two", handler2) + .addPath("/*/*/three", handler1).addPath("/one/*/three", handler3) + .addPath("/one/two/*/four", handler4).addPath("/one/two/three/*/five", handler5).build(); + assertNotMatched(matcher, "/any-value"); + assertMatched(matcher, "/one/two/three/four/five", handler5); + assertMatched(matcher, "/one/two/three/4/five", handler5); + assertMatched(matcher, "/one/two/three/sergey/five", handler5); + assertNotMatched(matcher, "/one/two/three/sergey/five-ish"); + assertNotMatched(matcher, "/one/two/three/sergey/five/"); + assertMatched(matcher, "/one/two/three/four", handler4); + assertMatched(matcher, "/one/two/3/four", handler4); + assertMatched(matcher, "/one/two/three", handler3); + assertMatched(matcher, "/one/2/three", handler3); + assertMatched(matcher, "/one/some-very-long-text/three", handler3); + assertNotMatched(matcher, "/two"); + assertMatched(matcher, "/two/two", handler2); + assertMatched(matcher, "/2/two", handler2); + assertMatched(matcher, "/ho-hey/two", handler2); + assertNotMatched(matcher, "/ho-hey/two2"); + assertNotMatched(matcher, "/ho-hey/two2/"); + assertNotMatched(matcher, "/ho-hey/two/"); + assertMatched(matcher, "/ho-hey/hey-ho/three", handler1); + assertMatched(matcher, "/1/2/three", handler1); + assertMatched(matcher, "/1/two/three", handler1); + assertNotMatched(matcher, "/1/two/three/"); + assertNotMatched(matcher, "/1/two/three/f"); + } + + @Test + public void testInnerWildcardWithEndingWildcard() { + final Object handler1 = new Object(); + final Object handler2 = new Object(); + final Object handler3 = new Object(); + final Object handler4 = new Object(); + final Object handler5 = new Object(); + // with default handler + ImmutablePathMatcher matcher = ImmutablePathMatcher.builder().addPath("/*/two/*", handler1) + .addPath("/one/*/*", handler2).addPath("/one/two/*/four*", handler3) + .addPath("/one/*/three/*", handler4).addPath("/one/two/*/*", handler5) + .addPath("/*", HANDLER).build(); + assertMatched(matcher, "/one/two/three/four/five/six", handler3); + assertMatched(matcher, "/one/two/three/four/five", handler3); + assertMatched(matcher, "/one/two/three/four/", handler3); + assertMatched(matcher, "/one/two/three/four", handler3); + assertMatched(matcher, "/one/two/3/four", handler3); + assertMatched(matcher, "/one/two/three/4", handler5); + assertMatched(matcher, "/one/two/three/4/", handler5); + assertMatched(matcher, "/one/two/three/4/five", handler5); + assertMatched(matcher, "/one/2/three/four/five", handler4); + assertMatched(matcher, "/one/2/3/four/five", handler2); + assertMatched(matcher, "/1/two/three/four/five", handler1); + assertMatched(matcher, "/1/2/three/four/five"); + } + + @Test + public void testInnerWildcardsDefaultHandler() { + final Object handler1 = new Object(); + final Object handler2 = new Object(); + final Object handler3 = new Object(); + // both default root path handler and sub-path handler + ImmutablePathMatcher matcher = ImmutablePathMatcher.builder().addPath("/*/*", handler1) + .addPath("/*/*/three", handler3).addPath("/*", handler2).build(); + assertMatched(matcher, "/one/two/three", handler3); + assertMatched(matcher, "/one/two/four", handler1); + assertMatched(matcher, "/one/two", handler1); + assertMatched(matcher, "/one", handler2); + assertMatched(matcher, "/", handler2); + } + + @Test + public void testInvalidPathPattern() { + // path must start with a path separator + assertThrows(IllegalArgumentException.class, () -> ImmutablePathMatcher.builder().addPath("one", HANDLER).build()); + // inner wildcard must always be only path segment character + assertThrows(ConfigurationException.class, () -> ImmutablePathMatcher.builder().addPath("/one*/", HANDLER).build()); + assertThrows(ConfigurationException.class, () -> ImmutablePathMatcher.builder().addPath("/*one/", HANDLER).build()); + assertThrows(ConfigurationException.class, () -> ImmutablePathMatcher.builder().addPath("/o*ne/", HANDLER).build()); + assertThrows(ConfigurationException.class, () -> ImmutablePathMatcher.builder().addPath("/one/*two/", HANDLER).build()); + assertThrows(ConfigurationException.class, () -> ImmutablePathMatcher.builder().addPath("/one/*two/", HANDLER).build()); + assertThrows(ConfigurationException.class, () -> ImmutablePathMatcher.builder().addPath("/one/two*/", HANDLER).build()); + assertThrows(ConfigurationException.class, + () -> ImmutablePathMatcher.builder().addPath("/one/*two*/", HANDLER).build()); + } + + @Test + public void testExactPathHandlerMerging() { + List handler1 = new ArrayList<>(); + handler1.add("Neo"); + List handler2 = new ArrayList<>(); + handler2.add("Trinity"); + List handler3 = new ArrayList<>(); + handler3.add("Morpheus"); + var matcher = ImmutablePathMatcher.> builder().handlerAccumulator(List::addAll) + .addPath("/exact-path", handler1).addPath("/exact-path", handler2) + .addPath("/exact-not-matched", handler3).build(); + var handler = matcher.match("/exact-path").getValue(); + assertNotNull(handler); + assertTrue(handler.contains("Neo")); + assertTrue(handler.contains("Trinity")); + assertEquals(2, handler.size()); + handler = matcher.match("/exact-not-matched").getValue(); + assertNotNull(handler); + assertEquals(1, handler.size()); + } + + @Test + public void testPrefixPathHandlerMerging() { + List handler1 = new ArrayList<>(); + handler1.add("Neo"); + List handler2 = new ArrayList<>(); + handler2.add("Trinity"); + List handler3 = new ArrayList<>(); + handler3.add("Morpheus"); + List handler4 = new ArrayList<>(); + handler4.add("AgentSmith"); + List handler5 = new ArrayList<>(); + handler5.add("TheOracle"); + List handler6 = new ArrayList<>(); + handler6.add("AgentBrown"); + var matcher = ImmutablePathMatcher.> builder().handlerAccumulator(List::addAll).addPath("/path*", handler1) + .addPath("/path*", handler2).addPath("/path/*", handler3).addPath("/path/", handler4) + .addPath("/path/*/", handler5).addPath("/*", handler6).build(); + var handler = matcher.match("/path").getValue(); + assertNotNull(handler); + assertTrue(handler.contains("Neo")); + assertTrue(handler.contains("Trinity")); + assertTrue(handler.contains("Morpheus")); + assertEquals(3, handler.size()); + handler = matcher.match("/path/").getValue(); + assertNotNull(handler); + assertEquals(1, handler.size()); + assertTrue(handler.contains("AgentSmith")); + handler = matcher.match("/stuart").getValue(); + assertNotNull(handler); + assertEquals(1, handler.size()); + assertTrue(handler.contains("AgentBrown")); + handler = matcher.match("/path/ozzy/").getValue(); + assertNotNull(handler); + assertEquals(1, handler.size()); + assertTrue(handler.contains("TheOracle")); + } + + @Test + public void testInnerWildcardPathHandlerMerging() { + List handler1 = new ArrayList<>(); + handler1.add("Neo"); + List handler2 = new ArrayList<>(); + handler2.add("Trinity"); + List handler3 = new ArrayList<>(); + handler3.add("Morpheus"); + List handler4 = new ArrayList<>(); + handler4.add("AgentSmith"); + List handler5 = new ArrayList<>(); + handler5.add("TheOracle"); + List handler6 = new ArrayList<>(); + handler6.add("AgentBrown"); + List handler7 = new ArrayList<>(); + handler7.add("TheOperator"); + List handler8 = new ArrayList<>(); + handler8.add("TheSpoonBoy"); + List handler9 = new ArrayList<>(); + handler9.add("TheArchitect"); + List handler10 = new ArrayList<>(); + handler10.add("KeyMan"); + List handler11 = new ArrayList<>(); + handler11.add("Revolutions"); + List handler12 = new ArrayList<>(); + handler12.add("Reloaded-1"); + List handler13 = new ArrayList<>(); + handler13.add("Reloaded-2"); + List handler14 = new ArrayList<>(); + handler14.add("Reloaded-3"); + var matcher = ImmutablePathMatcher.> builder().handlerAccumulator(List::addAll) + .addPath("/*/one", handler1).addPath("/*/*", handler2).addPath("/*/*", handler3) + .addPath("/*/one", handler4).addPath("/*/two", handler5).addPath("/*", handler6) + .addPath("/one/*/three", handler7).addPath("/one/*", handler8).addPath("/one/*/*", handler9) + .addPath("/one/*/three", handler10).addPath("/one/*/*", handler11) + .addPath("/one/*/*/*", handler12).addPath("/one/*/*/*", handler13) + .addPath("/one/*/*/*", handler14).build(); + var handler = matcher.match("/one/two/three").getValue(); + assertNotNull(handler); + assertEquals(2, handler.size()); + assertTrue(handler.contains("TheOperator")); + assertTrue(handler.contains("KeyMan")); + handler = matcher.match("/one/two/three/four").getValue(); + assertNotNull(handler); + assertEquals(3, handler.size()); + assertTrue(handler.contains("Reloaded-1")); + assertTrue(handler.contains("Reloaded-2")); + assertTrue(handler.contains("Reloaded-3")); + handler = matcher.match("/one/2/3").getValue(); + assertNotNull(handler); + assertEquals(2, handler.size()); + assertTrue(handler.contains("TheArchitect")); + assertTrue(handler.contains("Revolutions")); + handler = matcher.match("/one/two").getValue(); + assertNotNull(handler); + assertEquals(1, handler.size()); + assertTrue(handler.contains("TheSpoonBoy")); + handler = matcher.match("/1/one").getValue(); + assertNotNull(handler); + assertEquals(2, handler.size()); + assertTrue(handler.contains("Neo")); + assertTrue(handler.contains("AgentSmith")); + handler = matcher.match("/1/two").getValue(); + assertNotNull(handler); + assertEquals(1, handler.size()); + assertTrue(handler.contains("TheOracle")); + handler = matcher.match("/father-brown").getValue(); + assertNotNull(handler); + assertEquals(1, handler.size()); + assertTrue(handler.contains("AgentBrown")); + handler = matcher.match("/welcome/to/the/jungle").getValue(); + assertNotNull(handler); + assertEquals(2, handler.size()); + assertTrue(handler.contains("Trinity")); + assertTrue(handler.contains("Morpheus")); + } + + @Test + public void testDefaultHandlerInnerWildcardAndEndingWildcard() { + // calling it default handler inner wildcard because first '/' path is matched and then '/one*' + // '/one*' is matched as prefix path + final ImmutablePathMatcher matcher = ImmutablePathMatcher.builder().addPath("/*/one*", HANDLER).build(); + assertMatched(matcher, "/1/one"); + assertMatched(matcher, "/2/one"); + assertMatched(matcher, "/3/one"); + assertMatched(matcher, "/4/one"); + assertMatched(matcher, "/4/one"); + assertMatched(matcher, "/1/one/"); + assertMatched(matcher, "/1/one/two"); + assertNotMatched(matcher, "/"); + assertNotMatched(matcher, "/1"); + assertNotMatched(matcher, "/1/"); + assertNotMatched(matcher, "/1/one1"); + assertNotMatched(matcher, "/1/two"); + assertNotMatched(matcher, "/1/on"); + } + + @Test + public void testDefaultHandlerOneInnerWildcard() { + // calling it default handler inner wildcard because first '/' path is matched and then '/one' + // '/one' is matched as exact path + final ImmutablePathMatcher matcher = ImmutablePathMatcher.builder().addPath("/*/one", HANDLER).build(); + assertMatched(matcher, "/1/one"); + assertMatched(matcher, "/2/one"); + assertMatched(matcher, "/3/one"); + assertMatched(matcher, "/4/one"); + assertMatched(matcher, "/4/one"); + assertNotMatched(matcher, "/"); + assertNotMatched(matcher, "/1"); + assertNotMatched(matcher, "/1/"); + assertNotMatched(matcher, "/1/two"); + assertNotMatched(matcher, "/1/one/"); + assertNotMatched(matcher, "/1/one1"); + assertNotMatched(matcher, "/1/on"); + assertNotMatched(matcher, "/1/one/two"); + } + + private static void assertMatched(ImmutablePathMatcher matcher, String path, Object handler) { + var match = matcher.match(path); + assertEquals(handler, match.getValue()); + } + + private static void assertMatched(ImmutablePathMatcher matcher, String path) { + assertMatched(matcher, path, HANDLER); + } + + private static void assertNotMatched(ImmutablePathMatcher matcher, String path) { + var match = matcher.match(path); + assertNull(match.getValue()); + } + +} diff --git a/extensions/vertx/deployment/src/test/java/io/quarkus/vertx/deployment/ConsumerNonexistingConfigPropertyTest.java b/extensions/vertx/deployment/src/test/java/io/quarkus/vertx/deployment/ConsumerNonexistingConfigPropertyTest.java new file mode 100644 index 00000000000000..7a44591d0c5789 --- /dev/null +++ b/extensions/vertx/deployment/src/test/java/io/quarkus/vertx/deployment/ConsumerNonexistingConfigPropertyTest.java @@ -0,0 +1,43 @@ +package io.quarkus.vertx.deployment; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.runtime.util.ExceptionUtil; +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.vertx.ConsumeEvent; +import io.vertx.mutiny.core.eventbus.Message; + +public class ConsumerNonexistingConfigPropertyTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot(root -> root.addClasses(MessageConsumers.class)) + .assertException(t -> { + Throwable root = ExceptionUtil.getRootCause(t); + assertTrue( + root.getMessage().contains( + "Could not expand value address.does.not.exist in property ${address.does.not.exist}"), + t.toString()); + }); + + @Test + public void test() throws InterruptedException { + fail(); + } + + @ApplicationScoped + static class MessageConsumers { + + @ConsumeEvent("${address.does.not.exist}") + void pub(Message name) { + } + + } + +} diff --git a/extensions/vertx/deployment/src/test/java/io/quarkus/vertx/deployment/MessageConsumerFailureTest.java b/extensions/vertx/deployment/src/test/java/io/quarkus/vertx/deployment/MessageConsumerFailureTest.java index c282c6c1bb49a7..888acb58d3d079 100644 --- a/extensions/vertx/deployment/src/test/java/io/quarkus/vertx/deployment/MessageConsumerFailureTest.java +++ b/extensions/vertx/deployment/src/test/java/io/quarkus/vertx/deployment/MessageConsumerFailureTest.java @@ -58,6 +58,12 @@ public void testFailure() throws InterruptedException { @Test public void testFailureNoReplyHandler() throws InterruptedException { + verifyFailureNoReply("foo", "Foo is dead", IllegalStateException.class); + verifyFailureNoReply("foo-blocking", "Red is dead", IllegalStateException.class); + } + + void verifyFailureNoReply(String address, String expectedMessage, Class expectedException) + throws InterruptedException { Handler oldHandler = vertx.exceptionHandler(); try { BlockingQueue synchronizer = new LinkedBlockingQueue<>(); @@ -71,10 +77,10 @@ public void handle(Throwable event) { } } }); - eventBus.send("foo", "bar"); + eventBus.send(address, "hello"); Object ret = synchronizer.poll(2, TimeUnit.SECONDS); - assertTrue(ret instanceof IllegalStateException); - assertEquals("Foo is dead", ((IllegalStateException) ret).getMessage()); + assertTrue(expectedException.isAssignableFrom(ret.getClass())); + assertEquals(expectedMessage, ((Throwable) ret).getMessage()); } finally { vertx.exceptionHandler(oldHandler); } diff --git a/extensions/vertx/deployment/src/test/java/io/quarkus/vertx/deployment/MessageConsumerMethodTest.java b/extensions/vertx/deployment/src/test/java/io/quarkus/vertx/deployment/MessageConsumerMethodTest.java index abb459f515f87d..6c62711ec84f1b 100644 --- a/extensions/vertx/deployment/src/test/java/io/quarkus/vertx/deployment/MessageConsumerMethodTest.java +++ b/extensions/vertx/deployment/src/test/java/io/quarkus/vertx/deployment/MessageConsumerMethodTest.java @@ -32,7 +32,8 @@ public class MessageConsumerMethodTest { @RegisterExtension static final QuarkusUnitTest config = new QuarkusUnitTest() - .withApplicationRoot((jar) -> jar.addClasses(SimpleBean.class, Transformer.class)); + .withApplicationRoot(root -> root.addClasses(SimpleBean.class, Transformer.class)) + .overrideConfigKey("foo", "foo-config"); @Inject SimpleBean simpleBean; @@ -200,6 +201,40 @@ public void testBlockingConsumerUsingSmallRyeBlocking() throws InterruptedExcept assertTrue(message.contains("hello::true")); } + @Test + public void testConfiguredAddress() throws InterruptedException { + BlockingQueue synchronizer = new LinkedBlockingQueue<>(); + eventBus.request("foo-config", "hello", ar -> { + if (ar.succeeded()) { + try { + synchronizer.put(ar.result().body()); + } catch (InterruptedException e) { + fail(e); + } + } else { + fail(ar.cause()); + } + }); + assertEquals("HELLO!", synchronizer.poll(2, TimeUnit.SECONDS)); + } + + @Test + public void testConfiguredAddressDefault() throws InterruptedException { + BlockingQueue synchronizer = new LinkedBlockingQueue<>(); + eventBus.request("foo-config-default", "hello", ar -> { + if (ar.succeeded()) { + try { + synchronizer.put(ar.result().body()); + } catch (InterruptedException e) { + fail(e); + } + } else { + fail(ar.cause()); + } + }); + assertEquals("hello!", synchronizer.poll(2, TimeUnit.SECONDS)); + } + static class SimpleBean { static volatile CountDownLatch latch; @@ -275,6 +310,17 @@ String blockingRequestContextActive(String message) { int reply(List numbers) { return numbers.stream().collect(Collectors.summingInt(Integer::intValue)); } + + @ConsumeEvent("${foo}") + String replyFooConfig(String message) { + return (message + "!").toUpperCase(); + } + + @ConsumeEvent("${non-existent.address:foo-config-default}") + String replyFooConfigDefault(String message) { + return (message + "!").toLowerCase(); + } + } @RequestScoped diff --git a/extensions/vertx/runtime/src/main/java/io/quarkus/vertx/ConsumeEvent.java b/extensions/vertx/runtime/src/main/java/io/quarkus/vertx/ConsumeEvent.java index 7e18bfb55dfbb3..c25c9f683076ff 100644 --- a/extensions/vertx/runtime/src/main/java/io/quarkus/vertx/ConsumeEvent.java +++ b/extensions/vertx/runtime/src/main/java/io/quarkus/vertx/ConsumeEvent.java @@ -95,6 +95,10 @@ /** * The address the consumer will be registered to. By default, the fully qualified name of the declaring bean class is * assumed. + *

+ * The value can be a config property expression. In this case, the configured value is used instead: + * {@code @ConsumeEvent("${my.consumer.address}")}. Additionally, the property expression can specify a default value: + * {@code @ConsumeEvent("${my.consumer.address:defaultAddress}")}. * * @return the address */ diff --git a/extensions/vertx/runtime/src/main/java/io/quarkus/vertx/runtime/VertxEventBusConsumerRecorder.java b/extensions/vertx/runtime/src/main/java/io/quarkus/vertx/runtime/VertxEventBusConsumerRecorder.java index 7a3edda5247662..6c8e6633064a4e 100644 --- a/extensions/vertx/runtime/src/main/java/io/quarkus/vertx/runtime/VertxEventBusConsumerRecorder.java +++ b/extensions/vertx/runtime/src/main/java/io/quarkus/vertx/runtime/VertxEventBusConsumerRecorder.java @@ -2,18 +2,25 @@ import static io.quarkus.vertx.core.runtime.context.VertxContextSafetyToggle.setContextSafe; import static io.quarkus.vertx.core.runtime.context.VertxContextSafetyToggle.setCurrentContextSafe; +import static io.smallrye.common.expression.Expression.Flag.LENIENT_SYNTAX; +import static io.smallrye.common.expression.Expression.Flag.NO_TRIM; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.NoSuchElementException; +import java.util.Optional; import java.util.concurrent.Callable; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CountDownLatch; +import java.util.function.BiConsumer; import java.util.function.Function; import java.util.function.Supplier; +import org.eclipse.microprofile.config.Config; +import org.eclipse.microprofile.config.spi.ConfigProviderResolver; import org.jboss.logging.Logger; import io.quarkus.arc.CurrentContextFactory; @@ -25,9 +32,12 @@ import io.quarkus.vertx.ConsumeEvent; import io.quarkus.vertx.LocalEventBusCodec; import io.quarkus.virtual.threads.VirtualThreadsRecorder; +import io.smallrye.common.expression.Expression; +import io.smallrye.common.expression.ResolveContext; import io.smallrye.common.vertx.VertxContext; import io.vertx.core.AsyncResult; import io.vertx.core.Context; +import io.vertx.core.Future; import io.vertx.core.Handler; import io.vertx.core.Vertx; import io.vertx.core.eventbus.EventBus; @@ -92,7 +102,7 @@ void registerMessageConsumers(Map messageConsumerConfigura final List registrationFailures = new ArrayList<>(); for (Entry entry : messageConsumerConfigurations.entrySet()) { EventConsumerInvoker invoker = createInvoker(entry.getKey()); - String address = entry.getValue().value(); + String address = lookUpPropertyValue(entry.getValue().value()); // Create a context attached to each consumer // If we don't all consumers will use the same event loop and so published messages (dispatched to all // consumers) delivery is serialized. @@ -138,7 +148,7 @@ public void run() { } }); } else { - dup.executeBlocking(new Callable() { + Future future = dup.executeBlocking(new Callable() { @Override public Void call() { try { @@ -154,6 +164,7 @@ public Void call() { return null; } }, invoker.isOrdered()); + future.onFailure(context::reportException); } } else { // Will run on the context used for the consumer registration. @@ -293,4 +304,55 @@ public String apply(Object messageBody) { public RuntimeValue forceStart(Supplier vertx) { return new RuntimeValue<>(vertx.get()); } + + /** + * Looks up the property value by checking whether the value is a configuration key and resolves it if so. + * + * @param propertyValue property value to look up. + * @return the resolved property value. + */ + private static String lookUpPropertyValue(String propertyValue) { + String value = propertyValue.stripLeading(); + if (!value.isEmpty() && isConfigExpression(value)) { + value = resolvePropertyExpression(value); + } + return value; + } + + /** + * Adapted from {@link io.smallrye.config.ExpressionConfigSourceInterceptor} + */ + private static String resolvePropertyExpression(String expr) { + // Force the runtime CL in order to make the DEV UI page work + final ClassLoader cl = VertxEventBusConsumerRecorder.class.getClassLoader(); + final Config config = ConfigProviderResolver.instance().getConfig(cl); + final Expression expression = Expression.compile(expr, LENIENT_SYNTAX, NO_TRIM); + final String expanded = expression.evaluate(new BiConsumer, StringBuilder>() { + @Override + public void accept(ResolveContext resolveContext, StringBuilder stringBuilder) { + final Optional resolve = config.getOptionalValue(resolveContext.getKey(), String.class); + if (resolve.isPresent()) { + stringBuilder.append(resolve.get()); + } else if (resolveContext.hasDefault()) { + resolveContext.expandDefault(); + } else { + throw new NoSuchElementException(String.format("Could not expand value %s in property %s", + resolveContext.getKey(), expr)); + } + } + }); + return expanded; + } + + private static boolean isConfigExpression(String val) { + if (val == null) { + return false; + } + int exprStart = val.indexOf("${"); + int exprEnd = -1; + if (exprStart >= 0) { + exprEnd = val.indexOf('}', exprStart + 2); + } + return exprEnd > 0; + } } diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/ApplicationModelBuilder.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/ApplicationModelBuilder.java index b4db9c3811fc5c..0dcc5c24da3257 100644 --- a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/ApplicationModelBuilder.java +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/ApplicationModelBuilder.java @@ -240,31 +240,28 @@ private boolean isExcluded(ArtifactCoords coords) { List buildDependencies() { for (ArtifactKey key : parentFirstArtifacts) { final ResolvedDependencyBuilder d = dependencies.get(key); - if (d != null && !d.isFlagSet(DependencyFlags.REMOVED)) { + if (d != null) { d.setFlags(DependencyFlags.CLASSLOADER_PARENT_FIRST); } } for (ArtifactKey key : runnerParentFirstArtifacts) { final ResolvedDependencyBuilder d = dependencies.get(key); - if (d != null && !d.isFlagSet(DependencyFlags.REMOVED)) { + if (d != null) { d.setFlags(DependencyFlags.CLASSLOADER_RUNNER_PARENT_FIRST); } } for (ArtifactKey key : lesserPriorityArtifacts) { final ResolvedDependencyBuilder d = dependencies.get(key); - if (d != null && !d.isFlagSet(DependencyFlags.REMOVED)) { + if (d != null) { d.setFlags(DependencyFlags.CLASSLOADER_LESSER_PRIORITY); } } final List result = new ArrayList<>(dependencies.size()); for (ResolvedDependencyBuilder db : this.dependencies.values()) { - if (isExcluded(db.getArtifactCoords())) { - db.setFlags(DependencyFlags.REMOVED); - db.clearFlag(DependencyFlags.DEPLOYMENT_CP); - db.clearFlag(DependencyFlags.RUNTIME_CP); + if (!isExcluded(db.getArtifactCoords())) { + result.add(db.build()); } - result.add(db.build()); } return result; } diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/maven/dependency/DependencyFlags.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/maven/dependency/DependencyFlags.java index dc700789874bf7..8d9c50148784a0 100644 --- a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/maven/dependency/DependencyFlags.java +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/maven/dependency/DependencyFlags.java @@ -23,13 +23,6 @@ public interface DependencyFlags { // once the processing of the whole tree has completed. int VISITED = 0b00100000000000; - /** - * Dependencies that were removed from the application model - * following {@code removed-artifacts} - * configuration properties collected from extension metadata. - */ - int REMOVED = 0b01000000000000; - /* @formatter:on */ } diff --git a/independent-projects/ide-config/pom.xml b/independent-projects/ide-config/pom.xml index f54b08aa6ab192..4437dc80e4461d 100644 --- a/independent-projects/ide-config/pom.xml +++ b/independent-projects/ide-config/pom.xml @@ -41,7 +41,6 @@ 11 3.2.1 3.1.2 - 1.6.8 @@ -117,62 +116,6 @@ true - - release - - - - org.sonatype.plugins - nexus-staging-maven-plugin - ${version.nexus-staging-maven-plugin} - true - - https://s01.oss.sonatype.org/ - ossrh - false - true - - - - org.apache.maven.plugins - maven-source-plugin - - - attach-sources - - jar-no-fork - - - - - - org.apache.maven.plugins - maven-javadoc-plugin - - - attach-javadocs - - jar - - - - - - org.apache.maven.plugins - maven-gpg-plugin - - - sign-artifacts - verify - - sign - - - - - - - diff --git a/independent-projects/parent/pom.xml b/independent-projects/parent/pom.xml index 67ff724afd4c05..be3075bf1548b7 100644 --- a/independent-projects/parent/pom.xml +++ b/independent-projects/parent/pom.xml @@ -422,7 +422,7 @@ - **/quarkus-ide-launcher-*.jar + META-INF/ide-deps/** @@ -562,6 +562,8 @@ .settings/* target/* .cache/* + .factorypath + *.log true diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateGlobalProvider.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateGlobalProvider.java new file mode 100644 index 00000000000000..7918f914ac5c94 --- /dev/null +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateGlobalProvider.java @@ -0,0 +1,10 @@ +package io.quarkus.qute; + +/** + * An implementation is generated for each class declaring a template global. + * + * @see TemplateGlobal + */ +public interface TemplateGlobalProvider extends TemplateInstance.Initializer, NamespaceResolver { + +} diff --git a/independent-projects/qute/generator/src/main/java/io/quarkus/qute/generator/AbstractGenerator.java b/independent-projects/qute/generator/src/main/java/io/quarkus/qute/generator/AbstractGenerator.java new file mode 100644 index 00000000000000..84475ab5f42a5d --- /dev/null +++ b/independent-projects/qute/generator/src/main/java/io/quarkus/qute/generator/AbstractGenerator.java @@ -0,0 +1,180 @@ +package io.quarkus.qute.generator; + +import static org.objectweb.asm.Opcodes.ACC_PRIVATE; + +import java.util.HashSet; +import java.util.Set; + +import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.DotName; +import org.jboss.jandex.FieldInfo; +import org.jboss.jandex.IndexView; +import org.jboss.jandex.PrimitiveType.Primitive; +import org.jboss.jandex.Type; + +import io.quarkus.gizmo.BranchResult; +import io.quarkus.gizmo.BytecodeCreator; +import io.quarkus.gizmo.ClassCreator; +import io.quarkus.gizmo.ClassOutput; +import io.quarkus.gizmo.FieldDescriptor; +import io.quarkus.gizmo.Gizmo; +import io.quarkus.gizmo.IfThenElse; +import io.quarkus.gizmo.MethodCreator; +import io.quarkus.gizmo.ResultHandle; +import io.quarkus.qute.CompletedStage; + +public abstract class AbstractGenerator { + + protected final Set generatedTypes; + protected final IndexView index; + protected final ClassOutput classOutput; + + protected AbstractGenerator(IndexView index, ClassOutput classOutput) { + this.generatedTypes = new HashSet<>(); + this.index = index; + this.classOutput = classOutput; + } + + public Set getGeneratedTypes() { + return generatedTypes; + } + + protected void completeBoolean(BytecodeCreator bc, ResultHandle result) { + BranchResult isTrue = bc.ifTrue(result); + BytecodeCreator trueBranch = isTrue.trueBranch(); + trueBranch.returnValue(trueBranch.readStaticField(Descriptors.RESULTS_TRUE)); + BytecodeCreator falseBranch = isTrue.falseBranch(); + falseBranch.returnValue(falseBranch.readStaticField(Descriptors.RESULTS_FALSE)); + } + + protected boolean isEnum(Type returnType) { + if (returnType.kind() != org.jboss.jandex.Type.Kind.CLASS) { + return false; + } + ClassInfo maybeEnum = index.getClassByName(returnType.name()); + return maybeEnum != null && maybeEnum.isEnum(); + } + + protected boolean hasCompletionStage(Type type) { + return !skipMemberType(type) && hasCompletionStageInTypeClosure(index.getClassByName(type.name()), index); + } + + protected boolean hasCompletionStageInTypeClosure(ClassInfo classInfo, + IndexView index) { + return hasClassInTypeClosure(classInfo, DotNames.COMPLETION_STAGE, index); + } + + protected boolean hasClassInTypeClosure(ClassInfo classInfo, DotName className, + IndexView index) { + + if (classInfo == null) { + // TODO cannot perform analysis + return false; + } + if (classInfo.name().equals(className)) { + return true; + } + // Interfaces + for (Type interfaceType : classInfo.interfaceTypes()) { + ClassInfo interfaceClassInfo = index.getClassByName(interfaceType.name()); + if (interfaceClassInfo != null && hasCompletionStageInTypeClosure(interfaceClassInfo, index)) { + return true; + } + } + // Superclass + if (classInfo.superClassType() != null) { + ClassInfo superClassInfo = index.getClassByName(classInfo.superName()); + if (superClassInfo != null && hasClassInTypeClosure(superClassInfo, className, index)) { + return true; + } + } + return false; + } + + protected void processReturnVal(BytecodeCreator bc, Type type, ResultHandle ret, ClassCreator classCreator) { + if (hasCompletionStage(type)) { + bc.returnValue(ret); + } else { + // Try to use some shared CompletedStage constants + if (type.kind() == org.jboss.jandex.Type.Kind.PRIMITIVE + && type.asPrimitiveType().primitive() == Primitive.BOOLEAN) { + completeBoolean(bc, ret); + } else if (type.name().equals(DotNames.BOOLEAN)) { + BytecodeCreator isNull = bc.ifNull(ret).trueBranch(); + isNull.returnValue(isNull.readStaticField(Descriptors.RESULTS_NULL)); + completeBoolean(bc, bc.invokeVirtualMethod(Descriptors.BOOLEAN_VALUE, ret)); + } else if (isEnum(type)) { + BytecodeCreator isNull = bc.ifNull(ret).trueBranch(); + isNull.returnValue(isNull.readStaticField(Descriptors.RESULTS_NULL)); + completeEnum(index.getClassByName(type.name()), classCreator, ret, bc); + } else { + bc.returnValue(bc.invokeStaticMethod(Descriptors.COMPLETED_STAGE_OF, ret)); + } + } + } + + protected boolean completeEnum(ClassInfo enumClass, ClassCreator valueResolver, ResultHandle result, BytecodeCreator bc) { + IfThenElse ifThenElse = null; + for (FieldInfo enumConstant : enumClass.enumConstants()) { + String name = enumClass.name().toString().replace(".", "_") + "$$" + + enumConstant.name(); + FieldDescriptor enumConstantField = FieldDescriptor.of(enumClass.name().toString(), + enumConstant.name(), enumClass.name().toString()); + + // Additional methods and fields are generated for enums that are part of the index + // We don't care about visibility and atomicity here + // private CompletedStage org_acme_MyEnum$$CONSTANT; + FieldDescriptor csField = valueResolver + .getFieldCreator(name, CompletedStage.class).setModifiers(ACC_PRIVATE) + .getFieldDescriptor(); + // private CompletedStage org_acme_MyEnum$$CONSTANT() { + // if (org_acme_MyEnum$$CONSTANT == null) { + // org_acme_MyEnum$$CONSTANT = CompletedStage.of(MyEnum.CONSTANT); + // } + // return org_acme_MyEnum$$CONSTANT; + // } + MethodCreator enumConstantMethod = valueResolver.getMethodCreator(name, + CompletedStage.class).setModifiers(ACC_PRIVATE); + BytecodeCreator isNull = enumConstantMethod.ifNull(enumConstantMethod + .readInstanceField(csField, enumConstantMethod.getThis())) + .trueBranch(); + ResultHandle val = isNull.readStaticField(enumConstantField); + isNull.writeInstanceField(csField, enumConstantMethod.getThis(), + isNull.invokeStaticMethod(Descriptors.COMPLETED_STAGE_OF, val)); + enumConstantMethod.returnValue(enumConstantMethod + .readInstanceField(csField, enumConstantMethod.getThis())); + + // Unfortunately, we can't use the BytecodeCreator#enumSwitch() here because the enum class is not loaded + // if(val.equals(MyEnum.CONSTANT)) + // return org_acme_MyEnum$$CONSTANT(); + BytecodeCreator match; + if (ifThenElse == null) { + ifThenElse = bc.ifThenElse( + Gizmo.equals(bc, bc.readStaticField(enumConstantField), result)); + match = ifThenElse.then(); + } else { + match = ifThenElse.elseIf( + b -> Gizmo.equals(b, b.readStaticField(enumConstantField), result)); + } + match.returnValue(match.invokeVirtualMethod( + enumConstantMethod.getMethodDescriptor(), match.getThis())); + } + return true; + } + + protected boolean skipMemberType(Type type) { + switch (type.kind()) { + case VOID: + case PRIMITIVE: + case ARRAY: + case TYPE_VARIABLE: + case UNRESOLVED_TYPE_VARIABLE: + case TYPE_VARIABLE_REFERENCE: + case WILDCARD_TYPE: + return true; + default: + return false; + } + } + +} diff --git a/independent-projects/qute/generator/src/main/java/io/quarkus/qute/generator/ExtensionMethodGenerator.java b/independent-projects/qute/generator/src/main/java/io/quarkus/qute/generator/ExtensionMethodGenerator.java index a9448bf545ef36..1eea9f6d65045e 100644 --- a/independent-projects/qute/generator/src/main/java/io/quarkus/qute/generator/ExtensionMethodGenerator.java +++ b/independent-projects/qute/generator/src/main/java/io/quarkus/qute/generator/ExtensionMethodGenerator.java @@ -14,7 +14,6 @@ import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; @@ -60,7 +59,7 @@ * @see ValueResolver * @see NamespaceResolver */ -public class ExtensionMethodGenerator { +public class ExtensionMethodGenerator extends AbstractGenerator { public static final DotName TEMPLATE_EXTENSION = DotName.createSimple(TemplateExtension.class.getName()); public static final DotName TEMPLATE_ATTRIBUTE = DotName.createSimple(TemplateExtension.TemplateAttribute.class.getName()); @@ -74,14 +73,8 @@ public class ExtensionMethodGenerator { public static final String NAMESPACE = "namespace"; public static final String PATTERN = "pattern"; - private final Set generatedTypes; - private final ClassOutput classOutput; - private final IndexView index; - public ExtensionMethodGenerator(IndexView index, ClassOutput classOutput) { - this.index = index; - this.classOutput = classOutput; - this.generatedTypes = new HashSet<>(); + super(index, classOutput); } public Set getGeneratedTypes() { @@ -235,8 +228,7 @@ private void implementResolve(ClassCreator valueResolver, ClassInfo declaringCla ResultHandle evalContext = resolve.getMethodParam(0); ResultHandle base = resolve.invokeInterfaceMethod(Descriptors.GET_BASE, evalContext); boolean isNameParamRequired = patternField != null || !matchNames.isEmpty() || matchName.equals(TemplateExtension.ANY); - boolean returnsCompletionStage = method.returnType().kind() != Kind.PRIMITIVE && ValueResolverGenerator - .hasCompletionStageInTypeClosure(index.getClassByName(method.returnType().name()), index); + boolean returnsCompletionStage = hasCompletionStage(method.returnType()); ResultHandle ret; if (!params.needsEvaluation()) { diff --git a/independent-projects/qute/generator/src/main/java/io/quarkus/qute/generator/TemplateGlobalGenerator.java b/independent-projects/qute/generator/src/main/java/io/quarkus/qute/generator/TemplateGlobalGenerator.java index e6a62d5122a370..6d259cb04ee9d5 100644 --- a/independent-projects/qute/generator/src/main/java/io/quarkus/qute/generator/TemplateGlobalGenerator.java +++ b/independent-projects/qute/generator/src/main/java/io/quarkus/qute/generator/TemplateGlobalGenerator.java @@ -6,43 +6,50 @@ import static org.objectweb.asm.Opcodes.ACC_PUBLIC; import java.lang.reflect.Modifier; -import java.util.HashSet; import java.util.Map; import java.util.Map.Entry; import java.util.Set; +import java.util.concurrent.CompletionStage; +import java.util.function.Consumer; import org.jboss.jandex.AnnotationTarget; import org.jboss.jandex.ClassInfo; import org.jboss.jandex.DotName; import org.jboss.jandex.FieldInfo; +import org.jboss.jandex.IndexView; import org.jboss.jandex.MethodInfo; import org.jboss.jandex.Type.Kind; +import io.quarkus.gizmo.BytecodeCreator; import io.quarkus.gizmo.ClassCreator; import io.quarkus.gizmo.ClassOutput; import io.quarkus.gizmo.FieldDescriptor; import io.quarkus.gizmo.MethodCreator; import io.quarkus.gizmo.MethodDescriptor; import io.quarkus.gizmo.ResultHandle; +import io.quarkus.gizmo.Switch.StringSwitch; +import io.quarkus.qute.EvalContext; import io.quarkus.qute.TemplateGlobal; +import io.quarkus.qute.TemplateGlobalProvider; import io.quarkus.qute.TemplateInstance; /** * Generates {@link TemplateInstance.Initializer}s for {@link TemplateGlobal} annotations. */ -public class TemplateGlobalGenerator { +public class TemplateGlobalGenerator extends AbstractGenerator { public static final DotName TEMPLATE_GLOBAL = DotName.createSimple(TemplateGlobal.class.getName()); public static final String NAME = "name"; public static final String SUFFIX = "_Globals"; - private final Set generatedTypes; - private final ClassOutput classOutput; + private final String namespace; + private int priority; - public TemplateGlobalGenerator(ClassOutput classOutput) { - this.generatedTypes = new HashSet<>(); - this.classOutput = classOutput; + public TemplateGlobalGenerator(ClassOutput classOutput, String namespace, int priority, IndexView index) { + super(index, classOutput); + this.namespace = namespace; + this.priority = priority; } public void generate(ClassInfo declaringClass, Map targets) { @@ -58,10 +65,11 @@ public void generate(ClassInfo declaringClass, Map tar String generatedName = generatedNameFromTarget(targetPackage, baseName, SUFFIX); generatedTypes.add(generatedName.replace('/', '.')); - ClassCreator initializer = ClassCreator.builder().classOutput(classOutput).className(generatedName) - .interfaces(TemplateInstance.Initializer.class).build(); + ClassCreator provider = ClassCreator.builder().classOutput(classOutput).className(generatedName) + .interfaces(TemplateGlobalProvider.class).build(); - MethodCreator accept = initializer.getMethodCreator("accept", void.class, Object.class) + // TemplateInstance.Initializer#accept() + MethodCreator accept = provider.getMethodCreator("accept", void.class, Object.class) .setModifiers(ACC_PUBLIC); for (Entry entry : targets.entrySet()) { @@ -84,7 +92,48 @@ public void generate(ClassInfo declaringClass, Map tar accept.invokeInterfaceMethod(Descriptors.TEMPLATE_INSTANCE_DATA, accept.getMethodParam(0), name, global); } accept.returnValue(null); - initializer.close(); + + // NamespaceResolver#getNamespace() + MethodCreator getNamespace = provider.getMethodCreator("getNamespace", String.class); + getNamespace.returnValue(getNamespace.load(namespace)); + + // WithPriority#getPriority() + MethodCreator getPriority = provider.getMethodCreator("getPriority", int.class); + // Namespace resolvers for the same namespace may not share the same priority + // So we increase the initial priority for each provider + getPriority.returnValue(getPriority.load(priority++)); + + // Resolver#resolve() + MethodCreator resolve = provider.getMethodCreator("resolve", CompletionStage.class, EvalContext.class) + .setModifiers(ACC_PUBLIC); + ResultHandle evalContext = resolve.getMethodParam(0); + ResultHandle name = resolve.invokeInterfaceMethod(Descriptors.GET_NAME, evalContext); + StringSwitch nameSwitch = resolve.stringSwitch(name); + for (Entry e : targets.entrySet()) { + Consumer readGlobal = new Consumer() { + @Override + public void accept(BytecodeCreator bc) { + switch (e.getValue().kind()) { + case FIELD: + FieldInfo field = e.getValue().asField(); + processReturnVal(bc, field.type(), bc.readStaticField(FieldDescriptor.of(field)), provider); + break; + case METHOD: + MethodInfo method = e.getValue().asMethod(); + processReturnVal(bc, method.returnType(), bc.invokeStaticMethod(MethodDescriptor.of(method)), + provider); + break; + default: + throw new IllegalStateException("Unsupported target: " + e.getValue()); + } + + } + }; + nameSwitch.caseOf(e.getKey(), readGlobal); + } + resolve.returnValue(resolve.invokeStaticMethod(Descriptors.RESULTS_NOT_FOUND_EC, evalContext)); + + provider.close(); } public Set getGeneratedTypes() { diff --git a/independent-projects/qute/generator/src/main/java/io/quarkus/qute/generator/ValueResolverGenerator.java b/independent-projects/qute/generator/src/main/java/io/quarkus/qute/generator/ValueResolverGenerator.java index ff869c8a15e7ad..f6d13de82b2678 100644 --- a/independent-projects/qute/generator/src/main/java/io/quarkus/qute/generator/ValueResolverGenerator.java +++ b/independent-projects/qute/generator/src/main/java/io/quarkus/qute/generator/ValueResolverGenerator.java @@ -1,7 +1,6 @@ package io.quarkus.qute.generator; import static java.util.function.Predicate.not; -import static org.objectweb.asm.Opcodes.ACC_PRIVATE; import static org.objectweb.asm.Opcodes.ACC_PUBLIC; import java.lang.reflect.Modifier; @@ -34,7 +33,6 @@ import org.jboss.jandex.IndexView; import org.jboss.jandex.MethodInfo; import org.jboss.jandex.PrimitiveType; -import org.jboss.jandex.PrimitiveType.Primitive; import org.jboss.jandex.Type; import org.jboss.logging.Logger; @@ -48,13 +46,11 @@ import io.quarkus.gizmo.FieldDescriptor; import io.quarkus.gizmo.FunctionCreator; import io.quarkus.gizmo.Gizmo; -import io.quarkus.gizmo.IfThenElse; import io.quarkus.gizmo.MethodCreator; import io.quarkus.gizmo.MethodDescriptor; import io.quarkus.gizmo.ResultHandle; import io.quarkus.gizmo.Switch; import io.quarkus.gizmo.TryBlock; -import io.quarkus.qute.CompletedStage; import io.quarkus.qute.EvalContext; import io.quarkus.qute.EvaluatedParams; import io.quarkus.qute.NamespaceResolver; @@ -66,7 +62,7 @@ * * @see ValueResolver */ -public class ValueResolverGenerator { +public class ValueResolverGenerator extends AbstractGenerator { public static Builder builder() { return new Builder(); @@ -92,9 +88,6 @@ public static Builder builder() { public static final int DEFAULT_PRIORITY = 10; - private final Set generatedTypes; - private final IndexView index; - private final ClassOutput classOutput; private final Map nameToClass; private final Map nameToTemplateData; @@ -103,18 +96,12 @@ public static Builder builder() { ValueResolverGenerator(IndexView index, ClassOutput classOutput, Map nameToClass, Map nameToTemplateData, Function> forceGettersFunction) { - this.generatedTypes = new HashSet<>(); - this.classOutput = classOutput; - this.index = index; + super(index, classOutput); this.nameToClass = new HashMap<>(nameToClass); this.nameToTemplateData = new HashMap<>(nameToTemplateData); this.forceGettersFunction = forceGettersFunction; } - public Set getGeneratedTypes() { - return generatedTypes; - } - /** * Generate value resolvers for all classes added via {@link Builder#addClass(ClassInfo, AnnotationInstance)}. */ @@ -377,33 +364,13 @@ private boolean implementResolve(ClassCreator valueResolver, String clazzName, C @Override public void accept(BytecodeCreator bc) { Type returnType = method.returnType(); - boolean hasCompletionStage = !skipMemberType(returnType) - && hasCompletionStageInTypeClosure(index.getClassByName(method.returnType().name()), index); ResultHandle invokeRet; if (Modifier.isInterface(clazz.flags())) { invokeRet = bc.invokeInterfaceMethod(MethodDescriptor.of(method), base); } else { invokeRet = bc.invokeVirtualMethod(MethodDescriptor.of(method), base); } - if (hasCompletionStage) { - bc.returnValue(invokeRet); - } else { - // Try to use some shared CompletedStage constants - if (returnType.kind() == org.jboss.jandex.Type.Kind.PRIMITIVE - && returnType.asPrimitiveType().primitive() == Primitive.BOOLEAN) { - completeBoolean(bc, invokeRet); - } else if (method.returnType().name().equals(DotNames.BOOLEAN)) { - BytecodeCreator isNull = bc.ifNull(invokeRet).trueBranch(); - isNull.returnValue(isNull.readStaticField(Descriptors.RESULTS_NULL)); - completeBoolean(bc, bc.invokeVirtualMethod(Descriptors.BOOLEAN_VALUE, invokeRet)); - } else if (isEnum(returnType)) { - BytecodeCreator isNull = bc.ifNull(invokeRet).trueBranch(); - isNull.returnValue(isNull.readStaticField(Descriptors.RESULTS_NULL)); - completeEnum(index.getClassByName(returnType.name()), valueResolver, invokeRet, bc); - } else { - bc.returnValue(bc.invokeStaticMethod(Descriptors.COMPLETED_STAGE_OF, invokeRet)); - } - } + processReturnVal(bc, returnType, invokeRet, valueResolver); } }; nameSwitch.caseOf(matchingNames, invokeMethod); @@ -487,71 +454,6 @@ public void accept(BytecodeCreator bc) { return true; } - private void completeBoolean(BytecodeCreator bc, ResultHandle result) { - BranchResult isTrue = bc.ifTrue(result); - BytecodeCreator trueBranch = isTrue.trueBranch(); - trueBranch.returnValue(trueBranch.readStaticField(Descriptors.RESULTS_TRUE)); - BytecodeCreator falseBranch = isTrue.falseBranch(); - falseBranch.returnValue(falseBranch.readStaticField(Descriptors.RESULTS_FALSE)); - } - - private boolean isEnum(Type returnType) { - if (returnType.kind() != org.jboss.jandex.Type.Kind.CLASS) { - return false; - } - ClassInfo maybeEnum = index.getClassByName(returnType.name()); - return maybeEnum != null && maybeEnum.isEnum(); - } - - private boolean completeEnum(ClassInfo enumClass, ClassCreator valueResolver, ResultHandle result, BytecodeCreator bc) { - IfThenElse ifThenElse = null; - for (FieldInfo enumConstant : enumClass.enumConstants()) { - String name = enumClass.name().toString().replace(".", "_") + "$$" - + enumConstant.name(); - FieldDescriptor enumConstantField = FieldDescriptor.of(enumClass.name().toString(), - enumConstant.name(), enumClass.name().toString()); - - // Additional methods and fields are generated for enums that are part of the index - // We don't care about visibility and atomicity here - // private CompletedStage org_acme_MyEnum$$CONSTANT; - FieldDescriptor csField = valueResolver - .getFieldCreator(name, CompletedStage.class).setModifiers(ACC_PRIVATE) - .getFieldDescriptor(); - // private CompletedStage org_acme_MyEnum$$CONSTANT() { - // if (org_acme_MyEnum$$CONSTANT == null) { - // org_acme_MyEnum$$CONSTANT = CompletedStage.of(MyEnum.CONSTANT); - // } - // return org_acme_MyEnum$$CONSTANT; - // } - MethodCreator enumConstantMethod = valueResolver.getMethodCreator(name, - CompletedStage.class).setModifiers(ACC_PRIVATE); - BytecodeCreator isNull = enumConstantMethod.ifNull(enumConstantMethod - .readInstanceField(csField, enumConstantMethod.getThis())) - .trueBranch(); - ResultHandle val = isNull.readStaticField(enumConstantField); - isNull.writeInstanceField(csField, enumConstantMethod.getThis(), - isNull.invokeStaticMethod(Descriptors.COMPLETED_STAGE_OF, val)); - enumConstantMethod.returnValue(enumConstantMethod - .readInstanceField(csField, enumConstantMethod.getThis())); - - // Unfortunately, we can't use the BytecodeCreator#enumSwitch() here because the enum class is not loaded - // if(val.equals(MyEnum.CONSTANT)) - // return org_acme_MyEnum$$CONSTANT(); - BytecodeCreator match; - if (ifThenElse == null) { - ifThenElse = bc.ifThenElse( - Gizmo.equals(bc, bc.readStaticField(enumConstantField), result)); - match = ifThenElse.then(); - } else { - match = ifThenElse.elseIf( - b -> Gizmo.equals(b, b.readStaticField(enumConstantField), result)); - } - match.returnValue(match.invokeVirtualMethod( - enumConstantMethod.getMethodDescriptor(), match.getThis())); - } - return true; - } - private boolean implementNamespaceResolve(ClassCreator valueResolver, String clazzName, ClassInfo clazz, Predicate filter) { MethodCreator resolve = valueResolver.getMethodCreator("resolve", CompletionStage.class, EvalContext.class) @@ -611,15 +513,13 @@ private boolean implementNamespaceResolve(ClassCreator valueResolver, String cla try (BytecodeCreator matchScope = createMatchScope(resolve, method.name(), 0, method.returnType(), name, params, paramsCount)) { ResultHandle ret; - boolean hasCompletionStage = !skipMemberType(method.returnType()) - && hasCompletionStageInTypeClosure(index.getClassByName(method.returnType().name()), index); ResultHandle invokeRet; if (Modifier.isInterface(clazz.flags())) { invokeRet = matchScope.invokeStaticInterfaceMethod(MethodDescriptor.of(method)); } else { invokeRet = matchScope.invokeStaticMethod(MethodDescriptor.of(method)); } - if (hasCompletionStage) { + if (hasCompletionStage(method.returnType())) { ret = invokeRet; } else { ret = matchScope.invokeStaticMethod(Descriptors.COMPLETED_STAGE_OF, invokeRet); @@ -702,11 +602,8 @@ private void matchMethod(MethodInfo method, ClassInfo clazz, MethodCreator resol paramsCount); // Invoke the method - ResultHandle ret; - boolean hasCompletionStage = !skipMemberType(method.returnType()) - && hasCompletionStageInTypeClosure(index.getClassByName(method.returnType().name()), index); // Evaluate the params first - ret = matchScope + ResultHandle ret = matchScope .newInstance(MethodDescriptor.ofConstructor(CompletableFuture.class)); // The CompletionStage upon which we invoke whenComplete() @@ -809,7 +706,7 @@ private void matchMethod(MethodInfo method, ClassInfo clazz, MethodCreator resol } } - if (hasCompletionStage) { + if (hasCompletionStage(method.returnType())) { FunctionCreator invokeWhenCompleteFun = tryCatch.createFunction(BiConsumer.class); tryCatch.invokeInterfaceMethod(Descriptors.CF_WHEN_COMPLETE, invokeRet, invokeWhenCompleteFun.getInstance()); @@ -908,9 +805,6 @@ private void matchMethods(String matchName, int matchParamsCount, Collection initFilters(AnnotationInstance templateData) { Predicate filter = ValueResolverGenerator::defaultFilter; if (templateData != null) { @@ -1332,38 +1211,6 @@ private static boolean noneMethodMatches(List methods, String name) { return true; } - public static boolean hasCompletionStageInTypeClosure(ClassInfo classInfo, - IndexView index) { - return hasClassInTypeClosure(classInfo, DotNames.COMPLETION_STAGE, index); - } - - public static boolean hasClassInTypeClosure(ClassInfo classInfo, DotName className, - IndexView index) { - - if (classInfo == null) { - // TODO cannot perform analysis - return false; - } - if (classInfo.name().equals(className)) { - return true; - } - // Interfaces - for (Type interfaceType : classInfo.interfaceTypes()) { - ClassInfo interfaceClassInfo = index.getClassByName(interfaceType.name()); - if (interfaceClassInfo != null && hasCompletionStageInTypeClosure(interfaceClassInfo, index)) { - return true; - } - } - // Superclass - if (classInfo.superClassType() != null) { - ClassInfo superClassInfo = index.getClassByName(classInfo.superName()); - if (superClassInfo != null && hasClassInTypeClosure(superClassInfo, className, index)) { - return true; - } - } - return false; - } - public static boolean isVarArgs(MethodInfo method) { return (method.flags() & 0x00000080) != 0; } diff --git a/independent-projects/qute/pom.xml b/independent-projects/qute/pom.xml index af929866376141..4a03e88808107a 100644 --- a/independent-projects/qute/pom.xml +++ b/independent-projects/qute/pom.xml @@ -49,7 +49,6 @@ 3.11.0 3.2.1 3.1.2 - 1.6.8 2.5.1 @@ -259,68 +258,6 @@ true - - release - - - - org.sonatype.plugins - nexus-staging-maven-plugin - ${version.nexus-staging-maven-plugin} - true - - https://s01.oss.sonatype.org/ - ossrh - false - true - - - - org.apache.maven.plugins - maven-source-plugin - - - attach-sources - - jar-no-fork - - - - - - org.apache.maven.plugins - maven-javadoc-plugin - - - attach-javadocs - - jar - - - - - - - org.apache.maven.plugins - maven-gpg-plugin - - - sign-artifacts - verify - - sign - - - - - - - format diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/SseEvent.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/SseEvent.java new file mode 100644 index 00000000000000..bcbee51c809dc4 --- /dev/null +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/SseEvent.java @@ -0,0 +1,42 @@ +package org.jboss.resteasy.reactive.client; + +/** + * Represents the entire SSE response from the server + */ +public interface SseEvent { + + /** + * Get event identifier. + *

+ * Contains value of SSE {@code "id"} field. This field is optional. Method may return {@code null}, if the event + * identifier is not specified. + * + * @return event id. + */ + String id(); + + /** + * Get event name. + *

+ * Contains value of SSE {@code "event"} field. This field is optional. Method may return {@code null}, if the event + * name is not specified. + * + * @return event name, or {@code null} if not set. + */ + String name(); + + /** + * Get a comment string that accompanies the event. + *

+ * Contains value of the comment associated with SSE event. This field is optional. Method may return {@code null}, if + * the event comment is not specified. + * + * @return comment associated with the event. + */ + String comment(); + + /** + * Get event data. + */ + T data(); +} diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/SseEventFilter.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/SseEventFilter.java new file mode 100644 index 00000000000000..d9419dca5dfdb2 --- /dev/null +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/SseEventFilter.java @@ -0,0 +1,22 @@ +package org.jboss.resteasy.reactive.client; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.function.Predicate; + +/** + * Used when not all SSE events streamed from the server should be included in the event stream returned by the client. + *

+ * IMPORTANT: implementations MUST contain a no-args constructor + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface SseEventFilter { + + /** + * Predicate which decides whether an event should be included in the event stream returned by the client. + */ + Class>> value(); +} diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/MultiInvoker.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/MultiInvoker.java index fe6a93492c42f4..4459e66000227a 100644 --- a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/MultiInvoker.java +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/MultiInvoker.java @@ -2,14 +2,19 @@ import java.io.ByteArrayInputStream; import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Predicate; import jakarta.ws.rs.client.Entity; import jakarta.ws.rs.core.GenericType; import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; +import org.jboss.resteasy.reactive.client.SseEvent; +import org.jboss.resteasy.reactive.client.SseEventFilter; import org.jboss.resteasy.reactive.common.jaxrs.ResponseImpl; import org.jboss.resteasy.reactive.common.util.RestMediaType; @@ -43,8 +48,8 @@ public Multi get(GenericType responseType) { /** * We need this class to work around a bug in Mutiny where we can register our cancel listener - * after the subscription is cancelled and we never get notified - * See https://github.com/smallrye/smallrye-mutiny/issues/417 + * after the subscription is cancelled, and we never get notified + * See ... */ static class MultiRequest { @@ -125,9 +130,11 @@ public Multi method(String name, Entity entity, GenericType respons if (!emitter.isCancelled()) { if (response.getStatus() == 200 && MediaType.SERVER_SENT_EVENTS_TYPE.isCompatible(response.getMediaType())) { - registerForSse(multiRequest, responseType, response, vertxResponse, + registerForSse( + multiRequest, responseType, vertxResponse, (String) restClientRequestContext.getProperties() - .get(RestClientRequestContext.DEFAULT_CONTENT_TYPE_PROP)); + .get(RestClientRequestContext.DEFAULT_CONTENT_TYPE_PROP), + restClientRequestContext.getInvokedMethod()); } else if (response.getStatus() == 200 && RestMediaType.APPLICATION_STREAM_JSON_TYPE.isCompatible(response.getMediaType())) { registerForJsonStream(multiRequest, restClientRequestContext, responseType, response, @@ -151,10 +158,19 @@ private boolean isNewlineDelimited(ResponseImpl response) { RestMediaType.APPLICATION_NDJSON_TYPE.isCompatible(response.getMediaType()); } + @SuppressWarnings({ "unchecked", "rawtypes" }) private void registerForSse(MultiRequest multiRequest, GenericType responseType, - Response response, - HttpClientResponse vertxResponse, String defaultContentType) { + HttpClientResponse vertxResponse, String defaultContentType, + Method invokedMethod) { + + boolean returnSseEvent = SseEvent.class.equals(responseType.getRawType()); + GenericType responseTypeFirstParam = responseType.getType() instanceof ParameterizedType + ? new GenericType(((ParameterizedType) responseType.getType()).getActualTypeArguments()[0]) + : null; + + Predicate> eventPredicate = createEventPredicate(invokedMethod); + // honestly, isn't reconnect contradictory with completion? // FIXME: Reconnect settings? // For now we don't want multi to reconnect @@ -163,17 +179,94 @@ private void registerForSse(MultiRequest multiRequest, multiRequest.onCancel(sseSource::close); sseSource.register(event -> { + + // TODO: we might want to cut down on the allocations here... + + if (eventPredicate != null) { + boolean keep = eventPredicate.test(new SseEvent<>() { + @Override + public String id() { + return event.getId(); + } + + @Override + public String name() { + return event.getName(); + } + + @Override + public String comment() { + return event.getComment(); + } + + @Override + public String data() { + return event.readData(); + } + }); + if (!keep) { + return; + } + } + // DO NOT pass the response mime type because it's SSE: let the event pick between the X-SSE-Content-Type header or // the content-type SSE field - R item = event.readData(responseType); - if (item != null) { // we don't emit null because it breaks Multi (by design) - multiRequest.emit(item); + + if (returnSseEvent) { + multiRequest.emit((R) new SseEvent() { + @Override + public String id() { + return event.getId(); + } + + @Override + public String name() { + return event.getName(); + } + + @Override + public String comment() { + return event.getComment(); + } + + @Override + public Object data() { + if (responseTypeFirstParam != null) { + return event.readData(responseTypeFirstParam); + } else { + return event.readData(); // TODO: is this correct? + } + } + }); + } else { + R item = event.readData(responseType); + if (item != null) { // we don't emit null because it breaks Multi (by design) + multiRequest.emit(item); + } } + }, multiRequest::fail, multiRequest::complete); // watch for user cancelling sseSource.registerAfterRequest(vertxResponse); } + private Predicate> createEventPredicate(Method invokedMethod) { + if (invokedMethod == null) { + return null; // should never happen + } + + SseEventFilter filterAnnotation = invokedMethod.getAnnotation(SseEventFilter.class); + if (filterAnnotation == null) { + return null; + } + + try { + return filterAnnotation.value().getConstructor().newInstance(); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + throw new RuntimeException(e); + } + } + private void registerForChunks(MultiRequest multiRequest, RestClientRequestContext restClientRequestContext, GenericType responseType, diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/multipart/MultipartFormDataOutput.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/multipart/MultipartFormDataOutput.java index df175a86839c8e..3b8212907a3ca9 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/multipart/MultipartFormDataOutput.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/multipart/MultipartFormDataOutput.java @@ -1,7 +1,7 @@ package org.jboss.resteasy.reactive.server.multipart; import java.util.Collections; -import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.Map; import jakarta.ws.rs.core.MediaType; @@ -10,7 +10,7 @@ * Used when a Resource method needs to return a multipart output */ public final class MultipartFormDataOutput { - private final Map parts = new HashMap<>(); + private final Map parts = new LinkedHashMap<>(); public Map getFormData() { return Collections.unmodifiableMap(parts); diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/providers/serialisers/ServerFileBodyHandler.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/providers/serialisers/ServerFileBodyHandler.java index aa974beede41fb..bd33ed659fda61 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/providers/serialisers/ServerFileBodyHandler.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/providers/serialisers/ServerFileBodyHandler.java @@ -3,13 +3,18 @@ import java.io.File; import java.lang.annotation.Annotation; import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.Produces; import jakarta.ws.rs.WebApplicationException; import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import org.jboss.logging.Logger; import org.jboss.resteasy.reactive.common.providers.serialisers.FileBodyHandler; +import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext; import org.jboss.resteasy.reactive.server.spi.ResteasyReactiveResourceInfo; import org.jboss.resteasy.reactive.server.spi.ServerMessageBodyWriter; import org.jboss.resteasy.reactive.server.spi.ServerRequestContext; @@ -30,6 +35,139 @@ public boolean isWriteable(Class type, Type genericType, ResteasyReactiveReso @Override public void writeResponse(File o, Type genericType, ServerRequestContext context) throws WebApplicationException { - context.serverResponse().sendFile(o.getAbsolutePath(), 0, o.length()); + sendFile(o, context); + } + + static void sendFile(File file, ServerRequestContext context) { + ResteasyReactiveRequestContext ctx = ((ResteasyReactiveRequestContext) context); + Object rangeObj = ctx.getHeader("Range", true); + ByteRange byteRange = rangeObj == null ? null : ByteRange.parse(rangeObj.toString()); + if ((byteRange != null) && (byteRange.ranges.size() == 1)) { + ByteRange.Range range = byteRange.ranges.get(0); + long length = range.getEnd() == -1 ? Long.MAX_VALUE : range.getEnd() - range.getStart() + 1; + context.serverResponse() + .setStatusCode(Response.Status.PARTIAL_CONTENT.getStatusCode()) + .sendFile(file.getAbsolutePath(), range.getStart(), length); + + } else { + context.serverResponse().sendFile(file.getAbsolutePath(), 0, file.length()); + } + } + + /** + * Represents a byte range for a range request + * + * @author Stuart Douglas + * + * NOTE: copied from Quarkus HTTP + */ + public static class ByteRange { + + private static final Logger log = Logger.getLogger(ByteRange.class); + + private final List ranges; + + public ByteRange(List ranges) { + this.ranges = ranges; + } + + public int getRanges() { + return ranges.size(); + } + + /** + * Gets the start of the specified range segment, of -1 if this is a suffix range segment + * + * @param range The range segment to get + * @return The range start + */ + public long getStart(int range) { + return ranges.get(range).getStart(); + } + + /** + * Gets the end of the specified range segment, or the number of bytes if this is a suffix range segment + * + * @param range The range segment to get + * @return The range end + */ + public long getEnd(int range) { + return ranges.get(range).getEnd(); + } + + /** + * Attempts to parse a range request. If the range request is invalid it will just return null so that + * it may be ignored. + * + * @param rangeHeader The range spec + * @return A range spec, or null if the range header could not be parsed + */ + public static ByteRange parse(String rangeHeader) { + if (rangeHeader == null || rangeHeader.length() < 7) { + return null; + } + if (!rangeHeader.startsWith("bytes=")) { + return null; + } + List ranges = new ArrayList<>(); + String[] parts = rangeHeader.substring(6).split(","); + for (String part : parts) { + try { + int index = part.indexOf('-'); + if (index == 0) { + //suffix range spec + //represents the last N bytes + //internally we represent this using a -1 as the start position + long val = Long.parseLong(part.substring(1)); + if (val < 0) { + log.debugf("Invalid range spec %s", rangeHeader); + return null; + } + ranges.add(new Range(-1, val)); + } else { + if (index == -1) { + log.debugf("Invalid range spec %s", rangeHeader); + return null; + } + long start = Long.parseLong(part.substring(0, index)); + if (start < 0) { + log.debugf("Invalid range spec %s", rangeHeader); + return null; + } + long end; + if (index + 1 < part.length()) { + end = Long.parseLong(part.substring(index + 1)); + } else { + end = -1; + } + ranges.add(new Range(start, end)); + } + } catch (NumberFormatException e) { + log.debugf("Invalid range spec %s", rangeHeader); + return null; + } + } + if (ranges.isEmpty()) { + return null; + } + return new ByteRange(ranges); + } + + public static class Range { + private final long start, end; + + public Range(long start, long end) { + this.start = start; + this.end = end; + } + + public long getStart() { + return start; + } + + public long getEnd() { + return end; + } + } } } diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/providers/serialisers/ServerPathBodyHandler.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/providers/serialisers/ServerPathBodyHandler.java index 0585d844262860..252ddd98a8dffd 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/providers/serialisers/ServerPathBodyHandler.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/providers/serialisers/ServerPathBodyHandler.java @@ -12,7 +12,6 @@ import org.jboss.resteasy.reactive.common.providers.serialisers.PathBodyHandler; import org.jboss.resteasy.reactive.server.spi.ResteasyReactiveResourceInfo; -import org.jboss.resteasy.reactive.server.spi.ServerHttpResponse; import org.jboss.resteasy.reactive.server.spi.ServerMessageBodyWriter; import org.jboss.resteasy.reactive.server.spi.ServerRequestContext; @@ -36,8 +35,6 @@ public boolean isWriteable(Class type, Type genericType, ResteasyReactiveReso @Override public void writeResponse(java.nio.file.Path o, Type genericType, ServerRequestContext context) throws WebApplicationException { - ServerHttpResponse serverResponse = context.serverResponse(); - // sendFile implies end(), even though javadoc doesn't say, if you add end() it will throw - serverResponse.sendFile(o.toString(), 0, Long.MAX_VALUE); + ServerFileBodyHandler.sendFile(o.toFile(), context); } } diff --git a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-extension/code/extension-base/java/pom.tpl.qute.xml b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-extension/code/extension-base/java/pom.tpl.qute.xml index 495ab9aeb16b8e..e69db96309167e 100644 --- a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-extension/code/extension-base/java/pom.tpl.qute.xml +++ b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-extension/code/extension-base/java/pom.tpl.qute.xml @@ -32,7 +32,7 @@ UTF-8 UTF-8 - 11 + {java.version} {#if quarkus.version}{quarkus.version}{/if} {#if maven.compiler-plugin-version}{maven.compiler-plugin-version}{/if} {#if maven.surefire-plugin.version} diff --git a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-extension/code/integration-tests/java/integration-tests/pom.tpl.qute.xml b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-extension/code/integration-tests/java/integration-tests/pom.tpl.qute.xml index bd9fbadde6637a..4e3da1d3726389 100644 --- a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-extension/code/integration-tests/java/integration-tests/pom.tpl.qute.xml +++ b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-extension/code/integration-tests/java/integration-tests/pom.tpl.qute.xml @@ -77,15 +77,15 @@ integration-test verify - - - $\{project.build.directory}/$\{project.build.finalName}-runner - org.jboss.logmanager.LogManager - $\{maven.home} - - + + + $\{project.build.directory}/$\{project.build.finalName}-runner + org.jboss.logmanager.LogManager + $\{maven.home} + + diff --git a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-extension/code/quarkiverse/java/.github/workflows/build.tpl.qute.yml b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-extension/code/quarkiverse/java/.github/workflows/build.tpl.qute.yml new file mode 100644 index 00000000000000..7fb0447cfde84f --- /dev/null +++ b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-extension/code/quarkiverse/java/.github/workflows/build.tpl.qute.yml @@ -0,0 +1,59 @@ +name: Build + +on: + push: + branches: + - "main" + paths-ignore: + - '.gitignore' + - 'CODEOWNERS' + - 'LICENSE' + - '*.md' + - '*.adoc' + - '*.txt' + - '.all-contributorsrc' + pull_request: + paths-ignore: + - '.gitignore' + - 'CODEOWNERS' + - 'LICENSE' + - '*.md' + - '*.adoc' + - '*.txt' + - '.all-contributorsrc' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +defaults: + run: + shell: bash + +jobs: + build: + name: Build on ${{ matrix.os }} + strategy: + fail-fast: false + matrix: +# os: [windows-latest, macos-latest, ubuntu-latest] + os: [ubuntu-latest] + runs-on: ${{ matrix.os }} + steps: + - name: Prepare git + run: git config --global core.autocrlf false + if: startsWith(matrix.os, 'windows') + + - uses: actions/checkout@v3 + - name: Set up JDK {java.version} + uses: actions/setup-java@v3 + with: + distribution: temurin + java-version: {java.version} + cache: 'maven' + + - name: Build with Maven + run: mvn -B clean install -Dno-format + + - name: Build with Maven (Native) + run: mvn -B install -Dnative -Dquarkus.native.container-build -Dnative.surefire.skip \ No newline at end of file diff --git a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-extension/code/quarkiverse/java/.github/workflows/quarkus-snapshot.tpl.qute.yaml b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-extension/code/quarkiverse/java/.github/workflows/quarkus-snapshot.tpl.qute.yaml index ed334a3b39e751..2f2022972f3e5b 100644 --- a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-extension/code/quarkiverse/java/.github/workflows/quarkus-snapshot.tpl.qute.yaml +++ b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-extension/code/quarkiverse/java/.github/workflows/quarkus-snapshot.tpl.qute.yaml @@ -10,7 +10,7 @@ on: env: ECOSYSTEM_CI_REPO: quarkusio/quarkus-ecosystem-ci ECOSYSTEM_CI_REPO_FILE: context.yaml - JAVA_VERSION: 11 + JAVA_VERSION: {java.version} ######################### # Repo specific setting # diff --git a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-extension/code/quarkiverse/java/.github/workflows/release.tpl.qute.yml b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-extension/code/quarkiverse/java/.github/workflows/release.tpl.qute.yml new file mode 100644 index 00000000000000..075d011604ab56 --- /dev/null +++ b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-extension/code/quarkiverse/java/.github/workflows/release.tpl.qute.yml @@ -0,0 +1,74 @@ +name: Quarkiverse Release + +on: + pull_request: + types: [closed] + paths: + - '.github/project.yml' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +defaults: + run: + shell: bash + +jobs: + release: + runs-on: ubuntu-latest + name: release + if: ${{github.event.pull_request.merged == true}} + + steps: + - uses: radcortez/project-metadata-action@main + name: Retrieve project metadata + id: metadata + with: + github-token: ${{secrets.GITHUB_TOKEN}} + metadata-file-path: '.github/project.yml' + + - uses: actions/checkout@v3 + + - name: Import GPG key + id: import_gpg + uses: crazy-max/ghaction-import-gpg@v6 + with: + gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} + passphrase: ${{ secrets.GPG_PASSPHRASE }} + + - name: Set up JDK {java.version} + uses: actions/setup-java@v3 + with: + distribution: temurin + java-version: {java.version} + cache: 'maven' + server-id: ossrh + server-username: MAVEN_USERNAME + server-password: MAVEN_PASSWORD + + - name: Configure Git author + run: | + git config --local user.email "action@github.com" + git config --local user.name "GitHub Action" + + - name: Update latest release version in docs + run: | + mvn -B -ntp -pl docs -am package -DskipTests -DskipITs -Denforcer.skip -Dformatter.skip -Dimpsort.skip + if ! git diff --quiet docs/modules/ROOT/pages/includes/attributes.adoc; then + git add docs/modules/ROOT/pages/includes/attributes.adoc + git commit -m "Update the latest release version ${{steps.metadata.outputs.current-version}} in documentation" + fi + + - name: Maven release ${{steps.metadata.outputs.current-version}} + run: | + mvn -B release:prepare -Prelease -DreleaseVersion=${{steps.metadata.outputs.current-version}} -DdevelopmentVersion=${{steps.metadata.outputs.next-version}} + mvn -B release:perform -Darguments=-DperformRelease -DperformRelease -Prelease + env: + MAVEN_USERNAME: ${{ secrets.OSSRH_USERNAME }} + MAVEN_PASSWORD: ${{ secrets.OSSRH_TOKEN }} + + - name: Push changes to ${{github.base_ref}} branch + run: | + git push + git push origin ${{steps.metadata.outputs.current-version}} diff --git a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-extension/code/quarkiverse/java/README.tpl.qute.md b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-extension/code/quarkiverse/java/README.tpl.qute.md index 1184c7f8f67eba..0e5dbe880f4e59 100644 --- a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-extension/code/quarkiverse/java/README.tpl.qute.md +++ b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-extension/code/quarkiverse/java/README.tpl.qute.md @@ -1,6 +1,6 @@ # {extension.full-name} -[![Version](https://img.shields.io/maven-central/v/{group-id}/{namespace.id}{extension.id}?logo=apache-maven&style=flat-square)](https://search.maven.org/artifact/{group-id}/{namespace.id}{extension.id}) +[![Version](https://img.shields.io/maven-central/v/{group-id}/{namespace.id}{extension.id}?logo=apache-maven&style=flat-square)](https://central.sonatype.com/artifact/{group-id}/{namespace.id}{extension.id}-parent) ## Welcome to Quarkiverse! @@ -22,4 +22,6 @@ The documentation for this extension should be maintained as part of this reposi The layout should follow the [Antora's Standard File and Directory Set](https://docs.antora.org/antora/2.3/standard-directories/). -Once the docs are ready to be published, please open a PR including this repository in the [Quarkiverse Docs Antora playbook](https://github.com/quarkiverse/quarkiverse-docs/blob/main/antora-playbook.yml#L7). See an example [here](https://github.com/quarkiverse/quarkiverse-docs/pull/1). +Once the docs are ready to be published, please open a PR including this repository in the [Quarkiverse Docs Antora playbook](https://github.com/quarkiverse/quarkiverse-docs/blob/main/antora-playbook.yml#L7). See an example [here](https://github.com/quarkiverse/quarkiverse-docs/pull/1) + +Your documentation will then be published to the https://docs.quarkiverse.io/ website. diff --git a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-extension/code/quarkiverse/java/docs/antora.tpl.qute.yml b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-extension/code/quarkiverse/java/docs/antora.tpl.qute.yml index b5e30215664320..b38bbf37c0b893 100644 --- a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-extension/code/quarkiverse/java/docs/antora.tpl.qute.yml +++ b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-extension/code/quarkiverse/java/docs/antora.tpl.qute.yml @@ -1,5 +1,5 @@ name: {namespace.id}{extension.id} -title: {extension.full-name} +title: {extension.name} version: dev nav: - modules/ROOT/nav.adoc diff --git a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-extension/code/quarkiverse/java/docs/modules/ROOT/nav.tpl.qute.adoc b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-extension/code/quarkiverse/java/docs/modules/ROOT/nav.tpl.qute.adoc index 3c4fdef0a75c10..d2454cdb59abce 100644 --- a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-extension/code/quarkiverse/java/docs/modules/ROOT/nav.tpl.qute.adoc +++ b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-extension/code/quarkiverse/java/docs/modules/ROOT/nav.tpl.qute.adoc @@ -1 +1 @@ -* xref:index.adoc[{extension.full-name}] +* xref:index.adoc[Getting started] diff --git a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/buildtool/maven/base/pom.tpl.qute.xml b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/buildtool/maven/base/pom.tpl.qute.xml index 1cc019c90e0ac4..f4d187b6246624 100644 --- a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/buildtool/maven/base/pom.tpl.qute.xml +++ b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/buildtool/maven/base/pom.tpl.qute.xml @@ -177,17 +177,17 @@ integration-test verify - - - {#if generate-native} - $\{project.build.directory}/$\{project.build.finalName}-runner - {/if} - org.jboss.logmanager.LogManager - $\{maven.home} - - + + + {#if generate-native} + $\{project.build.directory}/$\{project.build.finalName}-runner + {/if} + org.jboss.logmanager.LogManager + $\{maven.home} + + diff --git a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/tooling/dockerfiles/base/Dockerfile-layout.include.qute b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/tooling/dockerfiles/base/Dockerfile-layout.include.qute index c3dab99e60b8f7..eade03aa1a9396 100644 --- a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/tooling/dockerfiles/base/Dockerfile-layout.include.qute +++ b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/tooling/dockerfiles/base/Dockerfile-layout.include.qute @@ -77,7 +77,7 @@ # accessed directly. (example: "foo.example.com,bar.example.com") # ### -FROM registry.access.redhat.com/ubi8/openjdk-{java.version}:1.17 +FROM registry.access.redhat.com/ubi8/openjdk-{java.version}:1.18 ENV LANGUAGE='en_US:en' diff --git a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/tooling/dockerfiles/codestart.yml b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/tooling/dockerfiles/codestart.yml index 6cda40b09636a7..22e7403ac8e26e 100644 --- a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/tooling/dockerfiles/codestart.yml +++ b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/tooling/dockerfiles/codestart.yml @@ -5,6 +5,6 @@ language: data: dockerfile: native: - from: registry.access.redhat.com/ubi8/ubi-minimal:8.8 + from: registry.access.redhat.com/ubi8/ubi-minimal:8.9 native-micro: from: quay.io/quarkus/quarkus-micro-image:2.0 diff --git a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/codestarts/extension/QuarkusExtensionCodestartCatalog.java b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/codestarts/extension/QuarkusExtensionCodestartCatalog.java index 9575a522ba100a..c3cedb84178232 100644 --- a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/codestarts/extension/QuarkusExtensionCodestartCatalog.java +++ b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/codestarts/extension/QuarkusExtensionCodestartCatalog.java @@ -38,6 +38,7 @@ public enum QuarkusExtensionData implements DataKey { QUARKUS_BOM_GROUP_ID("quarkus.bom.group-id"), QUARKUS_BOM_ARTIFACT_ID("quarkus.bom.artifact-id"), QUARKUS_BOM_VERSION("quarkus.bom.version"), + JAVA_VERSION("java.version"), PROPERTIES_FROM_PARENT("properties.from-parent"), PARENT_GROUP_ID("parent.group-id"), PARENT_ARTIFACT_ID("parent.artifact-id"), diff --git a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/commands/CreateExtension.java b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/commands/CreateExtension.java index ced0076c1d651b..9ea5fb45a326ab 100644 --- a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/commands/CreateExtension.java +++ b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/commands/CreateExtension.java @@ -1,7 +1,36 @@ package io.quarkus.devtools.commands; -import static io.quarkus.devtools.codestarts.extension.QuarkusExtensionCodestartCatalog.QuarkusExtensionData.*; +import static io.quarkus.devtools.codestarts.extension.QuarkusExtensionCodestartCatalog.QuarkusExtensionData.CLASS_NAME_BASE; +import static io.quarkus.devtools.codestarts.extension.QuarkusExtensionCodestartCatalog.QuarkusExtensionData.EXTENSION_DESCRIPTION; +import static io.quarkus.devtools.codestarts.extension.QuarkusExtensionCodestartCatalog.QuarkusExtensionData.EXTENSION_FULL_NAME; +import static io.quarkus.devtools.codestarts.extension.QuarkusExtensionCodestartCatalog.QuarkusExtensionData.EXTENSION_GUIDE; +import static io.quarkus.devtools.codestarts.extension.QuarkusExtensionCodestartCatalog.QuarkusExtensionData.EXTENSION_ID; +import static io.quarkus.devtools.codestarts.extension.QuarkusExtensionCodestartCatalog.QuarkusExtensionData.EXTENSION_NAME; +import static io.quarkus.devtools.codestarts.extension.QuarkusExtensionCodestartCatalog.QuarkusExtensionData.GROUP_ID; +import static io.quarkus.devtools.codestarts.extension.QuarkusExtensionCodestartCatalog.QuarkusExtensionData.HAS_DOCS_MODULE; +import static io.quarkus.devtools.codestarts.extension.QuarkusExtensionCodestartCatalog.QuarkusExtensionData.IT_PARENT_ARTIFACT_ID; +import static io.quarkus.devtools.codestarts.extension.QuarkusExtensionCodestartCatalog.QuarkusExtensionData.IT_PARENT_GROUP_ID; +import static io.quarkus.devtools.codestarts.extension.QuarkusExtensionCodestartCatalog.QuarkusExtensionData.IT_PARENT_RELATIVE_PATH; +import static io.quarkus.devtools.codestarts.extension.QuarkusExtensionCodestartCatalog.QuarkusExtensionData.IT_PARENT_VERSION; +import static io.quarkus.devtools.codestarts.extension.QuarkusExtensionCodestartCatalog.QuarkusExtensionData.JAVA_VERSION; +import static io.quarkus.devtools.codestarts.extension.QuarkusExtensionCodestartCatalog.QuarkusExtensionData.MAVEN_COMPILER_PLUGIN_VERSION; +import static io.quarkus.devtools.codestarts.extension.QuarkusExtensionCodestartCatalog.QuarkusExtensionData.MAVEN_QUARKUS_EXTENSION_PLUGIN; +import static io.quarkus.devtools.codestarts.extension.QuarkusExtensionCodestartCatalog.QuarkusExtensionData.MAVEN_SUREFIRE_PLUGIN_VERSION; +import static io.quarkus.devtools.codestarts.extension.QuarkusExtensionCodestartCatalog.QuarkusExtensionData.NAMESPACE_ID; +import static io.quarkus.devtools.codestarts.extension.QuarkusExtensionCodestartCatalog.QuarkusExtensionData.NAMESPACE_NAME; +import static io.quarkus.devtools.codestarts.extension.QuarkusExtensionCodestartCatalog.QuarkusExtensionData.PACKAGE_NAME; +import static io.quarkus.devtools.codestarts.extension.QuarkusExtensionCodestartCatalog.QuarkusExtensionData.PARENT_ARTIFACT_ID; +import static io.quarkus.devtools.codestarts.extension.QuarkusExtensionCodestartCatalog.QuarkusExtensionData.PARENT_GROUP_ID; +import static io.quarkus.devtools.codestarts.extension.QuarkusExtensionCodestartCatalog.QuarkusExtensionData.PARENT_RELATIVE_PATH; +import static io.quarkus.devtools.codestarts.extension.QuarkusExtensionCodestartCatalog.QuarkusExtensionData.PARENT_VERSION; +import static io.quarkus.devtools.codestarts.extension.QuarkusExtensionCodestartCatalog.QuarkusExtensionData.PROPERTIES_FROM_PARENT; +import static io.quarkus.devtools.codestarts.extension.QuarkusExtensionCodestartCatalog.QuarkusExtensionData.QUARKUS_BOM_ARTIFACT_ID; +import static io.quarkus.devtools.codestarts.extension.QuarkusExtensionCodestartCatalog.QuarkusExtensionData.QUARKUS_BOM_GROUP_ID; +import static io.quarkus.devtools.codestarts.extension.QuarkusExtensionCodestartCatalog.QuarkusExtensionData.QUARKUS_BOM_VERSION; +import static io.quarkus.devtools.codestarts.extension.QuarkusExtensionCodestartCatalog.QuarkusExtensionData.QUARKUS_VERSION; +import static io.quarkus.devtools.codestarts.extension.QuarkusExtensionCodestartCatalog.QuarkusExtensionData.VERSION; import static io.quarkus.devtools.commands.handlers.CreateExtensionCommandHandler.readPom; +import static io.quarkus.devtools.project.JavaVersion.computeJavaVersion; import static java.util.Objects.requireNonNull; import static org.apache.commons.lang3.StringUtils.isEmpty; @@ -29,6 +58,8 @@ import io.quarkus.devtools.commands.data.QuarkusCommandOutcome; import io.quarkus.devtools.commands.handlers.CreateExtensionCommandHandler; import io.quarkus.devtools.messagewriter.MessageWriter; +import io.quarkus.devtools.project.JavaVersion; +import io.quarkus.devtools.project.SourceType; import io.quarkus.maven.utilities.MojoUtils; /** @@ -73,6 +104,7 @@ public enum LayoutType { private String bomRelativeDir = "bom/application"; private String extensionsRelativeDir = "extensions"; private boolean withCodestart; + private String javaVersion; public CreateExtension(final Path baseDir) { this.baseDir = requireNonNull(baseDir, "extensionDirPath is required"); @@ -165,6 +197,11 @@ public CreateExtension quarkusBomVersion(String quarkusBomVersion) { return this; } + public CreateExtension javaVersion(String javaVersion) { + this.javaVersion = javaVersion; + return this; + } + public CreateExtension withCodestart(boolean withCodestart) { this.withCodestart = withCodestart; return this; @@ -227,6 +264,10 @@ public CreateExtensionCommandHandler prepare() throws QuarkusCommandException { data.put(EXTENSION_FULL_NAME, data.getRequiredStringValue(NAMESPACE_NAME) + data.getRequiredStringValue(EXTENSION_NAME)); + // for now, we only support Java extensions + data.put(JAVA_VERSION, javaVersion == null ? JavaVersion.DEFAULT_JAVA_VERSION_FOR_EXTENSION + : computeJavaVersion(SourceType.JAVA, javaVersion)); + final String runtimeArtifactId = getRuntimeArtifactIdFromData(); ensureRequiredStringData(GROUP_ID, resolveGroupId(baseModel)); diff --git a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/project/JavaVersion.java b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/project/JavaVersion.java index 6dd09ec9c887b0..2e53d71d8f422d 100644 --- a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/project/JavaVersion.java +++ b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/project/JavaVersion.java @@ -6,6 +6,7 @@ import java.util.TreeSet; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.Collectors; public final class JavaVersion { @@ -63,6 +64,8 @@ public String toString() { // ordering is important here, so let's keep them ordered public static final SortedSet JAVA_VERSIONS_LTS = new TreeSet<>(List.of(11, 17, 21)); public static final int DEFAULT_JAVA_VERSION = 11; + // we want to maximize the compatibility of extensions with the Quarkus ecosystem so let's stick to 11 by default + public static final String DEFAULT_JAVA_VERSION_FOR_EXTENSION = "11"; public static final int MAX_LTS_SUPPORTED_BY_KOTLIN = 17; public static final String DETECT_JAVA_RUNTIME_VERSION = "<>"; public static final Pattern JAVA_VERSION_PATTERN = Pattern.compile("(\\d+)(?:\\..*)?"); @@ -71,6 +74,15 @@ public static int determineBestJavaLtsVersion() { return determineBestJavaLtsVersion(Runtime.version().feature()); } + public static SortedSet getCompatibleLTSVersions(JavaVersion minimumJavaVersion) { + if (minimumJavaVersion.isEmpty()) { + return JAVA_VERSIONS_LTS; + } + return JAVA_VERSIONS_LTS.stream() + .filter(v -> v >= minimumJavaVersion.getAsInt()) + .collect(Collectors.toCollection(TreeSet::new)); + } + public static int determineBestJavaLtsVersion(int runtimeVersion) { int bestLtsVersion = DEFAULT_JAVA_VERSION; for (int ltsVersion : JAVA_VERSIONS_LTS) { diff --git a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/platform/catalog/processor/CatalogProcessor.java b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/platform/catalog/processor/CatalogProcessor.java index 8fbd392efdce0d..ae0037e90f61b8 100644 --- a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/platform/catalog/processor/CatalogProcessor.java +++ b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/platform/catalog/processor/CatalogProcessor.java @@ -85,4 +85,12 @@ public List getProcessedCategoriesInOrder() { public static MetadataValue getMetadataValue(ExtensionCatalog catalog, String path) { return MetadataValue.get(catalog.getMetadata(), path); } + + public static String getMinimumJavaVersion(ExtensionCatalog catalog) { + return getMetadataValue(catalog, ExtensionCatalog.MD_MINIMUM_JAVA_VERSION).asString(); + } + + public static String getRecommendedJavaVersion(ExtensionCatalog catalog) { + return getMetadataValue(catalog, ExtensionCatalog.MD_RECOMMENDED_JAVA_VERSION).asString(); + } } diff --git a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/platform/catalog/processor/MetadataValue.java b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/platform/catalog/processor/MetadataValue.java index 11ffee9d9a9dfd..13a71dabb43215 100644 --- a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/platform/catalog/processor/MetadataValue.java +++ b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/platform/catalog/processor/MetadataValue.java @@ -5,7 +5,7 @@ import java.util.Locale; import java.util.Map; -final class MetadataValue { +public final class MetadataValue { private static final MetadataValue EMPTY_METADATA_VALUE = new MetadataValue(null); private final Object val; diff --git a/independent-projects/tools/devtools-common/src/test/java/io/quarkus/devtools/project/JavaVersionTest.java b/independent-projects/tools/devtools-common/src/test/java/io/quarkus/devtools/project/JavaVersionTest.java index 285a582e81b2c9..3e0d82dd752414 100644 --- a/independent-projects/tools/devtools-common/src/test/java/io/quarkus/devtools/project/JavaVersionTest.java +++ b/independent-projects/tools/devtools-common/src/test/java/io/quarkus/devtools/project/JavaVersionTest.java @@ -1,10 +1,13 @@ package io.quarkus.devtools.project; import static io.quarkus.devtools.project.JavaVersion.DETECT_JAVA_RUNTIME_VERSION; +import static io.quarkus.devtools.project.JavaVersion.JAVA_VERSIONS_LTS; import static io.quarkus.devtools.project.JavaVersion.computeJavaVersion; import static io.quarkus.devtools.project.JavaVersion.determineBestJavaLtsVersion; +import static io.quarkus.devtools.project.JavaVersion.getCompatibleLTSVersions; import static io.quarkus.devtools.project.SourceType.JAVA; import static io.quarkus.devtools.project.SourceType.KOTLIN; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import org.junit.jupiter.api.Test; @@ -31,6 +34,14 @@ public void givenJavaVersion18ShouldReturn17() { assertEquals("17", computeJavaVersion(JAVA, "18")); } + @Test + void shouldProperlyUseMinJavaVersion() { + assertThat(getCompatibleLTSVersions(new JavaVersion("11"))).isEqualTo(JAVA_VERSIONS_LTS); + assertThat(getCompatibleLTSVersions(new JavaVersion("17"))).containsExactly(17, 21); + assertThat(getCompatibleLTSVersions(new JavaVersion("100"))).isEmpty(); + assertThat(getCompatibleLTSVersions(JavaVersion.NA)).isEqualTo(JAVA_VERSIONS_LTS); + } + @Test public void givenAutoDetectShouldReturnAppropriateVersion() { final String bestJavaLtsVersion = String.valueOf(determineBestJavaLtsVersion(Runtime.version().feature())); diff --git a/independent-projects/tools/devtools-testing/src/main/resources/fake-catalog.json b/independent-projects/tools/devtools-testing/src/main/resources/fake-catalog.json index c8e7e561183686..3b970eaec4e5a5 100644 --- a/independent-projects/tools/devtools-testing/src/main/resources/fake-catalog.json +++ b/independent-projects/tools/devtools-testing/src/main/resources/fake-catalog.json @@ -386,6 +386,8 @@ "gradle-plugin-id": "io.quarkus", "gradle-plugin-version": "999-FAKE", "supported-maven-versions": "[3.6.2,)", + "minimum-java-version": "11", + "recommended-java-version": "17", "proposed-maven-version": "3.9.5", "maven-wrapper-version": "3.2.0", "gradle-wrapper-version": "8.4" diff --git a/independent-projects/tools/devtools-testing/src/test/java/io/quarkus/devtools/codestarts/extension/QuarkusExtensionCodestartGenerationTest.java b/independent-projects/tools/devtools-testing/src/test/java/io/quarkus/devtools/codestarts/extension/QuarkusExtensionCodestartGenerationTest.java index 0d32e3e9dcba5b..fae3f7d36961dd 100644 --- a/independent-projects/tools/devtools-testing/src/test/java/io/quarkus/devtools/codestarts/extension/QuarkusExtensionCodestartGenerationTest.java +++ b/independent-projects/tools/devtools-testing/src/test/java/io/quarkus/devtools/codestarts/extension/QuarkusExtensionCodestartGenerationTest.java @@ -31,7 +31,8 @@ private QuarkusExtensionCodestartProjectInputBuilder prepareInput() { .putData(QuarkusExtensionData.EXTENSION_NAME, "My Extension") .putData(QuarkusExtensionData.VERSION, "1.0.0-SNAPSHOT") .putData(QuarkusExtensionData.PACKAGE_NAME, "org.extension") - .putData(QuarkusExtensionData.CLASS_NAME_BASE, "MyExtension"); + .putData(QuarkusExtensionData.CLASS_NAME_BASE, "MyExtension") + .putData(QuarkusExtensionData.JAVA_VERSION, "11"); } @Test diff --git a/independent-projects/tools/devtools-testing/src/test/java/io/quarkus/devtools/codestarts/quarkus/QuarkusCodestartGenerationTest.java b/independent-projects/tools/devtools-testing/src/test/java/io/quarkus/devtools/codestarts/quarkus/QuarkusCodestartGenerationTest.java index c9585530d468fa..e822bd176c4ea0 100644 --- a/independent-projects/tools/devtools-testing/src/test/java/io/quarkus/devtools/codestarts/quarkus/QuarkusCodestartGenerationTest.java +++ b/independent-projects/tools/devtools-testing/src/test/java/io/quarkus/devtools/codestarts/quarkus/QuarkusCodestartGenerationTest.java @@ -315,13 +315,13 @@ private void checkDockerfilesWithMaven(Path projectDir) { assertThat(projectDir.resolve("src/main/docker/Dockerfile.jvm")).exists() .satisfies(checkContains("./mvnw package")) .satisfies(checkContains("docker build -f src/main/docker/Dockerfile.jvm")) - .satisfies(checkContains("registry.access.redhat.com/ubi8/openjdk-11:1.17"))//TODO: make a test for java17 + .satisfies(checkContains("registry.access.redhat.com/ubi8/openjdk-11:1.18"))//TODO: make a test for java17 .satisfies(checkContains("ENV JAVA_APP_JAR=\"/deployments/quarkus-run.jar\"")) .satisfies(checkContains("ENTRYPOINT [ \"/opt/jboss/container/java/run/run-java.sh\" ]")); assertThat(projectDir.resolve("src/main/docker/Dockerfile.legacy-jar")).exists() .satisfies(checkContains("./mvnw package -Dquarkus.package.type=legacy-jar")) .satisfies(checkContains("docker build -f src/main/docker/Dockerfile.legacy-jar")) - .satisfies(checkContains("registry.access.redhat.com/ubi8/openjdk-11:1.17")) + .satisfies(checkContains("registry.access.redhat.com/ubi8/openjdk-11:1.18")) .satisfies(checkContains("EXPOSE 8080")) .satisfies(checkContains("USER 185")) .satisfies(checkContains("ENV JAVA_APP_JAR=\"/deployments/quarkus-run.jar\"")) @@ -341,13 +341,13 @@ private void checkDockerfilesWithGradle(Path projectDir) { assertThat(projectDir.resolve("src/main/docker/Dockerfile.jvm")).exists() .satisfies(checkContains("./gradlew build")) .satisfies(checkContains("docker build -f src/main/docker/Dockerfile.jvm")) - .satisfies(checkContains("registry.access.redhat.com/ubi8/openjdk-11:1.17"))//TODO: make a test for java17 + .satisfies(checkContains("registry.access.redhat.com/ubi8/openjdk-11:1.18"))//TODO: make a test for java17 .satisfies(checkContains("ENV JAVA_APP_JAR=\"/deployments/quarkus-run.jar\"")) .satisfies(checkContains("ENTRYPOINT [ \"/opt/jboss/container/java/run/run-java.sh\" ]")); assertThat(projectDir.resolve("src/main/docker/Dockerfile.legacy-jar")).exists() .satisfies(checkContains("./gradlew build -Dquarkus.package.type=legacy-jar")) .satisfies(checkContains("docker build -f src/main/docker/Dockerfile.legacy-jar")) - .satisfies(checkContains("registry.access.redhat.com/ubi8/openjdk-11:1.17")) + .satisfies(checkContains("registry.access.redhat.com/ubi8/openjdk-11:1.18")) .satisfies(checkContains("EXPOSE 8080")) .satisfies(checkContains("USER 185")) .satisfies(checkContains("ENV JAVA_APP_JAR=\"/deployments/quarkus-run.jar\"")) diff --git a/independent-projects/tools/devtools-testing/src/test/resources/__snapshots__/QuarkusCodestartGenerationTest/generateDefault/pom.xml b/independent-projects/tools/devtools-testing/src/test/resources/__snapshots__/QuarkusCodestartGenerationTest/generateDefault/pom.xml index 936986bec57053..51ef9c95e7f4e5 100644 --- a/independent-projects/tools/devtools-testing/src/test/resources/__snapshots__/QuarkusCodestartGenerationTest/generateDefault/pom.xml +++ b/independent-projects/tools/devtools-testing/src/test/resources/__snapshots__/QuarkusCodestartGenerationTest/generateDefault/pom.xml @@ -83,15 +83,15 @@ integration-test verify - - - ${project.build.directory}/${project.build.finalName}-runner - org.jboss.logmanager.LogManager - ${maven.home} - - + + + ${project.build.directory}/${project.build.finalName}-runner + org.jboss.logmanager.LogManager + ${maven.home} + + diff --git a/independent-projects/tools/devtools-testing/src/test/resources/__snapshots__/QuarkusCodestartGenerationTest/generateMavenWithCustomDep/pom.xml b/independent-projects/tools/devtools-testing/src/test/resources/__snapshots__/QuarkusCodestartGenerationTest/generateMavenWithCustomDep/pom.xml index cc3b1dd6e7174d..9871c74a9df19f 100644 --- a/independent-projects/tools/devtools-testing/src/test/resources/__snapshots__/QuarkusCodestartGenerationTest/generateMavenWithCustomDep/pom.xml +++ b/independent-projects/tools/devtools-testing/src/test/resources/__snapshots__/QuarkusCodestartGenerationTest/generateMavenWithCustomDep/pom.xml @@ -98,15 +98,15 @@ integration-test verify - - - ${project.build.directory}/${project.build.finalName}-runner - org.jboss.logmanager.LogManager - ${maven.home} - - + + + ${project.build.directory}/${project.build.finalName}-runner + org.jboss.logmanager.LogManager + ${maven.home} + + diff --git a/independent-projects/tools/pom.xml b/independent-projects/tools/pom.xml index 48814923bfe7a6..b2ad75132b5521 100644 --- a/independent-projects/tools/pom.xml +++ b/independent-projects/tools/pom.xml @@ -55,7 +55,7 @@ 2.15.3 4.0.1 5.10.0 - 1.24.0 + 1.25.0 3.5.3.Final 5.3.1 3.2.1 diff --git a/independent-projects/tools/registry-client/src/main/java/io/quarkus/registry/catalog/ExtensionCatalog.java b/independent-projects/tools/registry-client/src/main/java/io/quarkus/registry/catalog/ExtensionCatalog.java index 2bcdb7eef419a9..91f7d027e61cb0 100644 --- a/independent-projects/tools/registry-client/src/main/java/io/quarkus/registry/catalog/ExtensionCatalog.java +++ b/independent-projects/tools/registry-client/src/main/java/io/quarkus/registry/catalog/ExtensionCatalog.java @@ -11,6 +11,9 @@ public interface ExtensionCatalog extends ExtensionOrigin { + String MD_MINIMUM_JAVA_VERSION = "project.properties.minimum-java-version"; + String MD_RECOMMENDED_JAVA_VERSION = "project.properties.recommended-java-version"; + /** * All the origins this catalog is derived from. * diff --git a/integration-tests/awt/src/main/docker/Dockerfile.native b/integration-tests/awt/src/main/docker/Dockerfile.native index f1fd2c79bd135d..e2ff5cf61ed948 100644 --- a/integration-tests/awt/src/main/docker/Dockerfile.native +++ b/integration-tests/awt/src/main/docker/Dockerfile.native @@ -1,4 +1,4 @@ -FROM registry.access.redhat.com/ubi8/ubi-minimal:8.8 +FROM registry.access.redhat.com/ubi8/ubi-minimal:8.9 # Dependencies for AWT RUN microdnf install freetype fontconfig \ && microdnf clean all diff --git a/integration-tests/container-image/maven-invoker-way/src/it/container-build-docker/src/main/docker/Dockerfile.jvm b/integration-tests/container-image/maven-invoker-way/src/it/container-build-docker/src/main/docker/Dockerfile.jvm index 53ed09abd23c22..566ba7b5a17bf0 100644 --- a/integration-tests/container-image/maven-invoker-way/src/it/container-build-docker/src/main/docker/Dockerfile.jvm +++ b/integration-tests/container-image/maven-invoker-way/src/it/container-build-docker/src/main/docker/Dockerfile.jvm @@ -21,7 +21,7 @@ # docker run -i --rm -p 8080:8080 -p 5005:5005 -e JAVA_ENABLE_DEBUG="true" quarkus/rest-client-quickstart-jvm # ### -FROM registry.access.redhat.com/ubi8/ubi-minimal:8.8 +FROM registry.access.redhat.com/ubi8/ubi-minimal:8.9 ARG JAVA_PACKAGE=java-11-openjdk-headless ARG RUN_JAVA_VERSION=1.3.8 diff --git a/integration-tests/container-image/maven-invoker-way/src/it/container-build-multiple-tags-docker/src/main/docker/Dockerfile.jvm b/integration-tests/container-image/maven-invoker-way/src/it/container-build-multiple-tags-docker/src/main/docker/Dockerfile.jvm index 53ed09abd23c22..566ba7b5a17bf0 100644 --- a/integration-tests/container-image/maven-invoker-way/src/it/container-build-multiple-tags-docker/src/main/docker/Dockerfile.jvm +++ b/integration-tests/container-image/maven-invoker-way/src/it/container-build-multiple-tags-docker/src/main/docker/Dockerfile.jvm @@ -21,7 +21,7 @@ # docker run -i --rm -p 8080:8080 -p 5005:5005 -e JAVA_ENABLE_DEBUG="true" quarkus/rest-client-quickstart-jvm # ### -FROM registry.access.redhat.com/ubi8/ubi-minimal:8.8 +FROM registry.access.redhat.com/ubi8/ubi-minimal:8.9 ARG JAVA_PACKAGE=java-11-openjdk-headless ARG RUN_JAVA_VERSION=1.3.8 diff --git a/integration-tests/devtools/src/test/java/io/quarkus/platform/catalog/CatalogProcessorTest.java b/integration-tests/devtools/src/test/java/io/quarkus/platform/catalog/CatalogProcessorTest.java index 0b7f884660c55d..83dbe859c7c00a 100644 --- a/integration-tests/devtools/src/test/java/io/quarkus/platform/catalog/CatalogProcessorTest.java +++ b/integration-tests/devtools/src/test/java/io/quarkus/platform/catalog/CatalogProcessorTest.java @@ -1,7 +1,9 @@ package io.quarkus.platform.catalog; import static io.quarkus.devtools.testing.FakeExtensionCatalog.newFakeExtensionCatalog; +import static io.quarkus.platform.catalog.processor.CatalogProcessor.getMinimumJavaVersion; import static io.quarkus.platform.catalog.processor.CatalogProcessor.getProcessedCategoriesInOrder; +import static io.quarkus.platform.catalog.processor.CatalogProcessor.getRecommendedJavaVersion; import static org.assertj.core.api.Assertions.assertThat; import java.util.Objects; @@ -27,6 +29,13 @@ void testCategoryOrder() { .startsWith("web", "core", "reactive", "serialization", "compatibility", "alt-languages", "uncategorized"); } + @Test + void testJavaVersions() { + final ExtensionCatalog catalog = newFakeExtensionCatalog(); + assertThat(getMinimumJavaVersion(catalog)).isEqualTo("11"); + assertThat(getRecommendedJavaVersion(catalog)).isEqualTo("17"); + } + @Test void testExtensionsOrder() { final ExtensionCatalog catalog = newFakeExtensionCatalog(); diff --git a/integration-tests/devtools/src/test/resources/platform-metadata.json b/integration-tests/devtools/src/test/resources/platform-metadata.json index 59160da30fb5b1..b4b073f5b6adf5 100644 --- a/integration-tests/devtools/src/test/resources/platform-metadata.json +++ b/integration-tests/devtools/src/test/resources/platform-metadata.json @@ -43,7 +43,9 @@ "supported-maven-versions": "${supported-maven-versions}", "proposed-maven-version": "${proposed-maven-version}", "maven-wrapper-version": "${maven-wrapper.version}", - "gradle-wrapper-version": "${gradle-wrapper.version}" + "gradle-wrapper-version": "${gradle-wrapper.version}", + "minimum-java-version": "${minimum-java-version}", + "recommended-java-version": "${recommended-java-version}" } }, "codestarts-artifacts": [ diff --git a/integration-tests/jpa-postgresql-withxml/pom.xml b/integration-tests/jpa-postgresql-withxml/pom.xml index b506ff5f451080..ae1238d34096f0 100644 --- a/integration-tests/jpa-postgresql-withxml/pom.xml +++ b/integration-tests/jpa-postgresql-withxml/pom.xml @@ -10,7 +10,7 @@ 4.0.0 quarkus-integration-test-jpa-postgresql-withxml - Quarkus - Integration Tests - JPA - PostgreSQL + Quarkus - Integration Tests - JPA - PostgreSQL with XML Module that contains JPA related tests running with the PostgreSQL database @@ -30,6 +30,10 @@ io.quarkus quarkus-jdbc-postgresql + + io.quarkus + quarkus-jaxb + @@ -83,6 +87,19 @@ + + io.quarkus + quarkus-jaxb-deployment + ${project.version} + pom + test + + + * + * + + + diff --git a/integration-tests/jpa-postgresql-withxml/src/main/java/io/quarkus/it/jpa/postgresql/EntityWithXml.java b/integration-tests/jpa-postgresql-withxml/src/main/java/io/quarkus/it/jpa/postgresql/EntityWithXml.java new file mode 100644 index 00000000000000..470ce79c8f3657 --- /dev/null +++ b/integration-tests/jpa-postgresql-withxml/src/main/java/io/quarkus/it/jpa/postgresql/EntityWithXml.java @@ -0,0 +1,74 @@ +package io.quarkus.it.jpa.postgresql; + +import java.time.LocalDate; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.xml.bind.annotation.XmlElement; +import jakarta.xml.bind.annotation.XmlRootElement; +import jakarta.xml.bind.annotation.adapters.XmlAdapter; +import jakarta.xml.bind.annotation.adapters.XmlJavaTypeAdapter; + +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.type.SqlTypes; + +import io.quarkus.runtime.annotations.RegisterForReflection; + +@Entity +public class EntityWithXml { + @Id + @GeneratedValue + Long id; + + @JdbcTypeCode(SqlTypes.SQLXML) + ToBeSerializedWithDateTime xml; + + public EntityWithXml() { + } + + public EntityWithXml(ToBeSerializedWithDateTime data) { + this.xml = data; + } + + @Override + public String toString() { + return "EntityWithXml{" + + "id=" + id + + ", xml=" + xml + + '}'; + } + + @RegisterForReflection + @XmlRootElement + public static class ToBeSerializedWithDateTime { + @XmlElement + @XmlJavaTypeAdapter(value = LocalDateXmlAdapter.class) + LocalDate date; + + public ToBeSerializedWithDateTime() { + } + + public ToBeSerializedWithDateTime(LocalDate date) { + this.date = date; + } + + @Override + public String toString() { + return "ToBeSerializedWithDateTime{" + + "date=" + date + + '}'; + } + } + + @RegisterForReflection + public static class LocalDateXmlAdapter extends XmlAdapter { + public LocalDate unmarshal(String string) { + return string == null ? null : LocalDate.parse(string); + } + + public String marshal(LocalDate localDate) { + return localDate == null ? null : localDate.toString(); + } + } +} diff --git a/integration-tests/jpa-postgresql-withxml/src/main/java/io/quarkus/it/jpa/postgresql/JPAFunctionalityTestEndpoint.java b/integration-tests/jpa-postgresql-withxml/src/main/java/io/quarkus/it/jpa/postgresql/JPAFunctionalityTestEndpoint.java index e8f1a786615cc3..fa06314f454a1f 100644 --- a/integration-tests/jpa-postgresql-withxml/src/main/java/io/quarkus/it/jpa/postgresql/JPAFunctionalityTestEndpoint.java +++ b/integration-tests/jpa-postgresql-withxml/src/main/java/io/quarkus/it/jpa/postgresql/JPAFunctionalityTestEndpoint.java @@ -9,6 +9,7 @@ import java.sql.SQLException; import java.sql.SQLXML; import java.sql.Statement; +import java.time.LocalDate; import java.util.List; import java.util.UUID; @@ -34,6 +35,9 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import io.quarkus.hibernate.orm.PersistenceUnit; +import io.quarkus.it.jpa.postgresql.otherpu.EntityWithXmlOtherPU; + /** * First we run a smoke test for basic Hibernate ORM functionality, * then we specifically focus on supporting the PgSQLXML mapping abilities for XML types: @@ -44,6 +48,9 @@ public class JPAFunctionalityTestEndpoint extends HttpServlet { @Inject EntityManagerFactory entityManagerFactory; + @Inject + @PersistenceUnit("other") + EntityManagerFactory otherEntityManagerFactory; @Inject DataSource ds; @@ -51,7 +58,7 @@ public class JPAFunctionalityTestEndpoint extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { try { - doStuffWithHibernate(entityManagerFactory); + doStuffWithHibernate(entityManagerFactory, otherEntityManagerFactory); doStuffWithDatasource(); } catch (Exception e) { reportException("An error occurred while performing Hibernate operations", e, resp); @@ -123,7 +130,8 @@ private void deleteXmlSchema(Connection con) { /** * Lists the various operations we want to test for: */ - private static void doStuffWithHibernate(EntityManagerFactory entityManagerFactory) { + private static void doStuffWithHibernate(EntityManagerFactory entityManagerFactory, + EntityManagerFactory otherEntityManagerFactory) { //Store some well known Person instances we can then test on: storeTestPersons(entityManagerFactory); @@ -133,6 +141,8 @@ private static void doStuffWithHibernate(EntityManagerFactory entityManagerFacto //Try a JPA named query: verifyJPANamedQuery(entityManagerFactory); + + doXmlStuff(entityManagerFactory, otherEntityManagerFactory); } private static void verifyJPANamedQuery(final EntityManagerFactory emf) { @@ -210,6 +220,47 @@ private static String randomName() { return UUID.randomUUID().toString(); } + private static void doXmlStuff(EntityManagerFactory emf, EntityManagerFactory otherEmf) { + try (EntityManager em = emf.createEntityManager()) { + EntityTransaction transaction = em.getTransaction(); + transaction.begin(); + + EntityWithXml entity = new EntityWithXml( + new EntityWithXml.ToBeSerializedWithDateTime(LocalDate.of(2023, 7, 28))); + em.persist(entity); + transaction.commit(); + + transaction.begin(); + List entities = em + .createQuery("select e from EntityWithXml e", EntityWithXml.class) + .getResultList(); + if (entities.isEmpty()) { + throw new AssertionError("No entities with XML were found"); + } + transaction.commit(); + + transaction.begin(); + em.createQuery("delete from EntityWithXml").executeUpdate(); + transaction.commit(); + } + + try (EntityManager em = otherEmf.createEntityManager()) { + EntityTransaction transaction = em.getTransaction(); + transaction.begin(); + EntityWithXmlOtherPU otherPU = new EntityWithXmlOtherPU( + new EntityWithXmlOtherPU.ToBeSerializedWithDateTime(LocalDate.of(2023, 7, 28))); + em.persist(otherPU); + transaction.commit(); + throw new AssertionError( + "Our custom XML format mapper throws exceptions. So we were expecting commit to fail, but it did not!"); + } catch (Exception e) { + if (!(e.getCause() instanceof IllegalArgumentException) + && !e.getCause().getMessage().contains("I cannot convert anything to XML")) { + throw new AssertionError("Transaction failed for a different reason than expected.", e); + } + } + } + private void reportException(String errorMessage, final Exception e, final HttpServletResponse resp) throws IOException { final PrintWriter writer = resp.getWriter(); if (errorMessage != null) { diff --git a/integration-tests/jpa-postgresql-withxml/src/main/java/io/quarkus/it/jpa/postgresql/otherpu/EntityWithXmlOtherPU.java b/integration-tests/jpa-postgresql-withxml/src/main/java/io/quarkus/it/jpa/postgresql/otherpu/EntityWithXmlOtherPU.java new file mode 100644 index 00000000000000..c8f16813eca444 --- /dev/null +++ b/integration-tests/jpa-postgresql-withxml/src/main/java/io/quarkus/it/jpa/postgresql/otherpu/EntityWithXmlOtherPU.java @@ -0,0 +1,57 @@ + +package io.quarkus.it.jpa.postgresql.otherpu; + +import java.time.LocalDate; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; + +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.type.SqlTypes; + +import io.quarkus.runtime.annotations.RegisterForReflection; + +@Entity +public class EntityWithXmlOtherPU { + @Id + @GeneratedValue + Long id; + + @JdbcTypeCode(SqlTypes.SQLXML) + ToBeSerializedWithDateTime xml; + + public EntityWithXmlOtherPU() { + } + + public EntityWithXmlOtherPU(ToBeSerializedWithDateTime data) { + this.xml = data; + } + + @Override + public String toString() { + return "EntityWithXmlOtherPU{" + + "id=" + id + + ", xml" + xml + + '}'; + } + + @RegisterForReflection + public static class ToBeSerializedWithDateTime { + LocalDate date; + + public ToBeSerializedWithDateTime() { + } + + public ToBeSerializedWithDateTime(LocalDate date) { + this.date = date; + } + + @Override + public String toString() { + return "ToBeSerializedWithDateTime{" + + "date=" + date + + '}'; + } + } +} diff --git a/integration-tests/jpa-postgresql-withxml/src/main/java/io/quarkus/it/jpa/postgresql/otherpu/XmlFormatMapper.java b/integration-tests/jpa-postgresql-withxml/src/main/java/io/quarkus/it/jpa/postgresql/otherpu/XmlFormatMapper.java new file mode 100644 index 00000000000000..8c9c57bc003ed1 --- /dev/null +++ b/integration-tests/jpa-postgresql-withxml/src/main/java/io/quarkus/it/jpa/postgresql/otherpu/XmlFormatMapper.java @@ -0,0 +1,23 @@ +package io.quarkus.it.jpa.postgresql.otherpu; + +import org.hibernate.type.descriptor.WrapperOptions; +import org.hibernate.type.descriptor.java.JavaType; +import org.hibernate.type.format.FormatMapper; + +import io.quarkus.hibernate.orm.PersistenceUnitExtension; +import io.quarkus.hibernate.orm.XmlFormat; + +@XmlFormat +@PersistenceUnitExtension("other") +public class XmlFormatMapper implements FormatMapper { + + @Override + public T fromString(CharSequence charSequence, JavaType javaType, WrapperOptions wrapperOptions) { + throw new UnsupportedOperationException("I cannot convert anything from XML."); + } + + @Override + public String toString(T value, JavaType javaType, WrapperOptions wrapperOptions) { + throw new UnsupportedOperationException("I cannot convert anything to XML."); + } +} diff --git a/integration-tests/jpa-postgresql-withxml/src/main/resources/application.properties b/integration-tests/jpa-postgresql-withxml/src/main/resources/application.properties index b93311019b0499..c0ff6325328d00 100644 --- a/integration-tests/jpa-postgresql-withxml/src/main/resources/application.properties +++ b/integration-tests/jpa-postgresql-withxml/src/main/resources/application.properties @@ -3,6 +3,7 @@ quarkus.datasource.password=hibernate_orm_test quarkus.datasource.jdbc.url=${postgres.url} quarkus.datasource.jdbc.max-size=8 +quarkus.hibernate-orm.packages=io.quarkus.it.jpa.postgresql quarkus.hibernate-orm.database.generation=drop-and-create #Necessary for assertions in JPAFunctionalityInGraalITCase: @@ -10,3 +11,7 @@ quarkus.native.enable-reports=true #Useful to get some more insight in the trigger: quarkus.native.additional-build-args=-J-Dio.quarkus.jdbc.postgresql.graalvm.diagnostics=true + +# Define non-default PU so that we can configure a custom XML format mapper. The default PU is using the default mapper. +quarkus.hibernate-orm."other".datasource= +quarkus.hibernate-orm."other".packages=io.quarkus.it.jpa.postgresql.otherpu \ No newline at end of file diff --git a/integration-tests/jpa-postgresql/pom.xml b/integration-tests/jpa-postgresql/pom.xml index d4d9ad27b2f6a0..73b860e0209f3b 100644 --- a/integration-tests/jpa-postgresql/pom.xml +++ b/integration-tests/jpa-postgresql/pom.xml @@ -30,6 +30,10 @@ io.quarkus quarkus-jdbc-postgresql + + io.quarkus + quarkus-jackson + @@ -93,6 +97,19 @@ + + io.quarkus + quarkus-jackson-deployment + ${project.version} + pom + test + + + * + * + + + diff --git a/integration-tests/jpa-postgresql/src/main/java/io/quarkus/it/jpa/postgresql/EntityWithJson.java b/integration-tests/jpa-postgresql/src/main/java/io/quarkus/it/jpa/postgresql/EntityWithJson.java new file mode 100644 index 00000000000000..e513a7d8b1d232 --- /dev/null +++ b/integration-tests/jpa-postgresql/src/main/java/io/quarkus/it/jpa/postgresql/EntityWithJson.java @@ -0,0 +1,59 @@ +package io.quarkus.it.jpa.postgresql; + +import java.time.LocalDate; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; + +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.type.SqlTypes; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import io.quarkus.runtime.annotations.RegisterForReflection; + +@Entity +public class EntityWithJson { + @Id + @GeneratedValue + Long id; + + @JdbcTypeCode(SqlTypes.JSON) + ToBeSerializedWithDateTime json; + + public EntityWithJson() { + } + + public EntityWithJson(ToBeSerializedWithDateTime data) { + this.json = data; + } + + @Override + public String toString() { + return "EntityWithJson{" + + "id=" + id + + ", json=" + json + + '}'; + } + + @RegisterForReflection + public static class ToBeSerializedWithDateTime { + @JsonProperty + LocalDate date; + + public ToBeSerializedWithDateTime() { + } + + public ToBeSerializedWithDateTime(LocalDate date) { + this.date = date; + } + + @Override + public String toString() { + return "ToBeSerializedWithDateTime{" + + "date=" + date + + '}'; + } + } +} diff --git a/integration-tests/jpa-postgresql/src/main/java/io/quarkus/it/jpa/postgresql/JPAFunctionalityTestEndpoint.java b/integration-tests/jpa-postgresql/src/main/java/io/quarkus/it/jpa/postgresql/JPAFunctionalityTestEndpoint.java index ac23341e763f4f..5f5f9e2991db8c 100644 --- a/integration-tests/jpa-postgresql/src/main/java/io/quarkus/it/jpa/postgresql/JPAFunctionalityTestEndpoint.java +++ b/integration-tests/jpa-postgresql/src/main/java/io/quarkus/it/jpa/postgresql/JPAFunctionalityTestEndpoint.java @@ -3,6 +3,7 @@ import java.io.IOException; import java.io.PrintWriter; import java.time.Duration; +import java.time.LocalDate; import java.util.List; import java.util.UUID; @@ -19,6 +20,9 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import io.quarkus.hibernate.orm.PersistenceUnit; +import io.quarkus.it.jpa.postgresql.otherpu.EntityWithJsonOtherPU; + /** * Various tests covering JPA functionality. All tests should work in both standard JVM and in native mode. */ @@ -27,11 +31,14 @@ public class JPAFunctionalityTestEndpoint extends HttpServlet { @Inject EntityManagerFactory entityManagerFactory; + @Inject + @PersistenceUnit("other") + EntityManagerFactory otherEntityManagerFactory; @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { try { - doStuffWithHibernate(entityManagerFactory); + doStuffWithHibernate(entityManagerFactory, otherEntityManagerFactory); } catch (Exception e) { reportException("An error occurred while performing Hibernate operations", e, resp); } @@ -41,7 +48,8 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO /** * Lists the various operations we want to test for: */ - private static void doStuffWithHibernate(EntityManagerFactory entityManagerFactory) { + private static void doStuffWithHibernate(EntityManagerFactory entityManagerFactory, + EntityManagerFactory otherEntityManagerFactory) { //Cleanup any existing data: deleteAllPerson(entityManagerFactory); @@ -59,6 +67,9 @@ private static void doStuffWithHibernate(EntityManagerFactory entityManagerFacto // Try an entity using a UUID verifyUUIDEntity(entityManagerFactory); + + doJsonStuff(entityManagerFactory, otherEntityManagerFactory); + } private static void verifyJPANamedQuery(final EntityManagerFactory emf) { @@ -167,6 +178,47 @@ private static void verifyUUIDEntity(final EntityManagerFactory emf) { em.close(); } + private static void doJsonStuff(EntityManagerFactory emf, EntityManagerFactory otherEmf) { + try (EntityManager em = emf.createEntityManager()) { + EntityTransaction transaction = em.getTransaction(); + transaction.begin(); + + EntityWithJson entity = new EntityWithJson( + new EntityWithJson.ToBeSerializedWithDateTime(LocalDate.of(2023, 7, 28))); + em.persist(entity); + transaction.commit(); + + transaction.begin(); + List entities = em + .createQuery("select e from EntityWithJson e", EntityWithJson.class) + .getResultList(); + if (entities.isEmpty()) { + throw new AssertionError("No entities with json were found"); + } + transaction.commit(); + + transaction.begin(); + em.createQuery("delete from EntityWithJson").executeUpdate(); + transaction.commit(); + } + + try (EntityManager em = otherEmf.createEntityManager()) { + EntityTransaction transaction = em.getTransaction(); + transaction.begin(); + EntityWithJsonOtherPU otherPU = new EntityWithJsonOtherPU( + new EntityWithJsonOtherPU.ToBeSerializedWithDateTime(LocalDate.of(2023, 7, 28))); + em.persist(otherPU); + transaction.commit(); + throw new AssertionError( + "Default mapper cannot process date/time properties. So we were expecting commit to fail, but it did not!"); + } catch (Exception e) { + if (!(e.getCause() instanceof IllegalArgumentException) + && !e.getCause().getMessage().contains("I cannot convert anything to JSON")) { + throw new AssertionError("Transaction failed for a different reason than expected.", e); + } + } + } + private void reportException(String errorMessage, final Exception e, final HttpServletResponse resp) throws IOException { final PrintWriter writer = resp.getWriter(); if (errorMessage != null) { diff --git a/integration-tests/jpa-postgresql/src/main/java/io/quarkus/it/jpa/postgresql/otherpu/EntityWithJsonOtherPU.java b/integration-tests/jpa-postgresql/src/main/java/io/quarkus/it/jpa/postgresql/otherpu/EntityWithJsonOtherPU.java new file mode 100644 index 00000000000000..0793b93f3fcf66 --- /dev/null +++ b/integration-tests/jpa-postgresql/src/main/java/io/quarkus/it/jpa/postgresql/otherpu/EntityWithJsonOtherPU.java @@ -0,0 +1,60 @@ + +package io.quarkus.it.jpa.postgresql.otherpu; + +import java.time.LocalDate; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; + +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.type.SqlTypes; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import io.quarkus.runtime.annotations.RegisterForReflection; + +@Entity +public class EntityWithJsonOtherPU { + @Id + @GeneratedValue + Long id; + + @JdbcTypeCode(SqlTypes.JSON) + ToBeSerializedWithDateTime json; + + public EntityWithJsonOtherPU() { + } + + public EntityWithJsonOtherPU(ToBeSerializedWithDateTime data) { + this.json = data; + } + + @Override + public String toString() { + return "EntityWithJsonOtherPU{" + + "id=" + id + + ", json=" + json + + '}'; + } + + @RegisterForReflection + public static class ToBeSerializedWithDateTime { + @JsonProperty + LocalDate date; + + public ToBeSerializedWithDateTime() { + } + + public ToBeSerializedWithDateTime(LocalDate date) { + this.date = date; + } + + @Override + public String toString() { + return "ToBeSerializedWithDateTime{" + + "date=" + date + + '}'; + } + } +} diff --git a/integration-tests/jpa-postgresql/src/main/java/io/quarkus/it/jpa/postgresql/otherpu/JsonFormatMapper.java b/integration-tests/jpa-postgresql/src/main/java/io/quarkus/it/jpa/postgresql/otherpu/JsonFormatMapper.java new file mode 100644 index 00000000000000..70925dc93bf48c --- /dev/null +++ b/integration-tests/jpa-postgresql/src/main/java/io/quarkus/it/jpa/postgresql/otherpu/JsonFormatMapper.java @@ -0,0 +1,23 @@ +package io.quarkus.it.jpa.postgresql.otherpu; + +import org.hibernate.type.descriptor.WrapperOptions; +import org.hibernate.type.descriptor.java.JavaType; +import org.hibernate.type.format.FormatMapper; + +import io.quarkus.hibernate.orm.JsonFormat; +import io.quarkus.hibernate.orm.PersistenceUnitExtension; + +@JsonFormat +@PersistenceUnitExtension("other") +public class JsonFormatMapper implements FormatMapper { + + @Override + public T fromString(CharSequence charSequence, JavaType javaType, WrapperOptions wrapperOptions) { + throw new UnsupportedOperationException("I cannot convert anything from JSON."); + } + + @Override + public String toString(T value, JavaType javaType, WrapperOptions wrapperOptions) { + throw new UnsupportedOperationException("I cannot convert anything to JSON."); + } +} diff --git a/integration-tests/jpa-postgresql/src/main/resources/application.properties b/integration-tests/jpa-postgresql/src/main/resources/application.properties index c3f79713879977..90370fd6289842 100644 --- a/integration-tests/jpa-postgresql/src/main/resources/application.properties +++ b/integration-tests/jpa-postgresql/src/main/resources/application.properties @@ -3,8 +3,12 @@ quarkus.datasource.password=hibernate_orm_test quarkus.datasource.jdbc.url=${postgres.url} quarkus.datasource.jdbc.max-size=8 +quarkus.hibernate-orm.packages=io.quarkus.it.jpa.postgresql quarkus.hibernate-orm.database.generation=drop-and-create quarkus.hibernate-orm.database.generation.create-schemas=true +# Define non-default PU so that we can configure a custom JSON format mapper. The default PU is using the default mapper. +quarkus.hibernate-orm."other".datasource= +quarkus.hibernate-orm."other".packages=io.quarkus.it.jpa.postgresql.otherpu #Necessary for assertions in JPAFunctionalityInGraalITCase: quarkus.native.enable-reports=true \ No newline at end of file diff --git a/integration-tests/kubernetes-client-hack-extension/deployment/pom.xml b/integration-tests/kubernetes-client-hack-extension/deployment/pom.xml deleted file mode 100644 index 913462707b124c..00000000000000 --- a/integration-tests/kubernetes-client-hack-extension/deployment/pom.xml +++ /dev/null @@ -1,58 +0,0 @@ - - - 4.0.0 - - - io.quarkus - quarkus-integration-test-kubernetes-client-hack-extension-parent - 999-SNAPSHOT - - - quarkus-integration-test-kubernetes-client-hack-extension-deployment - Quarkus - Integration Tests - Kubernetes Client Hack Extension - Deployment - - - - io.quarkus - quarkus-core-deployment - ${project.version} - - - io.quarkus - quarkus-integration-test-kubernetes-client-hack-extension - ${project.version} - - - - - - - maven-compiler-plugin - - - - io.quarkus - quarkus-extension-processor - ${project.version} - - - - - - org.codehaus.mojo - templating-maven-plugin - 1.0.0 - - - filtering-java-templates - - filter-sources - - - - - - - diff --git a/integration-tests/kubernetes-client-hack-extension/deployment/src/main/java/io/quarkus/kubernetes/client/deployment/NativeOverrides.java b/integration-tests/kubernetes-client-hack-extension/deployment/src/main/java/io/quarkus/kubernetes/client/deployment/NativeOverrides.java deleted file mode 100644 index 91372ea46589e1..00000000000000 --- a/integration-tests/kubernetes-client-hack-extension/deployment/src/main/java/io/quarkus/kubernetes/client/deployment/NativeOverrides.java +++ /dev/null @@ -1,12 +0,0 @@ -package io.quarkus.kubernetes.client.deployment; - -import io.quarkus.deployment.annotations.BuildStep; -import io.quarkus.deployment.builditem.nativeimage.NativeImageAllowIncompleteClasspathBuildItem; - -public class NativeOverrides { - - @BuildStep - NativeImageAllowIncompleteClasspathBuildItem incompleteModel() { - return new NativeImageAllowIncompleteClasspathBuildItem("quarkus-kubernetes-client"); - } -} diff --git a/integration-tests/kubernetes-client-hack-extension/pom.xml b/integration-tests/kubernetes-client-hack-extension/pom.xml deleted file mode 100644 index c29e01f59239c3..00000000000000 --- a/integration-tests/kubernetes-client-hack-extension/pom.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - 4.0.0 - - - quarkus-extensions-parent - io.quarkus - 999-SNAPSHOT - ../../extensions/pom.xml - - - quarkus-integration-test-kubernetes-client-hack-extension-parent - Quarkus - Integration Tests - Kubernetes Client Hack Extension - pom - - deployment - runtime - - diff --git a/integration-tests/kubernetes-client-hack-extension/runtime/pom.xml b/integration-tests/kubernetes-client-hack-extension/runtime/pom.xml deleted file mode 100644 index a0278c43dafaa9..00000000000000 --- a/integration-tests/kubernetes-client-hack-extension/runtime/pom.xml +++ /dev/null @@ -1,48 +0,0 @@ - - - 4.0.0 - - - io.quarkus - quarkus-integration-test-kubernetes-client-hack-extension-parent - 999-SNAPSHOT - - - quarkus-integration-test-kubernetes-client-hack-extension - Quarkus - Integration Tests - Kubernetes Client Hack Extension - Runtime - - - - io.quarkus - quarkus-core - ${project.version} - - - - - - - io.quarkus - quarkus-extension-maven-plugin - ${project.version} - - ${project.groupId}:${project.artifactId}-deployment:${project.version} - - - - maven-compiler-plugin - - - - io.quarkus - quarkus-extension-processor - ${project.version} - - - - - - - diff --git a/integration-tests/kubernetes-client-hack-extension/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/integration-tests/kubernetes-client-hack-extension/runtime/src/main/resources/META-INF/quarkus-extension.yaml deleted file mode 100644 index 42eda1b64d7c59..00000000000000 --- a/integration-tests/kubernetes-client-hack-extension/runtime/src/main/resources/META-INF/quarkus-extension.yaml +++ /dev/null @@ -1,11 +0,0 @@ ---- -artifact: ${project.groupId}:${project.artifactId}:${project.version} -name: "Kubernetes Client Hack Extension" -metadata: - keywords: - - "kubernetes-client" - guide: "https://quarkus.io/guides/kubernetes-client" - categories: - - "cloud" - status: "test" - config: diff --git a/integration-tests/kubernetes-client/pom.xml b/integration-tests/kubernetes-client/pom.xml index a08bd4306d3977..74540dca954064 100644 --- a/integration-tests/kubernetes-client/pom.xml +++ b/integration-tests/kubernetes-client/pom.xml @@ -22,22 +22,21 @@ io.quarkus quarkus-kubernetes-config - - io.quarkus - quarkus-integration-test-kubernetes-client-hack-extension - ${project.version} - io.quarkus quarkus-openshift-client io.fabric8 - openshift-model-operator + openshift-model-hive io.fabric8 - openshift-model-operator-hub + openshift-model-miscellaneous + + + io.fabric8 + openshift-model-operator @@ -90,19 +89,6 @@ - - io.quarkus - quarkus-integration-test-kubernetes-client-hack-extension-deployment - ${project.version} - pom - test - - - * - * - - - io.quarkus quarkus-openshift-client-deployment diff --git a/integration-tests/kubernetes/maven-invoker-way/src/it/kubernetes-docker-build-and-deploy-deployment/src/main/docker/Dockerfile.jvm b/integration-tests/kubernetes/maven-invoker-way/src/it/kubernetes-docker-build-and-deploy-deployment/src/main/docker/Dockerfile.jvm index 53ed09abd23c22..566ba7b5a17bf0 100644 --- a/integration-tests/kubernetes/maven-invoker-way/src/it/kubernetes-docker-build-and-deploy-deployment/src/main/docker/Dockerfile.jvm +++ b/integration-tests/kubernetes/maven-invoker-way/src/it/kubernetes-docker-build-and-deploy-deployment/src/main/docker/Dockerfile.jvm @@ -21,7 +21,7 @@ # docker run -i --rm -p 8080:8080 -p 5005:5005 -e JAVA_ENABLE_DEBUG="true" quarkus/rest-client-quickstart-jvm # ### -FROM registry.access.redhat.com/ubi8/ubi-minimal:8.8 +FROM registry.access.redhat.com/ubi8/ubi-minimal:8.9 ARG JAVA_PACKAGE=java-11-openjdk-headless ARG RUN_JAVA_VERSION=1.3.8 diff --git a/integration-tests/kubernetes/maven-invoker-way/src/it/kubernetes-docker-build-and-deploy-statefulset/src/main/docker/Dockerfile.jvm b/integration-tests/kubernetes/maven-invoker-way/src/it/kubernetes-docker-build-and-deploy-statefulset/src/main/docker/Dockerfile.jvm index 53ed09abd23c22..566ba7b5a17bf0 100644 --- a/integration-tests/kubernetes/maven-invoker-way/src/it/kubernetes-docker-build-and-deploy-statefulset/src/main/docker/Dockerfile.jvm +++ b/integration-tests/kubernetes/maven-invoker-way/src/it/kubernetes-docker-build-and-deploy-statefulset/src/main/docker/Dockerfile.jvm @@ -21,7 +21,7 @@ # docker run -i --rm -p 8080:8080 -p 5005:5005 -e JAVA_ENABLE_DEBUG="true" quarkus/rest-client-quickstart-jvm # ### -FROM registry.access.redhat.com/ubi8/ubi-minimal:8.8 +FROM registry.access.redhat.com/ubi8/ubi-minimal:8.9 ARG JAVA_PACKAGE=java-11-openjdk-headless ARG RUN_JAVA_VERSION=1.3.8 diff --git a/integration-tests/kubernetes/maven-invoker-way/src/it/kubernetes-jib-build-and-deploy/src/main/docker/Dockerfile.jvm b/integration-tests/kubernetes/maven-invoker-way/src/it/kubernetes-jib-build-and-deploy/src/main/docker/Dockerfile.jvm index 53ed09abd23c22..566ba7b5a17bf0 100644 --- a/integration-tests/kubernetes/maven-invoker-way/src/it/kubernetes-jib-build-and-deploy/src/main/docker/Dockerfile.jvm +++ b/integration-tests/kubernetes/maven-invoker-way/src/it/kubernetes-jib-build-and-deploy/src/main/docker/Dockerfile.jvm @@ -21,7 +21,7 @@ # docker run -i --rm -p 8080:8080 -p 5005:5005 -e JAVA_ENABLE_DEBUG="true" quarkus/rest-client-quickstart-jvm # ### -FROM registry.access.redhat.com/ubi8/ubi-minimal:8.8 +FROM registry.access.redhat.com/ubi8/ubi-minimal:8.9 ARG JAVA_PACKAGE=java-11-openjdk-headless ARG RUN_JAVA_VERSION=1.3.8 diff --git a/integration-tests/kubernetes/maven-invoker-way/src/it/kubernetes-with-existing-selectorless-manifest/src/main/docker/Dockerfile.jvm b/integration-tests/kubernetes/maven-invoker-way/src/it/kubernetes-with-existing-selectorless-manifest/src/main/docker/Dockerfile.jvm index 53ed09abd23c22..566ba7b5a17bf0 100644 --- a/integration-tests/kubernetes/maven-invoker-way/src/it/kubernetes-with-existing-selectorless-manifest/src/main/docker/Dockerfile.jvm +++ b/integration-tests/kubernetes/maven-invoker-way/src/it/kubernetes-with-existing-selectorless-manifest/src/main/docker/Dockerfile.jvm @@ -21,7 +21,7 @@ # docker run -i --rm -p 8080:8080 -p 5005:5005 -e JAVA_ENABLE_DEBUG="true" quarkus/rest-client-quickstart-jvm # ### -FROM registry.access.redhat.com/ubi8/ubi-minimal:8.8 +FROM registry.access.redhat.com/ubi8/ubi-minimal:8.9 ARG JAVA_PACKAGE=java-11-openjdk-headless ARG RUN_JAVA_VERSION=1.3.8 diff --git a/integration-tests/kubernetes/maven-invoker-way/src/it/openshift-docker-build-and-deploy/src/main/docker/Dockerfile.jvm b/integration-tests/kubernetes/maven-invoker-way/src/it/openshift-docker-build-and-deploy/src/main/docker/Dockerfile.jvm index 53ed09abd23c22..566ba7b5a17bf0 100644 --- a/integration-tests/kubernetes/maven-invoker-way/src/it/openshift-docker-build-and-deploy/src/main/docker/Dockerfile.jvm +++ b/integration-tests/kubernetes/maven-invoker-way/src/it/openshift-docker-build-and-deploy/src/main/docker/Dockerfile.jvm @@ -21,7 +21,7 @@ # docker run -i --rm -p 8080:8080 -p 5005:5005 -e JAVA_ENABLE_DEBUG="true" quarkus/rest-client-quickstart-jvm # ### -FROM registry.access.redhat.com/ubi8/ubi-minimal:8.8 +FROM registry.access.redhat.com/ubi8/ubi-minimal:8.9 ARG JAVA_PACKAGE=java-11-openjdk-headless ARG RUN_JAVA_VERSION=1.3.8 diff --git a/integration-tests/kubernetes/maven-invoker-way/src/it/openshift-s2i-build-and-deploy/src/main/docker/Dockerfile.jvm b/integration-tests/kubernetes/maven-invoker-way/src/it/openshift-s2i-build-and-deploy/src/main/docker/Dockerfile.jvm index 53ed09abd23c22..566ba7b5a17bf0 100644 --- a/integration-tests/kubernetes/maven-invoker-way/src/it/openshift-s2i-build-and-deploy/src/main/docker/Dockerfile.jvm +++ b/integration-tests/kubernetes/maven-invoker-way/src/it/openshift-s2i-build-and-deploy/src/main/docker/Dockerfile.jvm @@ -21,7 +21,7 @@ # docker run -i --rm -p 8080:8080 -p 5005:5005 -e JAVA_ENABLE_DEBUG="true" quarkus/rest-client-quickstart-jvm # ### -FROM registry.access.redhat.com/ubi8/ubi-minimal:8.8 +FROM registry.access.redhat.com/ubi8/ubi-minimal:8.9 ARG JAVA_PACKAGE=java-11-openjdk-headless ARG RUN_JAVA_VERSION=1.3.8 diff --git a/integration-tests/kubernetes/quarkus-standard-way/pom.xml b/integration-tests/kubernetes/quarkus-standard-way/pom.xml index c5841892d9c2f2..af55a47197ebb2 100644 --- a/integration-tests/kubernetes/quarkus-standard-way/pom.xml +++ b/integration-tests/kubernetes/quarkus-standard-way/pom.xml @@ -130,6 +130,17 @@ + + io.dekorate + prometheus-annotations + noapt + + + io.fabric8 + kubernetes-client + + + io.fabric8 kubernetes-client diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithMetricsCustomAbsoluteTest.java b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithMetricsCustomAbsoluteTest.java index d39d852925cd28..65287b49e0d015 100644 --- a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithMetricsCustomAbsoluteTest.java +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithMetricsCustomAbsoluteTest.java @@ -12,6 +12,8 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; +import io.dekorate.prometheus.model.Endpoint; +import io.dekorate.prometheus.model.ServiceMonitor; import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.api.model.apps.Deployment; import io.quarkus.builder.Version; @@ -30,7 +32,10 @@ public class KubernetesWithMetricsCustomAbsoluteTest { .setApplicationVersion("0.1-SNAPSHOT") .setRun(true) .setLogFileName("k8s.log") - .withConfigurationResource("kubernetes-with-metrics-custom-absolute.properties") + .overrideConfigKey("quarkus.http.port", "9090") + .overrideConfigKey("quarkus.micrometer.export.prometheus.path", "/absolute-metrics") + .overrideConfigKey("quarkus.kubernetes.prometheus.prefix", "example.io") + .overrideConfigKey("quarkus.kubernetes.prometheus.scrape", "example.io/should_be_scraped") .setForcedDependencies(List.of( Dependency.of("io.quarkus", "quarkus-micrometer-registry-prometheus", Version.getVersion()))); @@ -76,6 +81,22 @@ public void assertGeneratedResources() throws IOException { }); }); }); + + assertThat(kubernetesList).filteredOn(i -> i.getKind().equals("ServiceMonitor")).singleElement() + .isInstanceOfSatisfying(ServiceMonitor.class, s -> { + assertThat(s.getMetadata()).satisfies(m -> { + assertThat(m.getName()).isEqualTo("metrics"); + }); + + assertThat(s.getSpec()).satisfies(spec -> { + assertThat(spec.getEndpoints()).hasSize(1); + assertThat(spec.getEndpoints().get(0)).isInstanceOfSatisfying(Endpoint.class, e -> { + assertThat(e.getScheme()).isEqualTo("http"); + assertThat(e.getTargetPort().getStrVal()).isEqualTo("9090"); + assertThat(e.getPath()).isEqualTo("/absolute-metrics"); + }); + }); + }); }); } diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithMetricsCustomRelativeTest.java b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithMetricsCustomRelativeTest.java index f7540a26616cd3..b9b9a113331ac1 100644 --- a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithMetricsCustomRelativeTest.java +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithMetricsCustomRelativeTest.java @@ -12,6 +12,8 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; +import io.dekorate.prometheus.model.Endpoint; +import io.dekorate.prometheus.model.ServiceMonitor; import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.api.model.apps.Deployment; import io.quarkus.builder.Version; @@ -30,8 +32,12 @@ public class KubernetesWithMetricsCustomRelativeTest { .setApplicationVersion("0.1-SNAPSHOT") .setRun(true) .setLogFileName("k8s.log") - .withConfigurationResource("kubernetes-with-metrics-custom-relative.properties") - .setForcedDependencies(List.of(Dependency.of("io.quarkus", "quarkus-smallrye-metrics", Version.getVersion()))); + .overrideConfigKey("quarkus.http.port", "9090") + .overrideConfigKey("quarkus.micrometer.export.prometheus.path", "met") + .overrideConfigKey("quarkus.kubernetes.prometheus.prefix", "example.io") + .overrideConfigKey("quarkus.kubernetes.prometheus.scrape", "example.io/should_be_scraped") + .setForcedDependencies(List.of( + Dependency.of("io.quarkus", "quarkus-micrometer-registry-prometheus", Version.getVersion()))); @ProdBuildResults private ProdModeTestResults prodModeTestResults; @@ -76,6 +82,22 @@ public void assertGeneratedResources() throws IOException { }); }); }); + + assertThat(kubernetesList).filteredOn(i -> i.getKind().equals("ServiceMonitor")).singleElement() + .isInstanceOfSatisfying(ServiceMonitor.class, s -> { + assertThat(s.getMetadata()).satisfies(m -> { + assertThat(m.getName()).isEqualTo("metrics"); + }); + + assertThat(s.getSpec()).satisfies(spec -> { + assertThat(spec.getEndpoints()).hasSize(1); + assertThat(spec.getEndpoints().get(0)).isInstanceOfSatisfying(Endpoint.class, e -> { + assertThat(e.getScheme()).isEqualTo("http"); + assertThat(e.getTargetPort().getStrVal()).isEqualTo("9090"); + assertThat(e.getPath()).isEqualTo("/q/met"); + }); + }); + }); } } diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithMetricsNoAnnotationsTest.java b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithMetricsNoAnnotationsTest.java index 23ff2055c42dca..aa2d0878db11ed 100644 --- a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithMetricsNoAnnotationsTest.java +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithMetricsNoAnnotationsTest.java @@ -12,6 +12,8 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; +import io.dekorate.prometheus.model.Endpoint; +import io.dekorate.prometheus.model.ServiceMonitor; import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.api.model.apps.Deployment; import io.quarkus.builder.Version; @@ -30,7 +32,10 @@ public class KubernetesWithMetricsNoAnnotationsTest { .setApplicationVersion("0.1-SNAPSHOT") .setRun(true) .setLogFileName("k8s.log") - .withConfigurationResource("kubernetes-with-metrics-no-annotations.properties") + .overrideConfigKey("quarkus.http.port", "9090") + .overrideConfigKey("quarkus.smallrye-metrics.path", "/met") + .overrideConfigKey("quarkus.kubernetes.prometheus.annotations", "false") + .overrideConfigKey("quarkus.kubernetes.prometheus.prefix", "example.io") .setForcedDependencies(List.of( Dependency.of("io.quarkus", "quarkus-micrometer-registry-prometheus", Version.getVersion()))); @@ -76,6 +81,22 @@ public void assertGeneratedResources() throws IOException { }); }); }); + + assertThat(kubernetesList).filteredOn(i -> i.getKind().equals("ServiceMonitor")).singleElement() + .isInstanceOfSatisfying(ServiceMonitor.class, s -> { + assertThat(s.getMetadata()).satisfies(m -> { + assertThat(m.getName()).isEqualTo("metrics"); + }); + + assertThat(s.getSpec()).satisfies(spec -> { + assertThat(spec.getEndpoints()).hasSize(1); + assertThat(spec.getEndpoints().get(0)).isInstanceOfSatisfying(Endpoint.class, e -> { + assertThat(e.getScheme()).isEqualTo("http"); + assertThat(e.getTargetPort().getStrVal()).isEqualTo("9090"); + assertThat(e.getPath()).isEqualTo("/q/metrics"); + }); + }); + }); } } diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithMetricsNoServiceMonitor.java b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithMetricsNoServiceMonitor.java new file mode 100644 index 00000000000000..c996e15047d2f0 --- /dev/null +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithMetricsNoServiceMonitor.java @@ -0,0 +1,87 @@ +package io.quarkus.it.kubernetes; + +import static io.restassured.RestAssured.given; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; +import static org.hamcrest.Matchers.is; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.api.model.apps.Deployment; +import io.quarkus.builder.Version; +import io.quarkus.maven.dependency.Dependency; +import io.quarkus.test.LogFile; +import io.quarkus.test.ProdBuildResults; +import io.quarkus.test.ProdModeTestResults; +import io.quarkus.test.QuarkusProdModeTest; + +public class KubernetesWithMetricsNoServiceMonitor { + + @RegisterExtension + static final QuarkusProdModeTest config = new QuarkusProdModeTest() + .withApplicationRoot((jar) -> jar.addClasses(GreetingResource.class)) + .setApplicationName("metrics") + .setApplicationVersion("0.1-SNAPSHOT") + .setRun(true) + .setLogFileName("k8s.log") + .overrideConfigKey("quarkus.http.port", "9090") + .overrideConfigKey("quarkus.smallrye-metrics.path", "/met") + .overrideConfigKey("quarkus.kubernetes.prometheus.generate-service-monitor", "false") + .overrideConfigKey("quarkus.kubernetes.prometheus.prefix", "example.io") + .setForcedDependencies(List.of( + Dependency.of("io.quarkus", "quarkus-micrometer-registry-prometheus", Version.getVersion()))); + + @ProdBuildResults + private ProdModeTestResults prodModeTestResults; + + @LogFile + private Path logfile; + + @Test + public void assertApplicationRuns() { + assertThat(logfile).isRegularFile().hasFileName("k8s.log"); + TestUtil.assertLogFileContents(logfile, "kubernetes", "metrics"); + + given() + .when().get("/greeting") + .then() + .statusCode(200) + .body(is("hello")); + } + + @Test + public void assertGeneratedResources() throws IOException { + final Path kubernetesDir = prodModeTestResults.getBuildDir().resolve("kubernetes"); + assertThat(kubernetesDir) + .isDirectoryContaining(p -> p.getFileName().endsWith("kubernetes.json")) + .isDirectoryContaining(p -> p.getFileName().endsWith("kubernetes.yml")); + List kubernetesList = DeserializationUtil + .deserializeAsList(kubernetesDir.resolve("kubernetes.yml")); + assertThat(kubernetesList.get(0)).isInstanceOfSatisfying(Deployment.class, d -> { + assertThat(d.getMetadata()).satisfies(m -> { + assertThat(m.getName()).isEqualTo("metrics"); + }); + + assertThat(d.getSpec()).satisfies(deploymentSpec -> { + assertThat(deploymentSpec.getTemplate()).satisfies(t -> { + assertThat(t.getMetadata()).satisfies(meta -> { + // Annotations should not have been created in this configuration. + assertThat(meta.getAnnotations()).contains( + entry("prometheus.io/scrape", "true"), + entry("prometheus.io/path", "/met"), + entry("prometheus.io/port", "9090"), + entry("prometheus.io/scheme", "http")); + }); + }); + }); + }); + + assertThat(kubernetesList).filteredOn(i -> i.getKind().equals("ServiceMonitor")).isEmpty(); + } +} diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithMetricsTest.java b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithMetricsTest.java index 934de87fa74ed6..37c51a12afcdc2 100644 --- a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithMetricsTest.java +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithMetricsTest.java @@ -12,6 +12,8 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; +import io.dekorate.prometheus.model.Endpoint; +import io.dekorate.prometheus.model.ServiceMonitor; import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.api.model.apps.Deployment; import io.quarkus.builder.Version; @@ -30,7 +32,7 @@ public class KubernetesWithMetricsTest { .setApplicationVersion("0.1-SNAPSHOT") .setRun(true) .setLogFileName("k8s.log") - .withConfigurationResource("kubernetes-with-metrics.properties") + .overrideConfigKey("quarkus.http.port", "9090") .setForcedDependencies(List.of( Dependency.of("io.quarkus", "quarkus-smallrye-metrics", Version.getVersion()), Dependency.of("io.quarkus", "quarkus-kubernetes-client", Version.getVersion()))); @@ -83,6 +85,22 @@ public void assertGeneratedResources() throws IOException { entry("prometheus.io/scheme", "http")); }); + assertThat(kubernetesList).filteredOn(i -> i.getKind().equals("ServiceMonitor")).singleElement() + .isInstanceOfSatisfying(ServiceMonitor.class, s -> { + assertThat(s.getMetadata()).satisfies(m -> { + assertThat(m.getName()).isEqualTo("metrics"); + }); + + assertThat(s.getSpec()).satisfies(spec -> { + assertThat(spec.getEndpoints()).hasSize(1); + assertThat(spec.getEndpoints().get(0)).isInstanceOfSatisfying(Endpoint.class, e -> { + assertThat(e.getScheme()).isEqualTo("http"); + assertThat(e.getTargetPort().getStrVal()).isEqualTo("9090"); + assertThat(e.getPath()).isEqualTo("/q/metrics"); + }); + }); + }); + assertThat(kubernetesList).filteredOn(h -> "ServiceAccount".equals(h.getKind())).singleElement().satisfies(h -> { if (h.getMetadata().getAnnotations() != null) { assertThat(h.getMetadata().getAnnotations()).doesNotContainKeys("prometheus.io/scrape", "prometheus.io/path", diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithMicrometerTest.java b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithMicrometerTest.java index 799842d43212ff..b41e32d046c142 100644 --- a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithMicrometerTest.java +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithMicrometerTest.java @@ -12,6 +12,8 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; +import io.dekorate.prometheus.model.Endpoint; +import io.dekorate.prometheus.model.ServiceMonitor; import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.api.model.apps.Deployment; import io.quarkus.builder.Version; @@ -59,21 +61,39 @@ public void assertGeneratedResources() throws IOException { .isDirectoryContaining(p -> p.getFileName().endsWith("kubernetes.yml")); List kubernetesList = DeserializationUtil .deserializeAsList(kubernetesDir.resolve("kubernetes.yml")); - assertThat(kubernetesList.get(0)).isInstanceOfSatisfying(Deployment.class, d -> { - assertThat(d.getMetadata()).satisfies(m -> { - assertThat(m.getName()).isEqualTo("metrics"); - }); - - assertThat(d.getSpec()).satisfies(deploymentSpec -> { - assertThat(deploymentSpec.getTemplate()).satisfies(t -> { - assertThat(t.getMetadata()).satisfies(meta -> { - assertThat(meta.getAnnotations()).contains(entry("prometheus.io/scrape", "true"), - entry("prometheus.io/path", "/q/metrics"), entry("prometheus.io/port", "8080"), - entry("prometheus.io/scheme", "http")); + + assertThat(kubernetesList).filteredOn(i -> i.getKind().equals("ServiceMonitor")).singleElement() + .isInstanceOfSatisfying(ServiceMonitor.class, s -> { + assertThat(s.getMetadata()).satisfies(m -> { + assertThat(m.getName()).isEqualTo("metrics"); + }); + + assertThat(s.getSpec()).satisfies(spec -> { + assertThat(spec.getEndpoints()).hasSize(1); + assertThat(spec.getEndpoints().get(0)).isInstanceOfSatisfying(Endpoint.class, e -> { + assertThat(e.getScheme()).isEqualTo("http"); + assertThat(e.getTargetPort().getStrVal()).isEqualTo("8080"); + assertThat(e.getPath()).isEqualTo("/q/metrics"); + }); + }); + }); + + assertThat(kubernetesList).filteredOn(i -> i instanceof Deployment).singleElement() + .isInstanceOfSatisfying(Deployment.class, d -> { + assertThat(d.getMetadata()).satisfies(m -> { + assertThat(m.getName()).isEqualTo("metrics"); + }); + + assertThat(d.getSpec()).satisfies(deploymentSpec -> { + assertThat(deploymentSpec.getTemplate()).satisfies(t -> { + assertThat(t.getMetadata()).satisfies(meta -> { + assertThat(meta.getAnnotations()).contains(entry("prometheus.io/scrape", "true"), + entry("prometheus.io/path", "/q/metrics"), entry("prometheus.io/port", "8080"), + entry("prometheus.io/scheme", "http")); + }); + }); }); }); - }); - }); } } diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-metrics-custom-absolute.properties b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-metrics-custom-absolute.properties deleted file mode 100644 index befe3e634b923f..00000000000000 --- a/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-metrics-custom-absolute.properties +++ /dev/null @@ -1,4 +0,0 @@ -quarkus.http.port=9090 -quarkus.micrometer.export.prometheus.path=/absolute-metrics -quarkus.kubernetes.prometheus.prefix=example.io -quarkus.kubernetes.prometheus.scrape=example.io/should_be_scraped \ No newline at end of file diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-metrics-custom-relative.properties b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-metrics-custom-relative.properties deleted file mode 100644 index ce7853e6736148..00000000000000 --- a/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-metrics-custom-relative.properties +++ /dev/null @@ -1,4 +0,0 @@ -quarkus.http.port=9090 -quarkus.smallrye-metrics.path=met -quarkus.kubernetes.prometheus.prefix=example.io -quarkus.kubernetes.prometheus.scrape=example.io/should_be_scraped \ No newline at end of file diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-metrics-no-annotations.properties b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-metrics-no-annotations.properties deleted file mode 100644 index 232bf76c33de67..00000000000000 --- a/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-metrics-no-annotations.properties +++ /dev/null @@ -1,4 +0,0 @@ -quarkus.http.port=9090 -quarkus.smallrye-metrics.path=/met -quarkus.kubernetes.prometheus.annotations=false -quarkus.kubernetes.prometheus.prefix=example.io diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-metrics.properties b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-metrics.properties deleted file mode 100644 index 2e40601cd49faa..00000000000000 --- a/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-metrics.properties +++ /dev/null @@ -1 +0,0 @@ -quarkus.http.port=9090 diff --git a/integration-tests/maven/src/test/java/io/quarkus/maven/it/CreateExtensionMojoIT.java b/integration-tests/maven/src/test/java/io/quarkus/maven/it/CreateExtensionMojoIT.java index ba64fbdd9b6cb2..90a6ffe4daaaab 100644 --- a/integration-tests/maven/src/test/java/io/quarkus/maven/it/CreateExtensionMojoIT.java +++ b/integration-tests/maven/src/test/java/io/quarkus/maven/it/CreateExtensionMojoIT.java @@ -111,6 +111,19 @@ public void testCreateQuarkiverseExtension(TestInfo testInfo) throws Throwable { "quarkus-my-quarkiverse-ext/integration-tests/src/test/java/io/quarkiverse/my/quarkiverse/ext/it/MyQuarkiverseExtResourceTest.java"); assertThatMatchSnapshot(testInfo, testDirPath, "quarkus-my-quarkiverse-ext/pom.xml"); assertThatMatchSnapshot(testInfo, testDirPath, "quarkus-my-quarkiverse-ext/runtime/pom.xml"); + assertThatMatchSnapshot(testInfo, testDirPath, + "quarkus-my-quarkiverse-ext/runtime/src/main/resources/META-INF/quarkus-extension.yaml"); + assertThatMatchSnapshot(testInfo, testDirPath, "quarkus-my-quarkiverse-ext/LICENSE"); + assertThatMatchSnapshot(testInfo, testDirPath, "quarkus-my-quarkiverse-ext/README.md"); + assertThatMatchSnapshot(testInfo, testDirPath, "quarkus-my-quarkiverse-ext/.github/workflows/build.yml"); + assertThatMatchSnapshot(testInfo, testDirPath, "quarkus-my-quarkiverse-ext/.github/workflows/pre-release.yml"); + assertThatMatchSnapshot(testInfo, testDirPath, "quarkus-my-quarkiverse-ext/.github/workflows/quarkus-snapshot.yaml"); + assertThatMatchSnapshot(testInfo, testDirPath, "quarkus-my-quarkiverse-ext/.github/workflows/release.yml"); + assertThatMatchSnapshot(testInfo, testDirPath, "quarkus-my-quarkiverse-ext/docs/pom.xml"); + assertThatMatchSnapshot(testInfo, testDirPath, "quarkus-my-quarkiverse-ext/docs/antora.yml"); + assertThatMatchSnapshot(testInfo, testDirPath, "quarkus-my-quarkiverse-ext/docs/modules/ROOT/nav.adoc"); + assertThatMatchSnapshot(testInfo, testDirPath, "quarkus-my-quarkiverse-ext/docs/modules/ROOT/pages/index.adoc"); + assertThatMatchSnapshot(testInfo, testDirPath, "quarkus-my-quarkiverse-ext/integration-tests/pom.xml"); } @Test diff --git a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-extension/code/quarkiverse/java/.github/workflows/build.yml b/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateQuarkiverseExtension/quarkus-my-quarkiverse-ext_.github_workflows_build.yml similarity index 100% rename from independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-extension/code/quarkiverse/java/.github/workflows/build.yml rename to integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateQuarkiverseExtension/quarkus-my-quarkiverse-ext_.github_workflows_build.yml diff --git a/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateQuarkiverseExtension/quarkus-my-quarkiverse-ext_.github_workflows_pre-release.yml b/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateQuarkiverseExtension/quarkus-my-quarkiverse-ext_.github_workflows_pre-release.yml new file mode 100644 index 00000000000000..0a9e64eaa882f1 --- /dev/null +++ b/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateQuarkiverseExtension/quarkus-my-quarkiverse-ext_.github_workflows_pre-release.yml @@ -0,0 +1,33 @@ +name: Quarkiverse Pre Release + +on: + pull_request: + paths: + - '.github/project.yml' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +defaults: + run: + shell: bash + +jobs: + release: + runs-on: ubuntu-latest + name: pre release + + steps: + - uses: radcortez/project-metadata-action@master + name: retrieve project metadata + id: metadata + with: + github-token: ${{secrets.GITHUB_TOKEN}} + metadata-file-path: '.github/project.yml' + + - name: Validate version + if: contains(steps.metadata.outputs.current-version, 'SNAPSHOT') + run: | + echo '::error::Cannot release a SNAPSHOT version.' + exit 1 \ No newline at end of file diff --git a/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateQuarkiverseExtension/quarkus-my-quarkiverse-ext_.github_workflows_quarkus-snapshot.yaml b/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateQuarkiverseExtension/quarkus-my-quarkiverse-ext_.github_workflows_quarkus-snapshot.yaml new file mode 100644 index 00000000000000..ed1e496f85d6be --- /dev/null +++ b/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateQuarkiverseExtension/quarkus-my-quarkiverse-ext_.github_workflows_quarkus-snapshot.yaml @@ -0,0 +1,60 @@ +name: "Quarkus ecosystem CI" +on: + workflow_dispatch: + watch: + types: [started] + + # For this CI to work, ECOSYSTEM_CI_TOKEN needs to contain a GitHub with rights to close the Quarkus issue that the user/bot has opened, + # while 'ECOSYSTEM_CI_REPO_PATH' needs to be set to the corresponding path in the 'quarkusio/quarkus-ecosystem-ci' repository + +env: + ECOSYSTEM_CI_REPO: quarkusio/quarkus-ecosystem-ci + ECOSYSTEM_CI_REPO_FILE: context.yaml + JAVA_VERSION: 11 + + ######################### + # Repo specific setting # + ######################### + + ECOSYSTEM_CI_REPO_PATH: quarkiverse-my-quarkiverse-ext + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +defaults: + run: + shell: bash + +jobs: + build: + name: "Build against latest Quarkus snapshot" + runs-on: ubuntu-latest + # Allow to manually launch the ecosystem CI in addition to the bots + if: github.actor == 'quarkusbot' || github.actor == 'quarkiversebot' || github.actor == '' + + steps: + - name: Install yq + uses: dcarbone/install-yq-action@v1.0.1 + + - name: Set up Java + uses: actions/setup-java@v3 + with: + distribution: temurin + java-version: ${{ env.JAVA_VERSION }} + + - name: Checkout repo + uses: actions/checkout@v3 + with: + path: current-repo + + - name: Checkout Ecosystem + uses: actions/checkout@v3 + with: + repository: ${{ env.ECOSYSTEM_CI_REPO }} + path: ecosystem-ci + + - name: Setup and Run Tests + run: ./ecosystem-ci/setup-and-test + env: + ECOSYSTEM_CI_TOKEN: ${{ secrets.ECOSYSTEM_CI_TOKEN }} \ No newline at end of file diff --git a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-extension/code/quarkiverse/java/.github/workflows/release.yml b/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateQuarkiverseExtension/quarkus-my-quarkiverse-ext_.github_workflows_release.yml similarity index 95% rename from independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-extension/code/quarkiverse/java/.github/workflows/release.yml rename to integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateQuarkiverseExtension/quarkus-my-quarkiverse-ext_.github_workflows_release.yml index 0a3894f1bdd33d..9576f81ad7c059 100644 --- a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-extension/code/quarkiverse/java/.github/workflows/release.yml +++ b/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateQuarkiverseExtension/quarkus-my-quarkiverse-ext_.github_workflows_release.yml @@ -54,7 +54,7 @@ jobs: - name: Update latest release version in docs run: | - mvn -B -ntp -pl docs -am generate-resources -Denforcer.skip -Dformatter.skip -Dimpsort.skip + mvn -B -ntp -pl docs -am package -DskipTests -DskipITs -Denforcer.skip -Dformatter.skip -Dimpsort.skip if ! git diff --quiet docs/modules/ROOT/pages/includes/attributes.adoc; then git add docs/modules/ROOT/pages/includes/attributes.adoc git commit -m "Update the latest release version ${{steps.metadata.outputs.current-version}} in documentation" diff --git a/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateQuarkiverseExtension/quarkus-my-quarkiverse-ext_LICENSE b/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateQuarkiverseExtension/quarkus-my-quarkiverse-ext_LICENSE new file mode 100644 index 00000000000000..261eeb9e9f8b2b --- /dev/null +++ b/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateQuarkiverseExtension/quarkus-my-quarkiverse-ext_LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateQuarkiverseExtension/quarkus-my-quarkiverse-ext_README.md b/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateQuarkiverseExtension/quarkus-my-quarkiverse-ext_README.md new file mode 100644 index 00000000000000..cb86bfbcd72d6d --- /dev/null +++ b/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateQuarkiverseExtension/quarkus-my-quarkiverse-ext_README.md @@ -0,0 +1,27 @@ +# Quarkus My Quarkiverse extension + +[![Version](https://img.shields.io/maven-central/v/io.quarkiverse.my-quarkiverse-ext/quarkus-my-quarkiverse-ext?logo=apache-maven&style=flat-square)](https://central.sonatype.com/artifact/io.quarkiverse.my-quarkiverse-ext/quarkus-my-quarkiverse-ext-parent) + +## Welcome to Quarkiverse! + +Congratulations and thank you for creating a new Quarkus extension project in Quarkiverse! + +Feel free to replace this content with the proper description of your new project and necessary instructions how to use and contribute to it. + +You can find the basic info, Quarkiverse policies and conventions in [the Quarkiverse wiki](https://github.com/quarkiverse/quarkiverse/wiki). + +In case you are creating a Quarkus extension project for the first time, please follow [Building My First Extension](https://quarkus.io/guides/building-my-first-extension) guide. + +Other useful articles related to Quarkus extension development can be found under the [Writing Extensions](https://quarkus.io/guides/#writing-extensions) guide category on the [Quarkus.io](https://quarkus.io) website. + +Thanks again, good luck and have fun! + +## Documentation + +The documentation for this extension should be maintained as part of this repository and it is stored in the `docs/` directory. + +The layout should follow the [Antora's Standard File and Directory Set](https://docs.antora.org/antora/2.3/standard-directories/). + +Once the docs are ready to be published, please open a PR including this repository in the [Quarkiverse Docs Antora playbook](https://github.com/quarkiverse/quarkiverse-docs/blob/main/antora-playbook.yml#L7). See an example [here](https://github.com/quarkiverse/quarkiverse-docs/pull/1) + +Your documentation will then be published to the https://docs.quarkiverse.io/ website. diff --git a/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateQuarkiverseExtension/quarkus-my-quarkiverse-ext_docs_antora.yml b/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateQuarkiverseExtension/quarkus-my-quarkiverse-ext_docs_antora.yml new file mode 100644 index 00000000000000..c56ac65d6aced2 --- /dev/null +++ b/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateQuarkiverseExtension/quarkus-my-quarkiverse-ext_docs_antora.yml @@ -0,0 +1,5 @@ +name: quarkus-my-quarkiverse-ext +title: My Quarkiverse extension +version: dev +nav: + - modules/ROOT/nav.adoc diff --git a/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateQuarkiverseExtension/quarkus-my-quarkiverse-ext_docs_modules_ROOT_nav.adoc b/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateQuarkiverseExtension/quarkus-my-quarkiverse-ext_docs_modules_ROOT_nav.adoc new file mode 100644 index 00000000000000..d2454cdb59abce --- /dev/null +++ b/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateQuarkiverseExtension/quarkus-my-quarkiverse-ext_docs_modules_ROOT_nav.adoc @@ -0,0 +1 @@ +* xref:index.adoc[Getting started] diff --git a/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateQuarkiverseExtension/quarkus-my-quarkiverse-ext_docs_modules_ROOT_pages_index.adoc b/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateQuarkiverseExtension/quarkus-my-quarkiverse-ext_docs_modules_ROOT_pages_index.adoc new file mode 100644 index 00000000000000..483daf11f0c0c0 --- /dev/null +++ b/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateQuarkiverseExtension/quarkus-my-quarkiverse-ext_docs_modules_ROOT_pages_index.adoc @@ -0,0 +1,27 @@ += Quarkus My Quarkiverse extension + +include::./includes/attributes.adoc[] + +TIP: Describe what the extension does here. + +== Installation + +If you want to use this extension, you need to add the `io.quarkiverse.my-quarkiverse-ext:quarkus-my-quarkiverse-ext` extension first to your build file. + +For instance, with Maven, add the following dependency to your POM file: + +[source,xml,subs=attributes+] +---- + + io.quarkiverse.my-quarkiverse-ext + quarkus-my-quarkiverse-ext + {project-version} + +---- + +[[extension-configuration-reference]] +== Extension Configuration Reference + +TIP: Remove this section if you don't have Quarkus configuration properties in your extension. + +include::includes/quarkus-my-quarkiverse-ext.adoc[leveloffset=+1, opts=optional] diff --git a/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateQuarkiverseExtension/quarkus-my-quarkiverse-ext_docs_pom.xml b/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateQuarkiverseExtension/quarkus-my-quarkiverse-ext_docs_pom.xml new file mode 100644 index 00000000000000..655dffbc523eca --- /dev/null +++ b/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateQuarkiverseExtension/quarkus-my-quarkiverse-ext_docs_pom.xml @@ -0,0 +1,106 @@ + + + 4.0.0 + + io.quarkiverse.my-quarkiverse-ext + quarkus-my-quarkiverse-ext-parent + 999-SNAPSHOT + ../pom.xml + + + quarkus-my-quarkiverse-ext-docs + Quarkus My Quarkiverse extension - Documentation + + + + + io.quarkiverse.my-quarkiverse-ext + quarkus-my-quarkiverse-ext-deployment + ${project.version} + + + + + modules/ROOT/examples + + + io.quarkus + quarkus-maven-plugin + + + + build + + + + + + it.ozimov + yaml-properties-maven-plugin + + + initialize + + read-project-properties + + + + ${project.basedir}/../.github/project.yml + + + + + + + maven-resources-plugin + + + copy-resources + generate-resources + + copy-resources + + + ${project.basedir}/modules/ROOT/pages/includes/ + + + ${project.basedir}/../target/asciidoc/generated/config/ + quarkus-my-quarkiverse-ext.adoc + false + + + ${project.basedir}/templates/includes + attributes.adoc + true + + + + + + copy-images + prepare-package + + copy-resources + + + ${project.build.directory}/generated-docs/_images/ + + + ${project.basedir}/modules/ROOT/assets/images/ + false + + + + + + + + org.asciidoctor + asciidoctor-maven-plugin + + + + + diff --git a/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateQuarkiverseExtension/quarkus-my-quarkiverse-ext_integration-tests_pom.xml b/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateQuarkiverseExtension/quarkus-my-quarkiverse-ext_integration-tests_pom.xml new file mode 100644 index 00000000000000..efeae2164b76c0 --- /dev/null +++ b/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateQuarkiverseExtension/quarkus-my-quarkiverse-ext_integration-tests_pom.xml @@ -0,0 +1,98 @@ + + + 4.0.0 + + io.quarkiverse.my-quarkiverse-ext + quarkus-my-quarkiverse-ext-parent + 999-SNAPSHOT + + quarkus-my-quarkiverse-ext-integration-tests + Quarkus My Quarkiverse extension - Integration Tests + + true + + + + io.quarkus + quarkus-resteasy + + + io.quarkiverse.my-quarkiverse-ext + quarkus-my-quarkiverse-ext + ${project.version} + + + io.quarkus + quarkus-junit5 + test + + + io.rest-assured + rest-assured + test + + + io.quarkus + quarkus-devtools-testing + test + + + + + + io.quarkus + quarkus-maven-plugin + + + + build + + + + + + maven-failsafe-plugin + + + + integration-test + verify + + + + + + ${project.build.directory}/${project.build.finalName}-runner + org.jboss.logmanager.LogManager + ${maven.home} + + + + + + + + native-image + + + native + + + + + + maven-surefire-plugin + + ${native.surefire.skip} + + + + + + false + native + + + + diff --git a/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateQuarkiverseExtension/quarkus-my-quarkiverse-ext_runtime_src_main_resources_META-INF_quarkus-extension.yaml b/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateQuarkiverseExtension/quarkus-my-quarkiverse-ext_runtime_src_main_resources_META-INF_quarkus-extension.yaml new file mode 100644 index 00000000000000..d635fe2e40fc40 --- /dev/null +++ b/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateQuarkiverseExtension/quarkus-my-quarkiverse-ext_runtime_src_main_resources_META-INF_quarkus-extension.yaml @@ -0,0 +1,14 @@ +name: My Quarkiverse extension +#description: My Quarkiverse extension description +metadata: +# keywords: +# - my-quarkiverse-ext +# guide: https://quarkiverse.github.io/quarkiverse-docs/my-quarkiverse-ext/dev/ # To create and publish this guide, see https://github.com/quarkiverse/quarkiverse/wiki#documenting-your-extension +# categories: +# - "miscellaneous" +# status: "preview" + codestart: + name: my-quarkiverse-ext + languages: + - "java" + artifact: "io.quarkiverse.my-quarkiverse-ext:quarkus-my-quarkiverse-ext:codestarts:jar:${project.version}" diff --git a/integration-tests/oidc-client-reactive/pom.xml b/integration-tests/oidc-client-reactive/pom.xml index 5c9c5f1f2e838a..5d504c6944136e 100644 --- a/integration-tests/oidc-client-reactive/pom.xml +++ b/integration-tests/oidc-client-reactive/pom.xml @@ -156,6 +156,9 @@ + + META-INF/ide-deps/** + application.properties diff --git a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CustomTenantResolver.java b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CustomTenantResolver.java index 1df7467d064adc..34ffd429732e19 100644 --- a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CustomTenantResolver.java +++ b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CustomTenantResolver.java @@ -44,6 +44,9 @@ public String resolve(RoutingContext context) { if (path.endsWith("bearer")) { return "bearer"; } + if (path.endsWith("bearer-id")) { + return "bearer-id"; + } if (path.endsWith("bearer-required-algorithm")) { return "bearer-required-algorithm"; } diff --git a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/UsersResource.java b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/UsersResource.java index 3e55f570cfbe91..37f4565cbca0bf 100644 --- a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/UsersResource.java +++ b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/UsersResource.java @@ -29,6 +29,14 @@ public User principalName() { return new User(identity.getPrincipal().getName()); } + @GET + @Path("/me/bearer-id") + @RolesAllowed("user") + @Produces(MediaType.APPLICATION_JSON) + public User principalNameId() { + return new User(identity.getPrincipal().getName()); + } + @GET @Path("/preferredUserName/bearer") @RolesAllowed("user") diff --git a/integration-tests/oidc-wiremock/src/main/resources/application.properties b/integration-tests/oidc-wiremock/src/main/resources/application.properties index 1a0e9556492dee..6ac2dbb4c4537c 100644 --- a/integration-tests/oidc-wiremock/src/main/resources/application.properties +++ b/integration-tests/oidc-wiremock/src/main/resources/application.properties @@ -121,6 +121,12 @@ quarkus.oidc.bearer.credentials.secret=secret quarkus.oidc.bearer.token.audience=https://service.example.com quarkus.oidc.bearer.allow-token-introspection-cache=false +quarkus.oidc.bearer-id.auth-server-url=${keycloak.url}/realms/quarkus/ +quarkus.oidc.bearer-id.client-id=quarkus-app +quarkus.oidc.bearer-id.credentials.secret=secret +quarkus.oidc.bearer-id.allow-token-introspection-cache=false +quarkus.oidc.bearer-id.token.authorization-scheme=ID + quarkus.oidc.bearer-required-algorithm.auth-server-url=${keycloak.url}/realms/quarkus/ quarkus.oidc.bearer-required-algorithm.client-id=quarkus-app quarkus.oidc.bearer-required-algorithm.credentials.secret=secret diff --git a/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/BearerTokenAuthorizationTest.java b/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/BearerTokenAuthorizationTest.java index 4e31443081776b..5cf6f2cc4f5f85 100644 --- a/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/BearerTokenAuthorizationTest.java +++ b/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/BearerTokenAuthorizationTest.java @@ -262,6 +262,17 @@ public void testExpiredBearerToken() { .header("WWW-Authenticate", equalTo("Bearer")); } + @Test + public void testBearerToken() { + String token = getAccessToken("alice", Set.of("user")); + + RestAssured.given().auth().oauth2(token).when() + .get("/api/users/me/bearer") + .then() + .statusCode(200) + .body(Matchers.containsString("alice")); + } + @Test public void testBearerTokenWrongIssuer() { String token = getAccessTokenWrongIssuer("alice", Set.of("user")); @@ -284,6 +295,38 @@ public void testBearerTokenWrongAudience() { .header("WWW-Authenticate", equalTo("Bearer")); } + @Test + public void testBearerTokenIdScheme() { + String token = getAccessToken("alice", Set.of("user")); + + RestAssured.given().header("Authorization", "ID " + token).when() + .get("/api/users/me/bearer-id") + .then() + .statusCode(200) + .body(Matchers.containsString("alice")); + } + + @Test + public void testBearerTokenIdSchemeButBearerSchemeIsUsed() { + String token = getAccessToken("alice", Set.of("user")); + + RestAssured.given().auth().oauth2(token).when() + .get("/api/users/me/bearer-id") + .then() + .statusCode(401); + } + + @Test + public void testBearerTokenIdSchemeWrongIssuer() { + String token = getAccessTokenWrongIssuer("alice", Set.of("user")); + + RestAssured.given().auth().oauth2(token).when() + .get("/api/users/me/bearer-id") + .then() + .statusCode(401) + .header("WWW-Authenticate", equalTo("ID")); + } + @Test public void testAcquiringIdentityOutsideOfHttpRequest() { String tenant = "bearer"; diff --git a/integration-tests/openshift-client/pom.xml b/integration-tests/openshift-client/pom.xml index eef764d55f5e64..82629e66d5b56d 100644 --- a/integration-tests/openshift-client/pom.xml +++ b/integration-tests/openshift-client/pom.xml @@ -22,11 +22,6 @@ io.quarkus quarkus-resteasy-jackson - - io.quarkus - quarkus-integration-test-kubernetes-client-hack-extension - ${project.version} - io.quarkus quarkus-openshift-client @@ -34,11 +29,15 @@ io.fabric8 - openshift-model-operator + openshift-model-hive io.fabric8 - openshift-model-operator-hub + openshift-model-miscellaneous + + + io.fabric8 + openshift-model-operator @@ -70,19 +69,6 @@ - - io.quarkus - quarkus-integration-test-kubernetes-client-hack-extension-deployment - ${project.version} - pom - test - - - * - * - - - io.quarkus quarkus-openshift-client-deployment diff --git a/integration-tests/opentelemetry-grpc/src/test/java/io/quarkus/it/opentelemetry/grpc/OpenTelemetryGrpcTest.java b/integration-tests/opentelemetry-grpc/src/test/java/io/quarkus/it/opentelemetry/grpc/OpenTelemetryGrpcTest.java index ff16ac9dadda24..b9a958be852421 100644 --- a/integration-tests/opentelemetry-grpc/src/test/java/io/quarkus/it/opentelemetry/grpc/OpenTelemetryGrpcTest.java +++ b/integration-tests/opentelemetry-grpc/src/test/java/io/quarkus/it/opentelemetry/grpc/OpenTelemetryGrpcTest.java @@ -2,18 +2,18 @@ import static io.opentelemetry.api.trace.SpanKind.CLIENT; import static io.opentelemetry.api.trace.SpanKind.SERVER; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HTTP_METHOD; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HTTP_ROUTE; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HTTP_STATUS_CODE; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HTTP_TARGET; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NET_HOST_PORT; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NET_SOCK_HOST_ADDR; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NET_SOCK_PEER_ADDR; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NET_SOCK_PEER_PORT; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.RPC_GRPC_STATUS_CODE; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.RPC_METHOD; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.RPC_SERVICE; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.RPC_SYSTEM; +import static io.opentelemetry.semconv.SemanticAttributes.HTTP_METHOD; +import static io.opentelemetry.semconv.SemanticAttributes.HTTP_ROUTE; +import static io.opentelemetry.semconv.SemanticAttributes.HTTP_STATUS_CODE; +import static io.opentelemetry.semconv.SemanticAttributes.HTTP_TARGET; +import static io.opentelemetry.semconv.SemanticAttributes.NET_HOST_PORT; +import static io.opentelemetry.semconv.SemanticAttributes.NET_SOCK_HOST_ADDR; +import static io.opentelemetry.semconv.SemanticAttributes.NET_SOCK_PEER_ADDR; +import static io.opentelemetry.semconv.SemanticAttributes.NET_SOCK_PEER_PORT; +import static io.opentelemetry.semconv.SemanticAttributes.RPC_GRPC_STATUS_CODE; +import static io.opentelemetry.semconv.SemanticAttributes.RPC_METHOD; +import static io.opentelemetry.semconv.SemanticAttributes.RPC_SERVICE; +import static io.opentelemetry.semconv.SemanticAttributes.RPC_SYSTEM; import static io.restassured.RestAssured.given; import static io.restassured.RestAssured.when; import static java.net.HttpURLConnection.HTTP_OK; diff --git a/integration-tests/opentelemetry-reactive/src/test/java/io/quarkus/it/opentelemetry/reactive/OpenTelemetryReactiveClientTest.java b/integration-tests/opentelemetry-reactive/src/test/java/io/quarkus/it/opentelemetry/reactive/OpenTelemetryReactiveClientTest.java index 9307ce24f602b5..d552573c744086 100644 --- a/integration-tests/opentelemetry-reactive/src/test/java/io/quarkus/it/opentelemetry/reactive/OpenTelemetryReactiveClientTest.java +++ b/integration-tests/opentelemetry-reactive/src/test/java/io/quarkus/it/opentelemetry/reactive/OpenTelemetryReactiveClientTest.java @@ -3,10 +3,10 @@ import static io.opentelemetry.api.trace.SpanKind.CLIENT; import static io.opentelemetry.api.trace.SpanKind.INTERNAL; import static io.opentelemetry.api.trace.SpanKind.SERVER; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HTTP_METHOD; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HTTP_ROUTE; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HTTP_STATUS_CODE; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HTTP_TARGET; +import static io.opentelemetry.semconv.SemanticAttributes.HTTP_METHOD; +import static io.opentelemetry.semconv.SemanticAttributes.HTTP_ROUTE; +import static io.opentelemetry.semconv.SemanticAttributes.HTTP_STATUS_CODE; +import static io.opentelemetry.semconv.SemanticAttributes.HTTP_TARGET; import static io.quarkus.it.opentelemetry.reactive.Utils.getSpanByKindAndParentId; import static io.quarkus.it.opentelemetry.reactive.Utils.getSpans; import static io.restassured.RestAssured.given; diff --git a/integration-tests/opentelemetry-reactive/src/test/java/io/quarkus/it/opentelemetry/reactive/OpenTelemetryReactiveTest.java b/integration-tests/opentelemetry-reactive/src/test/java/io/quarkus/it/opentelemetry/reactive/OpenTelemetryReactiveTest.java index 5b9ef8c7f98182..cdaba582dcb6ec 100644 --- a/integration-tests/opentelemetry-reactive/src/test/java/io/quarkus/it/opentelemetry/reactive/OpenTelemetryReactiveTest.java +++ b/integration-tests/opentelemetry-reactive/src/test/java/io/quarkus/it/opentelemetry/reactive/OpenTelemetryReactiveTest.java @@ -3,8 +3,8 @@ import static io.opentelemetry.api.trace.SpanKind.CLIENT; import static io.opentelemetry.api.trace.SpanKind.INTERNAL; import static io.opentelemetry.api.trace.SpanKind.SERVER; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HTTP_TARGET; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HTTP_URL; +import static io.opentelemetry.semconv.SemanticAttributes.HTTP_TARGET; +import static io.opentelemetry.semconv.SemanticAttributes.HTTP_URL; import static io.quarkus.it.opentelemetry.reactive.Utils.getExceptionEventData; import static io.quarkus.it.opentelemetry.reactive.Utils.getSpanByKindAndParentId; import static io.quarkus.it.opentelemetry.reactive.Utils.getSpans; diff --git a/integration-tests/opentelemetry-spi/pom.xml b/integration-tests/opentelemetry-spi/pom.xml index d3ec4d9140e2e1..dc77bd4e72af5c 100644 --- a/integration-tests/opentelemetry-spi/pom.xml +++ b/integration-tests/opentelemetry-spi/pom.xml @@ -15,7 +15,7 @@ Quarkus - Integration Tests - OpenTelemetry SPI - 1.30.0-alpha + 1.31.0-alpha diff --git a/integration-tests/opentelemetry-spi/src/main/java/io/quarkus/it/opentelemetry/spi/CustomSPISampler.java b/integration-tests/opentelemetry-spi/src/main/java/io/quarkus/it/opentelemetry/spi/CustomSPISampler.java index be7e75f98d4ba6..3d6f47cc55932d 100644 --- a/integration-tests/opentelemetry-spi/src/main/java/io/quarkus/it/opentelemetry/spi/CustomSPISampler.java +++ b/integration-tests/opentelemetry-spi/src/main/java/io/quarkus/it/opentelemetry/spi/CustomSPISampler.java @@ -8,7 +8,7 @@ import io.opentelemetry.sdk.trace.data.LinkData; import io.opentelemetry.sdk.trace.samplers.Sampler; import io.opentelemetry.sdk.trace.samplers.SamplingResult; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.SemanticAttributes; public class CustomSPISampler implements Sampler { @Override diff --git a/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/AbstractExporterTest.java b/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/AbstractExporterTest.java index 3fffe846f12fa0..0dc4e49f6413f5 100644 --- a/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/AbstractExporterTest.java +++ b/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/AbstractExporterTest.java @@ -16,7 +16,7 @@ import io.opentelemetry.proto.trace.v1.ResourceSpans; import io.opentelemetry.proto.trace.v1.ScopeSpans; import io.opentelemetry.proto.trace.v1.Span; -import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; +import io.opentelemetry.semconv.ResourceAttributes; public abstract class AbstractExporterTest { diff --git a/integration-tests/opentelemetry-vertx/src/test/java/io/quarkus/it/opentelemetry/vertx/HelloRouterTest.java b/integration-tests/opentelemetry-vertx/src/test/java/io/quarkus/it/opentelemetry/vertx/HelloRouterTest.java index 0bb1b5455a464d..7069126936f96e 100644 --- a/integration-tests/opentelemetry-vertx/src/test/java/io/quarkus/it/opentelemetry/vertx/HelloRouterTest.java +++ b/integration-tests/opentelemetry-vertx/src/test/java/io/quarkus/it/opentelemetry/vertx/HelloRouterTest.java @@ -3,14 +3,14 @@ import static io.opentelemetry.api.trace.SpanKind.CONSUMER; import static io.opentelemetry.api.trace.SpanKind.PRODUCER; import static io.opentelemetry.api.trace.SpanKind.SERVER; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HTTP_METHOD; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HTTP_ROUTE; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HTTP_STATUS_CODE; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.MESSAGING_DESTINATION_NAME; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.MESSAGING_OPERATION; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.MESSAGING_SYSTEM; +import static io.opentelemetry.semconv.SemanticAttributes.HTTP_METHOD; +import static io.opentelemetry.semconv.SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH; +import static io.opentelemetry.semconv.SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH; +import static io.opentelemetry.semconv.SemanticAttributes.HTTP_ROUTE; +import static io.opentelemetry.semconv.SemanticAttributes.HTTP_STATUS_CODE; +import static io.opentelemetry.semconv.SemanticAttributes.MESSAGING_DESTINATION_NAME; +import static io.opentelemetry.semconv.SemanticAttributes.MESSAGING_OPERATION; +import static io.opentelemetry.semconv.SemanticAttributes.MESSAGING_SYSTEM; import static io.restassured.RestAssured.get; import static io.restassured.RestAssured.given; import static java.net.HttpURLConnection.HTTP_OK; diff --git a/integration-tests/opentelemetry-vertx/src/test/java/io/quarkus/it/opentelemetry/vertx/SqlClientTest.java b/integration-tests/opentelemetry-vertx/src/test/java/io/quarkus/it/opentelemetry/vertx/SqlClientTest.java index ce5b827923b5a4..9a66684bcbbf01 100644 --- a/integration-tests/opentelemetry-vertx/src/test/java/io/quarkus/it/opentelemetry/vertx/SqlClientTest.java +++ b/integration-tests/opentelemetry-vertx/src/test/java/io/quarkus/it/opentelemetry/vertx/SqlClientTest.java @@ -1,11 +1,11 @@ package io.quarkus.it.opentelemetry.vertx; import static io.opentelemetry.api.trace.SpanKind.CLIENT; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.DB_CONNECTION_STRING; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.DB_OPERATION; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.DB_STATEMENT; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.DB_USER; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HTTP_STATUS_CODE; +import static io.opentelemetry.semconv.SemanticAttributes.DB_CONNECTION_STRING; +import static io.opentelemetry.semconv.SemanticAttributes.DB_OPERATION; +import static io.opentelemetry.semconv.SemanticAttributes.DB_STATEMENT; +import static io.opentelemetry.semconv.SemanticAttributes.DB_USER; +import static io.opentelemetry.semconv.SemanticAttributes.HTTP_STATUS_CODE; import static io.restassured.RestAssured.given; import static java.net.HttpURLConnection.HTTP_OK; import static org.awaitility.Awaitility.await; diff --git a/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/EndUserDisabledTest.java b/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/EndUserDisabledTest.java index 2fe83999e1c9ef..8fee9dcd3a0bac 100644 --- a/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/EndUserDisabledTest.java +++ b/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/EndUserDisabledTest.java @@ -5,7 +5,7 @@ import jakarta.enterprise.inject.Instance; import io.opentelemetry.api.common.Attributes; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.SemanticAttributes; import io.quarkus.test.junit.QuarkusTest; @QuarkusTest diff --git a/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/EndUserEnabledTest.java b/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/EndUserEnabledTest.java index 965b7ddd0c166a..dd3427fd0dd6b6 100644 --- a/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/EndUserEnabledTest.java +++ b/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/EndUserEnabledTest.java @@ -5,7 +5,7 @@ import jakarta.enterprise.inject.Instance; import io.opentelemetry.api.common.Attributes; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.SemanticAttributes; import io.quarkus.it.opentelemetry.util.EndUserProfile; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.junit.TestProfile; diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml index 3fdb4245dcf4b0..1b406e057b3d8a 100644 --- a/integration-tests/pom.xml +++ b/integration-tests/pom.xml @@ -285,7 +285,6 @@ amazon-lambda-http-resteasy amazon-lambda-http-resteasy-reactive container-image - kubernetes-client-hack-extension kubernetes kubernetes-client kubernetes-client-devservices @@ -318,6 +317,7 @@ smallrye-metrics smallrye-graphql smallrye-graphql-client + smallrye-graphql-client-keycloak smallrye-stork-registration jpa-without-entity quartz diff --git a/integration-tests/rest-client-reactive/pom.xml b/integration-tests/rest-client-reactive/pom.xml index 05fc6fb2cf4c59..e70e0e07b177d2 100644 --- a/integration-tests/rest-client-reactive/pom.xml +++ b/integration-tests/rest-client-reactive/pom.xml @@ -156,6 +156,9 @@ + + META-INF/ide-deps/** + application.properties diff --git a/integration-tests/smallrye-graphql-client-keycloak/pom.xml b/integration-tests/smallrye-graphql-client-keycloak/pom.xml new file mode 100644 index 00000000000000..b1481bb6df0bf1 --- /dev/null +++ b/integration-tests/smallrye-graphql-client-keycloak/pom.xml @@ -0,0 +1,260 @@ + + + + quarkus-integration-tests-parent + io.quarkus + 999-SNAPSHOT + + 4.0.0 + + quarkus-integration-test-smallrye-graphql-client-keycloak + Quarkus - Integration Tests - SmallRye GraphQL Client with Keycloak + + + http://localhost:8180/auth + + + + + io.quarkus + quarkus-smallrye-graphql + + + io.quarkus + quarkus-smallrye-graphql-client + + + io.quarkus + quarkus-resteasy-deployment + + + io.quarkus + quarkus-oidc + + + io.rest-assured + rest-assured + test + + + + io.quarkus + quarkus-rest-client-deployment + + + io.quarkus + quarkus-junit5 + test + + + + + io.quarkus + quarkus-smallrye-graphql-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-smallrye-graphql-client-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-test-keycloak-server + test + + + junit + junit + + + + + io.quarkus + quarkus-oidc-deployment + ${project.version} + pom + test + + + * + * + + + + + + + + + maven-surefire-plugin + + true + + + + maven-failsafe-plugin + + true + + + + io.quarkus + quarkus-maven-plugin + + + + build + + + + + + + + + + test-keycloak + + + test-containers + + + + + + maven-surefire-plugin + + false + + ${keycloak.url} + + + + + maven-failsafe-plugin + + false + + ${keycloak.url} + + + + + io.quarkus + quarkus-maven-plugin + + + + build + + + + + + + + + + docker-keycloak + + + start-containers + + + + http://localhost:8180/auth + + + + + io.fabric8 + docker-maven-plugin + + + + ${keycloak.docker.legacy.image} + quarkus-test-keycloak + + + 8180:8080 + + + admin + admin + + + Keycloak: + default + cyan + + + + + http://localhost:8180 + + + + + + + true + + + + docker-start + compile + + stop + start + + + + docker-stop + post-integration-test + + stop + + + + + + + + + + + diff --git a/integration-tests/smallrye-graphql-client-keycloak/src/main/java/io/quarkus/io/smallrye/graphql/keycloak/GraphQLAuthExpiryTester.java b/integration-tests/smallrye-graphql-client-keycloak/src/main/java/io/quarkus/io/smallrye/graphql/keycloak/GraphQLAuthExpiryTester.java new file mode 100644 index 00000000000000..9a8c49e6b28f45 --- /dev/null +++ b/integration-tests/smallrye-graphql-client-keycloak/src/main/java/io/quarkus/io/smallrye/graphql/keycloak/GraphQLAuthExpiryTester.java @@ -0,0 +1,62 @@ +package io.quarkus.io.smallrye.graphql.keycloak; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; + +import io.smallrye.common.annotation.Blocking; +import io.smallrye.graphql.client.dynamic.api.DynamicGraphQLClient; +import io.smallrye.graphql.client.dynamic.api.DynamicGraphQLClientBuilder; + +/** + * We can't perform these tests in the `@Test` methods directly, because the GraphQL client + * relies on CDI, and CDI is not available in native mode on the `@Test` side. + * Therefore the test only calls this REST endpoint which then performs all the client related work. + *
+ * This test establishes connections to the server, and ensures that if authentication has an expiry, that following the + * expiry of their access the connection is correctly terminated. + */ +@Path("/") +public class GraphQLAuthExpiryTester { + + @GET + @Path("/dynamic-subscription-auth-expiry/{token}/{url}") + @Blocking + public void dynamicSubscription(@PathParam("token") String token, @PathParam("url") String url) + throws Exception { + DynamicGraphQLClientBuilder clientBuilder = DynamicGraphQLClientBuilder.newBuilder() + .url(url + "/graphql") + .header("Authorization", "Bearer " + token) + .executeSingleOperationsOverWebsocket(true); + + try (DynamicGraphQLClient client = clientBuilder.build()) { + CompletableFuture authenticationExpired = new CompletableFuture<>(); + AtomicBoolean receivedValue = new AtomicBoolean(false); + client.subscription("subscription { sub { value } }").subscribe().with(item -> { + if (item.hasData()) { + receivedValue.set(true); + } else { + authenticationExpired.completeExceptionally(new RuntimeException("Subscription provided no data")); + } + }, cause -> { + if (cause.getMessage().contains("Authentication expired")) { + authenticationExpired.complete(null); + } else { + authenticationExpired + .completeExceptionally(new RuntimeException("Invalid close response from server.", cause)); + } + }, () -> authenticationExpired + .completeExceptionally(new RuntimeException("Subscription should not complete successfully"))); + + authenticationExpired.get(10, TimeUnit.SECONDS); + if (!receivedValue.get()) { + throw new RuntimeException("Did not receive subscription value"); + } + } + } + +} diff --git a/integration-tests/smallrye-graphql-client-keycloak/src/main/java/io/quarkus/io/smallrye/graphql/keycloak/SecuredResource.java b/integration-tests/smallrye-graphql-client-keycloak/src/main/java/io/quarkus/io/smallrye/graphql/keycloak/SecuredResource.java new file mode 100644 index 00000000000000..1957991834db27 --- /dev/null +++ b/integration-tests/smallrye-graphql-client-keycloak/src/main/java/io/quarkus/io/smallrye/graphql/keycloak/SecuredResource.java @@ -0,0 +1,42 @@ +package io.quarkus.io.smallrye.graphql.keycloak; + +import jakarta.annotation.security.RolesAllowed; + +import org.eclipse.microprofile.graphql.GraphQLApi; +import org.eclipse.microprofile.graphql.Query; + +import io.smallrye.common.annotation.NonBlocking; +import io.smallrye.graphql.api.Subscription; +import io.smallrye.mutiny.Multi; + +@GraphQLApi +public class SecuredResource { + + // Seems to be a requirement to have a query or mutation in a GraphQLApi. + // This is a workaround for the time being. + @Query + public TestResponse unusedQuery() { + return null; + } + + @Subscription + @RolesAllowed("user") + @NonBlocking + public Multi sub() { + return Multi.createFrom().emitter(emitter -> emitter.emit(new TestResponse("Hello World"))); + } + + public static class TestResponse { + + private final String value; + + public TestResponse(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + } + +} diff --git a/integration-tests/smallrye-graphql-client-keycloak/src/main/resources/application.properties b/integration-tests/smallrye-graphql-client-keycloak/src/main/resources/application.properties new file mode 100644 index 00000000000000..20c981d528c150 --- /dev/null +++ b/integration-tests/smallrye-graphql-client-keycloak/src/main/resources/application.properties @@ -0,0 +1,4 @@ +quarkus.oidc.client-id=quarkus-app +quarkus.oidc.credentials.secret=secret +quarkus.smallrye-graphql.log-payload=queryAndVariables +quarkus.keycloak.devservices.enabled=false diff --git a/integration-tests/smallrye-graphql-client-keycloak/src/test/java/io/quarkus/it/smallrye/graphql/keycloak/GraphQLAuthExpiryIT.java b/integration-tests/smallrye-graphql-client-keycloak/src/test/java/io/quarkus/it/smallrye/graphql/keycloak/GraphQLAuthExpiryIT.java new file mode 100644 index 00000000000000..758e4780144ae6 --- /dev/null +++ b/integration-tests/smallrye-graphql-client-keycloak/src/test/java/io/quarkus/it/smallrye/graphql/keycloak/GraphQLAuthExpiryIT.java @@ -0,0 +1,7 @@ +package io.quarkus.it.smallrye.graphql.keycloak; + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +public class GraphQLAuthExpiryIT extends GraphQLAuthExpiryTest { +} diff --git a/integration-tests/smallrye-graphql-client-keycloak/src/test/java/io/quarkus/it/smallrye/graphql/keycloak/GraphQLAuthExpiryTest.java b/integration-tests/smallrye-graphql-client-keycloak/src/test/java/io/quarkus/it/smallrye/graphql/keycloak/GraphQLAuthExpiryTest.java new file mode 100644 index 00000000000000..01338e9915215c --- /dev/null +++ b/integration-tests/smallrye-graphql-client-keycloak/src/test/java/io/quarkus/it/smallrye/graphql/keycloak/GraphQLAuthExpiryTest.java @@ -0,0 +1,33 @@ +package io.quarkus.it.smallrye.graphql.keycloak; + +import static io.restassured.RestAssured.when; + +import java.net.URL; + +import org.junit.jupiter.api.Test; + +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.common.http.TestHTTPResource; +import io.quarkus.test.junit.QuarkusTest; + +/** + * See `GraphQLClientTester` for the actual testing code that uses GraphQL clients. + */ +@QuarkusTest +@QuarkusTestResource(KeycloakRealmResourceManager.class) +public class GraphQLAuthExpiryTest { + + @TestHTTPResource + URL url; + + @Test + public void testDynamicClientWebSocketAuthenticationExpiry() { + String token = KeycloakRealmResourceManager.getAccessToken(); + when() + .get("/dynamic-subscription-auth-expiry/" + token + "/" + url.toString()) + .then() + .log().everything() + .statusCode(204); + } + +} diff --git a/integration-tests/smallrye-graphql-client-keycloak/src/test/java/io/quarkus/it/smallrye/graphql/keycloak/KeycloakRealmResourceManager.java b/integration-tests/smallrye-graphql-client-keycloak/src/test/java/io/quarkus/it/smallrye/graphql/keycloak/KeycloakRealmResourceManager.java new file mode 100644 index 00000000000000..a527c265f76af9 --- /dev/null +++ b/integration-tests/smallrye-graphql-client-keycloak/src/test/java/io/quarkus/it/smallrye/graphql/keycloak/KeycloakRealmResourceManager.java @@ -0,0 +1,146 @@ +package io.quarkus.it.smallrye.graphql.keycloak; + +import java.io.IOException; +import java.util.*; + +import org.keycloak.representations.AccessTokenResponse; +import org.keycloak.representations.idm.*; +import org.keycloak.util.JsonSerialization; +import org.testcontainers.containers.GenericContainer; + +import io.quarkus.test.common.QuarkusTestResourceLifecycleManager; +import io.restassured.RestAssured; +import io.restassured.response.Response; + +public class KeycloakRealmResourceManager implements QuarkusTestResourceLifecycleManager { + + private static final String KEYCLOAK_SERVER_URL = System.getProperty("keycloak.url", "http://localhost:8180/auth"); + //private static String KEYCLOAK_SERVER_URL; + private static final String KEYCLOAK_REALM = "quarkus"; + //private static final String KEYCLOAK_IMAGE = "quay.io/keycloak/keycloak:22.0.5"; + + private GenericContainer keycloak; + + @Override + public Map start() { + RealmRepresentation realm = createRealm(KEYCLOAK_REALM); + realm.setRevokeRefreshToken(true); + realm.setRefreshTokenMaxReuse(0); + realm.setAccessTokenLifespan(3); + + realm.getClients().add(createClient("quarkus-app")); + realm.getUsers().add(createUser("alice", "user")); + + try { + Response response = RestAssured + .given() + .auth().oauth2(getAdminAccessToken()) + .contentType("application/json") + .body(JsonSerialization.writeValueAsBytes(realm)) + .when() + .post(KEYCLOAK_SERVER_URL + "/admin/realms"); + response.then() + .statusCode(201); + + } catch (IOException e) { + throw new RuntimeException(e); + } + + Map properties = new HashMap<>(); + + properties.put("quarkus.oidc.auth-server-url", KEYCLOAK_SERVER_URL + "/realms/" + KEYCLOAK_REALM); + properties.put("keycloak.url", KEYCLOAK_SERVER_URL); + + return properties; + } + + private static String getAdminAccessToken() { + return RestAssured + .given() + .param("grant_type", "password") + .param("username", "admin") + .param("password", "admin") + .param("client_id", "admin-cli") + .when() + .post(KEYCLOAK_SERVER_URL + "/realms/master/protocol/openid-connect/token") + .as(AccessTokenResponse.class).getToken(); + } + + private static RealmRepresentation createRealm(String name) { + RealmRepresentation realm = new RealmRepresentation(); + + realm.setRealm(name); + realm.setEnabled(true); + realm.setUsers(new ArrayList<>()); + realm.setClients(new ArrayList<>()); + realm.setAccessTokenLifespan(3); + realm.setSsoSessionMaxLifespan(3); + RolesRepresentation roles = new RolesRepresentation(); + List realmRoles = new ArrayList<>(); + + roles.setRealm(realmRoles); + realm.setRoles(roles); + + realm.getRoles().getRealm().add(new RoleRepresentation("user", null, false)); + realm.getRoles().getRealm().add(new RoleRepresentation("admin", null, false)); + + return realm; + } + + private static ClientRepresentation createClient(String clientId) { + ClientRepresentation client = new ClientRepresentation(); + + client.setClientId(clientId); + client.setPublicClient(false); + client.setSecret("secret"); + client.setDirectAccessGrantsEnabled(true); + client.setServiceAccountsEnabled(true); + client.setRedirectUris(Arrays.asList("*")); + client.setEnabled(true); + client.setDefaultClientScopes(List.of("microprofile-jwt")); + + return client; + } + + private static UserRepresentation createUser(String username, String... realmRoles) { + UserRepresentation user = new UserRepresentation(); + + user.setUsername(username); + user.setEnabled(true); + user.setCredentials(new ArrayList<>()); + user.setRealmRoles(Arrays.asList(realmRoles)); + user.setEmail(username + "@gmail.com"); + + CredentialRepresentation credential = new CredentialRepresentation(); + + credential.setType(CredentialRepresentation.PASSWORD); + credential.setValue(username); + credential.setTemporary(false); + + user.getCredentials().add(credential); + + return user; + } + + @Override + public void stop() { + RestAssured + .given() + .auth().oauth2(getAdminAccessToken()) + .when() + .delete(KEYCLOAK_SERVER_URL + "/admin/realms/" + KEYCLOAK_REALM).then().statusCode(204); + } + + public static String getAccessToken() { + io.restassured.response.Response response = RestAssured.given() + .contentType("application/x-www-form-urlencoded") + .accept("application/json") + .formParam("username", "alice") + .formParam("password", "alice") + .param("client_id", "quarkus-app") + .param("client_secret", "secret") + .formParam("grant_type", "password") + .post(KEYCLOAK_SERVER_URL + "/realms/" + KEYCLOAK_REALM + "/protocol/openid-connect/token"); + return response.getBody().jsonPath().getString("access_token"); + } +} diff --git a/integration-tests/test-extension/tests/src/test/resources-filtered/projects/project-using-test-template-from-extension/pom.xml b/integration-tests/test-extension/tests/src/test/resources-filtered/projects/project-using-test-template-from-extension/pom.xml index 2750c013fadbf7..eef420dac85897 100644 --- a/integration-tests/test-extension/tests/src/test/resources-filtered/projects/project-using-test-template-from-extension/pom.xml +++ b/integration-tests/test-extension/tests/src/test/resources-filtered/projects/project-using-test-template-from-extension/pom.xml @@ -46,7 +46,7 @@ io.quarkus integration-test-extension-that-defines-junit-test-extensions ${quarkus.version} - test + test
@@ -109,7 +109,7 @@ - ${project.build.directory}/${project.build.finalName}-runner + \${project.build.directory}/\${project.build.finalName}-runner org.jboss.logmanager.LogManager \${maven.home} diff --git a/pom.xml b/pom.xml index 6fc1bffd9723e9..fc0f5a8d1bd126 100644 --- a/pom.xml +++ b/pom.xml @@ -60,7 +60,7 @@ jdbc:postgresql:hibernate_orm_test 4.5.1 - 0.0.97 + 0.0.99 false false diff --git a/tcks/microprofile-opentelemetry/pom.xml b/tcks/microprofile-opentelemetry/pom.xml index 085f512f14d5e9..bdcab791b5eb64 100644 --- a/tcks/microprofile-opentelemetry/pom.xml +++ b/tcks/microprofile-opentelemetry/pom.xml @@ -13,7 +13,7 @@ Quarkus - TCK - MicroProfile OpenTelemetry - 1.0 + 1.1