diff --git a/.github/develocity-preapproved-developers.json b/.github/develocity-preapproved-developers.json new file mode 100644 index 00000000000000..670c4d84f00ac1 --- /dev/null +++ b/.github/develocity-preapproved-developers.json @@ -0,0 +1,56 @@ +{ + "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/quarkus-github-bot.yml b/.github/quarkus-github-bot.yml index 3507f5cfbb5349..3e580c63ce5843 100644 --- a/.github/quarkus-github-bot.yml +++ b/.github/quarkus-github-bot.yml @@ -407,13 +407,13 @@ triage: - extensions/google-cloud-functions - integration-tests/google-cloud-functions - id: mandrel - labels: [area/mandrel] + labels: [area/native-image] titleBody: "mandrel" notify: [galderz, zakkak, Karm] - id: native-image labels: [area/native-image] title: "\\bnative\\b" - notify: [] + notify: [zakkak] - id: awt labels: [area/graphics] expression: | @@ -421,7 +421,7 @@ triage: || matches("sun.java2d", titleBody) || matches("javax.imageio", titleBody) || matches("sun.awt", titleBody) - notify: [galderz, zakkak, Karm] + notify: [galderz, Karm] notifyInPullRequest: true directories: - extensions/awt/ diff --git a/.github/workflows/ci-actions-incremental.yml b/.github/workflows/ci-actions-incremental.yml index 85ae65268d611a..33e0b6c4b65c1c 100644 --- a/.github/workflows/ci-actions-incremental.yml +++ b/.github/workflows/ci-actions-incremental.yml @@ -43,7 +43,7 @@ env: # Workaround testsuite locale issue LANG: en_US.UTF-8 COMMON_MAVEN_ARGS: "-e -B --settings .github/mvn-settings.xml --fail-at-end" - NATIVE_TEST_MAVEN_ARGS: "-Dtest-containers -Dstart-containers -Dquarkus.native.native-image-xmx=5g -Dnative -Dnative.surefire.skip -Dformat.skip -Dno-descriptor-tests clean install -DskipDocs" + NATIVE_TEST_MAVEN_ARGS: "-Dtest-containers -Dstart-containers -Dquarkus.native.native-image-xmx=6g -Dnative -Dnative.surefire.skip -Dformat.skip -Dno-descriptor-tests clean install -DskipDocs" JVM_TEST_MAVEN_ARGS: "-Dtest-containers -Dstart-containers -Dformat.skip -DskipDocs -Dquarkus.test.hang-detection-timeout=60" DB_USER: hibernate_orm_test DB_PASSWORD: hibernate_orm_test @@ -199,6 +199,11 @@ jobs: target/gradle-build-scan-url.txt LICENSE.txt retention-days: 2 + - name: Save Build Scan + if: always() + uses: gradle/github-actions/maven-build-scan/save@v1-beta + with: + job-name: "Initial JDK 11 Build" calculate-test-jobs: name: Calculate Test Jobs @@ -388,6 +393,17 @@ jobs: path: | **/build.log retention-days: 2 + - name: Save Build Scan + if: always() + uses: gradle/github-actions/maven-build-scan/save@v1-beta + with: + job-name: "JVM Tests - JDK ${{matrix.java.name}}" + - name: Upload quarkus-ide-launcher jar + uses: actions/upload-artifact@v3 + with: + name: "quarkus-ide-launcher-999-SNAPSHOT.jar - JDK ${{matrix.java.name}}" + path: | + core/launcher/target/quarkus-ide-launcher-999-SNAPSHOT.jar maven-tests: name: Maven Tests - JDK ${{matrix.java.name}} @@ -445,7 +461,6 @@ jobs: distribution: temurin java-version: ${{ matrix.java.java-version }} - name: Build - # Important: keep -pl ... in sync with "Calculate run flags"! # Despite the pre-calculated run_jvm flag, GIB has to be re-run here to figure out the exact submodules to build. run: ./mvnw $COMMON_MAVEN_ARGS $JVM_TEST_MAVEN_ARGS clean install -pl 'integration-tests/maven' -pl 'integration-tests/devmode' ${{ needs.build-jdk11.outputs.gib_args }} @@ -477,6 +492,11 @@ jobs: target/gradle-build-scan-url.txt LICENSE.txt retention-days: 2 + - name: Save Build Scan + if: always() + uses: gradle/github-actions/maven-build-scan/save@v1-beta + with: + job-name: "Maven Tests - JDK ${{matrix.java.name}}" gradle-tests: name: Gradle Tests - JDK ${{matrix.java.name}} @@ -549,6 +569,11 @@ jobs: target/gradle-build-scan-url.txt LICENSE.txt retention-days: 2 + - name: Save Build Scan + if: always() + uses: gradle/github-actions/maven-build-scan/save@v1-beta + with: + job-name: "Gradle Tests - JDK ${{matrix.java.name}}" devtools-tests: name: Devtools Tests - JDK ${{matrix.java.name}} @@ -627,6 +652,11 @@ jobs: target/gradle-build-scan-url.txt LICENSE.txt retention-days: 2 + - name: Save Build Scan + if: always() + uses: gradle/github-actions/maven-build-scan/save@v1-beta + with: + job-name: "Devtools Tests - JDK ${{matrix.java.name}}" kubernetes-tests: name: Kubernetes Tests - JDK ${{matrix.java.name}} @@ -655,6 +685,10 @@ jobs: os-name: "windows-latest" } steps: + - name: Gradle Enterprise environment + run: | + echo "GE_TAGS=jdk-${{matrix.java.name}}" >> "$GITHUB_ENV" + echo "GE_CUSTOM_VALUES=gh-job-name=Kubernetes Tests - JDK ${{matrix.java.name}}" >> "$GITHUB_ENV" - name: Support longpaths on Windows if: "startsWith(matrix.java.os-name, 'windows')" run: git config --global core.longpaths true @@ -700,6 +734,11 @@ jobs: target/build-report.json LICENSE.txt retention-days: 2 + - name: Save Build Scan + if: always() + uses: gradle/github-actions/maven-build-scan/save@v1-beta + with: + job-name: "Kubernetes Tests - JDK ${{matrix.java.name}}" quickstarts-tests: name: Quickstarts Compilation - JDK ${{matrix.java.name}} @@ -756,6 +795,12 @@ jobs: quarkus-quickstarts/target/build-report.json quarkus-quickstarts/LICENSE retention-days: 2 + - name: Save Build Scan + if: always() + uses: gradle/github-actions/maven-build-scan/save@v1-beta + with: + job-name: "Quickstarts Compilation - JDK ${{matrix.java.name}}" + virtual-thread-native-tests: name: Native Tests - Virtual Thread - ${{matrix.category}} runs-on: ${{matrix.os-name}} @@ -812,6 +857,12 @@ jobs: integration-tests/virtual-threads/target/build-report.json integration-tests/virtual-threads/target/gradle-build-scan-url.txt retention-days: 2 + - name: Save Build Scan + if: always() + uses: gradle/github-actions/maven-build-scan/save@v1-beta + with: + job-name: "Native Tests - Virtual Thread - ${{matrix.category}}" + tcks-test: name: MicroProfile TCKs Tests needs: [build-jdk11, calculate-test-jobs] @@ -819,7 +870,6 @@ jobs: if: "needs.calculate-test-jobs.outputs.run_tcks == 'true' && (github.repository == 'quarkusio/quarkus' || !endsWith(github.ref, '/main'))" runs-on: ubuntu-latest timeout-minutes: 150 - steps: - name: Gradle Enterprise environment run: | @@ -879,6 +929,11 @@ jobs: target/gradle-build-scan-url.txt LICENSE.txt retention-days: 2 + - name: Save Build Scan + if: always() + uses: gradle/github-actions/maven-build-scan/save@v1-beta + with: + job-name: "MicroProfile TCKs Tests" native-tests: name: Native Tests - ${{matrix.category}} @@ -971,6 +1026,11 @@ jobs: target/gradle-build-scan-url.txt LICENSE.txt retention-days: 2 + - name: Save Build Scan + if: always() + uses: gradle/github-actions/maven-build-scan/save@v1-beta + with: + job-name: "Native Tests - ${{matrix.category}}" build-report: runs-on: ubuntu-latest diff --git a/.github/workflows/develocity-publish-build-scans.yml b/.github/workflows/develocity-publish-build-scans.yml new file mode 100644 index 00000000000000..a285e66a8445be --- /dev/null +++ b/.github/workflows/develocity-publish-build-scans.yml @@ -0,0 +1,43 @@ +name: Develocity - Publish Maven Build Scans + +on: + workflow_run: + workflows: [ "Quarkus CI" ] + types: [ completed ] + +defaults: + run: + shell: bash + +jobs: + publish-build-scans: + if: github.repository == 'quarkusio/quarkus' && github.event.workflow_run.event == 'pull_request' + runs-on: ubuntu-latest + permissions: + pull-requests: 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) }} + run: | + echo -n "Pull request: " >> ${GITHUB_STEP_SUMMARY} + cat pr-number.out >> ${GITHUB_STEP_SUMMARY} + echo >> ${GITHUB_STEP_SUMMARY} + echo >> ${GITHUB_STEP_SUMMARY} + echo "| Job | Status | Build scan |" >> ${GITHUB_STEP_SUMMARY} + echo "|---|---|---|" >> ${GITHUB_STEP_SUMMARY} + cat publication.out >> ${GITHUB_STEP_SUMMARY} diff --git a/bom/application/pom.xml b/bom/application/pom.xml index f6aebc9e5a2e4b..d7b812e82f0e2c 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -23,7 +23,7 @@ 3.1.5 1.3.2 1 - 1.1.4 + 1.1.5 2.1.4.Final 3.0.2.Final 6.2.6.Final @@ -51,7 +51,7 @@ 2.0 3.1.1 2.2.0 - 3.4.1 + 3.4.4 4.0.4 4.0.0 3.7.0 @@ -92,9 +92,9 @@ 2.1.0 23.0.1 1.7.0 - 2.15.2 + 2.15.3 1.0.0.Final - 3.12.0 + 3.13.0 1.16.0 1.5.1 42.6.0 3.2.0 - 8.0.30 + 8.0.33 12.4.0.jre11 1.6.7 23.3.0.23.09 @@ -140,7 +140,7 @@ 2.2 5.10.0 1.5.0 - 14.0.19.Final + 14.0.20.Final 4.6.5.Final 3.1.5 4.1.100.Final @@ -163,7 +163,7 @@ 1.7.3 0.27.0 1.6.0 - 4.0.0 + 4.0.3 3.2.0 4.2.0 3.0.2.Final @@ -3291,7 +3291,7 @@ importmap ${importmap.version} - + biz.paluch.logging @@ -6343,6 +6343,12 @@ test junit:junit + + + + + + org.jboss:jdk-misc diff --git a/build-parent/pom.xml b/build-parent/pom.xml index 075fab33030356..0f6bdebd71f877 100644 --- a/build-parent/pom.xml +++ b/build-parent/pom.xml @@ -187,7 +187,7 @@ 1.7.0 5.4.3 2.1.0 - 1.8.0 + 1.8.1 2.4.0 diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildStep.java index c4a367b89fceeb..77ad5a2dc78ebd 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildStep.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildStep.java @@ -911,8 +911,10 @@ public NativeImageInvokerInfo build() { if (nativeConfig.autoServiceLoaderRegistration()) { addExperimentalVMOption(nativeImageArgs, "-H:+UseServiceLoaderFeature"); - //When enabling, at least print what exactly is being added: - nativeImageArgs.add("-H:+TraceServiceLoaderFeature"); + if (graalVMVersion.compareTo(GraalVM.Version.VERSION_23_1_0) < 0) { + // When enabling, at least print what exactly is being added. Only possible in <23.1.0 + nativeImageArgs.add("-H:+TraceServiceLoaderFeature"); + } } else { addExperimentalVMOption(nativeImageArgs, "-H:-UseServiceLoaderFeature"); } diff --git a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/AbstractQuarkusExtension.java b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/AbstractQuarkusExtension.java index 8d59abd2a90ef9..f2696a9266f233 100644 --- a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/AbstractQuarkusExtension.java +++ b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/AbstractQuarkusExtension.java @@ -130,6 +130,37 @@ private EffectiveConfig buildEffectiveConfiguration(Map properti .build(); } + /** + * Filters resolved Gradle configuration for properties in the Quarkus namespace + * (as in start with quarkus.). This avoids exposing configuration that may contain secrets or + * passwords not related to Quarkus (for instance environment variables storing sensitive data for other systems). + * + * @param appArtifact the application dependency to retrive the quarkus application name and version. + * @return a filtered view of the configuration only with quarkus. names. + */ + protected Map buildSystemProperties(ResolvedDependency appArtifact) { + Map buildSystemProperties = new HashMap<>(); + buildSystemProperties.putIfAbsent("quarkus.application.name", appArtifact.getArtifactId()); + buildSystemProperties.putIfAbsent("quarkus.application.version", appArtifact.getVersion()); + + for (Map.Entry entry : forcedPropertiesProperty.get().entrySet()) { + if (entry.getKey().startsWith("quarkus.")) { + buildSystemProperties.put(entry.getKey(), entry.getValue()); + } + } + for (Map.Entry entry : quarkusBuildProperties.get().entrySet()) { + if (entry.getKey().startsWith("quarkus.")) { + buildSystemProperties.put(entry.getKey(), entry.getValue()); + } + } + for (Map.Entry entry : project.getProperties().entrySet()) { + if (entry.getKey().startsWith("quarkus.") && entry.getValue() != null) { + buildSystemProperties.put(entry.getKey(), entry.getValue().toString()); + } + } + return buildSystemProperties; + } + private String quarkusProfile() { String profile = System.getProperty(QUARKUS_PROFILE); if (profile == null) { diff --git a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusBuildTask.java b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusBuildTask.java index 106bca49411305..13c97cfd722a18 100644 --- a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusBuildTask.java +++ b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusBuildTask.java @@ -4,6 +4,7 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.HashMap; import java.util.Map; import java.util.stream.Collectors; @@ -205,14 +206,20 @@ void generateBuild() { }); ApplicationModel appModel = resolveAppModelForBuild(); - Map configMap = extension().buildEffectiveConfiguration(appModel.getAppArtifact()).configMap(); + Map configMap = new HashMap<>(); + for (Map.Entry entry : extension().buildEffectiveConfiguration(appModel.getAppArtifact()).configMap() + .entrySet()) { + if (entry.getKey().startsWith("quarkus.")) { + configMap.put(entry.getKey(), entry.getValue()); + } + } getLogger().info("Starting Quarkus application build for package type {}", packageType); if (getLogger().isEnabled(LogLevel.INFO)) { getLogger().info("Effective properties: {}", configMap.entrySet().stream() - .filter(e -> e.getKey().startsWith("quarkus.")).map(Object::toString) + .map(Object::toString) .sorted() .collect(Collectors.joining("\n ", "\n ", ""))); } @@ -220,7 +227,7 @@ void generateBuild() { WorkQueue workQueue = workQueue(configMap, () -> extension().buildForkOptions); workQueue.submit(BuildWorker.class, params -> { - params.getBuildSystemProperties().putAll(configMap); + params.getBuildSystemProperties().putAll(extension().buildSystemProperties(appModel.getAppArtifact())); params.getBaseName().set(extension().finalName()); params.getTargetDirectory().set(buildDir.toFile()); params.getAppModel().set(appModel); diff --git a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusTask.java b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusTask.java index 09d16509a05e52..3294472909fb14 100644 --- a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusTask.java +++ b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusTask.java @@ -20,8 +20,7 @@ import io.quarkus.utilities.OS; public abstract class QuarkusTask extends DefaultTask { - private static final List WORKER_BUILD_FORK_OPTIONS = List.of("quarkus.package.", - "quarkus.application.", "quarkus.gradle-worker.", "quarkus.analytics."); + private static final List WORKER_BUILD_FORK_OPTIONS = List.of("quarkus."); private final transient QuarkusPluginExtension extension; protected final File projectDir; @@ -89,7 +88,7 @@ private void configureProcessWorkerSpec(ProcessWorkerSpec processWorkerSpec, Map } // It's kind of a "very big hammer" here, but this way we ensure that all necessary properties - // ("quarkus.package.*","quarkus.application,*", "quarkus.gradle-worker.*") from all configuration sources + // "quarkus.*" from all configuration sources // are (forcefully) used in the Quarkus build - even properties defined on the QuarkusPluginExtension. // This prevents that settings from e.g. a application.properties takes precedence over an explicit // setting in Gradle project properties, the Quarkus extension or even via the environment or system diff --git a/devtools/gradle/gradle/libs.versions.toml b/devtools/gradle/gradle/libs.versions.toml index e07d048c4e6754..9902875d78e44e 100644 --- a/devtools/gradle/gradle/libs.versions.toml +++ b/devtools/gradle/gradle/libs.versions.toml @@ -3,7 +3,7 @@ plugin-publish = "1.2.1" # updating Kotlin here makes QuarkusPluginTest > shouldNotFailOnProjectDependenciesWithoutMain(Path) fail kotlin = "1.9.20" -smallrye-config = "3.4.1" +smallrye-config = "3.4.4" junit5 = "5.10.1" assertj = "3.24.2" diff --git a/docs/src/main/asciidoc/amqp-dev-services.adoc b/docs/src/main/asciidoc/amqp-dev-services.adoc index 2c1bb488b362e1..f68fb219e3abce 100644 --- a/docs/src/main/asciidoc/amqp-dev-services.adoc +++ b/docs/src/main/asciidoc/amqp-dev-services.adoc @@ -8,7 +8,7 @@ include::_attributes.adoc[] :categories: messaging :summary: Start AMQP automatically in dev and test modes. :extensions: io.quarkus:quarkus-smallrye-reactive-messaging-amqp -:topics: messaging,amqp,devservices,tooling,testing,devmode +:topics: messaging,amqp,dev-services,testing,dev-mode Dev Services for AMQP automatically starts an AMQP 1.0 broker in dev mode and when running tests. So, you don't have to start a broker manually. diff --git a/docs/src/main/asciidoc/apicurio-registry-dev-services.adoc b/docs/src/main/asciidoc/apicurio-registry-dev-services.adoc index 0eb728d1d844a5..fc4f80b948176f 100644 --- a/docs/src/main/asciidoc/apicurio-registry-dev-services.adoc +++ b/docs/src/main/asciidoc/apicurio-registry-dev-services.adoc @@ -7,7 +7,7 @@ https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc include::_attributes.adoc[] :categories: messaging :summary: Start Apicurio Registry automatically in dev and test modes. -:topics: messaging,kafka,apicurio,registry,devservices,tooling,testing,devmode +:topics: messaging,kafka,apicurio,registry,dev-services,dev-mode,testing :extensions: io.quarkus:quarkus-apicurio-registry-avro,io.quarkus:quarkus-smallrye-reactive-messaging-kafka If an extension for schema registry, such as `quarkus-apicurio-registry-avro` or `quarkus-confluent-registry-avro`, is present, Dev Services for Apicurio Registry automatically starts an Apicurio Registry instance in dev mode and when running tests. diff --git a/docs/src/main/asciidoc/appcds.adoc b/docs/src/main/asciidoc/appcds.adoc index 543d7da0c8a7d4..59a82eaa0e8c5c 100644 --- a/docs/src/main/asciidoc/appcds.adoc +++ b/docs/src/main/asciidoc/appcds.adoc @@ -8,7 +8,6 @@ include::_attributes.adoc[] :categories: core, cloud :summary: This reference guide explains how to enable AppCDS with Quarkus. :topics: appcds,serverless -:extensions: io.quarkus:quarkus-core This reference guide explains how to enable Application Class Data Sharing in your Quarkus applications. diff --git a/docs/src/main/asciidoc/blaze-persistence.adoc b/docs/src/main/asciidoc/blaze-persistence.adoc index 4defc700a386be..e2ad4aca60fb12 100644 --- a/docs/src/main/asciidoc/blaze-persistence.adoc +++ b/docs/src/main/asciidoc/blaze-persistence.adoc @@ -33,10 +33,13 @@ In Quarkus, you just need to: Add the following dependencies to your project: -* the Blaze-Persistence extension: `com.blazebit:blaze-persistence-integration-quarkus` +* the Blaze-Persistence extension: `com.blazebit:blaze-persistence-integration-quarkus-3` * further Blaze-Persistence integrations as needed: - - `blaze-persistence-integration-jackson` for link:https://persistence.blazebit.com/documentation/entity-view/manual/en_US/index.html#Jackson%20integration[Jackson] + - `blaze-persistence-integration-jackson-jakarta` for link:https://persistence.blazebit.com/documentation/entity-view/manual/en_US/index.html#Jackson%20integration[Jackson] + - `blaze-persistence-integration-jsonb-jakarta` for link:https://persistence.blazebit.com/documentation/1.6/entity-view/manual/en_US/#jsonb-integration[JSONB] - `blaze-persistence-integration-jaxrs` for link:https://persistence.blazebit.com/documentation/entity-view/manual/en_US/index.html#jaxrs-integration[Jakarta REST] + - `blaze-persistence-integration-jaxrs-jackson-jakarta` for link:https://persistence.blazebit.com/documentation/entity-view/manual/en_US/index.html#jaxrs-integration[Jakarta REST with Jackson] + - `blaze-persistence-integration-jaxrs-jsonb-jakarta` for link:https://persistence.blazebit.com/documentation/entity-view/manual/en_US/index.html#jaxrs-integration[Jakarta REST with JSONB] [source,xml,role="primary asciidoc-tabs-target-sync-cli asciidoc-tabs-target-sync-maven"] .Example dependencies using Maven @@ -44,11 +47,11 @@ Add the following dependencies to your project: com.blazebit - blaze-persistence-integration-quarkus + blaze-persistence-integration-quarkus-3 com.blazebit - blaze-persistence-integration-hibernate-5.6 + blaze-persistence-integration-hibernate-6.2 runtime ---- @@ -56,8 +59,8 @@ Add the following dependencies to your project: [source,gradle,role="secondary asciidoc-tabs-target-sync-gradle"] .Using Gradle ---- -implementation("com.blazebit:blaze-persistence-integration-quarkus") -runtimeOnly("com.blazebit:blaze-persistence-integration-hibernate-5.6") +implementation("com.blazebit:blaze-persistence-integration-quarkus-3") +runtimeOnly("com.blazebit:blaze-persistence-integration-hibernate-6.2") ---- The use in native images requires a dependency on the entity view annotation processor that may be extracted into a separate `native` profile: @@ -70,7 +73,7 @@ The use in native images requires a dependency on the entity view annotation pro com.blazebit - blaze-persistence-entity-view-processor + blaze-persistence-entity-view-processor-jakarta provided diff --git a/docs/src/main/asciidoc/build-analytics.adoc b/docs/src/main/asciidoc/build-analytics.adoc index 820c2d34f868b7..ff94a46c57488e 100644 --- a/docs/src/main/asciidoc/build-analytics.adoc +++ b/docs/src/main/asciidoc/build-analytics.adoc @@ -6,7 +6,6 @@ https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc = Build analytics :categories: analytics :summary: This guide presents what build analytics is and how to configure it. -:extensions: io.quarkus:quarkus-core The Quarkus team has limited knowledge, from Maven download numbers, of the remarkable growth of Quarkus and the number of users reporting issues/concerns. Still, we need more insight into the platforms, operating system, Java combinations, and build tools our users employ. The build analytics tool aims to provide us with this information. diff --git a/docs/src/main/asciidoc/building-native-image.adoc b/docs/src/main/asciidoc/building-native-image.adoc index 4353b5fbff90ea..96e4e1640cdb01 100644 --- a/docs/src/main/asciidoc/building-native-image.adoc +++ b/docs/src/main/asciidoc/building-native-image.adoc @@ -8,7 +8,6 @@ include::_attributes.adoc[] :categories: getting-started, native :summary: Build native executables with GraalVM or Mandrel. :topics: native,graalvm,mandrel -:extensions: io.quarkus:quarkus-core This guide covers: diff --git a/docs/src/main/asciidoc/capabilities.adoc b/docs/src/main/asciidoc/capabilities.adoc index 6ef79dbf6a48fe..149fe8e36bfe77 100644 --- a/docs/src/main/asciidoc/capabilities.adoc +++ b/docs/src/main/asciidoc/capabilities.adoc @@ -8,7 +8,6 @@ include::_attributes.adoc[] :categories: writing-extensions :summary: How capabilities are implemented and used in Quarkus. :topics: extensions -:extensions: io.quarkus:quarkus-core Quarkus extensions may provide certain capabilities and require certain capabilities to be provided by other extensions in an application to function properly. diff --git a/docs/src/main/asciidoc/class-loading-reference.adoc b/docs/src/main/asciidoc/class-loading-reference.adoc index 65cd0c98e58289..6b740d078c24c0 100644 --- a/docs/src/main/asciidoc/class-loading-reference.adoc +++ b/docs/src/main/asciidoc/class-loading-reference.adoc @@ -8,7 +8,6 @@ include::_attributes.adoc[] :categories: architecture :summary: Learn more about Quarkus class loading infrastructure. :topics: internals,extensions -:extensions: io.quarkus:quarkus-core This document explains the Quarkus class loading architecture. It is intended for extension authors and advanced users who want to understand exactly how Quarkus works. diff --git a/docs/src/main/asciidoc/command-mode-reference.adoc b/docs/src/main/asciidoc/command-mode-reference.adoc index 7fc9b2a7ecdd8e..c4aeb11dbdaa9c 100644 --- a/docs/src/main/asciidoc/command-mode-reference.adoc +++ b/docs/src/main/asciidoc/command-mode-reference.adoc @@ -8,7 +8,6 @@ include::_attributes.adoc[] :categories: core, command-line :summary: This reference guide explains how to develop command line applications with Quarkus. :topics: command-line,cli -:extensions: io.quarkus:quarkus-core This reference covers how to write applications that run and then exit. diff --git a/docs/src/main/asciidoc/conditional-extension-dependencies.adoc b/docs/src/main/asciidoc/conditional-extension-dependencies.adoc index 1aec001d92ad96..576f4424f484e5 100644 --- a/docs/src/main/asciidoc/conditional-extension-dependencies.adoc +++ b/docs/src/main/asciidoc/conditional-extension-dependencies.adoc @@ -8,7 +8,6 @@ include::_attributes.adoc[] :categories: writing-extensions :summary: Trigger the inclusion on additional extensions based on certain conditions. :topics: extensions -:extensions: io.quarkus:quarkus-core Quarkus extension dependencies are usually configured in the same way as any other project dependencies in the project's build file, e.g. the Maven `pom.xml` or the Gradle build scripts. However, there are dependency types that aren't yet supported out-of-the-box by Maven and Gradle. What we refer here to as "conditional dependencies" is one example. diff --git a/docs/src/main/asciidoc/config-extending-support.adoc b/docs/src/main/asciidoc/config-extending-support.adoc index aca5e13344863e..0a4b215fabbfbb 100644 --- a/docs/src/main/asciidoc/config-extending-support.adoc +++ b/docs/src/main/asciidoc/config-extending-support.adoc @@ -11,7 +11,6 @@ include::_attributes.adoc[] :sectnums: :sectnumlevels: 4 :topics: configuration -:extensions: io.quarkus:quarkus-core [[custom-config-source]] == Custom `ConfigSource` diff --git a/docs/src/main/asciidoc/config-mappings.adoc b/docs/src/main/asciidoc/config-mappings.adoc index 89d8265828c006..61cb45b4839234 100644 --- a/docs/src/main/asciidoc/config-mappings.adoc +++ b/docs/src/main/asciidoc/config-mappings.adoc @@ -11,7 +11,6 @@ include::_attributes.adoc[] :sectnums: :sectnumlevels: 4 :topics: configuration -:extensions: io.quarkus:quarkus-core With config mappings it is possible to group multiple configuration properties in a single interface that share the same prefix. diff --git a/docs/src/main/asciidoc/config-reference.adoc b/docs/src/main/asciidoc/config-reference.adoc index 3703e48677817a..e3c22718953a04 100644 --- a/docs/src/main/asciidoc/config-reference.adoc +++ b/docs/src/main/asciidoc/config-reference.adoc @@ -11,7 +11,6 @@ include::_attributes.adoc[] :sectnums: :sectnumlevels: 4 :topics: configuration -:extensions: io.quarkus:quarkus-core IMPORTANT: The content of this guide has been revised and split into additional topics. Please check the <> section. diff --git a/docs/src/main/asciidoc/config.adoc b/docs/src/main/asciidoc/config.adoc index 4ef6b3cd9b2624..1ae0e1a14e7257 100644 --- a/docs/src/main/asciidoc/config.adoc +++ b/docs/src/main/asciidoc/config.adoc @@ -8,7 +8,6 @@ include::_attributes.adoc[] :categories: core :summary: Hardcoded values in your code is a no go (even if we all did it at some point ;-)). In this guide, we learn how to configure your application. :topics: configuration -:extensions: io.quarkus:quarkus-core IMPORTANT: The content of this guide and been revised and split into additional topics. Please check the <> section. diff --git a/docs/src/main/asciidoc/context-propagation.adoc b/docs/src/main/asciidoc/context-propagation.adoc index deffd04652f2f3..89955b1eddc97f 100644 --- a/docs/src/main/asciidoc/context-propagation.adoc +++ b/docs/src/main/asciidoc/context-propagation.adoc @@ -8,7 +8,6 @@ include::_attributes.adoc[] :categories: core :summary: Learn more about how you can pass contextual information with SmallRye Context Propagation. :topics: context-propagation -:extensions: io.quarkus:quarkus-core Traditional blocking code uses link:https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/ThreadLocal.html[`ThreadLocal`] variables to store contextual objects in order to avoid diff --git a/docs/src/main/asciidoc/continuous-testing.adoc b/docs/src/main/asciidoc/continuous-testing.adoc index 6be909bc953875..ef74b9c900785a 100644 --- a/docs/src/main/asciidoc/continuous-testing.adoc +++ b/docs/src/main/asciidoc/continuous-testing.adoc @@ -10,8 +10,7 @@ include::_attributes.adoc[] :numbered: :sectnums: :sectnumlevels: 4 -:topics: testing,dev-ui,tooling,devmode -:extensions: io.quarkus:quarkus-core +:topics: testing,dev-ui,tooling,dev-mode Learn how to use continuous testing in your Quarkus Application. diff --git a/docs/src/main/asciidoc/databases-dev-services.adoc b/docs/src/main/asciidoc/databases-dev-services.adoc index f30efd79d9f482..2590c8751cac8c 100644 --- a/docs/src/main/asciidoc/databases-dev-services.adoc +++ b/docs/src/main/asciidoc/databases-dev-services.adoc @@ -6,7 +6,7 @@ https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc = Dev Services for Databases :categories: data, tooling include::_attributes.adoc[] -:topics: devservices,data,database,datasource,tooling,testing,devmode +:topics: dev-services,data,database,datasource,dev-mode,testing :extensions: io.quarkus:quarkus-agroal,io.quarkus:quarkus-reactive-mysql-client,io.quarkus:quarkus-reactive-oracle-client,io.quarkus:quarkus-reactive-pg-client,io.quarkus:quarkus-jdbc-db2,io.quarkus:quarkus-jdbc-derby,io.quarkus:quarkus-jdbc-h2,io.quarkus:quarkus-jdbc-mariadb,io.quarkus:quarkus-jdbc-mssql,io.quarkus:quarkus-jdbc-mysql,io.quarkus:quarkus-jdbc-oracle,io.quarkus:quarkus-jdbc-postgresql When testing or running in dev mode Quarkus can provide you with a zero-config database out of the box, a feature we refer to as Dev Services. diff --git a/docs/src/main/asciidoc/dev-mode-differences.adoc b/docs/src/main/asciidoc/dev-mode-differences.adoc index ed3312ac24a329..b49ee27d3ca6bc 100644 --- a/docs/src/main/asciidoc/dev-mode-differences.adoc +++ b/docs/src/main/asciidoc/dev-mode-differences.adoc @@ -7,8 +7,7 @@ https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc include::_attributes.adoc[] :categories: architecture :summary: How dev mode differs from a production application -:topics: internals,devmode -:extensions: io.quarkus:quarkus-core +:topics: internals,dev-mode This document explains how the dev mode in Quarkus differs from a production application. diff --git a/docs/src/main/asciidoc/dev-services.adoc b/docs/src/main/asciidoc/dev-services.adoc index 6e8517de9f7c42..39c49c6c55d391 100644 --- a/docs/src/main/asciidoc/dev-services.adoc +++ b/docs/src/main/asciidoc/dev-services.adoc @@ -7,7 +7,7 @@ https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc include::_attributes.adoc[] :categories: core :summary: A list of all extensions that support Dev Services and their configuration options. -:topics: devservices,tooling,testing,devmode +:topics: dev-services,dev-mode,testing Quarkus supports the automatic provisioning of unconfigured services in development and test mode. We refer to this capability as Dev Services. From a developer's perspective this means that if you include an extension and don't configure it then diff --git a/docs/src/main/asciidoc/dev-ui.adoc b/docs/src/main/asciidoc/dev-ui.adoc index 83fc9c33b9871f..38bcbb28de722b 100644 --- a/docs/src/main/asciidoc/dev-ui.adoc +++ b/docs/src/main/asciidoc/dev-ui.adoc @@ -7,8 +7,7 @@ https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc include::_attributes.adoc[] :categories: writing-extensions :summary: Learn how to get your extension to contribute features to the Dev UI (v2). -:topics: dev-ui,tooling,testing -:extensions: io.quarkus:quarkus-core +:topics: dev-ui,testing [NOTE] .Dev UI v2 diff --git a/docs/src/main/asciidoc/elasticsearch-dev-services.adoc b/docs/src/main/asciidoc/elasticsearch-dev-services.adoc index 2ede49bdb9c1fd..5d66575584a33a 100644 --- a/docs/src/main/asciidoc/elasticsearch-dev-services.adoc +++ b/docs/src/main/asciidoc/elasticsearch-dev-services.adoc @@ -7,7 +7,7 @@ https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc include::_attributes.adoc[] :categories: data :summary: Start Elasticsearch automatically in dev and test modes -:topics: data,search,elasticsearch,nosql,devservices,tooling,testing,devmode +:topics: data,search,elasticsearch,nosql,dev-services,testing,dev-mode :extensions: io.quarkus:quarkus-elasticsearch-java-client,io.quarkus:quarkus-elasticsearch-rest-client,io.quarkus:quarkus-hibernate-search-orm-elasticsearch If any Elasticsearch-related extension is present (e.g. `quarkus-elasticsearch-rest-client` or `quarkus-hibernate-search-orm-elasticsearch`), diff --git a/docs/src/main/asciidoc/extension-codestart.adoc b/docs/src/main/asciidoc/extension-codestart.adoc index 78d63ff702a734..5f36ce736aa24c 100644 --- a/docs/src/main/asciidoc/extension-codestart.adoc +++ b/docs/src/main/asciidoc/extension-codestart.adoc @@ -8,7 +8,6 @@ include::_attributes.adoc[] :categories: writing-extensions :summary: Provide users with initial code for extensions when generating Quarkus applications on code.quarkus.io and all the Quarkus tooling. This guide explains how to create and configure a Codestart for an extension. :topics: extensions,codestarts -:extensions: io.quarkus:quarkus-core This guide explains how to create and configure a Quarkus Codestart for an extension. diff --git a/docs/src/main/asciidoc/extension-metadata.adoc b/docs/src/main/asciidoc/extension-metadata.adoc index 9b6f7ca09cf6a3..0a0f6f88e01a15 100644 --- a/docs/src/main/asciidoc/extension-metadata.adoc +++ b/docs/src/main/asciidoc/extension-metadata.adoc @@ -7,7 +7,6 @@ https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc include::_attributes.adoc[] :categories: writing-extensions :topics: extensions,codestarts -:extensions: io.quarkus:quarkus-core Quarkus extensions are distributed as Maven JAR artifacts that application and other libraries may depend on. When a Quarkus application project is built, tested or edited using the Quarkus dev tools, Quarkus extension JAR artifacts will be identified on the application classpath by the presence of the Quarkus extension metadata files in them. This document describes the purpose of each Quarkus extension metadata file and its content. diff --git a/docs/src/main/asciidoc/getting-started-dev-services.adoc b/docs/src/main/asciidoc/getting-started-dev-services.adoc index 8869b3fd521686..14f62ced451bbc 100644 --- a/docs/src/main/asciidoc/getting-started-dev-services.adoc +++ b/docs/src/main/asciidoc/getting-started-dev-services.adoc @@ -9,7 +9,7 @@ include::_attributes.adoc[] :diataxis-type: tutorial :categories: getting-started, data, core :summary: Discover some of the features that make developing with Quarkus a joyful experience. -:topics: getting-started,devservices +:topics: getting-started,dev-services This tutorial shows you how to create an application which writes to and reads from a database. You will use Dev Services, so you will not actually download, configure, or even start the database yourself. diff --git a/docs/src/main/asciidoc/grpc-generation-reference.adoc b/docs/src/main/asciidoc/grpc-generation-reference.adoc index 6d871816c3c338..48c732bcd5123e 100644 --- a/docs/src/main/asciidoc/grpc-generation-reference.adoc +++ b/docs/src/main/asciidoc/grpc-generation-reference.adoc @@ -99,6 +99,21 @@ quarkus { } ---- +== Generating Descriptor Set +Protocol Buffers do not contain descriptions of their own types. Thus, given only a raw message without the corresponding .proto file defining its type, it is difficult to extract any useful data. However, the contents of a .proto file can itself be https://protobuf.dev/programming-guides/techniques/#self-description[represented using protocol buffers]. + +By default, Quarkus does not generate these descriptors. Quarkus does provide several configuration options for generating them. These would be added to your `application.properties` or `application.yml` file: + +* `quarkus.generate-code.grpc.descriptor-set.generate` +** Set to `true` to enable generation +* `quarkus.generate-code.grpc.descriptor-set.output-dir` +** Set this to a value relative to the project's build directory (i.e. `target` for Maven, `build` for Gradle) +** Maven default value: `target/generated-sources/grpc` +** Gradle default value: `$buildDir/classes/java/quarkus-generated-sources/grpc` +* `quarkus.generate-code.grpc.descriptor-set.name` +** Name of the descriptor set file to generate +** Default value: `descriptor_set.dsc` + == Configuring gRPC code generation for dependencies You may have dependencies that contain `\*.proto` files you want to compile to Java sources. diff --git a/docs/src/main/asciidoc/hibernate-search-orm-elasticsearch.adoc b/docs/src/main/asciidoc/hibernate-search-orm-elasticsearch.adoc index f0e999d807b233..37798c850598cd 100644 --- a/docs/src/main/asciidoc/hibernate-search-orm-elasticsearch.adoc +++ b/docs/src/main/asciidoc/hibernate-search-orm-elasticsearch.adoc @@ -7,7 +7,8 @@ https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc include::_attributes.adoc[] :categories: data :summary: Hibernate Search allows you to index your entities in an Elasticsearch cluster and easily offer full text search in all your Hibernate ORM-based applications. -:topics: data,hibernate-search,elasticsearch,search,nosql +:keywords: elasticsearch hibernate orm search +:topics: data,hibernate-search,elasticsearch,search,nosql,hibernate-orm :extensions: io.quarkus:quarkus-hibernate-search-orm-elasticsearch You have a Hibernate ORM-based application? You want to provide a full-featured full-text search to your users? You're at the right place. diff --git a/docs/src/main/asciidoc/images/ot-to-otel-1.png b/docs/src/main/asciidoc/images/ot-to-otel-1.png new file mode 100644 index 00000000000000..56bc6307500575 Binary files /dev/null and b/docs/src/main/asciidoc/images/ot-to-otel-1.png differ diff --git a/docs/src/main/asciidoc/images/ot-to-otel-2.png b/docs/src/main/asciidoc/images/ot-to-otel-2.png new file mode 100644 index 00000000000000..c3afba21eb51b7 Binary files /dev/null and b/docs/src/main/asciidoc/images/ot-to-otel-2.png differ diff --git a/docs/src/main/asciidoc/images/ot-to-otel-3.png b/docs/src/main/asciidoc/images/ot-to-otel-3.png new file mode 100644 index 00000000000000..86c79a69282e42 Binary files /dev/null and b/docs/src/main/asciidoc/images/ot-to-otel-3.png differ diff --git a/docs/src/main/asciidoc/infinispan-client-reference.adoc b/docs/src/main/asciidoc/infinispan-client-reference.adoc index 6299db50b3b6f8..22674f6f605b47 100644 --- a/docs/src/main/asciidoc/infinispan-client-reference.adoc +++ b/docs/src/main/asciidoc/infinispan-client-reference.adoc @@ -95,28 +95,42 @@ quarkus.infinispan-client.hosts=localhost:11222 <1> quarkus.infinispan-client.username=admin <2> quarkus.infinispan-client.password=password <3> - -quarkus.infinispan-client.client-intelligence=BASIC <4> ---- <1> Sets Infinispan Server address list, separated with commas <2> Sets the authentication username <3> Sets the authentication password -<4> Sets the client intelligence. Use BASIC as a workaround if using Docker for Mac. Alternatively, you can use uri connection by providing a single connection property [source,properties] ---- quarkus.infinispan-client.uri=hotrod://admin:password@localhost:11222 <1> -quarkus.infinispan-client.client-intelligence=BASIC <2> ---- <1> Sets Infinispan URI connection. The following properties will be ignored: hosts, username and password. -<2> Sets the client intelligence. Use BASIC as a workaround if using Docker for Mac [TIP] ==== Use Infinispan Dev Services to run a server and connect without configuration. ==== +=== Client intelligence +Infinispan client uses intelligence mechanisms to efficiently send requests to Infinispan Server clusters. +By default, the *HASH_DISTRIBUTION_AWARE* intelligence mechanism is enabled. +However, locally with Docker for Mac, you might experience connectivity issues. +In this case, configure the client intelligence to *BASIC*. + +Learn more in the https://infinispan.org/docs/stable/titles/hotrod_java/hotrod_java.html#hotrod-client-intelligence_hotrod-java-client[Infinispan documentation]. + +[source,properties] +---- +quarkus.infinispan-client.client-intelligence=BASIC <1> +---- +<1> Docker for Mac workaround. + +[IMPORTANT] +==== +Don't use *BASIC* in production environments by default, performance might be impacted. +==== + === Configuring backup clusters in Cross-Site Replication In High Availability production deployments, it is common to have multiple Infinispan Clusters that are distributed across various Data Centers worldwide. Infinispan offers the capability to connect these clusters and diff --git a/docs/src/main/asciidoc/infinispan-client.adoc b/docs/src/main/asciidoc/infinispan-client.adoc index 06e08e5624225a..3e8ccc48ca277f 100644 --- a/docs/src/main/asciidoc/infinispan-client.adoc +++ b/docs/src/main/asciidoc/infinispan-client.adoc @@ -305,14 +305,22 @@ Then, open the `src/main/resources/application.properties` file and add: %prod.quarkus.infinispan-client.username=admin <2> %prod.quarkus.infinispan-client.password=password <3> -## Docker 4 Mac workaround -%prod.quarkus.infinispan-client.client-intelligence=BASIC <4> +## Docker 4 Mac workaround. Uncomment only if you are using Docker for Mac. +## Read more about it in the Infinispan Reference Guide +# %prod.quarkus.infinispan-client.client-intelligence=BASIC <4> ---- <1> Sets Infinispan Server address list, separated with commas <2> Sets the authentication username <3> Sets the authentication password <4> Sets the client intelligence. Use BASIC as a workaround if using Docker for Mac. +[IMPORTANT] +==== +Client intelligence changes impact your performance in production. +Don't change the client intelligence unless strictly necessary for your case. +Read more in the xref:infinispan-client-reference.adoc[Infinispan Client extension reference guide]. +==== + == Packaging and running in JVM mode You can run the application as a conventional jar file. diff --git a/docs/src/main/asciidoc/infinispan-dev-services.adoc b/docs/src/main/asciidoc/infinispan-dev-services.adoc index c2dfc3140a3ad6..436ab6b5a198ca 100644 --- a/docs/src/main/asciidoc/infinispan-dev-services.adoc +++ b/docs/src/main/asciidoc/infinispan-dev-services.adoc @@ -7,7 +7,7 @@ https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc include::_attributes.adoc[] :categories: data :summary: Start Infinispan automatically in dev and test modes. -:topics: devservices,data,infinispan,tooling,testing,devmode +:topics: dev-services,data,infinispan,testing,dev-mode :extensions: io.quarkus:quarkus-infinispan-client Quarkus supports a feature called Dev Services that allows you to create various datasources without any config. diff --git a/docs/src/main/asciidoc/kafka-dev-services.adoc b/docs/src/main/asciidoc/kafka-dev-services.adoc index b6d32ba55e9972..a94da965a3a21b 100644 --- a/docs/src/main/asciidoc/kafka-dev-services.adoc +++ b/docs/src/main/asciidoc/kafka-dev-services.adoc @@ -7,7 +7,7 @@ https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc include::_attributes.adoc[] :categories: messaging :summary: Start Apache Kafka automatically in dev and test modes. -:topics: messaging,kafka,devservices,tooling,testing,devmode +:topics: messaging,kafka,dev-services,testing,dev-mode :extensions: io.quarkus:quarkus-kafka-client,io.quarkus:quarkus-smallrye-reactive-messaging-kafka If any Kafka-related extension is present (e.g. `quarkus-smallrye-reactive-messaging-kafka`), Dev Services for Kafka automatically starts a Kafka broker in dev mode and when running tests. diff --git a/docs/src/main/asciidoc/kafka-dev-ui.adoc b/docs/src/main/asciidoc/kafka-dev-ui.adoc index 2625df6a7e224b..90cf6335f6961e 100644 --- a/docs/src/main/asciidoc/kafka-dev-ui.adoc +++ b/docs/src/main/asciidoc/kafka-dev-ui.adoc @@ -7,7 +7,7 @@ https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc include::_attributes.adoc[] :categories: messaging :summary: Dev UI extension for Apache Kafka for development purposes. -:topics: messaging,kafka,dev-ui,devmode +:topics: messaging,kafka,dev-ui,dev-mode :extensions: io.quarkus:quarkus-kafka-client,io.quarkus:quarkus-smallrye-reactive-messaging-kafka If any Kafka-related extension is present (e.g. `quarkus-smallrye-reactive-messaging-kafka`), diff --git a/docs/src/main/asciidoc/kubernetes-dev-services.adoc b/docs/src/main/asciidoc/kubernetes-dev-services.adoc index bd2e9bfa0e48f3..85f86d1ace39de 100644 --- a/docs/src/main/asciidoc/kubernetes-dev-services.adoc +++ b/docs/src/main/asciidoc/kubernetes-dev-services.adoc @@ -7,7 +7,7 @@ https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc include::_attributes.adoc[] :categories: cloud :summary: Start a Kubernetes API server automatically in dev and test modes. -:topics: devservices,kubernetes,tooling,testing,devmode +:topics: dev-services,kubernetes,testing,dev-mode :extensions: io.quarkus:quarkus-kubernetes-client Dev Services for Kubernetes automatically starts a Kubernetes API server in dev mode and when running tests. diff --git a/docs/src/main/asciidoc/lifecycle.adoc b/docs/src/main/asciidoc/lifecycle.adoc index 539b59006bb73e..911f10ff02f8b5 100644 --- a/docs/src/main/asciidoc/lifecycle.adoc +++ b/docs/src/main/asciidoc/lifecycle.adoc @@ -9,7 +9,7 @@ include::_attributes.adoc[] :keywords: lifecycle event :summary: You often need to execute custom actions when the application starts and clean up everything when the application stops. This guide explains how to be notified when an application stops or starts. :topics: lifecycle,observers -:extensions: io.quarkus:quarkus-core,io.quarkus:quarkus-arc +:extensions: io.quarkus:quarkus-arc You often need to execute custom actions when the application starts and clean up everything when the application stops. This guide explains how to: diff --git a/docs/src/main/asciidoc/logging.adoc b/docs/src/main/asciidoc/logging.adoc index c22add0efb800a..a9329d517f4b10 100644 --- a/docs/src/main/asciidoc/logging.adoc +++ b/docs/src/main/asciidoc/logging.adoc @@ -9,7 +9,6 @@ include::_attributes.adoc[] :categories: core,getting-started,observability :diataxis-type: reference :topics: logging,observability -:extensions: io.quarkus:quarkus-core Read about the use of logging API in Quarkus, configuring logging output, and using logging adapters to unify the output from other logging APIs. diff --git a/docs/src/main/asciidoc/opentelemetry.adoc b/docs/src/main/asciidoc/opentelemetry.adoc index 899a1ed320a618..d5ca44ee979599 100644 --- a/docs/src/main/asciidoc/opentelemetry.adoc +++ b/docs/src/main/asciidoc/opentelemetry.adoc @@ -13,15 +13,14 @@ include::_attributes.adoc[] This guide explains how your Quarkus application can utilize https://opentelemetry.io/[OpenTelemetry] (OTel) to provide distributed tracing for interactive web applications. -OpenTelemetry Metrics and Logging are not yet supported. - [NOTE] ==== +- OpenTelemetry Metrics and Logging are not yet supported. - Quarkus now supports the OpenTelemetry Autoconfiguration for Traces. The configurations match what you can see at https://github.com/open-telemetry/opentelemetry-java/blob/main/sdk-extensions/autoconfigure/README.md[OpenTelemetry SDK Autoconfigure] with the `quarkus.*` prefix. - - Extensions and the libraries they provide, are directly instrumented in Quarkus. The *use of the https://opentelemetry.io/docs/instrumentation/java/automatic/[OpenTelemetry Agent] is not needed nor recommended* due to context propagation issues between imperative and reactive libraries. +- If you come from the legacy OpenTracing extension, there is a xref:telemetry-opentracing-to-otel-tutorial.adoc[guide to help with the migration]. ==== == Prerequisites @@ -138,6 +137,33 @@ All configurations have been updated from `quarkus.opentelemetry.\*` -> `quarkus The legacy configurations are now deprecated but will still work during a transition period. ==== +=== Disable all or parts of the OpenTelemetry extension + +Once you add the dependency, the extension will be enabled by default but there are a few ways to disable the OpenTelemetry extension globally or partially. + +|=== +|Property name |Default value |Description + +|`quarkus.otel.enabled` +|true +|If false, disable the OpenTelemetry usage at *build* time. + +|`quarkus.otel.sdk.disabled` +|false +|Comes from the OpenTelemetry autoconfiguration. If true, will disable the OpenTelemetry SDK usage at *runtime*. + +|`quarkus.otel.traces.enabled` +|true +|If false, disable the OpenTelemetry tracing usage at *build* time. + +|`quarkus.otel.exporter.otlp.enabled` +|true +|If false will disable the default OTLP exporter at *build* time. +|=== + +If you need to enable or disable the exporter at runtime, you can use the <> because it has the ability to filter out all the spans if needed. + + == Run the application The first step is to configure and start the https://opentelemetry.io/docs/collector/[OpenTelemetry Collector] to receive, process and export telemetry data to https://www.jaegertracing.io/[Jaeger] that will display the captured traces. @@ -359,6 +385,7 @@ public class CustomConfiguration { By setting `quarkus.otel.traces.eusp.enabled=true` you can add information about the user related to each span. The user's ID and roles will be added to the span attributes, if available. +[[sampler]] === Sampler A https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk.md#sampling[sampler] decides whether a trace should be discarded or forwarded, effectively managing noise and reducing overhead by limiting the number of collected traces sent to the collector. diff --git a/docs/src/main/asciidoc/opentracing.adoc b/docs/src/main/asciidoc/opentracing.adoc index 88fb99d143a77e..c089dc6847c940 100644 --- a/docs/src/main/asciidoc/opentracing.adoc +++ b/docs/src/main/asciidoc/opentracing.adoc @@ -16,7 +16,7 @@ interactive web applications. [IMPORTANT] ==== -xref:opentelemetry.adoc[OpenTelemetry] is the recommended approach to tracing and telemetry for Quarkus. +xref:opentelemetry.adoc[OpenTelemetry] is the recommended approach to tracing and telemetry for Quarkus and xref:telemetry-opentracing-to-otel-tutorial.adoc[a guide to help with the migration] is available. When Quarkus will upgrade to Eclipse MicroProfile 6, the SmallRye OpenTracing support will be discontinued. ==== diff --git a/docs/src/main/asciidoc/pulsar-dev-services.adoc b/docs/src/main/asciidoc/pulsar-dev-services.adoc index 302f470f37749d..f1f8743bceadb4 100644 --- a/docs/src/main/asciidoc/pulsar-dev-services.adoc +++ b/docs/src/main/asciidoc/pulsar-dev-services.adoc @@ -6,7 +6,7 @@ https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc = Dev Services for Pulsar include::_attributes.adoc[] :categories: messaging -:topics: messaging,reactive-messaging,pulsar,devservices,tooling,testing,devmode +:topics: messaging,reactive-messaging,pulsar,dev-services,testing,dev-mode :extensions: io.quarkus:quarkus-smallrye-reactive-messaging-pulsar With Quarkus Smallrye Reactive Messaging Pulsar extension (`quarkus-smallrye-reactive-messaging-pulsar`) diff --git a/docs/src/main/asciidoc/rabbitmq-dev-services.adoc b/docs/src/main/asciidoc/rabbitmq-dev-services.adoc index c99349f7dd1e55..cf416e7ba075c7 100644 --- a/docs/src/main/asciidoc/rabbitmq-dev-services.adoc +++ b/docs/src/main/asciidoc/rabbitmq-dev-services.adoc @@ -6,7 +6,7 @@ https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc = Dev Services for RabbitMQ include::_attributes.adoc[] :categories: messaging -:topics: messaging,reactive-messaging,rabbitmq,devservices,tooling,testing,devmode +:topics: messaging,reactive-messaging,rabbitmq,dev-services,testing,dev-mode :extensions: io.quarkus:quarkus-smallrye-reactive-messaging-rabbitmq Dev Services for RabbitMQ automatically starts a RabbitMQ broker in dev mode and when running tests. diff --git a/docs/src/main/asciidoc/rabbitmq-reference.adoc b/docs/src/main/asciidoc/rabbitmq-reference.adoc index 586e338c046fc6..bd421a5b2b202a 100644 --- a/docs/src/main/asciidoc/rabbitmq-reference.adoc +++ b/docs/src/main/asciidoc/rabbitmq-reference.adoc @@ -7,7 +7,7 @@ https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc include::_attributes.adoc[] :extension-status: preview :categories: messaging -:topics: messaging,reactive-messaging,rabbitmq,devservices,tooling,testing,devmode +:topics: messaging,reactive-messaging,rabbitmq,dev-services,testing,dev-mode :extensions: io.quarkus:quarkus-smallrye-reactive-messaging-rabbitmq This guide is the companion from the xref:rabbitmq.adoc[Getting Started with RabbitMQ]. diff --git a/docs/src/main/asciidoc/rabbitmq.adoc b/docs/src/main/asciidoc/rabbitmq.adoc index 962edb7171c988..255969990afcf7 100644 --- a/docs/src/main/asciidoc/rabbitmq.adoc +++ b/docs/src/main/asciidoc/rabbitmq.adoc @@ -7,7 +7,7 @@ https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc :extension-status: preview include::_attributes.adoc[] :categories: messaging -:topics: messaging,reactive-messaging,rabbitmq,devservices,tooling,testing,devmode +:topics: messaging,reactive-messaging,rabbitmq :extensions: io.quarkus:quarkus-smallrye-reactive-messaging-rabbitmq This guide demonstrates how your Quarkus application can utilize SmallRye Reactive Messaging to interact with RabbitMQ. diff --git a/docs/src/main/asciidoc/reactive-sql-clients.adoc b/docs/src/main/asciidoc/reactive-sql-clients.adoc index 27d5101aecb606..4bc2423e03e522 100644 --- a/docs/src/main/asciidoc/reactive-sql-clients.adoc +++ b/docs/src/main/asciidoc/reactive-sql-clients.adoc @@ -207,7 +207,7 @@ client.query("DROP TABLE IF EXISTS fruits").execute() .await().indefinitely(); ---- -NOTE: Wondering why we need block until the latest query is completed? +NOTE: Wondering why we need to block until the latest query is completed? This code is part of a `@PostConstruct` method and Quarkus invokes it synchronously. As a consequence, returning prematurely could lead to serving requests while the database is not ready yet. @@ -413,7 +413,7 @@ We will use https://jquery.com/[jQuery] to simplify interactions with the backen ---- -In the Javascript code, we need a function to refresh the list of fruits when: +In the JavaScript code, we need a function to refresh the list of fruits when: * the page is loaded, or * a fruit is added, or @@ -814,7 +814,7 @@ public class PipeliningExample { PgPool client; public Uni favoriteFruitAndVegetable() { - // Explicitely acquire a connection + // Explicitly acquire a connection return client.withConnection(conn -> { Uni favoriteFruit = conn.query("SELECT name FROM fruits WHERE preferred IS TRUE").execute() .onItem().transform(rows -> rows.iterator().next().getString("name")); diff --git a/docs/src/main/asciidoc/reaugmentation.adoc b/docs/src/main/asciidoc/reaugmentation.adoc index 4e36e653dad3df..0c5c976565bdc1 100644 --- a/docs/src/main/asciidoc/reaugmentation.adoc +++ b/docs/src/main/asciidoc/reaugmentation.adoc @@ -21,7 +21,6 @@ Initialization steps that used to happen when an EAR file was deployed on a Jaka CDI beans added after augmentation won't work (because of the missing proxy classes) as well as build time properties (e.g. `quarkus.datasource.db-kind`) changed after augmentation will be ignored. Build time properties are marked with a lock icon (icon:lock[]) in the xref:all-config.adoc[list of all configuration options]. It doesn't matter if you use profiles or any other way to override the properties. -The build time properties that were active during augmentation are baked into the build. > Re-augmentation is the process of recreating the augmentation output for a different build time configuration @@ -33,7 +32,7 @@ If there are only two or three build time properties that depend on the user env Please notice that you won't be able to use native images with the package type `mutable-jar`. Think of the consequences and what other options you have! -It is not a good idea to do re-augmentation at runtime unless you miss the good old times when starting up a server took several minutes and you could enjoy a cup of coffee until it was ready. +It is not a good idea to do re-augmentation at runtime unless you miss the good old times when starting up a server took several minutes, and you could enjoy a cup of coffee until it was ready. == How to re-augment a Quarkus application @@ -46,6 +45,11 @@ TIP: By default, you'll get a warning if a build time property has been changed You may set the `quarkus.configuration.build-time-mismatch-at-runtime=fail` property to make sure your application does not start up if there is a mismatch. However, as of this writing changing `quarkus.datasource.db-kind` at runtime did neither fail nor produce a warning but was silently ignored. +WARNING: Build time configuration provided by build tools (`properties` in Maven `pom.xml` or `gradle.properties` +in Gradle) in the `quarkus` namespace will be part of the `mutable-jar` distribution, including configuration from +`quarkus` that reference secrets or passwords. Please, do not include sensitive information in the build tool +configuration files. + === 1. Build your application as `mutable-jar` [source,bash] diff --git a/docs/src/main/asciidoc/redis-dev-services.adoc b/docs/src/main/asciidoc/redis-dev-services.adoc index 0572c08e4343ae..737f4e0e0d8bbe 100644 --- a/docs/src/main/asciidoc/redis-dev-services.adoc +++ b/docs/src/main/asciidoc/redis-dev-services.adoc @@ -8,7 +8,7 @@ https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc include::_attributes.adoc[] :categories: data :summary: Start Redis automatically in dev and test modes. -:topics: data,redis,nosql,devservices,tooling,testing,devmode +:topics: data,redis,nosql,dev-services,testing,dev-mode :extensions: io.quarkus:quarkus-redis-client Quarkus supports a feature called Dev Services that allows you to create various datasources without any config. diff --git a/docs/src/main/asciidoc/resteasy-reactive-migration.adoc b/docs/src/main/asciidoc/resteasy-reactive-migration.adoc index 86eb5742cf2008..30ef22a2b7993b 100644 --- a/docs/src/main/asciidoc/resteasy-reactive-migration.adoc +++ b/docs/src/main/asciidoc/resteasy-reactive-migration.adoc @@ -155,6 +155,12 @@ public class ReactiveResource { The same is true for your third-party libraries. If they happen to depend on servlets you need to find a migration path for them. +=== Log authentication and authorization failures + +The RESTEasy Reactive endpoint security checks are performed before xref:cdi.adoc#interceptors[CDI interceptors] are invoked. +The safest approach to log Quarkus Security authentication exceptions is to ensure that proactive authentication is enabled and to use Vert.x HTTP route failure handlers. +For more information, see the xref:security-proactive-authentication.adoc#customize-auth-exception-responses[Customize authentication exception responses] section of the Proactive authentication guide. + == Client The Reactive REST Client (`quarkus-rest-client-reactive` and its dependencies) replace the legacy `quarkus-rest-client` but leverage Quarkus' build time processing diff --git a/docs/src/main/asciidoc/resteasy-reactive.adoc b/docs/src/main/asciidoc/resteasy-reactive.adoc index b2214898aa36b6..7d9792a76c145a 100644 --- a/docs/src/main/asciidoc/resteasy-reactive.adoc +++ b/docs/src/main/asciidoc/resteasy-reactive.adoc @@ -2219,6 +2219,8 @@ class Filters { } ---- +Such a response filter will also be called for <> exceptions. + Your filters may declare any of the following parameter types: .Filter parameters @@ -2235,7 +2237,7 @@ Your filters may declare any of the following parameter types: |A context object to access the current response |link:{jdkapi}/java/lang/Throwable.html[`Throwable`] -|Any thrown exception, or `null` (only for response filters) +|Any thrown and <> exception, or `null` (only for response filters). |=== @@ -2316,6 +2318,11 @@ Now, whenever a REST method is invoked, the request will be logged into the cons 2019-06-05 12:51:04,485 INFO [org.acm.res.jso.LoggingFilter] (executor-thread-1) Request GET /fruits from IP 127.0.0.1 ---- +[NOTE] +==== +A `ContainerResponseFilter` will also be called for <> exceptions. +==== + === Readers and Writers: mapping entities and HTTP bodies [[readers-writers]] 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 d503365f9be9b6..41a9d39bf3feda 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,52 @@ 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. +=== Custom HttpSecurityPolicy + +Sometimes it might be useful to register your own named policy. You can get it done by creating application scoped CDI +bean that implements the `io.quarkus.vertx.http.runtime.security.HttpSecurityPolicy` interface like in the example below: + +[source,java] +---- +import jakarta.enterprise.context.ApplicationScoped; + +import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.vertx.http.runtime.security.HttpSecurityPolicy; +import io.smallrye.mutiny.Uni; +import io.vertx.ext.web.RoutingContext; + +@ApplicationScoped +public class CustomNamedHttpSecPolicy implements HttpSecurityPolicy { + @Override + public Uni checkPermission(RoutingContext request, Uni identity, + AuthorizationRequestContext requestContext) { + if (customRequestAuthorization(request)) { + return Uni.createFrom().item(CheckResult.PERMIT); + } + return Uni.createFrom().item(CheckResult.DENY); + } + + @Override + public String name() { + return "custom"; <1> + } +} +---- +<1> Named HTTP Security policy will only be applied to requests matched by the `application.properties` path matching rules. + +.Example of custom named HttpSecurityPolicy referenced from configuration file +[source,properties] +---- +quarkus.http.auth.permission.custom1.paths=/custom/* +quarkus.http.auth.permission.custom1.policy=custom <1> +---- +<1> Custom policy name must match the value returned by the `io.quarkus.vertx.http.runtime.security.HttpSecurityPolicy.name` method. + +[TIP] +==== +You can also create global `HttpSecurityPolicy` invoked on every request. +Just do not implement the `io.quarkus.vertx.http.runtime.security.HttpSecurityPolicy.name` method and leave the policy nameless. +==== === Matching on paths and methods @@ -642,19 +688,22 @@ Similarly to the `CRUDResource` example, the following example shows how you can ---- package org.acme.library; +import io.quarkus.runtime.annotations.RegisterForReflection; import java.security.Permission; import java.util.Arrays; import java.util.Set; +@RegisterForReflection <1> public class MediaLibraryPermission extends LibraryPermission { public MediaLibraryPermission(String libraryName, String[] actions) { - super(libraryName, actions, new MediaLibrary()); <1> + super(libraryName, actions, new MediaLibrary()); <2> } } ---- -<1> We want to pass the `MediaLibrary` instance to the `LibraryPermission` constructor. +<1> When building a native executable, the permission class must be registered for reflection unless it is also used in at least one `io.quarkus.security.PermissionsAllowed#name` parameter. +<2> We want to pass the `MediaLibrary` instance to the `LibraryPermission` constructor. [source,properties] ---- diff --git a/docs/src/main/asciidoc/security-oidc-code-flow-authentication.adoc b/docs/src/main/asciidoc/security-oidc-code-flow-authentication.adoc index c896b0ac855503..75b5aad3acc2de 100644 --- a/docs/src/main/asciidoc/security-oidc-code-flow-authentication.adoc +++ b/docs/src/main/asciidoc/security-oidc-code-flow-authentication.adoc @@ -280,9 +280,9 @@ quarkus.oidc.introspection-credentials.secret=introspection-user-secret ---- [[oidc-client-filters]] -==== OIDC client request customization +==== OIDC request customization -You can customize OIDC client requests by registering one or more `OidcClientRequestFiler` implementations which can update or add new request headers, please see xref:security-openid-connect-client-reference#oidc-client-filters[Client request customization] for more information. +You can customize OIDC requests made by Quarkus to the OIDC provider by registering one or more `OidcRequestFiler` implementations which can update or add new request headers, please see xref:security-openid-connect-client-reference#oidc-client-filters[Client request customization] for more information. ==== Redirecting to and from the OIDC provider diff --git a/docs/src/main/asciidoc/security-openid-connect-client-reference.adoc b/docs/src/main/asciidoc/security-openid-connect-client-reference.adoc index c45c0bb707dcf8..692575db26ef7f 100644 --- a/docs/src/main/asciidoc/security-openid-connect-client-reference.adoc +++ b/docs/src/main/asciidoc/security-openid-connect-client-reference.adoc @@ -16,7 +16,7 @@ This reference guide explains how to use: The access tokens managed by these extensions can be used as HTTP Authorization Bearer tokens to access the remote services. -Please also see xref:security-openid-connect-client.adoc[OpenID Connect Client and Token Propagation Quickstart]. +Also see xref:security-openid-connect-client.adoc[OpenID Connect Client and Token Propagation Quickstart]. == OidcClient @@ -47,7 +47,7 @@ quarkus.oidc-client.auth-server-url=http://localhost:8180/auth/realms/quarkus `OidcClient` will discover that the token endpoint URL is `http://localhost:8180/auth/realms/quarkus/protocol/openid-connect/tokens`. -Alternatively, if the discovery endpoint is not available or you would like to save on the discovery endpoint round-trip, you can disable the discovery and configure the token endpoint address with a relative path value, for example: +Alternatively, if the discovery endpoint is not available or you want to save on the discovery endpoint round-trip, you can disable the discovery and configure the token endpoint address with a relative path value, for example: [source, properties] ---- @@ -64,7 +64,7 @@ A more compact way to configure the token endpoint URL without the discovery is quarkus.oidc-client.token-path=http://localhost:8180/auth/realms/quarkus/protocol/openid-connect/tokens ---- -Setting 'quarkus.oidc-client.auth-server-url' and 'quarkus.oidc-client.discovery-enabled' is not required in this case. +Setting `quarkus.oidc-client.auth-server-url` and `quarkus.oidc-client.discovery-enabled` is not required in this case. === Supported Token Grants @@ -111,7 +111,7 @@ It can be further customized using a `quarkus.oidc-client.grant-options.password ==== Other Grants -`OidcClient` can also help with acquiring the tokens using the grants which require some extra input parameters which can not be captured in the configuration. These grants are `refresh_token` (with the external refresh token), `authorization_code`, as well as two grants which can be used to exchange the current access token, `urn:ietf:params:oauth:grant-type:token-exchange` and `urn:ietf:params:oauth:grant-type:jwt-bearer`. +`OidcClient` can also help with acquiring the tokens using the grants which require some extra input parameters which cannot be captured in the configuration. These grants are `refresh_token` (with the external refresh token), `authorization_code`, as well as two grants which can be used to exchange the current access token, `urn:ietf:params:oauth:grant-type:token-exchange` and `urn:ietf:params:oauth:grant-type:jwt-bearer`. Using the `refresh_token` grant which uses an out-of-band refresh token to acquire a new set of tokens will be required if the existing refresh token has been posted to the current Quarkus endpoint for it to acquire the access token. In this case `OidcClient` needs to be configured as follows: @@ -125,7 +125,7 @@ quarkus.oidc-client.grant.type=refresh and then you can use `OidcClient.refreshTokens` method with a provided refresh token to get the access token. -Using the `urn:ietf:params:oauth:grant-type:token-exchange` or `urn:ietf:params:oauth:grant-type:jwt-bearer` grants may be required if you are building a complex microservices application and would like to avoid the same `Bearer` token be propagated to and used by more than one service. Please see <> and <> for more details. +Using the `urn:ietf:params:oauth:grant-type:token-exchange` or `urn:ietf:params:oauth:grant-type:jwt-bearer` grants might be required if you are building a complex microservices application and want to avoid the same `Bearer` token be propagated to and used by more than one service. See <> and <> for more details. Using `OidcClient` to support the `authorization code` grant might be required if for some reason you cannot use the xref:security-oidc-code-flow-authentication.adoc[Quarkus OIDC extension] to support Authorization Code Flow. If there is a very good reason for you to implement Authorization Code Flow then you can configure `OidcClient` as follows: @@ -153,7 +153,7 @@ and then you can use `OidcClient.accessTokens` method accepting a Map of extra p ==== Grant scopes -You may need to request that a specific set of scopes is associated with an issued access token. +You might need to request that a specific set of scopes is associated with an issued access token. Use a dedicated `quarkus.oidc-client.scopes` list property, for example: `quarkus.oidc-client.scopes=email,phone` === Use OidcClient directly @@ -215,7 +215,7 @@ public class OidcClientResource { @GET public String getResponse() { - // Get the access token which may have been refreshed. + // Get the access token, which might have been refreshed. String accessToken = tokens.getAccessToken(); // Use the access token to configure MP RestClient Authorization header/etc } @@ -395,7 +395,7 @@ It works similarly to the way `OidcClientRequestFilter` does (see < ---- -Write Wiremock based `QuarkusTestResourceLifecycleManager`, for example: +Write a Wiremock-based `QuarkusTestResourceLifecycleManager`, for example: [source, java] ---- package io.quarkus.it.keycloak; @@ -831,7 +831,7 @@ public class KeycloakRealmResourceManager implements QuarkusTestResourceLifecycl } ---- -Prepare the REST test endpoints, you can have the test frontend endpoint which uses the injected MP REST client with a registered OidcClient filter to invoke on the downstream endpoint which echoes the token back, for example, see the `integration-tests/oidc-client-wiremock` in the `main` Quarkus repository. +Prepare the REST test endpoints. You can have the test front-end endpoint, which uses the injected MP REST client with a registered OidcClient filter, call the downstream endpoint. This endpoint echoes the token back. For example, see the `integration-tests/oidc-client-wiremock` in the `main` Quarkus repository. Set `application.properties`, for example: @@ -856,7 +856,7 @@ If you work with Keycloak then you can use the same approach as described in the === How to check the errors in the logs -Please enable `io.quarkus.oidc.client.runtime.OidcClientImpl` `TRACE` level logging to see more details about the token acquisition and refresh errors: +Enable `io.quarkus.oidc.client.runtime.OidcClientImpl` `TRACE` level logging to see more details about the token acquisition and refresh errors: [source, properties] ---- @@ -864,7 +864,7 @@ quarkus.log.category."io.quarkus.oidc.client.runtime.OidcClientImpl".level=TRACE quarkus.log.category."io.quarkus.oidc.client.runtime.OidcClientImpl".min-level=TRACE ---- -Please enable `io.quarkus.oidc.client.runtime.OidcClientRecorder` `TRACE` level logging to see more details about the OidcClient initialization errors: +Enable `io.quarkus.oidc.client.runtime.OidcClientRecorder` `TRACE` level logging to see more details about the OidcClient initialization errors: [source, properties] ---- @@ -873,9 +873,9 @@ quarkus.log.category."io.quarkus.oidc.client.runtime.OidcClientRecorder".min-lev ---- [[oidc-client-filters]] -== Client request customization +== OIDC request customization -You can customize OIDC client requests by registering one or more `OidcClientRequestFiler` implementations which can update or add new request headers, for example, a filter can analyze the request body and add its digest as a new header value: +You can customize OIDC requests made by Quarkus to the OIDC provider by registering one or more `OidcRequestFiler` implementations which can update or add new request headers, for example, a filter can analyze the request body and add its digest as a new header value: [source,java] ---- @@ -884,17 +884,18 @@ package io.quarkus.it.keycloak; import jakarta.enterprise.context.ApplicationScoped; import io.quarkus.arc.Unremovable; -import io.quarkus.oidc.common.OidcClientRequestFilter; +import io.quarkus.oidc.common.OidcRequestContextProperties; +import io.quarkus.oidc.common.OidcRequestFilter; import io.vertx.core.http.HttpMethod; import io.vertx.mutiny.core.buffer.Buffer; import io.vertx.mutiny.ext.web.client.HttpRequest; @ApplicationScoped @Unremovable -public class OidcClientRequestCustomizer implements OidcClientRequestFilter { +public class OidcRequestCustomizer implements OidcRequestFilter { @Override - public void filter(HttpRequest request, Buffer buffer) { + public void filter(HttpRequest request, Buffer buffer, OidcRequestContextProperties contextProperties) { HttpMethod method = request.method(); String uri = request.uri(); if (method == HttpMethod.POST && uri.endsWith("/service") && buffer != null) { @@ -966,7 +967,7 @@ quarkus.oidc-token-propagation.exchange-token=true Note `AccessTokenRequestReactiveFilter` will use `OidcClient` to exchange the current token, and you can use `quarkus.oidc-client.grant-options.exchange` to set the additional exchange properties expected by your OpenID Connect Provider. -If you work with providers such as `Azure` that link:https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-on-behalf-of-flow#example[require using] link:https://www.rfc-editor.org/rfc/rfc7523#section-2.1[JWT bearer token grant] to exhange the current token then you can configure `AccessTokenRequestReactiveFilter` to exchange the token like this: +If you work with providers such as `Azure` that link:https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-on-behalf-of-flow#example[require using] link:https://www.rfc-editor.org/rfc/rfc7523#section-2.1[JWT bearer token grant] to exchange the current token then you can configure `AccessTokenRequestReactiveFilter` to exchange the token like this: [source,properties] ---- @@ -994,7 +995,7 @@ When you need to propagate the current Authorization Code Flow access token then However, the direct end to end Bearer token propagation should be avoided if possible. For example, `Client -> Service A -> Service B` where `Service B` receives a token sent by `Client` to `Service A`. In such cases `Service B` will not be able to distinguish if the token came from `Service A` or from `Client` directly. For `Service B` to verify the token came from `Service A` it should be able to assert a new issuer and audience claims. -Additionally, a complex application may need to exchange or update the tokens before propagating them. For example, the access context might be different when `Service A` is accessing `Service B`. In this case, `Service A` might be granted a narrow or a completely different set of scopes to access `Service B`. +Additionally, a complex application might need to exchange or update the tokens before propagating them. For example, the access context might be different when `Service A` is accessing `Service B`. In this case, `Service A` might be granted a narrow or a completely different set of scopes to access `Service B`. The following sections show how `AccessTokenRequestFilter` and `JsonWebTokenRequestFilter` can help. @@ -1053,7 +1054,7 @@ quarkus.oidc-client.grant-options.exchange.audience=quarkus-app-exchange quarkus.oidc-token-propagation.exchange-token=true ---- -If you work with providers such as `Azure` that link:https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-on-behalf-of-flow#example[require using] link:https://www.rfc-editor.org/rfc/rfc7523#section-2.1[JWT bearer token grant] to exhange the current token then you can configure `AccessTokenRequestFilter` to exchange the token like this: +If you work with providers such as `Azure` that link:https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-on-behalf-of-flow#example[require using] link:https://www.rfc-editor.org/rfc/rfc7523#section-2.1[JWT bearer token grant] to exchange the current token then you can configure `AccessTokenRequestFilter` to exchange the token like this: [source,properties] ---- @@ -1130,13 +1131,13 @@ smallrye.jwt.new-token.audience=http://downstream-resource smallrye.jwt.new-token.override-matching-claims=true ---- -As already noted above, please use `AccessTokenRequestFilter` if you work with Keycloak or OpenID Connect Provider which supports a Token Exchange protocol. +As already noted above, use `AccessTokenRequestFilter` if you work with Keycloak or OpenID Connect Provider which supports a Token Exchange protocol. [[integration-testing-token-propagation]] === Testing You can generate the tokens as described in xref:security-oidc-bearer-token-authentication.adoc#integration-testing[OpenID Connect Bearer Token Integration testing] section. -Prepare the REST test endpoints, you can have the test frontend endpoint which uses the injected MP REST client with a registered token propagation filter to invoke on the downstream endpoint, for example, see the `integration-tests/oidc-token-propagation` in the `main` Quarkus repository. +Prepare the REST test endpoints. You can have the test front-end endpoint, which uses the injected MP REST client with a registered token propagation filter, call the downstream endpoint. For example, see the `integration-tests/oidc-token-propagation` in the `main` Quarkus repository. [[reactive-token-propagation]] == Token Propagation Reactive @@ -1154,7 +1155,7 @@ Add the following Maven Dependency: The `quarkus-oidc-token-propagation-reactive` extension provides `io.quarkus.oidc.token.propagation.reactive.AccessTokenRequestReactiveFilter` which can be used to propagate the current `Bearer` or `Authorization Code Flow` access tokens. The `quarkus-oidc-token-propagation-reactive` extension (as opposed to the non-reactive `quarkus-oidc-token-propagation` extension) does not currently support the exchanging or resigning the tokens before the propagation. -However, these features may be added in the future. +However, these features might be added in the future. [[oidc-client-graphql-client]] == GraphQL client integration @@ -1185,7 +1186,7 @@ quarkus.oidc-client.oidc-client-for-graphql.credentials.client-secret.method=POS NOTE: If you don't specify the `quarkus.oidc-client-graphql.client-name` property, GraphQL clients will use the default OIDC client (without an explicit name). -Specifically for typesafe GraphQL clients, you can override this on a +Specifically for type-safe GraphQL clients, you can override this on a per-client basis by annotating the `GraphQLClientApi` interface with `@io.quarkus.oidc.client.filter.OidcClientFilter`. For example: diff --git a/docs/src/main/asciidoc/security-openid-connect-dev-services.adoc b/docs/src/main/asciidoc/security-openid-connect-dev-services.adoc index c405cabb19516d..3e4f7fc20672ef 100644 --- a/docs/src/main/asciidoc/security-openid-connect-dev-services.adoc +++ b/docs/src/main/asciidoc/security-openid-connect-dev-services.adoc @@ -8,7 +8,7 @@ include::_attributes.adoc[] :categories: security :keywords: sso oidc security keycloak :summary: Start Keycloak or other providers automatically in dev and test modes. -:topics: security,oidc,keycloak,devservices,tooling,testing,devmode +:topics: security,oidc,keycloak,dev-services,testing,dev-mode :extensions: io.quarkus:quarkus-oidc This guide covers the Dev Services and UI for OpenID Connect (OIDC) Keycloak provider and explains how to support Dev Services and UI for other OpenID Connect providers. diff --git a/docs/src/main/asciidoc/security-proactive-authentication.adoc b/docs/src/main/asciidoc/security-proactive-authentication.adoc index 0fbc81236a9020..22f50c364a8e23 100644 --- a/docs/src/main/asciidoc/security-proactive-authentication.adoc +++ b/docs/src/main/asciidoc/security-proactive-authentication.adoc @@ -94,6 +94,7 @@ public class HelloService { } ---- +[[customize-auth-exception-responses]] == Customize authentication exception responses You can use Jakarta REST `ExceptionMapper` to capture Quarkus Security authentication exceptions such as `io.quarkus.security.AuthenticationFailedException`, for example: diff --git a/docs/src/main/asciidoc/telemetry-opentracing-to-otel-tutorial.adoc b/docs/src/main/asciidoc/telemetry-opentracing-to-otel-tutorial.adoc new file mode 100644 index 00000000000000..63348b95ea7f13 --- /dev/null +++ b/docs/src/main/asciidoc/telemetry-opentracing-to-otel-tutorial.adoc @@ -0,0 +1,544 @@ +//// +This tutorial is maintained in the main Quarkus repository +and pull requests should be submitted there: +https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc +//// +[id="telemetry-opentracing-to-otel-tutorial"] += Migrate from OpenTracing to OpenTelemetry tracing +:categories: observability +:diataxis-type: tutorial +include::_attributes.adoc[] +:topics: observability,opentracing,opentelemetry,tracing,migration +:extensions: io.quarkus:quarkus-smallrye-opentracing,io.quarkus:quarkus-opentelemetry + +Migrate an application from xref:opentracing.adoc[OpenTracing] to xref:opentelemetry.adoc[OpenTelemetry tracing] in Quarkus 3.x. + +The legacy OpenTracing framework has been deprecated in favor of the new OpenTelemetry tracing framework. We announced the https://quarkus.io/blog/quarkus-observability-roadmap-2023/#opentracing-archived[OpenTracing deprecation on November 2022], and we are dropping the extension from Quarkus core repository and moving it to the Quarkiverse Hub. + +It is now time to migrate your application to OpenTelemetry tracing if you haven’t done it yet. + +If you need to migrate from Quarkus 2.16.x please beware that configuration properties are different and you should check the older Quarkus OpenTelemetry guide version, https://quarkus.io/version/2.16/guides/opentelemetry#configuration-reference[here]. + +== Prerequisites + +include::{includes}/prerequisites.adoc[] + +== Summary + +The demo has 5 parts. Please read the summary and then jump to the section that best fits your use case. + +1 - The *starting point* presents the quickstart app that uses OpenTracing + +2 - The first part is good for anyone performing a *big bang change* of OpenTracing when you don't have any manual instrumentation + +3 - This is the *big bang replacement* of OpenTracing when you have manually instrumented the code. We explain the main differences between OpenTracing and OpenTelemetry + +4 - The last part uses the *OpenTracing shim*. This is useful if you have a large application with manually instrumented code. It can help performing the migration step by step because it allows the use of the legacy OpenTracing API on top of new OpenTelemetry API + +5 - Conclusion and additional resources + +The tasks described below fall into 3 categories: + +* Dependencies +* Configuration +* Code + +[[starting-point]] +== Starting point + +This tutorial is built on top of the `opentracing-quickstart` legacy project. + +=== Generate the legacy project + +Create the legacy project by executing the following command: + +:create-app-artifact-id: opentracing-quickstart +:create-app-extensions: resteasy-reactive,quarkus-smallrye-opentracing +:create-app-code: +include::{includes}/devtools/create-app.adoc[] + +This command generates the Maven structure importing the `smallrye-opentracing` extension, which +includes the OpenTracing support and the default https://www.jaegertracing.io/[Jaeger] tracer. + +=== Check out the existing legacy project + +For convenience there is a project in github with all the steps from the tutorial. You can clone it with the following command: + +[source,bash] +---- +git clone git@github.com:quarkusio/opentracing-quickstart-migration.git +---- + +For convenience, https://github.com/quarkusio/opentracing-quickstart-migration[the repository] containing the app to migrate, includes several branches with commits mimicking the migration steps described in this tutorial. You can check out the `main` branch to start from the beginning. + +=== The application + +The Quarkus project has a single endpoint and the related class looks like this: + +[source,java] +---- +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +@Path("/hello") +public class GreetingResource { + + @GET + @Produces(MediaType.TEXT_PLAIN) + public String hello() { + return "Hello from RESTEasy Reactive"; + } +} +---- + +There is no OpenTracing specific code in the generated project, but the `smallrye-opentracing` extension is present and enabled by default, and it will automatically instrument the code. + +Let's start the Jaeger-all-in-one Docker image, where we will retrieve and see the captured traces: + +[source,bash] +---- +docker run -e COLLECTOR_OTLP_ENABLED=true -p 6831:6831/udp -p 6832:6832/udp -p 5778:5778 -p 16686:16686 -p 4317:4317 -p 4318:4318 -p 14250:14250 -p 14268:14268 -p 14269:14269 -p 9411:9411 jaegertracing/all-in-one:latest +---- + +At this point you can run the application with Quarkus dev mode: + +include::{includes}/devtools/dev.adoc[] + +If you call the http://localhost:8080/hello[`/hello` endpoint] the related traces can be retrieved in the Jaeger UI at this address: http://localhost:16686/ + +They will look like this: + +image::ot-to-otel-1.png[alt=OpenTracing span,role="center"] + +== Big bang change from OpenTracing to OpenTelemetry + +This is the happiest path, in this case there is no manual instrumentation. We can do a big bang change from OpenTracing to OpenTelemetry without side effects. + +=== Change dependencies + +To migrate between the two frameworks, you must drop the old `quarkus-smallrye-opentracing` extension and replace it by the `quarkus-opentelemetry` extension in the build file: + +The legacy extension is removed from the project: + +[source,xml,role="primary asciidoc-tabs-target-sync-cli asciidoc-tabs-target-sync-maven"] +.pom.xml +---- + + io.quarkus + quarkus-smallrye-opentracing + +---- + +[source,gradle,role="secondary asciidoc-tabs-target-sync-gradle"] +.build.gradle +---- +implementation("io.quarkus:quarkus-smallrye-opentracing") +---- + +The new one is added: + +[source,xml,role="primary asciidoc-tabs-target-sync-cli asciidoc-tabs-target-sync-maven"] +.pom.xml +---- + + io.quarkus + quarkus-opentelemetry + +---- + +[source,gradle,role="secondary asciidoc-tabs-target-sync-gradle"] +.build.gradle +---- +implementation("io.quarkus:quarkus-opentelemetry") +---- + +=== Application properties + +You should remove the old OpenTracing properties, starting with `quarkus.jaeger.*` from the `application.properties` file, like in this example: + +[source,application.properties] +---- +#Legacy OpenTracing properties to be removed +quarkus.jaeger.service-name=legume +quarkus.jaeger.sampler-type=const +quarkus.jaeger.sampler-param=1 +quarkus.jaeger.endpoint=http://localhost:14268/api/traces +quarkus.jaeger.log-trace-context=true +---- + +If you use the default values in the OpenTelemetry properties, there is no necessity to include anything in the `application.properties` file. + +Some common properties to migrate are: + +|=== +|Legacy OpenTracing property | New OpenTelemetry property + +|`quarkus.jaeger.service-name=legume` +|`quarkus.application.name=legume` + +|`quarkus.jaeger.endpoint=http://localhost:14268/api/traces` +|`quarkus.otel.exporter.otlp.traces.endpoint=http://localhost:4317` + +|`quarkus.jaeger.auth-token` +|`quarkus.otel.exporter.otlp.traces.headers` + +|`quarkus.jaeger.sampler-type` +|`quarkus.otel.traces.sampler` + +|`quarkus.jaeger.sampler-param` +|`quarkus.otel.traces.sampler.arg` + +|`quarkus.jaeger.tags` +|`quarkus.otel.resource.attributes` + +|`quarkus.jaeger.propagation` +|`quarkus.otel.propagators` +|=== + +The way the extensions can be enabled and disabled is very different. The OpenTelemetry extension is enabled by default and you can disable all or parts of it by checking xref:opentelemetry.adoc#disable-all-or-parts-of-the-opentelemetry-extension[this section of the OpenTelemetry guide]. + +All the OpenTelemetry properties and their defaults can be found in the xref:opentelemetry.adoc#configuration-reference[OpenTelemetry configuration reference]. + +=== Run the application + +Restarting Quarkus is not needed, auto-reload should have kicked in and you now can call the http://localhost:8080/hello[`/hello` endpoint] and then see the traces in the Jaeger UI: http://localhost:16686/ + +However, you can now see spans produced by the OpenTelemetry's auto-instrumentation instead of the OpenTracing one: + +image::ot-to-otel-2.png[alt=OpenTelemetry span,role="center"] + +If you don't have any manual instrumentation of your own, you are done! + +== The big bang replacement, when you have manual instrumentation + +Let's say instead of the `GreetingResource` class from above, you have something more complex. You will need additional work on top of the changes from the <>. + +This class now uses the `@Traced` annotation and creates a "manual" programmatic span. + +Copy/paste that code for the `GreetingResource` class in the quickstart project: + +[[greeting-resource-starting-point]] +=== The GreetingsResource with OpenTracing manual instrumentation + +[source,java] +---- +package org.acme; + +import io.opentracing.Scope; +import io.opentracing.Span; +import io.opentracing.tag.Tags; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import org.eclipse.microprofile.opentracing.Traced; + +@Path("/hello") +@ApplicationScoped +public class GreetingResource { + + @Inject + io.opentracing.Tracer legacyTracer; <1> + + @GET + @Produces(MediaType.TEXT_PLAIN) + @Traced(operationName = "Not needed, will change the current span name") <2> + public String hello() { + // Add a tag to the active span + legacyTracer.activeSpan().setTag(Tags.COMPONENT, "GreetingResource"); <3> + + // Create a manual inner span + Span innerSpan = legacyTracer.buildSpan("Count response chars").start(); + + try (Scope dbScope = legacyTracer.scopeManager().activate(innerSpan)) { + String response = "Hello from RESTEasy Reactive"; + innerSpan.setTag("response-chars-count", response.length()); + return response; + } catch (Exception e) { + innerSpan.setTag("error", true); <4> + innerSpan.setTag("error.message", e.getMessage()); + throw e; + } finally { + innerSpan.finish(); + } + } +} +---- + +<1> The legacy OpenTracing tracer, must be replaced by the new OpenTelemetry tracer. +<2> The `@Traced` annotation is replaced by the `@WithSpan` annotation but beware that this new annotation will always create a new Span. You shouldn't use it on JAX-RS endpoints because they are already instrumented. +<3> The `Tag` class is replaced by the `Attribute` class. `Tags` is replaced by the `SemanticAttributes` class, which should be used whenever possible, to keep attribute names consistent with the specification. +<4> There are new methods to handle errors in OpenTelemetry. + +The OpenTelemetry tracer is not compatible with the OpenTracing API. The main changes are summarized in the following table: + +|=== +|Note |MicroProfile OpenTracing v3 |OpenTelemetry + +|1 +|`@Inject io.opentracing.Tracer legacyTracer;` +|`@Injectio.opentelemetry.api.trace.Tracer otelTracer;` + +|2 +|`@Traced` +|`@WithSpan` + +|3 +|Tag +|Attribute + +|3 +|Tags +|SemanticAttributes + +|4 +|```innerSpan.setTag("error", true); +innerSpan.setTag("error.message", e.getMessage());``` +|```innerSpan.setStatus(ERROR); +innerSpan.recordException(e);``` + +|- +|Baggage carried by SpanContext in the Span | Baggage is an independent signal propagated in parallel with the OTel Context +|Baggage is an independent signal propagated in parallel with the OTel Context, it's not part of it. +|=== + +Once the dependencies have been updated, the above class will break the build because the quickstart project is now running with OpenTelemetry. Errors like this will show up in the logs: + +[source,bash] +---- +2023-10-27 16:11:12,454 ERROR [io.qua.dep.dev.IsolatedDevModeMain] (main) Failed to start quarkus: java.lang.RuntimeException: io.quarkus.builder.BuildException: Build failure: Build failed due to errors + [error]: Build step io.quarkus.arc.deployment.ArcProcessor#validate threw an exception: jakarta.enterprise.inject.spi.DeploymentException: jakarta.enterprise.inject.UnsatisfiedResolutionException: Unsatisfied dependency for type io.opentracing.Tracer and qualifiers [@Default] +... +---- + +The new OpenTelemetry API must be used instead. This is one way to migrate the code: + +=== GreetingsResource with OpenTelemetry manual instrumentation + +[source,java] +---- +package org.acme; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.annotations.WithSpan; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +import static io.opentelemetry.api.trace.StatusCode.*; + +@Path("/hello") +@ApplicationScoped +public class GreetingResource { + + @Inject + io.opentelemetry.api.trace.Tracer otelTracer; + + @GET + @Produces(MediaType.TEXT_PLAIN) + @WithSpan(value = "Not needed, will create a new span, child of the automatic JAX-RS span") + public String hello() { + // Add a tag to the active span + Span incomingSpan = Span.current(); + incomingSpan.setAttribute(SemanticAttributes.CODE_NAMESPACE, "GreetingResource"); + + // Create a manual inner span + Span innerSpan = otelTracer.spanBuilder("Count response chars").startSpan(); + try (Scope scope = innerSpan.makeCurrent()) { + final String response = "Hello from RESTEasy Reactive"; + innerSpan.setAttribute("response-chars-count", response.length()); + return response; + } catch (Exception e) { + innerSpan.setStatus(ERROR); + innerSpan.recordException(e); + throw e; + } finally { + innerSpan.end(); + } + } +} + +---- + +Once you remove all the OpenTracing dependencies the code will build. Don't forget to double check if the traces contain the right spans. You can see them in the Jaeger UI: http://localhost:16686/. + +== The OpenTracing shim + +In this section, we present an OpenTelemetry library that can smooth the transition by providing access to the legacy OpenTracing API. This can help with the migration of large applications with many manual instrumentation points. + +To proceed with this section, the code project must be its <>. If you have changes related to the previous sections, please revert them or re-generate the project according to the <> instructions before proceeding. + +=== The dependencies + +Remove the `quarkus-smallrye-opentracing` extension and add the `quarkus-opentelemetry` extension and the `opentelemetry-opentracing-shim` library to the build file: + +The legacy extension is removed from the project: + +[source,xml,role="primary asciidoc-tabs-target-sync-cli asciidoc-tabs-target-sync-maven"] +.pom.xml +---- + + io.quarkus + quarkus-smallrye-opentracing + +---- + +[source,gradle,role="secondary asciidoc-tabs-target-sync-gradle"] +.build.gradle +---- +implementation("io.quarkus:quarkus-smallrye-opentracing") +---- + +The new one is added: + +[source,xml,role="primary asciidoc-tabs-target-sync-cli asciidoc-tabs-target-sync-maven"] +.pom.xml +---- + + io.quarkus + quarkus-opentelemetry + + + io.opentelemetry + opentelemetry-opentracing-shim + + +---- + +[source,gradle,role="secondary asciidoc-tabs-target-sync-gradle"] +.build.gradle +---- +implementation("io.quarkus:quarkus-opentelemetry") +implementation("io.quarkus:opentelemetry-opentracing-shim") +---- + +=== The code changes + +Remembering the initial version of the `GreetingResource` class from the <>: +[source, java] +---- +package org.acme; + +import io.opentracing.Scope; +import io.opentracing.Span; +import io.opentracing.tag.Tags; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import org.eclipse.microprofile.opentracing.Traced; + +@Path("/hello") +@ApplicationScoped +public class GreetingResource { + + @Inject + io.opentracing.Tracer legacyTracer; <1> + + @GET + @Produces(MediaType.TEXT_PLAIN) + @Traced(operationName = "Not needed, will change the current span name") <2> + public String hello() { + // Add a tag to the active span + legacyTracer.activeSpan().setTag(Tags.COMPONENT, "GreetingResource"); <3> + + // Create a manual inner span + Span innerSpan = legacyTracer.buildSpan("Count response chars").start(); + + try (Scope dbScope = legacyTracer.scopeManager().activate(innerSpan)) { + String response = "Hello from RESTEasy Reactive"; + innerSpan.setTag("response-chars-count", response.length()); + return response; + } catch (Exception e) { + innerSpan.setTag("error", true); + innerSpan.setTag("error.message", e.getMessage()); + throw e; + } finally { + innerSpan.finish(); + } + } +} +---- + +<1> The `Tracer` annotation must be removed and instead, we need to inject the OpenTelemetry SDK. We will need it in <3>. +<2> The `@Traced` annotation is replaced by the `@WithSpan` annotation but beware that this new annotation will always create a new Span. You shouldn't use it on JAX-RS endpoints and we only have it here for demonstration purposes. +<3> We must obtain an instance of the `legacyTracer`. The Shim includes a utility class for this purpose: `Tracer legacyTracer = OpenTracingShim.createTracerShim(openTelemetry);` + +After the changes, the code will compile and you will be able to use both the OpenTracing and OpenTelemetry APIs at the same time: + +[source,java] +---- +package org.acme; + +import io.opentelemetry.instrumentation.annotations.WithSpan; +import io.opentelemetry.opentracingshim.OpenTracingShim; +import io.opentracing.Scope; +import io.opentracing.Span; +import io.opentracing.Tracer; +import io.opentracing.tag.Tags; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +@Path("/hello") +@ApplicationScoped +public class GreetingResource { + + @Inject + io.opentelemetry.api.OpenTelemetry openTelemetry; + + @GET + @Produces(MediaType.TEXT_PLAIN) + @WithSpan(value = "Not needed, will create a new span, child of the automatic JAX-RS span") + public String hello() { + // Add a tag to the active span + Tracer legacyTracer = OpenTracingShim.createTracerShim(openTelemetry); + legacyTracer.activeSpan().setTag(Tags.COMPONENT, "GreetingResource"); + + // Create a manual inner span + Span innerSpan = legacyTracer.buildSpan("Count response chars").start(); + + try (Scope dbScope = legacyTracer.scopeManager().activate(innerSpan)) { + String response = "Hello from RESTEasy Reactive"; + innerSpan.setTag("response-chars-count", response.length()); + return response; + } catch (Exception e) { + innerSpan.setTag("error", true); + innerSpan.setTag("error.message", e.getMessage()); + throw e; + } finally { + innerSpan.finish(); + } + } +} +---- + +[IMPORTANT] +==== +It's advised not to utilize the shim for a permanent solution but solely as a tool to smooth the migration. +==== + +== Conclusion and additional resources + +This tutorial showed how to migrate an application from OpenTracing to OpenTelemetry tracing in Quarkus 3.x. + +You can find more information about the migration to OpenTelemetry at: + +* https://github.com/quarkusio/opentracing-quickstart-migration[The companion GitHub repository for this tutorial] +* https://opentelemetry.io/docs/migration/opentracing/[Migrating from OpenTracing] +* https://opentelemetry.io/docs/specs/otel/compatibility/opentracing/[OpenTracing compatibility with OpenTelemetry] diff --git a/docs/src/main/asciidoc/virtual-threads.adoc b/docs/src/main/asciidoc/virtual-threads.adoc index 732931232ddb66..eba2cde1575509 100644 --- a/docs/src/main/asciidoc/virtual-threads.adoc +++ b/docs/src/main/asciidoc/virtual-threads.adoc @@ -15,7 +15,6 @@ include::_attributes.adoc[] :thread: https://docs.oracle.com/en/java/javase/18/docs/api/java.base/java/lang/Thread.html :pgsql-driver: https://javadoc.io/doc/org.postgresql/postgresql/latest/index.html :topics: virtual-threads -:extensions: io.quarkus:quarkus-core This guide explains how to benefit from Java 21+ virtual threads in Quarkus application. diff --git a/docs/src/main/java/io/quarkus/docs/generation/YamlMetadataGenerator.java b/docs/src/main/java/io/quarkus/docs/generation/YamlMetadataGenerator.java index 162c4df7484811..f999e542440c4c 100644 --- a/docs/src/main/java/io/quarkus/docs/generation/YamlMetadataGenerator.java +++ b/docs/src/main/java/io/quarkus/docs/generation/YamlMetadataGenerator.java @@ -7,11 +7,13 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Optional; import java.util.Set; import java.util.TreeMap; @@ -27,6 +29,7 @@ import org.asciidoctor.ast.Document; import org.asciidoctor.ast.StructuralNode; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.core.exc.StreamWriteException; @@ -55,6 +58,8 @@ public class YamlMetadataGenerator { final static String INCL_ATTRIBUTES = "include::_attributes.adoc[]\n"; final static String YAML_FRONTMATTER = "---\n"; + private static final String COMPATIBILITY_TOPIC = "compatibility"; + public static void main(String[] args) throws Exception { System.out.println("[INFO] Creating YAML metadata generator: " + List.of(args)); YamlMetadataGenerator generator = new YamlMetadataGenerator() @@ -120,6 +125,8 @@ public void writeYamlFiles() throws StreamWriteException, DatabindException, IOE om.writeValue(targetDir.resolve("indexByType.yaml").toFile(), index); om.writeValue(targetDir.resolve("indexByFile.yaml").toFile(), metadata); + om.writeValue(targetDir.resolve("relations.yaml").toFile(), index.relationsByUrl(metadata)); + om.writeValue(targetDir.resolve("errorsByType.yaml").toFile(), messages); om.writeValue(targetDir.resolve("errorsByFile.yaml").toFile(), messages.allByFile()); } @@ -455,6 +462,52 @@ public Map metadataByFile() { .collect(Collectors.toMap(v -> v.filename, v -> v, (o1, o2) -> o1, TreeMap::new)); } + public Map relationsByUrl(Map metadataByFile) { + Map relationsByUrl = new TreeMap<>(); + + for (Entry currentMetadataEntry : metadataByFile.entrySet()) { + DocRelations docRelations = new DocRelations(); + + for (Entry candidateMetadataEntry : metadataByFile.entrySet()) { + if (candidateMetadataEntry.getKey().equals(currentMetadataEntry.getKey())) { + continue; + } + + DocMetadata candidateMetadata = candidateMetadataEntry.getValue(); + int extensionMatches = 0; + for (String extension : currentMetadataEntry.getValue().getExtensions()) { + if (candidateMetadata.getExtensions().contains(extension)) { + extensionMatches++; + } + } + if (extensionMatches > 0) { + docRelations.sameExtensions.add( + new DocRelation(candidateMetadata.getTitle(), candidateMetadata.getUrl(), + candidateMetadata.getType(), extensionMatches)); + } + + int topicMatches = 0; + for (String topic : currentMetadataEntry.getValue().getTopics()) { + if (candidateMetadata.getTopics().contains(topic)) { + topicMatches++; + } + } + if (topicMatches > 0 && (!candidateMetadata.getTopics().contains(COMPATIBILITY_TOPIC) + || currentMetadataEntry.getValue().getTopics().contains(COMPATIBILITY_TOPIC))) { + docRelations.sameTopics + .add(new DocRelation(candidateMetadata.getTitle(), candidateMetadata.getUrl(), + candidateMetadata.getType(), topicMatches)); + } + } + + if (!docRelations.isEmpty()) { + relationsByUrl.put(currentMetadataEntry.getValue().getUrl(), docRelations); + } + } + + return relationsByUrl; + } + // convenience public Map messagesByFile() { return messages.allByFile(); @@ -595,6 +648,78 @@ public int compareTo(DocMetadata that) { } } + @JsonInclude(value = Include.NON_EMPTY) + public static class DocRelations { + + final Set sameTopics = new TreeSet<>(DocRelationComparator.INSTANCE); + + final Set sameExtensions = new TreeSet<>(DocRelationComparator.INSTANCE); + + public Set getSameTopics() { + return sameTopics; + } + + public Set getSameExtensions() { + return sameExtensions; + } + + @JsonIgnore + public boolean isEmpty() { + return sameTopics.isEmpty() && sameExtensions.isEmpty(); + } + } + + @JsonInclude(value = Include.NON_EMPTY) + public static class DocRelation { + + final String title; + + final String url; + + final String type; + + final int matches; + + DocRelation(String title, String url, String type, int matches) { + this.title = title; + this.url = url; + this.type = type; + this.matches = matches; + } + + public String getTitle() { + return title; + } + + public String getUrl() { + return url; + } + + public String getType() { + return type; + } + + public int getMatches() { + return matches; + } + } + + public static class DocRelationComparator implements Comparator { + + static final DocRelationComparator INSTANCE = new DocRelationComparator(); + + @Override + public int compare(DocRelation o1, DocRelation o2) { + int compareMatches = o2.matches - o1.matches; + + if (compareMatches != 0) { + return compareMatches; + } + + return o1.title.compareToIgnoreCase(o2.title); + } + } + @JsonInclude(value = Include.NON_EMPTY) public static class FileMessages { Collection errors; diff --git a/docs/sync-web-site.sh b/docs/sync-web-site.sh index 3e323cd748ec01..1041831b124778 100755 --- a/docs/sync-web-site.sh +++ b/docs/sync-web-site.sh @@ -120,6 +120,15 @@ if [ -f target/indexByType.yaml ]; then echo fi +if [ -f target/relations.yaml ]; then + echo + echo "Copying target/relations.yaml to $TARGET_INDEX/relations.yaml" + mkdir -p $TARGET_INDEX + echo "# Generated file. Do not edit" > $TARGET_INDEX/relations.yaml + cat target/relations.yaml >> $TARGET_INDEX/relations.yaml + echo +fi + echo "Sync done!" echo "==========" 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 32a0ed9d061726..6c5417239d0a39 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 @@ -1,5 +1,6 @@ package io.quarkus.grpc.deployment; +import static java.lang.Boolean.FALSE; import static java.lang.Boolean.TRUE; import static java.util.Arrays.asList; @@ -56,6 +57,9 @@ public class GrpcCodeGen implements CodeGenProvider { private static final String SCAN_FOR_IMPORTS = "quarkus.generate-code.grpc.scan-for-imports"; private static final String POST_PROCESS_SKIP = "quarkus.generate.code.grpc-post-processing.skip"; + private static final String GENERATE_DESCRIPTOR_SET = "quarkus.generate-code.grpc.descriptor-set.generate"; + private static final String DESCRIPTOR_SET_OUTPUT_DIR = "quarkus.generate-code.grpc.descriptor-set.output-dir"; + private static final String DESCRIPTOR_SET_FILENAME = "quarkus.generate-code.grpc.descriptor-set.name"; private Executables executables; private String input; @@ -149,6 +153,11 @@ public boolean trigger(CodeGenContext context) throws CodeGenException { "--q-grpc_out=" + outDir, "--grpc_out=" + outDir, "--java_out=" + outDir)); + + if (shouldGenerateDescriptorSet(context.config())) { + command.add(String.format("--descriptor_set_out=%s", getDescriptorSetOutputFile(context))); + } + command.addAll(protoFiles); ProcessBuilder processBuilder = new ProcessBuilder(command); @@ -262,6 +271,25 @@ private boolean isGeneratingFromAppDependenciesEnabled(Config config) { .filter(value -> !"none".equals(value)).isPresent(); } + private boolean shouldGenerateDescriptorSet(Config config) { + return config.getOptionalValue(GENERATE_DESCRIPTOR_SET, Boolean.class).orElse(FALSE); + } + + private Path getDescriptorSetOutputFile(CodeGenContext context) throws IOException { + var dscOutputDir = context.config().getOptionalValue(DESCRIPTOR_SET_OUTPUT_DIR, String.class) + .map(context.workDir()::resolve) + .orElseGet(context::outDir); + + if (Files.notExists(dscOutputDir)) { + Files.createDirectories(dscOutputDir); + } + + var dscFilename = context.config().getOptionalValue(DESCRIPTOR_SET_FILENAME, String.class) + .orElse("descriptor_set.dsc"); + + return dscOutputDir.resolve(dscFilename).normalize(); + } + private Collection gatherDirectoriesWithImports(Path workDir, CodeGenContext context) throws CodeGenException { Config properties = context.config(); diff --git a/extensions/info/deployment/src/main/resources/dev-ui/qwc-info.js b/extensions/info/deployment/src/main/resources/dev-ui/qwc-info.js index 5960477bd1aa82..a3f8d214766862 100644 --- a/extensions/info/deployment/src/main/resources/dev-ui/qwc-info.js +++ b/extensions/info/deployment/src/main/resources/dev-ui/qwc-info.js @@ -56,7 +56,7 @@ export class QwcInfo extends LitElement { } async load() { - const response = await fetch(this._infoUrl) + const response = await fetch(this._infoUrl); const data = await response.json(); this._info = data; } @@ -72,7 +72,7 @@ export class QwcInfo extends LitElement { }else{ return html`
-
Fetching infomation...
+
Fetching information...
`; diff --git a/extensions/jdbc/jdbc-mysql/runtime/src/main/java/io/quarkus/jdbc/mysql/runtime/graal/com/mysql/cj/jdbc/MySQLJDBCSubstitutions.java b/extensions/jdbc/jdbc-mysql/runtime/src/main/java/io/quarkus/jdbc/mysql/runtime/graal/com/mysql/cj/jdbc/MySQLJDBCSubstitutions.java index faa9f278d190a2..ced7df3594206b 100644 --- a/extensions/jdbc/jdbc-mysql/runtime/src/main/java/io/quarkus/jdbc/mysql/runtime/graal/com/mysql/cj/jdbc/MySQLJDBCSubstitutions.java +++ b/extensions/jdbc/jdbc-mysql/runtime/src/main/java/io/quarkus/jdbc/mysql/runtime/graal/com/mysql/cj/jdbc/MySQLJDBCSubstitutions.java @@ -9,6 +9,14 @@ @TargetClass(className = "com.mysql.cj.protocol.a.authentication.AuthenticationOciClient") final class AuthenticationOciClient { + @Substitute + private void loadOciConfig() { + throw ExceptionFactory + .createException("OciClient authentication is not available in Quarkus when compiling to native-image:" + + " the MySQL JDBC driver team needs to cleanup the dependency requirements to make this possible." + + " If you need this resolved, please open a support request."); + } + @Substitute private void initializePrivateKey() { throw ExceptionFactory diff --git a/extensions/kafka-streams/deployment/src/main/resources/dev-ui/qwc-kafka-streams-topology.js b/extensions/kafka-streams/deployment/src/main/resources/dev-ui/qwc-kafka-streams-topology.js index 7e69d75f9976e2..98d40dd857ada0 100644 --- a/extensions/kafka-streams/deployment/src/main/resources/dev-ui/qwc-kafka-streams-topology.js +++ b/extensions/kafka-streams/deployment/src/main/resources/dev-ui/qwc-kafka-streams-topology.js @@ -1,7 +1,8 @@ import { QwcHotReloadElement, html, css } from 'qwc-hot-reload-element'; import { unsafeHTML } from 'lit/directives/unsafe-html.js'; import { JsonRpc } from 'jsonrpc'; - +import { devuiState } from 'devui-state'; +import { notifier } from 'notifier'; import { Graphviz } from "@hpcc-js/wasm/graphviz.js"; import '@vaadin/details'; @@ -52,7 +53,7 @@ export class QwcKafkaStreamsTopology extends QwcHotReloadElement { Graphviz Mermaid -

${this._tabContent}

`; +

${this._tabContent}

`; } return html` this._downloadTopologyAsPng()}> + Download as PNG + `; } else { this._tabContent = html`Graph engine not started.`; } } + _downloadTopologyAsPng() { + let svgData = this.renderRoot?.querySelector('#svgSpan').getElementsByTagName("svg")[0]; + let img = new Image(svgData.width.baseVal.value, svgData.height.baseVal.value); + img.src = `data:image/svg+xml;base64,${btoa(new XMLSerializer().serializeToString(svgData))}`; + img.onload = function () { + let cnv = document.createElement('canvas'); + cnv.width = img.width; + cnv.height = img.height; + cnv.getContext("2d").drawImage(img, 0, 0); + cnv.toBlob((blob) => { + let lnk = document.createElement('a'); + lnk.href = URL.createObjectURL(blob); + lnk.download = "Topology-" + devuiState.applicationInfo.applicationName + "-" + new Date().toISOString().replace(/\D/g,'') + ".png"; + lnk.click(); + notifier.showSuccessMessage(lnk.download + " downloaded.", 'bottom-end'); + }); + } + } + _selectDetailsTab() { this._tabContent = html` diff --git a/extensions/kafka-streams/runtime/src/main/java/io/quarkus/kafka/streams/runtime/devui/KafkaStreamsJsonRPCService.java b/extensions/kafka-streams/runtime/src/main/java/io/quarkus/kafka/streams/runtime/devui/KafkaStreamsJsonRPCService.java index fc82604f416587..24f25e424e5c2c 100644 --- a/extensions/kafka-streams/runtime/src/main/java/io/quarkus/kafka/streams/runtime/devui/KafkaStreamsJsonRPCService.java +++ b/extensions/kafka-streams/runtime/src/main/java/io/quarkus/kafka/streams/runtime/devui/KafkaStreamsJsonRPCService.java @@ -66,7 +66,7 @@ public void accept(TopologyParserContext context) { private static final RawTopologyItemParser SOURCE = new RawTopologyItemParser() { private final Pattern sourcePattern = Pattern - .compile("Source:\\s+(?\\S+)\\s+\\(topics:\\s+\\[(?.*)\\]\\).*"); + .compile("Source:\\s+(?\\S+)\\s+\\(topics:\\s+((\\[(?.*)\\])|(?.*)\\)).*"); private Matcher matcher; @Override @@ -77,7 +77,11 @@ public boolean test(String line) { @Override public void accept(TopologyParserContext context) { - context.addSources(matcher.group("source"), matcher.group("topics").split(",")); + if (matcher.group("topics") != null) { + context.addSources(matcher.group("source"), matcher.group("topics").split(",")); + } else if (matcher.group("regex") != null) { + context.addRegexSource(matcher.group("source"), matcher.group("regex")); + } } }; diff --git a/extensions/kafka-streams/runtime/src/main/java/io/quarkus/kafka/streams/runtime/devui/TopologyParserContext.java b/extensions/kafka-streams/runtime/src/main/java/io/quarkus/kafka/streams/runtime/devui/TopologyParserContext.java index cdbf1ec7c1a51d..6a047065af724a 100644 --- a/extensions/kafka-streams/runtime/src/main/java/io/quarkus/kafka/streams/runtime/devui/TopologyParserContext.java +++ b/extensions/kafka-streams/runtime/src/main/java/io/quarkus/kafka/streams/runtime/devui/TopologyParserContext.java @@ -41,6 +41,16 @@ void addSources(String source, String[] topics) { }); } + void addRegexSource(String source, String regex) { + currentNode = source; + final var cleanRegex = regex.trim(); + if (!cleanRegex.isEmpty()) { + sources.add(cleanRegex); + graphviz.addRegexSource(source, cleanRegex); + mermaid.addRegexSource(source, cleanRegex); + } + } + void addStores(String[] stores, String processor, boolean join) { currentNode = processor; Arrays.stream(stores) @@ -71,8 +81,8 @@ String toGraph() { final var res = new ArrayList(); res.add("digraph {"); - res.add(" fontname=\"Helvetica\"; fontsize=\"10\";"); - res.add(" node [style=filled fillcolor=white color=\"#C9B7DD\" shape=box fontname=\"Helvetica\" fontsize=\"10\"];"); + res.add(" fontname=Helvetica; fontsize=10;"); + res.add(" node [style=filled fillcolor=white color=\"#C9B7DD\" shape=box fontname=Helvetica fontsize=10];"); nodes.forEach(n -> res.add(' ' + n + ';')); subGraphs.entrySet().forEach(e -> { res.add(" subgraph cluster" + e.getKey() + " {"); @@ -80,11 +90,6 @@ String toGraph() { e.getValue().forEach(v -> res.add(" " + v + ';')); res.add(" }"); }); - for (int i = 0; i < subGraphs.size(); i++) { - res.add(" subgraph cluster" + i + " {"); - res.add(" label=\"Sub-Topology: " + i + "\"; color=\"#C8C879\"; bgcolor=\"#FFFFDE\";"); - res.add(" }"); - } edges.forEach(e -> res.add(' ' + e + ';')); res.add("}"); @@ -108,6 +113,15 @@ private void addSource(String source, String topic) { subGraphs.get(currentGraph).add(toId(source)); } + private void addRegexSource(String source, String regex) { + final var regexId = "REGEX_" + nodes.size(); + final var regexLabel = regex.replaceAll("\\\\", "\\\\\\\\"); + nodes.add(regexId + " [label=\"" + regexLabel + "\" shape=invhouse style=dashed margin=\"0,0\"]"); + nodes.add(toId(source) + " [label=\"" + toLabel(source) + "\"]"); + edges.add(regexId + " -> " + toId(source)); + subGraphs.get(currentGraph).add(toId(source)); + } + private void addTarget(String target, String node) { nodes.add(toId(target) + " [label=\"" + toLabel(target) + "\"]"); edges.add(toId(node) + " -> " + toId(target)); @@ -164,6 +178,10 @@ private void addSource(String source, String topic) { endpoints.add(topic + '[' + topic + "] --> " + source + '(' + toName(source) + ')'); } + private void addRegexSource(String source, String regex) { + endpoints.add("REGEX_" + endpoints.size() + '[' + regex + "] --> " + source + '(' + toName(source) + ')'); + } + private void addTarget(String target, String node) { subTopologies.add(' ' + node + '[' + toName(node) + "] --> " + target + '(' + toName(target) + ')'); } diff --git a/extensions/kafka-streams/runtime/src/test/java/io/quarkus/kafka/streams/runtime/devui/KafkaStreamsJsonRPCServiceTest.java b/extensions/kafka-streams/runtime/src/test/java/io/quarkus/kafka/streams/runtime/devui/KafkaStreamsJsonRPCServiceTest.java index 8c40491616d87e..854ac92d972512 100644 --- a/extensions/kafka-streams/runtime/src/test/java/io/quarkus/kafka/streams/runtime/devui/KafkaStreamsJsonRPCServiceTest.java +++ b/extensions/kafka-streams/runtime/src/test/java/io/quarkus/kafka/streams/runtime/devui/KafkaStreamsJsonRPCServiceTest.java @@ -31,66 +31,77 @@ public void shouldParsingStayConstant() { + " --> KSTREAM-SINK-0000000007\n" + " <-- KSTREAM-AGGREGATE-0000000005\n" + " Sink: KSTREAM-SINK-0000000007 (topic: temperatures-aggregated)\n" - + " <-- KTABLE-TOSTREAM-0000000006"; + + " <-- KTABLE-TOSTREAM-0000000006\n" + + "\n" + + " Sub-topology: 2\n" + + " Source: KSTREAM-SOURCE-0000000008 (topics: notification\\..+)\n" + + " --> KSTREAM-FOREACH-0000000009\n" + + " Processor: KSTREAM-FOREACH-0000000009 (stores: [])\n" + + " --> none\n" + + " <-- KSTREAM-SOURCE-0000000008"; final var actual = rpcService.parseTopologyDescription(expectedDescribe); assertEquals(expectedDescribe, actual.getString("describe")); - assertEquals("[0, 1]", actual.getString("subTopologies")); - assertEquals("[temperature-values, weather-stations]", actual.getString("sources")); + assertEquals("[0, 1, 2]", actual.getString("subTopologies")); + assertEquals("[notification\\..+, temperature-values, weather-stations]", actual.getString("sources")); assertEquals("[temperatures-aggregated]", actual.getString("sinks")); assertEquals("[weather-stations-STATE-STORE-0000000000, weather-stations-store]", actual.getString("stores")); - assertEquals("digraph {\n" + - " fontname=\"Helvetica\"; fontsize=\"10\";\n" + - " node [style=filled fillcolor=white color=\"#C9B7DD\" shape=box fontname=\"Helvetica\" fontsize=\"10\"];\n" + - " weather_stations [label=\"weather\\nstations\" shape=invhouse margin=\"0,0\"];\n" + - " KSTREAM_SOURCE_0000000001 [label=\"KSTREAM\\nSOURCE\\n0000000001\"];\n" + - " KTABLE_SOURCE_0000000002 [label=\"KTABLE\\nSOURCE\\n0000000002\"];\n" + - " weather_stations_STATE_STORE_0000000000 [label=\"weather\\nstations\\nSTATE\\nSTORE\\n0000000000\" shape=cylinder];\n" - + - " temperature_values [label=\"temperature\\nvalues\" shape=invhouse margin=\"0,0\"];\n" + - " KSTREAM_SOURCE_0000000003 [label=\"KSTREAM\\nSOURCE\\n0000000003\"];\n" + - " KSTREAM_LEFTJOIN_0000000004 [label=\"KSTREAM\\nLEFTJOIN\\n0000000004\"];\n" + - " KSTREAM_AGGREGATE_0000000005 [label=\"KSTREAM\\nAGGREGATE\\n0000000005\"];\n" + - " weather_stations_store [label=\"weather\\nstations\\nstore\" shape=cylinder];\n" + - " KTABLE_TOSTREAM_0000000006 [label=\"KTABLE\\nTOSTREAM\\n0000000006\"];\n" + - " KSTREAM_SINK_0000000007 [label=\"KSTREAM\\nSINK\\n0000000007\"];\n" + - " temperatures_aggregated [label=\"temperatures\\naggregated\" shape=house margin=\"0,0\"];\n" + - " subgraph cluster0 {\n" + - " label=\"Sub-Topology: 0\"; color=\"#C8C879\"; bgcolor=\"#FFFFDE\";\n" + - " KSTREAM_SOURCE_0000000001;\n" + - " KTABLE_SOURCE_0000000002;\n" + - " }\n" + - " subgraph cluster1 {\n" + - " label=\"Sub-Topology: 1\"; color=\"#C8C879\"; bgcolor=\"#FFFFDE\";\n" + - " KSTREAM_SOURCE_0000000003;\n" + - " KSTREAM_LEFTJOIN_0000000004;\n" + - " KSTREAM_AGGREGATE_0000000005;\n" + - " KTABLE_TOSTREAM_0000000006;\n" + - " KSTREAM_SINK_0000000007;\n" + - " }\n" + - " subgraph cluster0 {\n" + - " label=\"Sub-Topology: 0\"; color=\"#C8C879\"; bgcolor=\"#FFFFDE\";\n" + - " }\n" + - " subgraph cluster1 {\n" + - " label=\"Sub-Topology: 1\"; color=\"#C8C879\"; bgcolor=\"#FFFFDE\";\n" + - " }\n" + - " weather_stations -> KSTREAM_SOURCE_0000000001;\n" + - " KSTREAM_SOURCE_0000000001 -> KTABLE_SOURCE_0000000002;\n" + - " KTABLE_SOURCE_0000000002 -> weather_stations_STATE_STORE_0000000000;\n" + - " temperature_values -> KSTREAM_SOURCE_0000000003;\n" + - " KSTREAM_SOURCE_0000000003 -> KSTREAM_LEFTJOIN_0000000004;\n" + - " KSTREAM_LEFTJOIN_0000000004 -> KSTREAM_AGGREGATE_0000000005;\n" + - " KSTREAM_AGGREGATE_0000000005 -> weather_stations_store;\n" + - " KSTREAM_AGGREGATE_0000000005 -> KTABLE_TOSTREAM_0000000006;\n" + - " KTABLE_TOSTREAM_0000000006 -> KSTREAM_SINK_0000000007;\n" + - " KSTREAM_SINK_0000000007 -> temperatures_aggregated;\n" + - "}", actual.getString("graphviz")); + assertEquals("digraph {\n" + + " fontname=Helvetica; fontsize=10;\n" + + " node [style=filled fillcolor=white color=\"#C9B7DD\" shape=box fontname=Helvetica fontsize=10];\n" + + " weather_stations [label=\"weather\\nstations\" shape=invhouse margin=\"0,0\"];\n" + + " KSTREAM_SOURCE_0000000001 [label=\"KSTREAM\\nSOURCE\\n0000000001\"];\n" + + " KTABLE_SOURCE_0000000002 [label=\"KTABLE\\nSOURCE\\n0000000002\"];\n" + + " weather_stations_STATE_STORE_0000000000 [label=\"weather\\nstations\\nSTATE\\nSTORE\\n0000000000\" shape=cylinder];\n" + + " temperature_values [label=\"temperature\\nvalues\" shape=invhouse margin=\"0,0\"];\n" + + " KSTREAM_SOURCE_0000000003 [label=\"KSTREAM\\nSOURCE\\n0000000003\"];\n" + + " KSTREAM_LEFTJOIN_0000000004 [label=\"KSTREAM\\nLEFTJOIN\\n0000000004\"];\n" + + " KSTREAM_AGGREGATE_0000000005 [label=\"KSTREAM\\nAGGREGATE\\n0000000005\"];\n" + + " weather_stations_store [label=\"weather\\nstations\\nstore\" shape=cylinder];\n" + + " KTABLE_TOSTREAM_0000000006 [label=\"KTABLE\\nTOSTREAM\\n0000000006\"];\n" + + " KSTREAM_SINK_0000000007 [label=\"KSTREAM\\nSINK\\n0000000007\"];\n" + + " temperatures_aggregated [label=\"temperatures\\naggregated\" shape=house margin=\"0,0\"];\n" + + " REGEX_12 [label=\"notification\\\\..+\" shape=invhouse style=dashed margin=\"0,0\"];\n" + + " KSTREAM_SOURCE_0000000008 [label=\"KSTREAM\\nSOURCE\\n0000000008\"];\n" + + " KSTREAM_FOREACH_0000000009 [label=\"KSTREAM\\nFOREACH\\n0000000009\"];\n" + + " subgraph cluster0 {\n" + + " label=\"Sub-Topology: 0\"; color=\"#C8C879\"; bgcolor=\"#FFFFDE\";\n" + + " KSTREAM_SOURCE_0000000001;\n" + + " KTABLE_SOURCE_0000000002;\n" + + " }\n" + + " subgraph cluster1 {\n" + + " label=\"Sub-Topology: 1\"; color=\"#C8C879\"; bgcolor=\"#FFFFDE\";\n" + + " KSTREAM_SOURCE_0000000003;\n" + + " KSTREAM_LEFTJOIN_0000000004;\n" + + " KSTREAM_AGGREGATE_0000000005;\n" + + " KTABLE_TOSTREAM_0000000006;\n" + + " KSTREAM_SINK_0000000007;\n" + + " }\n" + + " subgraph cluster2 {\n" + + " label=\"Sub-Topology: 2\"; color=\"#C8C879\"; bgcolor=\"#FFFFDE\";\n" + + " KSTREAM_SOURCE_0000000008;\n" + + " KSTREAM_FOREACH_0000000009;\n" + + " }\n" + + " weather_stations -> KSTREAM_SOURCE_0000000001;\n" + + " KSTREAM_SOURCE_0000000001 -> KTABLE_SOURCE_0000000002;\n" + + " KTABLE_SOURCE_0000000002 -> weather_stations_STATE_STORE_0000000000;\n" + + " temperature_values -> KSTREAM_SOURCE_0000000003;\n" + + " KSTREAM_SOURCE_0000000003 -> KSTREAM_LEFTJOIN_0000000004;\n" + + " KSTREAM_LEFTJOIN_0000000004 -> KSTREAM_AGGREGATE_0000000005;\n" + + " KSTREAM_AGGREGATE_0000000005 -> weather_stations_store;\n" + + " KSTREAM_AGGREGATE_0000000005 -> KTABLE_TOSTREAM_0000000006;\n" + + " KTABLE_TOSTREAM_0000000006 -> KSTREAM_SINK_0000000007;\n" + + " KSTREAM_SINK_0000000007 -> temperatures_aggregated;\n" + + " REGEX_12 -> KSTREAM_SOURCE_0000000008;\n" + + " KSTREAM_SOURCE_0000000008 -> KSTREAM_FOREACH_0000000009;\n" + + "}", actual.getString("graphviz")); assertEquals("graph TD\n" + " weather-stations[weather-stations] --> KSTREAM-SOURCE-0000000001(KSTREAM-
SOURCE-
0000000001)\n" + " KTABLE-SOURCE-0000000002[KTABLE-
SOURCE-
0000000002] --> weather-stations-STATE-STORE-0000000000(weather-
stations-
STATE-
STORE-
0000000000)\n" + " temperature-values[temperature-values] --> KSTREAM-SOURCE-0000000003(KSTREAM-
SOURCE-
0000000003)\n" + " KSTREAM-AGGREGATE-0000000005[KSTREAM-
AGGREGATE-
0000000005] --> weather-stations-store(weather-
stations-
store)\n" + " KSTREAM-SINK-0000000007[KSTREAM-
SINK-
0000000007] --> temperatures-aggregated(temperatures-aggregated)\n" + + " REGEX_5[notification\\..+] --> KSTREAM-SOURCE-0000000008(KSTREAM-
SOURCE-
0000000008)\n" + " subgraph Sub-Topology: 0\n" + " KSTREAM-SOURCE-0000000001[KSTREAM-
SOURCE-
0000000001] --> KTABLE-SOURCE-0000000002(KTABLE-
SOURCE-
0000000002)\n" + " end\n" @@ -99,6 +110,9 @@ public void shouldParsingStayConstant() { + " KSTREAM-LEFTJOIN-0000000004[KSTREAM-
LEFTJOIN-
0000000004] --> KSTREAM-AGGREGATE-0000000005(KSTREAM-
AGGREGATE-
0000000005)\n" + " KSTREAM-AGGREGATE-0000000005[KSTREAM-
AGGREGATE-
0000000005] --> KTABLE-TOSTREAM-0000000006(KTABLE-
TOSTREAM-
0000000006)\n" + " KTABLE-TOSTREAM-0000000006[KTABLE-
TOSTREAM-
0000000006] --> KSTREAM-SINK-0000000007(KSTREAM-
SINK-
0000000007)\n" + + " end\n" + + " subgraph Sub-Topology: 2\n" + + " KSTREAM-SOURCE-0000000008[KSTREAM-
SOURCE-
0000000008] --> KSTREAM-FOREACH-0000000009(KSTREAM-
FOREACH-
0000000009)\n" + " end", actual.getString("mermaid")); } } diff --git a/extensions/narayana-jta/runtime/pom.xml b/extensions/narayana-jta/runtime/pom.xml index 7fb2ff30c5230a..8b3389ee5881fc 100644 --- a/extensions/narayana-jta/runtime/pom.xml +++ b/extensions/narayana-jta/runtime/pom.xml @@ -73,6 +73,10 @@ jakarta.enterprise jakarta.enterprise.lang-model + + jakarta.ejb + jakarta.ejb-api + diff --git a/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/OidcClientImpl.java b/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/OidcClientImpl.java index 58e8018629860e..0f0252a3a003e6 100644 --- a/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/OidcClientImpl.java +++ b/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/OidcClientImpl.java @@ -17,7 +17,7 @@ import io.quarkus.oidc.client.OidcClientConfig; import io.quarkus.oidc.client.OidcClientException; import io.quarkus.oidc.client.Tokens; -import io.quarkus.oidc.common.OidcClientRequestFilter; +import io.quarkus.oidc.common.OidcRequestFilter; import io.quarkus.oidc.common.runtime.OidcCommonUtils; import io.quarkus.oidc.common.runtime.OidcConstants; import io.smallrye.mutiny.Uni; @@ -46,12 +46,12 @@ public class OidcClientImpl implements OidcClient { private final String clientSecretBasicAuthScheme; private final Key clientJwtKey; private final OidcClientConfig oidcConfig; - private final List filters; + private final List filters; private volatile boolean closed; public OidcClientImpl(WebClient client, String tokenRequestUri, String tokenRevokeUri, String grantType, MultiMap tokenGrantParams, MultiMap commonRefreshGrantParams, OidcClientConfig oidcClientConfig, - List filters) { + List filters) { this.client = client; this.tokenRequestUri = tokenRequestUri; this.tokenRevokeUri = tokenRevokeUri; @@ -260,8 +260,8 @@ private void checkClosed() { } private HttpRequest filter(HttpRequest request, Buffer body) { - for (OidcClientRequestFilter filter : filters) { - filter.filter(request, body); + for (OidcRequestFilter filter : filters) { + filter.filter(request, body, null); } return request; } diff --git a/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/OidcClientRecorder.java b/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/OidcClientRecorder.java index 771f8621401b23..88004463f2e5f0 100644 --- a/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/OidcClientRecorder.java +++ b/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/OidcClientRecorder.java @@ -17,7 +17,7 @@ import io.quarkus.oidc.client.OidcClientException; import io.quarkus.oidc.client.OidcClients; import io.quarkus.oidc.client.Tokens; -import io.quarkus.oidc.common.OidcClientRequestFilter; +import io.quarkus.oidc.common.OidcRequestFilter; import io.quarkus.oidc.common.runtime.OidcCommonUtils; import io.quarkus.oidc.common.runtime.OidcConstants; import io.quarkus.runtime.TlsConfig; @@ -122,7 +122,7 @@ protected static Uni createOidcClientUni(OidcClientConfig oidcConfig WebClient client = WebClient.create(new io.vertx.mutiny.core.Vertx(vertx.get()), options); - List clientRequestFilters = OidcCommonUtils.getClientRequestCustomizer(); + List clientRequestFilters = OidcCommonUtils.getClientRequestCustomizer(); Uni tokenUrisUni = null; if (OidcCommonUtils.isAbsoluteUrl(oidcConfig.tokenPath)) { @@ -211,7 +211,7 @@ private static void setGrantClientParams(OidcClientConfig oidcConfig, MultiMap g } private static Uni discoverTokenUris(WebClient client, - List clientRequestFilters, + List clientRequestFilters, String authServerUrl, OidcClientConfig oidcConfig) { final long connectionDelayInMillisecs = OidcCommonUtils.getConnectionDelayInMillis(oidcConfig); return OidcCommonUtils.discoverMetadata(client, clientRequestFilters, authServerUrl, connectionDelayInMillisecs) diff --git a/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/OidcClientRequestFilter.java b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/OidcClientRequestFilter.java deleted file mode 100644 index d8d653381329a0..00000000000000 --- a/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/OidcClientRequestFilter.java +++ /dev/null @@ -1,17 +0,0 @@ -package io.quarkus.oidc.common; - -import io.vertx.mutiny.core.buffer.Buffer; -import io.vertx.mutiny.ext.web.client.HttpRequest; - -/** - * Request filter which can be used to customize OIDC client requests - */ -public interface OidcClientRequestFilter { - /** - * Filter OIDC client requests - * - * @param request HTTP request - * @param body request body, will be null for HTTP GET methods, may be null for other HTTP methods - */ - void filter(HttpRequest request, Buffer body); -} diff --git a/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/OidcRequestContextProperties.java b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/OidcRequestContextProperties.java new file mode 100644 index 00000000000000..d7a1f620a48af7 --- /dev/null +++ b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/OidcRequestContextProperties.java @@ -0,0 +1,28 @@ +package io.quarkus.oidc.common; + +import java.util.Map; + +public class OidcRequestContextProperties { + + public static String TOKEN = "token"; + public static String TOKEN_CREDENTIAL = "token_credential"; + + private final Map properties; + + public OidcRequestContextProperties(Map properties) { + this.properties = properties; + } + + public Object get(String name) { + return properties.get(name); + } + + public String getString(String name) { + return (String) get(name); + } + + public T get(String name, Class type) { + return type.cast(get(name)); + } + +} diff --git a/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/OidcRequestFilter.java b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/OidcRequestFilter.java new file mode 100644 index 00000000000000..7318f34eff3b17 --- /dev/null +++ b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/OidcRequestFilter.java @@ -0,0 +1,19 @@ +package io.quarkus.oidc.common; + +import io.vertx.mutiny.core.buffer.Buffer; +import io.vertx.mutiny.ext.web.client.HttpRequest; + +/** + * Request filter which can be used to customize requests such as the verification JsonWebKey set and token grant requests + * which are made from the OIDC adapter to the OIDC provider + */ +public interface OidcRequestFilter { + /** + * Filter OIDC requests + * + * @param request HTTP request that can have its headers customized + * @param body request body, will be null for HTTP GET methods, may be null for other HTTP methods + * @param contextProperties context properties that can be available in context of some requests, can be null + */ + void filter(HttpRequest request, Buffer requestBody, OidcRequestContextProperties contextProperties); +} diff --git a/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcCommonConfig.java b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcCommonConfig.java index 1d2692ff68939a..45b1923d5d805c 100644 --- a/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcCommonConfig.java +++ b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcCommonConfig.java @@ -61,13 +61,12 @@ public class OidcCommonConfig { public Optional connectionDelay = Optional.empty(); /** - * The number of times an attempt to re-establish an already available connection will be repeated for. - * Note this property is different to the `connection-delay` property which is only effective during the initial OIDC + * The number of times an attempt to re-establish an already available connection will be repeated. + * Note this property is different from the `connection-delay` property, which is only effective during the initial OIDC * connection creation. - * This property is used to try to recover the existing connection which may have been temporarily lost. - * For example, if a request to the OIDC token endpoint fails due to a connection exception then the request will be retried - * for - * a number of times configured by this property. + * This property is used to try to recover an existing connection that may have been temporarily lost. + * For example, if a request to the OIDC token endpoint fails due to a connection exception, then the request will be + * retried the number of times configured by this property. */ @ConfigItem(defaultValue = "3") public int connectionRetryCount = 3; @@ -167,13 +166,13 @@ public static enum Method { BASIC, /** - * client_secret_post: client id and secret are submitted as the 'client_id' and 'client_secret' form + * client_secret_post: client id and secret are submitted as the `client_id` and `client_secret` form * parameters. */ POST, /** - * client_secret_jwt: client id and generated JWT secret are submitted as the 'client_id' and 'client_secret' + * client_secret_jwt: client id and generated JWT secret are submitted as the `client_id` and `client_secret` * form * parameters. */ @@ -224,7 +223,7 @@ public void setSecretProvider(Provider secretProvider) { } /** - * Supports the client authentication 'client_secret_jwt' and 'private_key_jwt' methods which involve sending a JWT + * Supports the client authentication 'client_secret_jwt' and `private_key_jwt` methods which involve sending a JWT * token * assertion signed with either a client secret or private key. * @@ -253,13 +252,13 @@ public static class Jwt { public Optional keyFile = Optional.empty(); /** - * If provided, indicates that JWT is signed using a private key from a key store + * If provided, indicates that JWT is signed using a private key from a keystore */ @ConfigItem public Optional keyStoreFile = Optional.empty(); /** - * A parameter to specify the password of the key store file. + * A parameter to specify the password of the keystore file. */ @ConfigItem public Optional keyStorePassword; @@ -290,7 +289,7 @@ public static class Jwt { public Optional tokenKeyId = Optional.empty(); /** - * Issuer of the signing key added as a JWT 'iss' claim (default: client id) + * Issuer of the signing key added as a JWT `iss` claim (default: client id) */ @ConfigItem public Optional issuer = Optional.empty(); @@ -442,41 +441,41 @@ public enum Verification { } /** - * Certificate validation and hostname verification, which can be one of the following values from enum - * {@link Verification}. Default is required. + * Certificate validation and hostname verification, which can be one of the following {@link Verification} values. + * Default is required. */ @ConfigItem public Optional verification = Optional.empty(); /** - * An optional key store which holds the certificate information instead of specifying separate files. + * An optional keystore which holds the certificate information instead of specifying separate files. */ @ConfigItem public Optional keyStoreFile = Optional.empty(); /** - * An optional parameter to specify type of the key store file. If not given, the type is automatically detected + * An optional parameter to specify type of the keystore file. If not given, the type is automatically detected * based on the file name. */ @ConfigItem public Optional keyStoreFileType = Optional.empty(); /** - * An optional parameter to specify a provider of the key store file. If not given, the provider is automatically + * An optional parameter to specify a provider of the keystore file. If not given, the provider is automatically * detected - * based on the key store file type. + * based on the keystore file type. */ @ConfigItem public Optional keyStoreProvider; /** - * A parameter to specify the password of the key store file. If not given, the default ("password") is used. + * A parameter to specify the password of the keystore file. If not given, the default ("password") is used. */ @ConfigItem public Optional keyStorePassword; /** - * An optional parameter to select a specific key in the key store. When SNI is disabled, if the key store contains + * An optional parameter to select a specific key in the keystore. When SNI is disabled, if the keystore contains * multiple * keys and no alias is specified, the behavior is undefined. */ @@ -490,34 +489,34 @@ public enum Verification { public Optional keyStoreKeyPassword = Optional.empty(); /** - * An optional trust store which holds the certificate information of the certificates to trust + * An optional truststore which holds the certificate information of the certificates to trust */ @ConfigItem public Optional trustStoreFile = Optional.empty(); /** - * A parameter to specify the password of the trust store file. + * A parameter to specify the password of the truststore file. */ @ConfigItem public Optional trustStorePassword = Optional.empty(); /** - * A parameter to specify the alias of the trust store certificate. + * A parameter to specify the alias of the truststore certificate. */ @ConfigItem public Optional trustStoreCertAlias = Optional.empty(); /** - * An optional parameter to specify type of the trust store file. If not given, the type is automatically detected + * An optional parameter to specify type of the truststore file. If not given, the type is automatically detected * based on the file name. */ @ConfigItem public Optional trustStoreFileType = Optional.empty(); /** - * An optional parameter to specify a provider of the trust store file. If not given, the provider is automatically + * An optional parameter to specify a provider of the truststore file. If not given, the provider is automatically * detected - * based on the trust store file type. + * based on the truststore file type. */ @ConfigItem public Optional trustStoreProvider; diff --git a/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcCommonUtils.java b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcCommonUtils.java index f5c74bdbb4c427..3b0e9dbdc8f768 100644 --- a/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcCommonUtils.java +++ b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcCommonUtils.java @@ -30,7 +30,7 @@ import io.quarkus.arc.ArcContainer; import io.quarkus.credentials.CredentialsProvider; import io.quarkus.credentials.runtime.CredentialsProviderFinder; -import io.quarkus.oidc.common.OidcClientRequestFilter; +import io.quarkus.oidc.common.OidcRequestFilter; import io.quarkus.oidc.common.runtime.OidcCommonConfig.Credentials; import io.quarkus.oidc.common.runtime.OidcCommonConfig.Credentials.Provider; import io.quarkus.oidc.common.runtime.OidcCommonConfig.Credentials.Secret; @@ -427,12 +427,12 @@ public static Predicate oidcEndpointNotAvailable() { || (t instanceof OidcEndpointAccessException && ((OidcEndpointAccessException) t).getErrorStatus() == 404)); } - public static Uni discoverMetadata(WebClient client, List filters, + public static Uni discoverMetadata(WebClient client, List filters, String authServerUrl, long connectionDelayInMillisecs) { final String discoveryUrl = authServerUrl + OidcConstants.WELL_KNOWN_CONFIGURATION; HttpRequest request = client.getAbs(discoveryUrl); - for (OidcClientRequestFilter filter : filters) { - filter.filter(request, null); + for (OidcRequestFilter filter : filters) { + filter.filter(request, null, null); } return request.send().onItem().transform(resp -> { if (resp.statusCode() == 200) { @@ -478,10 +478,10 @@ private static byte[] doRead(InputStream is) throws IOException { return out.toByteArray(); } - public static List getClientRequestCustomizer() { + public static List getClientRequestCustomizer() { ArcContainer container = Arc.container(); if (container != null) { - return container.listAll(OidcClientRequestFilter.class).stream().map(handle -> handle.get()) + return container.listAll(OidcRequestFilter.class).stream().map(handle -> handle.get()) .collect(Collectors.toList()); } return List.of(); diff --git a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/OidcBuildTimeConfig.java b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/OidcBuildTimeConfig.java index b9e2edd863702d..2a25611394c301 100644 --- a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/OidcBuildTimeConfig.java +++ b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/OidcBuildTimeConfig.java @@ -22,8 +22,8 @@ public class OidcBuildTimeConfig { public DevUiConfig devui; /** * Enable the registration of the Default TokenIntrospection and UserInfo Cache implementation bean. - * Note it only allows to use the default implementation, one needs to configure it in order to activate it, - * please see {@link OidcConfig#tokenCache}. + * Note: This only enables the default implementation. It requires configuration to be activated. + * See {@link OidcConfig#tokenCache}. */ @ConfigItem(defaultValue = "true") public boolean defaultTokenCacheEnabled; diff --git a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/DevServicesConfig.java b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/DevServicesConfig.java index 624675b4fd05c9..05a69f6df10150 100644 --- a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/DevServicesConfig.java +++ b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/DevServicesConfig.java @@ -23,11 +23,11 @@ public class DevServicesConfig { public boolean enabled = true; /** - * The container image name to use, for container based DevServices providers. + * The container image name to use, for container-based DevServices providers. * * Image with a Quarkus based distribution is used by default. * Image with a WildFly based distribution can be selected instead, for example: - * 'quay.io/keycloak/keycloak:19.0.3-legacy'. + * `quay.io/keycloak/keycloak:19.0.3-legacy`. *

* Note Keycloak Quarkus and Keycloak WildFly images are initialized differently. * By default, Dev Services for Keycloak will assume it is a Keycloak Quarkus image if the image version does not end with a @@ -106,17 +106,17 @@ public class DevServicesConfig { /** * The Keycloak realm name. - * This property will be used to create the realm if the realm file pointed to by the 'realm-path' property does not exist, - * default value is 'quarkus' in this case. - * If the realm file pointed to by the 'realm-path' property exists then it is still recommended to set this property - * for Dev Services for Keycloak to avoid parsing the realm file in order to determine the realm name. + * This property will be used to create the realm if the realm file pointed to by the `realm-path` property does not exist, + * default value is `quarkus` in this case. + * If the realm file pointed to by the `realm-path` property exists then it is still recommended to set this property + * for Dev Services for Keycloak to avoid parsing the realm file to determine the realm name. * */ @ConfigItem public Optional realmName; /** - * Indicates if the Keycloak realm has to be created when the realm file pointed to by the 'realm-path' property does not + * Indicates if the Keycloak realm has to be created when the realm file pointed to by the `realm-path` property does not * exist. * * Disable it if you'd like to create a realm using Keycloak Administration Console @@ -128,7 +128,7 @@ public class DevServicesConfig { /** * The Keycloak users map containing the username and password pairs. * If this map is empty then two users, 'alice' and 'bob' with the passwords matching their names will be created. - * This property will be used to create the Keycloak users if the realm file pointed to by the 'realm-path' property does + * This property will be used to create the Keycloak users if the realm file pointed to by the `realm-path` property does * not exist. */ @ConfigItem @@ -138,7 +138,7 @@ public class DevServicesConfig { * The Keycloak user roles. * If this map is empty then a user named 'alice' will get 'admin' and 'user' roles and all other users will get a 'user' * role. - * This property will be used to create the Keycloak roles if the realm file pointed to by the 'realm-path' property does + * This property will be used to create the Keycloak roles if the realm file pointed to by the `realm-path` property does * not exist. */ @ConfigItem diff --git a/extensions/oidc/runtime/pom.xml b/extensions/oidc/runtime/pom.xml index 2833e27e5ffd9c..1c5145df46d567 100644 --- a/extensions/oidc/runtime/pom.xml +++ b/extensions/oidc/runtime/pom.xml @@ -50,6 +50,11 @@ quarkus-junit5-internal test + + org.awaitility + awaitility + test + diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfig.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfig.java index 48d25c15adfa43..ccee7c9ca76c50 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfig.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfig.java @@ -40,7 +40,7 @@ public class OidcTenantConfig extends OidcCommonConfig { public boolean tenantEnabled = true; /** - * The application type, which can be one of the following values from enum {@link ApplicationType}. + * The application type, which can be one of the following {@link ApplicationType} values. */ @ConfigItem(defaultValueDocumentation = "service") public Optional applicationType = Optional.empty(); @@ -54,9 +54,9 @@ public class OidcTenantConfig extends OidcCommonConfig { public Optional authorizationPath = Optional.empty(); /** - * Relative path or absolute URL of the OIDC userinfo endpoint. + * Relative path or absolute URL of the OIDC UserInfo endpoint. * This property must only be set for the 'web-app' applications if OIDC discovery is disabled - * and 'authentication.user-info-required' property is enabled. + * and `authentication.user-info-required` property is enabled. * This property will be ignored if the discovery is enabled. */ @ConfigItem @@ -189,7 +189,7 @@ public void setIncludeClientId(boolean includeClientId) { /** * Allow caching the token introspection data. * Note enabling this property does not enable the cache itself but only permits to cache the token introspection - * for a given tenant. If the default token cache can be used then please see {@link OidcConfig.TokenCache} how to enable + * for a given tenant. If the default token cache can be used, see {@link OidcConfig.TokenCache} to enable * it. */ @ConfigItem(defaultValue = "true") @@ -198,7 +198,7 @@ public void setIncludeClientId(boolean includeClientId) { /** * Allow caching the user info data. * Note enabling this property does not enable the cache itself but only permits to cache the user info data - * for a given tenant. If the default token cache can be used then please see {@link OidcConfig.TokenCache} how to enable + * for a given tenant. If the default token cache can be used, see {@link OidcConfig.TokenCache} to enable * it. */ @ConfigItem(defaultValue = "true") @@ -381,6 +381,81 @@ public void setCleanUpTimerInterval(Duration cleanUpTimerInterval) { } } + /** + * Configuration for controlling how JsonWebKeySet containing verification keys should be acquired and managed. + */ + @ConfigItem + public Jwks jwks = new Jwks(); + + @ConfigGroup + public static class Jwks { + + /** + * If JWK verification keys should be fetched at the moment a connection to the OIDC provider + * is initialized. + *

+ * Disabling this property will delay the key acquisition until the moment the current token + * has to be verified. Typically it can only be necessary if the token or other telated request properties + * provide an additional context which is required to resolve the keys correctly. + */ + @ConfigItem(defaultValue = "true") + public boolean resolveEarly = true; + + /** + * Maximum number of JWK keys that can be cached. + * This property will be ignored if the {@link #resolveEarly} property is set to true. + */ + @ConfigItem(defaultValue = "10") + public int cacheSize = 10; + + /** + * Number of minutes a JWK key can be cached for. + * This property will be ignored if the {@link #resolveEarly} property is set to true. + */ + @ConfigItem(defaultValue = "10M") + public Duration cacheTimeToLive = Duration.ofMinutes(10); + + /** + * Cache timer interval. + * If this property is set then a timer will check and remove the stale entries periodically. + * This property will be ignored if the {@link #resolveEarly} property is set to true. + */ + @ConfigItem + public Optional cleanUpTimerInterval = Optional.empty(); + + public int getCacheSize() { + return cacheSize; + } + + public void setCacheSize(int cacheSize) { + this.cacheSize = cacheSize; + } + + public Duration getCacheTimeToLive() { + return cacheTimeToLive; + } + + public void setCacheTimeToLive(Duration cacheTimeToLive) { + this.cacheTimeToLive = cacheTimeToLive; + } + + public Optional getCleanUpTimerInterval() { + return cleanUpTimerInterval; + } + + public void setCleanUpTimerInterval(Duration cleanUpTimerInterval) { + this.cleanUpTimerInterval = Optional.of(cleanUpTimerInterval); + } + + public boolean isResolveEarly() { + return resolveEarly; + } + + public void setResolveEarly(boolean resolveEarly) { + this.resolveEarly = resolveEarly; + } + } + @ConfigGroup public static class Frontchannel { /** @@ -689,7 +764,7 @@ public enum CookieSameSite { */ public enum ResponseMode { /** - * Authorization response parameters are encoded in the query string added to the redirect_uri + * Authorization response parameters are encoded in the query string added to the `redirect_uri` */ QUERY, @@ -707,19 +782,19 @@ public enum ResponseMode { public Optional responseMode = Optional.empty(); /** - * Relative path for calculating a "redirect_uri" query parameter. + * Relative path for calculating a `redirect_uri` query parameter. * It has to start from a forward slash and will be appended to the request URI's host and port. - * For example, if the current request URI is 'https://localhost:8080/service' then a 'redirect_uri' parameter + * For example, if the current request URI is 'https://localhost:8080/service' then a `redirect_uri` parameter * will be set to 'https://localhost:8080/' if this property is set to '/' and be the same as the request URI * if this property has not been configured. * Note the original request URI will be restored after the user has authenticated if 'restorePathAfterRedirect' is set - * to 'true'. + * to `true`. */ @ConfigItem public Optional redirectPath = Optional.empty(); /** - * If this property is set to 'true' then the original request URI which was used before + * If this property is set to `true` then the original request URI which was used before * the authentication will be restored after the user has been redirected back to the application. * * Note if `redirectPath` property is not set, the original request URI will be restored even if this property is @@ -737,8 +812,8 @@ public enum ResponseMode { /** * Relative path to the public endpoint which will process the error response from the OIDC authorization endpoint. - * If the user authentication has failed then the OIDC provider will return an 'error' and an optional - * 'error_description' + * If the user authentication has failed then the OIDC provider will return an `error` and an optional + * `error_description` * parameters, instead of the expected authorization 'code'. * * If this property is set then the user will be redirected to the endpoint which can return a user-friendly @@ -769,7 +844,7 @@ public enum ResponseMode { public boolean verifyAccessToken; /** - * Force 'https' as the 'redirect_uri' parameter scheme when running behind an SSL terminating reverse proxy. + * Force 'https' as the `redirect_uri` parameter scheme when running behind an SSL/TLS terminating reverse proxy. * This property, if enabled, will also affect the logout `post_logout_redirect_uri` and the local redirect requests. */ @ConfigItem(defaultValueDocumentation = "false") @@ -791,7 +866,7 @@ public enum ResponseMode { public boolean nonceRequired = false; /** - * Add the 'openid' scope automatically to the list of scopes. This is required for OpenId Connect providers + * Add the `openid` scope automatically to the list of scopes. This is required for OpenId Connect providers * but will not work for OAuth2 providers such as Twitter OAuth2 which does not accept that scope and throws an error. */ @ConfigItem(defaultValueDocumentation = "true") @@ -811,8 +886,8 @@ public enum ResponseMode { public Optional> forwardParams = Optional.empty(); /** - * If enabled the state, session and post logout cookies will have their 'secure' parameter set to 'true' - * when HTTP is used. It may be necessary when running behind an SSL terminating reverse proxy. + * If enabled the state, session and post logout cookies will have their 'secure' parameter set to `true` + * when HTTP is used. It may be necessary when running behind an SSL/TLS terminating reverse proxy. * The cookies will always be secure if HTTPS is used even if this property is set to false. */ @ConfigItem(defaultValue = "false") @@ -820,8 +895,8 @@ public enum ResponseMode { /** * Cookie name suffix. - * For example, a session cookie name for the default OIDC tenant is 'q_session' but can be changed to 'q_session_test' - * if this property is set to 'test'. + * For example, a session cookie name for the default OIDC tenant is `q_session` but can be changed to `q_session_test` + * if this property is set to `test`. */ @ConfigItem public Optional cookieSuffix = Optional.empty(); @@ -861,8 +936,7 @@ public enum ResponseMode { * However, if multiple authentications are attempted from the same browser, for example, from the different * browser tabs, then the currently available state cookie may represent the authentication flow * initiated from another tab and not related to the current request. - * Disable this property if you would like to avoid supporting multiple authorization code flows running in the same - * browser. + * Disable this property to permit only a single authorization code flow in the same browser. * */ @ConfigItem(defaultValue = "true") @@ -886,14 +960,14 @@ public enum ResponseMode { * with {@link #redirectPath} may be needed to avoid such errors. *

* However, setting this property to `false` may help if the above options are not suitable. - * It will cause a new authentication redirect to OpenId Connect provider. Please be aware doing so may increase the + * It will cause a new authentication redirect to OpenId Connect provider. Doing so may increase the * risk of browser redirect loops. */ @ConfigItem(defaultValue = "false") public boolean failOnMissingStateParam = false; /** - * If this property is set to 'true' then an OIDC UserInfo endpoint will be called. + * If this property is set to `true` then an OIDC UserInfo endpoint will be called. * This property will be enabled if `quarkus.oidc.roles.source` is `userinfo` * or `quarkus.oidc.token.verify-access-token-with-user-info` is `true` * or `quarkus.oidc.authentication.id-token-required` is set to `false`, @@ -906,7 +980,7 @@ public enum ResponseMode { * Session age extension in minutes. * The user session age property is set to the value of the ID token life-span by default and * the user will be redirected to the OIDC provider to re-authenticate once the session has expired. - * If this property is set to a non-zero value then the expired ID token can be refreshed before + * If this property is set to a non-zero value, then the expired ID token can be refreshed before * the session has expired. * This property will be ignored if the `token.refresh-expired` property has not been enabled. */ @@ -914,7 +988,7 @@ public enum ResponseMode { public Duration sessionAgeExtension = Duration.ofMinutes(5); /** - * If this property is set to 'true' then a normal 302 redirect response will be returned + * If this property is set to `true` then a normal 302 redirect response will be returned * if the request was initiated via JavaScript API such as XMLHttpRequest or Fetch and the current user needs to be * (re)authenticated which may not be desirable for Single-page applications (SPA) since * it automatically following the redirect may not work given that OIDC authorization endpoints typically do not support @@ -1276,7 +1350,7 @@ public static Token fromAudience(String... audience) { } /** - * Expected issuer 'iss' claim value. + * Expected issuer `iss` claim value. * Note this property overrides the `issuer` property which may be set in OpenId Connect provider's well-known * configuration. * If the `iss` claim value varies depending on the host/IP address or tenant id of the provider then you may skip the @@ -1356,7 +1430,7 @@ public static Token fromAudience(String... audience) { public Optional age = Optional.empty(); /** - * Name of the claim which contains a principal name. By default, the 'upn', 'preferred_username' and `sub` claims are + * Name of the claim which contains a principal name. By default, the `upn`, `preferred_username` and `sub` claims are * checked. */ @ConfigItem @@ -1418,7 +1492,7 @@ public static Token fromAudience(String... audience) { * the providers may not control the private decryption keys. * In such cases set this property to point to the file containing the decryption private key in * PEM or JSON Web Key (JWK) format. - * Note that if a 'private_key_jwt' client authentication method is used then the private key + * Note that if a `private_key_jwt` client authentication method is used then the private key * which is used to sign client authentication JWT tokens will be used to try to decrypt an encrypted ID token * if this property is not set. */ @@ -1428,7 +1502,7 @@ public static Token fromAudience(String... audience) { /** * Allow the remote introspection of JWT tokens when no matching JWK key is available. * - * Note this property is set to 'true' by default for backward-compatibility reasons and will be set to `false` + * Note this property is set to `true` by default for backward-compatibility reasons and will be set to `false` * instead in one of the next releases. * * Also note this property will be ignored if JWK endpoint URI is not available and introspecting the tokens is @@ -1627,7 +1701,7 @@ public void setSubjectRequired(boolean subjectRequired) { public static enum ApplicationType { /** - * A {@code WEB_APP} is a client that serves pages, usually a frontend application. For this type of client the + * A {@code WEB_APP} is a client that serves pages, usually a front-end application. For this type of client the * Authorization Code Flow is defined as the preferred method for authenticating users. */ WEB_APP, diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/BackChannelLogoutTokenCache.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/BackChannelLogoutTokenCache.java index 4150096851cf27..180c374ac7631a 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/BackChannelLogoutTokenCache.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/BackChannelLogoutTokenCache.java @@ -1,103 +1,33 @@ package io.quarkus.oidc.runtime; -import java.util.Iterator; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.AtomicInteger; +import jakarta.enterprise.event.Observes; import io.quarkus.oidc.OidcTenantConfig; -import io.vertx.core.Handler; +import io.quarkus.runtime.ShutdownEvent; import io.vertx.core.Vertx; public class BackChannelLogoutTokenCache { - private OidcTenantConfig oidcConfig; - private Map cacheMap = new ConcurrentHashMap<>();; - private AtomicInteger size = new AtomicInteger(); + final MemoryCache cache; public BackChannelLogoutTokenCache(OidcTenantConfig oidcTenantConfig, Vertx vertx) { - this.oidcConfig = oidcTenantConfig; - init(vertx); - } - - private void init(Vertx vertx) { - cacheMap = new ConcurrentHashMap<>(); - if (oidcConfig.logout.backchannel.cleanUpTimerInterval.isPresent()) { - vertx.setPeriodic(oidcConfig.logout.backchannel.cleanUpTimerInterval.get().toMillis(), new Handler() { - @Override - public void handle(Long event) { - // Remove all the entries which have expired - removeInvalidEntries(); - } - }); - } + cache = new MemoryCache(vertx, oidcTenantConfig.logout.backchannel.cleanUpTimerInterval, + oidcTenantConfig.logout.backchannel.tokenCacheTimeToLive, oidcTenantConfig.logout.backchannel.tokenCacheSize); } public void addTokenVerification(String token, TokenVerificationResult result) { - if (!prepareSpaceForNewCacheEntry()) { - clearCache(); - } - cacheMap.put(token, new CacheEntry(result)); + cache.add(token, result); } public TokenVerificationResult removeTokenVerification(String token) { - CacheEntry entry = removeCacheEntry(token); - return entry == null ? null : entry.result; + return cache.remove(token); } public boolean containsTokenVerification(String token) { - return cacheMap.containsKey(token); - } - - public void clearCache() { - cacheMap.clear(); - size.set(0); - } - - private void removeInvalidEntries() { - long now = now(); - for (Iterator> it = cacheMap.entrySet().iterator(); it.hasNext();) { - Map.Entry next = it.next(); - if (isEntryExpired(next.getValue(), now)) { - it.remove(); - size.decrementAndGet(); - } - } - } - - private boolean prepareSpaceForNewCacheEntry() { - int currentSize; - do { - currentSize = size.get(); - if (currentSize == oidcConfig.logout.backchannel.tokenCacheSize) { - return false; - } - } while (!size.compareAndSet(currentSize, currentSize + 1)); - return true; + return cache.containsKey(token); } - private CacheEntry removeCacheEntry(String token) { - CacheEntry entry = cacheMap.remove(token); - if (entry != null) { - size.decrementAndGet(); - } - return entry; - } - - private boolean isEntryExpired(CacheEntry entry, long now) { - return entry.createdTime + oidcConfig.logout.backchannel.tokenCacheTimeToLive.toMillis() < now; - } - - private static long now() { - return System.currentTimeMillis(); - } - - private static class CacheEntry { - volatile TokenVerificationResult result; - long createdTime = System.currentTimeMillis(); - - public CacheEntry(TokenVerificationResult result) { - this.result = result; - } + void shutdown(@Observes ShutdownEvent event, Vertx vertx) { + cache.stopTimer(vertx); } } diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/DefaultTokenIntrospectionUserInfoCache.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/DefaultTokenIntrospectionUserInfoCache.java index 29c22f26430b50..214436d5e6988f 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/DefaultTokenIntrospectionUserInfoCache.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/DefaultTokenIntrospectionUserInfoCache.java @@ -1,10 +1,6 @@ package io.quarkus.oidc.runtime; -import java.util.Collections; -import java.util.Iterator; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.AtomicInteger; +import jakarta.enterprise.event.Observes; import io.quarkus.oidc.OidcRequestContext; import io.quarkus.oidc.OidcTenantConfig; @@ -12,9 +8,8 @@ import io.quarkus.oidc.TokenIntrospectionCache; import io.quarkus.oidc.UserInfo; import io.quarkus.oidc.UserInfoCache; -import io.quarkus.oidc.runtime.OidcConfig.TokenCache; +import io.quarkus.runtime.ShutdownEvent; import io.smallrye.mutiny.Uni; -import io.vertx.core.Handler; import io.vertx.core.Vertx; /** @@ -31,43 +26,21 @@ public class DefaultTokenIntrospectionUserInfoCache implements TokenIntrospectio private static final Uni NULL_INTROSPECTION_UNI = Uni.createFrom().nullItem(); private static final Uni NULL_USERINFO_UNI = Uni.createFrom().nullItem(); - private TokenCache cacheConfig; - - private Map cacheMap; - private AtomicInteger size = new AtomicInteger(); + final MemoryCache cache; public DefaultTokenIntrospectionUserInfoCache(OidcConfig oidcConfig, Vertx vertx) { - this.cacheConfig = oidcConfig.tokenCache; - init(vertx); - } - - private void init(Vertx vertx) { - if (cacheConfig.maxSize > 0) { - cacheMap = new ConcurrentHashMap<>(); - if (cacheConfig.cleanUpTimerInterval.isPresent()) { - vertx.setPeriodic(cacheConfig.cleanUpTimerInterval.get().toMillis(), new Handler() { - @Override - public void handle(Long event) { - // Remove all the entries which have expired - removeInvalidEntries(); - } - }); - } - } else { - cacheMap = Collections.emptyMap(); - } + cache = new MemoryCache(vertx, oidcConfig.tokenCache.cleanUpTimerInterval, + oidcConfig.tokenCache.timeToLive, oidcConfig.tokenCache.maxSize); } @Override public Uni addIntrospection(String token, TokenIntrospection introspection, OidcTenantConfig oidcTenantConfig, OidcRequestContext requestContext) { - if (cacheConfig.maxSize > 0) { - CacheEntry entry = findValidCacheEntry(token); - if (entry != null) { - entry.introspection = introspection; - } else if (prepareSpaceForNewCacheEntry()) { - cacheMap.put(token, new CacheEntry(introspection)); - } + CacheEntry entry = cache.get(token); + if (entry != null) { + entry.introspection = introspection; + } else { + cache.add(token, new CacheEntry(introspection)); } return CodeAuthenticationMechanism.VOID_UNI; @@ -76,20 +49,18 @@ public Uni addIntrospection(String token, TokenIntrospection introspection @Override public Uni getIntrospection(String token, OidcTenantConfig oidcConfig, OidcRequestContext requestContext) { - CacheEntry entry = findValidCacheEntry(token); + CacheEntry entry = cache.get(token); return entry == null ? NULL_INTROSPECTION_UNI : Uni.createFrom().item(entry.introspection); } @Override public Uni addUserInfo(String token, UserInfo userInfo, OidcTenantConfig oidcTenantConfig, OidcRequestContext requestContext) { - if (cacheConfig.maxSize > 0) { - CacheEntry entry = findValidCacheEntry(token); - if (entry != null) { - entry.userInfo = userInfo; - } else if (prepareSpaceForNewCacheEntry()) { - cacheMap.put(token, new CacheEntry(userInfo)); - } + CacheEntry entry = cache.get(token); + if (entry != null) { + entry.userInfo = userInfo; + } else { + cache.add(token, new CacheEntry(userInfo)); } return CodeAuthenticationMechanism.VOID_UNI; @@ -98,67 +69,13 @@ public Uni addUserInfo(String token, UserInfo userInfo, OidcTenantConfig o @Override public Uni getUserInfo(String token, OidcTenantConfig oidcConfig, OidcRequestContext requestContext) { - CacheEntry entry = findValidCacheEntry(token); + CacheEntry entry = cache.get(token); return entry == null ? NULL_USERINFO_UNI : Uni.createFrom().item(entry.userInfo); } - public int getCacheSize() { - return cacheMap.size(); - } - - public void clearCache() { - cacheMap.clear(); - size.set(0); - } - - private void removeInvalidEntries() { - long now = now(); - for (Iterator> it = cacheMap.entrySet().iterator(); it.hasNext();) { - Map.Entry next = it.next(); - if (isEntryExpired(next.getValue(), now)) { - it.remove(); - size.decrementAndGet(); - } - } - } - - private boolean prepareSpaceForNewCacheEntry() { - int currentSize; - do { - currentSize = size.get(); - if (currentSize == cacheConfig.maxSize) { - return false; - } - } while (!size.compareAndSet(currentSize, currentSize + 1)); - return true; - } - - private CacheEntry findValidCacheEntry(String token) { - CacheEntry entry = cacheMap.get(token); - if (entry != null) { - long now = now(); - if (isEntryExpired(entry, now)) { - // Entry has expired, remote introspection will be required - entry = null; - cacheMap.remove(token); - size.decrementAndGet(); - } - } - return entry; - } - - private boolean isEntryExpired(CacheEntry entry, long now) { - return entry.createdTime + cacheConfig.timeToLive.toMillis() < now; - } - - private static long now() { - return System.currentTimeMillis(); - } - private static class CacheEntry { volatile TokenIntrospection introspection; volatile UserInfo userInfo; - long createdTime = System.currentTimeMillis(); public CacheEntry(TokenIntrospection introspection) { this.introspection = introspection; @@ -168,4 +85,17 @@ public CacheEntry(UserInfo userInfo) { this.userInfo = userInfo; } } + + public void clearCache() { + cache.clearCache(); + } + + public int getCacheSize() { + return cache.getCacheSize(); + } + + void shutdown(@Observes ShutdownEvent event, Vertx vertx) { + cache.stopTimer(vertx); + } + } diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/DynamicVerificationKeyResolver.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/DynamicVerificationKeyResolver.java new file mode 100644 index 00000000000000..f9b9eb7b2a03e4 --- /dev/null +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/DynamicVerificationKeyResolver.java @@ -0,0 +1,177 @@ +package io.quarkus.oidc.runtime; + +import java.security.Key; +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +import jakarta.enterprise.event.Observes; + +import org.jboss.logging.Logger; +import org.jose4j.jws.JsonWebSignature; +import org.jose4j.jwx.HeaderParameterNames; +import org.jose4j.jwx.JsonWebStructure; +import org.jose4j.keys.resolvers.VerificationKeyResolver; +import org.jose4j.lang.UnresolvableKeyException; + +import io.quarkus.oidc.OidcTenantConfig; +import io.quarkus.oidc.common.OidcRequestContextProperties; +import io.quarkus.runtime.ShutdownEvent; +import io.quarkus.security.credential.TokenCredential; +import io.smallrye.mutiny.Uni; +import io.vertx.core.Vertx; +import io.vertx.core.json.JsonObject; + +public class DynamicVerificationKeyResolver { + private static final Logger LOG = Logger.getLogger(DynamicVerificationKeyResolver.class); + + private final OidcProviderClient client; + private final MemoryCache cache; + + public DynamicVerificationKeyResolver(OidcProviderClient client, OidcTenantConfig config) { + this.client = client; + this.cache = new MemoryCache(client.getVertx(), config.jwks.cleanUpTimerInterval, + config.jwks.cacheTimeToLive, config.jwks.cacheSize); + } + + public Uni resolve(TokenCredential tokenCred) { + JsonObject headers = OidcUtils.decodeJwtHeaders(tokenCred.getToken()); + Key key = findKeyInTheCache(headers); + if (key != null) { + return Uni.createFrom().item(new SingleKeyVerificationKeyResolver(key)); + } + + return client.getJsonWebKeySet(new OidcRequestContextProperties( + Map.of(OidcRequestContextProperties.TOKEN, tokenCred.getToken(), + OidcRequestContextProperties.TOKEN_CREDENTIAL, tokenCred))) + .onItem().transformToUni(new Function>() { + + @Override + public Uni apply(JsonWebKeySet jwks) { + Key newKey = null; + // Try 'kid' first + String kid = headers.getString(HeaderParameterNames.KEY_ID); + if (kid != null) { + newKey = getKeyWithId(jwks, kid); + if (newKey == null) { + // if `kid` was set then the key must exist + return Uni.createFrom().failure( + new UnresolvableKeyException(String.format("JWK with kid '%s' is not available", kid))); + } else { + cache.add(kid, newKey); + } + } + + String thumbprint = null; + if (newKey == null) { + thumbprint = headers.getString(HeaderParameterNames.X509_CERTIFICATE_SHA256_THUMBPRINT); + if (thumbprint != null) { + newKey = getKeyWithS256Thumbprint(jwks, thumbprint); + if (newKey == null) { + // if only `x5tS256` was set then the key must exist + return Uni.createFrom().failure( + new UnresolvableKeyException(String.format( + "JWK with the SHA256 certificate thumbprint '%s' is not available", + thumbprint))); + } else { + cache.add(thumbprint, newKey); + } + } + } + + if (newKey == null) { + thumbprint = headers.getString(HeaderParameterNames.X509_CERTIFICATE_THUMBPRINT); + if (thumbprint != null) { + newKey = getKeyWithThumbprint(jwks, thumbprint); + if (newKey == null) { + // if only `x5t` was set then the key must exist + return Uni.createFrom().failure(new UnresolvableKeyException( + String.format("JWK with the certificate thumbprint '%s' is not available", + thumbprint))); + } else { + cache.add(thumbprint, newKey); + } + } + } + + if (newKey == null && kid == null && thumbprint == null) { + newKey = jwks.getKeyWithoutKeyIdAndThumbprint("RSA"); + } + + if (newKey == null) { + return Uni.createFrom().failure(new UnresolvableKeyException( + String.format( + "JWK is not available, neither 'kid' nor 'x5t#S256' nor 'x5t' token headers are set", + kid))); + } else { + return Uni.createFrom().item(new SingleKeyVerificationKeyResolver(newKey)); + } + } + + }); + } + + private static Key getKeyWithId(JsonWebKeySet jwks, String kid) { + if (kid != null) { + return jwks.getKeyWithId(kid); + } else { + LOG.debug("Token 'kid' header is not set"); + return null; + } + } + + private Key getKeyWithThumbprint(JsonWebKeySet jwks, String thumbprint) { + if (thumbprint != null) { + return jwks.getKeyWithThumbprint(thumbprint); + } else { + LOG.debug("Token 'x5t' header is not set"); + return null; + } + } + + private Key getKeyWithS256Thumbprint(JsonWebKeySet jwks, String thumbprint) { + if (thumbprint != null) { + return jwks.getKeyWithS256Thumbprint(thumbprint); + } else { + LOG.debug("Token 'x5tS256' header is not set"); + return null; + } + } + + private Key findKeyInTheCache(JsonObject headers) { + String kid = headers.getString(HeaderParameterNames.KEY_ID); + if (kid != null && cache.containsKey(kid)) { + return cache.get(kid); + } + String thumbprint = headers.getString(HeaderParameterNames.X509_CERTIFICATE_SHA256_THUMBPRINT); + if (thumbprint != null && cache.containsKey(thumbprint)) { + return cache.get(thumbprint); + } + + thumbprint = headers.getString(HeaderParameterNames.X509_CERTIFICATE_THUMBPRINT); + if (thumbprint != null && cache.containsKey(thumbprint)) { + return cache.get(thumbprint); + } + + return null; + } + + static class SingleKeyVerificationKeyResolver implements VerificationKeyResolver { + + private Key key; + + SingleKeyVerificationKeyResolver(Key key) { + this.key = key; + } + + @Override + public Key resolveKey(JsonWebSignature jws, List nestingContext) + throws UnresolvableKeyException { + return key; + } + } + + void shutdown(@Observes ShutdownEvent event, Vertx vertx) { + cache.stopTimer(vertx); + } +} diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/JsonWebKeySet.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/JsonWebKeySet.java index 5e80ddfb94b172..dedfe32bf11565 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/JsonWebKeySet.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/JsonWebKeySet.java @@ -9,11 +9,8 @@ import org.jose4j.jwk.JsonWebKey; import org.jose4j.jwk.PublicJsonWebKey; -import org.jose4j.jws.JsonWebSignature; -import org.jose4j.lang.InvalidAlgorithmException; import org.jose4j.lang.JoseException; -import io.quarkus.logging.Log; import io.quarkus.oidc.OIDCException; public class JsonWebKeySet { @@ -86,13 +83,8 @@ public Key getKeyWithS256Thumbprint(String x5tS256) { return keysWithS256Thumbprints.get(x5tS256); } - public Key getKeyWithoutKeyIdAndThumbprint(JsonWebSignature jws) { - try { - List keys = keysWithoutKeyIdAndThumbprint.get(jws.getKeyType()); - return keys == null || keys.size() != 1 ? null : keys.get(0); - } catch (InvalidAlgorithmException ex) { - Log.debug("Token 'alg'(algorithm) header value is invalid", ex); - return null; - } + public Key getKeyWithoutKeyIdAndThumbprint(String keyType) { + List keys = keysWithoutKeyIdAndThumbprint.get(keyType); + return keys == null || keys.size() != 1 ? null : keys.get(0); } } diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/MemoryCache.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/MemoryCache.java new file mode 100644 index 00000000000000..dd8e4943ef029a --- /dev/null +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/MemoryCache.java @@ -0,0 +1,135 @@ +package io.quarkus.oidc.runtime; + +import java.time.Duration; +import java.util.Iterator; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; + +import io.vertx.core.Handler; +import io.vertx.core.Vertx; + +public class MemoryCache { + private volatile Long timerId = null; + + private final Map> cacheMap = new ConcurrentHashMap<>(); + private AtomicInteger size = new AtomicInteger(); + private final Duration cacheTimeToLive; + private final int cacheSize; + + public MemoryCache(Vertx vertx, Optional cleanUpTimerInterval, + Duration cacheTimeToLive, int cacheSize) { + this.cacheTimeToLive = cacheTimeToLive; + this.cacheSize = cacheSize; + init(vertx, cleanUpTimerInterval); + } + + private void init(Vertx vertx, Optional cleanUpTimerInterval) { + if (cleanUpTimerInterval.isPresent()) { + timerId = vertx.setPeriodic(cleanUpTimerInterval.get().toMillis(), new Handler() { + @Override + public void handle(Long event) { + // Remove all the entries which have expired + removeInvalidEntries(); + } + }); + } + } + + public void add(String key, T result) { + if (cacheSize > 0) { + if (!prepareSpaceForNewCacheEntry()) { + clearCache(); + } + cacheMap.put(key, new CacheEntry(result)); + } + } + + public T remove(String key) { + CacheEntry entry = removeCacheEntry(key); + return entry == null ? null : entry.result; + } + + public T get(String key) { + CacheEntry entry = cacheMap.get(key); + return entry == null ? null : entry.result; + } + + public boolean containsKey(String key) { + return cacheMap.containsKey(key); + } + + private void removeInvalidEntries() { + long now = now(); + for (Iterator>> it = cacheMap.entrySet().iterator(); it.hasNext();) { + Map.Entry> next = it.next(); + if (next != null) { + if (isEntryExpired(next.getValue(), now)) { + try { + it.remove(); + size.decrementAndGet(); + } catch (IllegalStateException ex) { + // continue + } + } + } + } + } + + private boolean prepareSpaceForNewCacheEntry() { + int currentSize; + do { + currentSize = size.get(); + if (currentSize == cacheSize) { + return false; + } + } while (!size.compareAndSet(currentSize, currentSize + 1)); + return true; + } + + private CacheEntry removeCacheEntry(String token) { + CacheEntry entry = cacheMap.remove(token); + if (entry != null) { + size.decrementAndGet(); + } + return entry; + } + + private boolean isEntryExpired(CacheEntry entry, long now) { + return entry.createdTime + cacheTimeToLive.toMillis() < now; + } + + private static long now() { + return System.currentTimeMillis(); + } + + private static class CacheEntry { + volatile T result; + long createdTime = System.currentTimeMillis(); + + public CacheEntry(T result) { + this.result = result; + } + } + + public int getCacheSize() { + return cacheMap.size(); + } + + public void clearCache() { + cacheMap.clear(); + size.set(0); + } + + public void stopTimer(Vertx vertx) { + if (timerId != null && vertx.cancelTimer(timerId)) { + timerId = null; + } + } + + public boolean isTimerRunning() { + return timerId != null; + } + +} diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcIdentityProvider.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcIdentityProvider.java index 26d053339d97ac..4cc968508c72f9 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcIdentityProvider.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcIdentityProvider.java @@ -142,7 +142,7 @@ public Uni apply(UserInfo userInfo, Throwable t) { primaryTokenUni = verifySelfSignedTokenUni(resolvedContext, request.getToken().getToken()); } } else { - primaryTokenUni = verifyTokenUni(requestData, resolvedContext, request.getToken().getToken(), + primaryTokenUni = verifyTokenUni(requestData, resolvedContext, request.getToken(), isIdToken(request), null); } @@ -192,7 +192,7 @@ public Uni apply(TokenVerificationResult codeAccessToken, Thro } Uni tokenUni = verifyTokenUni(requestData, resolvedContext, - request.getToken().getToken(), + request.getToken(), false, userInfo); return tokenUni.onItemOrFailure() @@ -421,14 +421,15 @@ private Uni verifyCodeFlowAccessTokenUni(Map verifyTokenUni(Map requestData, TenantConfigContext resolvedContext, - String token, boolean enforceAudienceVerification, UserInfo userInfo) { + TokenCredential tokenCred, boolean enforceAudienceVerification, UserInfo userInfo) { + final String token = tokenCred.getToken(); if (OidcUtils.isOpaqueToken(token)) { if (!resolvedContext.oidcConfig.token.allowOpaqueTokenIntrospection) { LOG.debug("Token is opaque but the opaque token introspection is not allowed"); @@ -452,7 +453,7 @@ private Uni verifyTokenUni(Map requestD // Verify JWT token with the remote introspection LOG.debug("Starting the JWT token introspection"); return introspectTokenUni(resolvedContext, token, false); - } else { + } else if (resolvedContext.oidcConfig.jwks.resolveEarly) { // Verify JWT token with the local JWK keys with a possible remote introspection fallback final String nonce = (String) requestData.get(OidcConstants.NONCE); try { @@ -470,6 +471,10 @@ private Uni verifyTokenUni(Map requestD return Uni.createFrom().failure(t); } } + } else { + final String nonce = (String) requestData.get(OidcConstants.NONCE); + return resolveJwksAndVerifyTokenUni(resolvedContext, tokenCred, enforceAudienceVerification, + resolvedContext.oidcConfig.token.isSubjectRequired(), nonce); } } @@ -488,6 +493,15 @@ private Uni refreshJwksAndVerifyTokenUni(TenantConfigCo .recoverWithUni(f -> introspectTokenUni(resolvedContext, token, true)); } + private Uni resolveJwksAndVerifyTokenUni(TenantConfigContext resolvedContext, + TokenCredential tokenCred, + boolean enforceAudienceVerification, boolean subjectRequired, String nonce) { + return resolvedContext.provider + .getKeyResolverAndVerifyJwtToken(tokenCred, enforceAudienceVerification, subjectRequired, nonce) + .onFailure(f -> fallbackToIntrospectionIfNoMatchingKey(f, resolvedContext)) + .recoverWithUni(f -> introspectTokenUni(resolvedContext, tokenCred.getToken(), true)); + } + private static boolean fallbackToIntrospectionIfNoMatchingKey(Throwable f, TenantConfigContext resolvedContext) { if (!(f.getCause() instanceof UnresolvableKeyException)) { LOG.debug("Local JWT token verification has failed, skipping the token introspection"); diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProvider.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProvider.java index 32bbc806e2d6c3..8d26b8aae936cb 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProvider.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProvider.java @@ -27,8 +27,10 @@ import org.jose4j.jwx.HeaderParameterNames; import org.jose4j.jwx.JsonWebStructure; import org.jose4j.keys.resolvers.VerificationKeyResolver; +import org.jose4j.lang.InvalidAlgorithmException; import org.jose4j.lang.UnresolvableKeyException; +import io.quarkus.logging.Log; import io.quarkus.oidc.AuthorizationCodeTokens; import io.quarkus.oidc.OIDCException; import io.quarkus.oidc.OidcConfigurationMetadata; @@ -38,6 +40,7 @@ import io.quarkus.oidc.UserInfo; import io.quarkus.oidc.common.runtime.OidcConstants; import io.quarkus.security.AuthenticationFailedException; +import io.quarkus.security.credential.TokenCredential; import io.smallrye.jwt.algorithm.SignatureAlgorithm; import io.smallrye.jwt.util.KeyUtils; import io.smallrye.mutiny.Uni; @@ -64,6 +67,7 @@ public class OidcProvider implements Closeable { final OidcProviderClient client; final RefreshableVerificationKeyResolver asymmetricKeyResolver; + final DynamicVerificationKeyResolver keyResolverProvider; final OidcTenantConfig oidcConfig; final TokenCustomizer tokenCustomizer; final String issuer; @@ -83,7 +87,11 @@ public OidcProvider(OidcProviderClient client, OidcTenantConfig oidcConfig, Json this.tokenCustomizer = tokenCustomizer; this.asymmetricKeyResolver = jwks == null ? null : new JsonWebKeyResolver(jwks, oidcConfig.token.forcedJwkRefreshInterval); - + if (client != null && oidcConfig != null && !oidcConfig.jwks.resolveEarly) { + this.keyResolverProvider = new DynamicVerificationKeyResolver(client, oidcConfig); + } else { + this.keyResolverProvider = null; + } this.issuer = checkIssuerProp(); this.audience = checkAudienceProp(); this.requiredClaims = checkRequiredClaimsProp(); @@ -96,6 +104,7 @@ public OidcProvider(String publicKeyEnc, OidcTenantConfig oidcConfig, Key tokenD this.oidcConfig = oidcConfig; this.tokenCustomizer = TokenCustomizerFinder.find(oidcConfig); this.asymmetricKeyResolver = new LocalPublicKeyResolver(publicKeyEnc); + this.keyResolverProvider = null; this.issuer = checkIssuerProp(); this.audience = checkAudienceProp(); this.requiredClaims = checkRequiredClaimsProp(); @@ -282,6 +291,30 @@ public Uni apply(Void v) { }); } + public Uni getKeyResolverAndVerifyJwtToken(TokenCredential tokenCred, + boolean enforceAudienceVerification, + boolean subjectRequired, String nonce) { + return keyResolverProvider.resolve(tokenCred).onItem() + .transformToUni(new Function>() { + + @Override + public Uni apply(VerificationKeyResolver resolver) { + try { + return Uni.createFrom() + .item(verifyJwtTokenInternal(customizeJwtToken(tokenCred.getToken()), + enforceAudienceVerification, + subjectRequired, nonce, + (requiredAlgorithmConstraints != null ? requiredAlgorithmConstraints + : ASYMMETRIC_ALGORITHM_CONSTRAINTS), + resolver, true)); + } catch (Throwable t) { + return Uni.createFrom().failure(t); + } + } + + }); + } + public Uni introspectToken(String token, boolean fallbackFromJwkMatch) { if (client.getMetadata().getIntrospectionUri() == null) { String errorMessage = String.format("Token issued to client %s " @@ -380,7 +413,7 @@ public Key resolveKey(JsonWebSignature jws, List nestingContex // Try 'kid' first String kid = jws.getKeyIdHeaderValue(); if (kid != null) { - key = getKeyWithId(jws, kid); + key = getKeyWithId(kid); if (key == null) { // if `kid` was set then the key must exist throw new UnresolvableKeyException(String.format("JWK with kid '%s' is not available", kid)); @@ -389,31 +422,35 @@ public Key resolveKey(JsonWebSignature jws, List nestingContex String thumbprint = null; if (key == null) { - thumbprint = jws.getHeader(HeaderParameterNames.X509_CERTIFICATE_THUMBPRINT); + thumbprint = jws.getHeader(HeaderParameterNames.X509_CERTIFICATE_SHA256_THUMBPRINT); if (thumbprint != null) { - key = getKeyWithThumbprint(jws, thumbprint); + key = getKeyWithS256Thumbprint(thumbprint); if (key == null) { - // if only `x5t` was set then the key must exist + // if only `x5tS256` was set then the key must exist throw new UnresolvableKeyException( - String.format("JWK with the certificate thumbprint '%s' is not available", thumbprint)); + String.format("JWK with the SHA256 certificate thumbprint '%s' is not available", thumbprint)); } } } if (key == null) { - thumbprint = jws.getHeader(HeaderParameterNames.X509_CERTIFICATE_SHA256_THUMBPRINT); + thumbprint = jws.getHeader(HeaderParameterNames.X509_CERTIFICATE_THUMBPRINT); if (thumbprint != null) { - key = getKeyWithS256Thumbprint(jws, thumbprint); + key = getKeyWithThumbprint(thumbprint); if (key == null) { - // if only `x5tS256` was set then the key must exist + // if only `x5t` was set then the key must exist throw new UnresolvableKeyException( - String.format("JWK with the SHA256 certificate thumbprint '%s' is not available", thumbprint)); + String.format("JWK with the certificate thumbprint '%s' is not available", thumbprint)); } } } if (key == null && kid == null && thumbprint == null) { - key = jwks.getKeyWithoutKeyIdAndThumbprint(jws); + try { + key = jwks.getKeyWithoutKeyIdAndThumbprint(jws.getKeyType()); + } catch (InvalidAlgorithmException ex) { + Log.debug("Token 'alg'(algorithm) header value is invalid", ex); + } } if (key == null) { @@ -425,7 +462,7 @@ public Key resolveKey(JsonWebSignature jws, List nestingContex } } - private Key getKeyWithId(JsonWebSignature jws, String kid) { + private Key getKeyWithId(String kid) { if (kid != null) { return jwks.getKeyWithId(kid); } else { @@ -434,7 +471,7 @@ private Key getKeyWithId(JsonWebSignature jws, String kid) { } } - private Key getKeyWithThumbprint(JsonWebSignature jws, String thumbprint) { + private Key getKeyWithThumbprint(String thumbprint) { if (thumbprint != null) { return jwks.getKeyWithThumbprint(thumbprint); } else { @@ -443,7 +480,7 @@ private Key getKeyWithThumbprint(JsonWebSignature jws, String thumbprint) { } } - private Key getKeyWithS256Thumbprint(JsonWebSignature jws, String thumbprint) { + private Key getKeyWithS256Thumbprint(String thumbprint) { if (thumbprint != null) { return jwks.getKeyWithS256Thumbprint(thumbprint); } else { @@ -456,15 +493,16 @@ public Uni refresh() { final long now = now(); if (now > lastForcedRefreshTime + forcedJwksRefreshIntervalMilliSecs) { lastForcedRefreshTime = now; - return client.getJsonWebKeySet().onItem().transformToUni(new Function>() { + return client.getJsonWebKeySet(null).onItem() + .transformToUni(new Function>() { - @Override - public Uni apply(JsonWebKeySet t) { - jwks = t; - return Uni.createFrom().voidItem(); - } + @Override + public Uni apply(JsonWebKeySet t) { + jwks = t; + return Uni.createFrom().voidItem(); + } - }); + }); } else { return Uni.createFrom().voidItem(); } diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProviderClient.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProviderClient.java index 77ba52c65ebeb9..204c38984259c3 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProviderClient.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProviderClient.java @@ -15,12 +15,14 @@ import io.quarkus.oidc.OidcTenantConfig; import io.quarkus.oidc.TokenIntrospection; import io.quarkus.oidc.UserInfo; -import io.quarkus.oidc.common.OidcClientRequestFilter; +import io.quarkus.oidc.common.OidcRequestContextProperties; +import io.quarkus.oidc.common.OidcRequestFilter; import io.quarkus.oidc.common.runtime.OidcCommonUtils; import io.quarkus.oidc.common.runtime.OidcConstants; import io.quarkus.oidc.common.runtime.OidcEndpointAccessException; import io.smallrye.mutiny.Uni; import io.smallrye.mutiny.groups.UniOnItem; +import io.vertx.core.Vertx; import io.vertx.core.http.HttpHeaders; import io.vertx.core.json.JsonObject; import io.vertx.mutiny.core.MultiMap; @@ -40,18 +42,21 @@ public class OidcProviderClient implements Closeable { private static final String APPLICATION_JSON = "application/json"; private final WebClient client; + private final Vertx vertx; private final OidcConfigurationMetadata metadata; private final OidcTenantConfig oidcConfig; private final String clientSecretBasicAuthScheme; private final String introspectionBasicAuthScheme; private final Key clientJwtKey; - private final List filters; + private final List filters; public OidcProviderClient(WebClient client, + Vertx vertx, OidcConfigurationMetadata metadata, OidcTenantConfig oidcConfig, - List filters) { + List filters) { this.client = client; + this.vertx = vertx; this.metadata = metadata; this.oidcConfig = oidcConfig; this.clientSecretBasicAuthScheme = OidcCommonUtils.initClientSecretBasicAuth(oidcConfig); @@ -74,14 +79,14 @@ public OidcConfigurationMetadata getMetadata() { return metadata; } - public Uni getJsonWebKeySet() { - return filter(client.getAbs(metadata.getJsonWebKeySetUri()), null).send().onItem() + public Uni getJsonWebKeySet(OidcRequestContextProperties contextProperties) { + return filter(client.getAbs(metadata.getJsonWebKeySetUri()), null, contextProperties).send().onItem() .transform(resp -> getJsonWebKeySet(resp)); } public Uni getUserInfo(String token) { LOG.debugf("Get UserInfo on: %s auth: %s", metadata.getUserInfoUri(), OidcConstants.BEARER_SCHEME + " " + token); - return filter(client.getAbs(metadata.getUserInfoUri()), null) + return filter(client.getAbs(metadata.getUserInfoUri()), null, null) .putHeader(AUTHORIZATION_HEADER, OidcConstants.BEARER_SCHEME + " " + token) .send().onItem().transform(resp -> getUserInfo(resp)); } @@ -163,7 +168,7 @@ private UniOnItem> getHttpResponse(String uri, MultiMap for LOG.debugf("Get token on: %s params: %s headers: %s", metadata.getTokenUri(), formBody, request.headers()); // Retry up to three times with a one-second delay between the retries if the connection is closed. Buffer buffer = OidcCommonUtils.encodeForm(formBody); - Uni> response = filter(request, buffer).sendBuffer(buffer) + Uni> response = filter(request, buffer, null).sendBuffer(buffer) .onFailure(ConnectException.class) .retry() .atMost(oidcConfig.connectionRetryCount).onFailure().transform(t -> t.getCause()); @@ -219,10 +224,15 @@ public Key getClientJwtKey() { return clientJwtKey; } - private HttpRequest filter(HttpRequest request, Buffer body) { - for (OidcClientRequestFilter filter : filters) { - filter.filter(request, body); + private HttpRequest filter(HttpRequest request, Buffer body, + OidcRequestContextProperties contextProperties) { + for (OidcRequestFilter filter : filters) { + filter.filter(request, body, contextProperties); } return request; } + + public Vertx getVertx() { + return vertx; + } } diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcRecorder.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcRecorder.java index f53e0d0a0e23bf..50264f617dfd54 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcRecorder.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcRecorder.java @@ -30,7 +30,7 @@ import io.quarkus.oidc.OidcTenantConfig.TokenStateManager.Strategy; import io.quarkus.oidc.TenantConfigResolver; import io.quarkus.oidc.TenantIdentityProvider; -import io.quarkus.oidc.common.OidcClientRequestFilter; +import io.quarkus.oidc.common.OidcRequestFilter; import io.quarkus.oidc.common.runtime.OidcCommonConfig; import io.quarkus.oidc.common.runtime.OidcCommonUtils; import io.quarkus.runtime.LaunchMode; @@ -352,17 +352,16 @@ protected static Uni createOidcProvider(OidcTenantConfig oidcConfi .transformToUni(new Function>() { @Override public Uni apply(OidcProviderClient client) { - if (client.getMetadata().getJsonWebKeySetUri() != null + if (oidcConfig.jwks.resolveEarly + && client.getMetadata().getJsonWebKeySetUri() != null && !oidcConfig.token.requireJwtIntrospectionOnly) { return getJsonWebSetUni(client, oidcConfig).onItem() .transform(new Function() { - @Override public OidcProvider apply(JsonWebKeySet jwks) { return new OidcProvider(client, oidcConfig, jwks, readTokenDecryptionKey(oidcConfig)); } - }); } else { return Uni.createFrom() @@ -405,7 +404,7 @@ private static Key readTokenDecryptionKey(OidcTenantConfig oidcConfig) { protected static Uni getJsonWebSetUni(OidcProviderClient client, OidcTenantConfig oidcConfig) { if (!oidcConfig.isDiscoveryEnabled().orElse(true)) { final long connectionDelayInMillisecs = OidcCommonUtils.getConnectionDelayInMillis(oidcConfig); - return client.getJsonWebKeySet().onFailure(OidcCommonUtils.oidcEndpointNotAvailable()) + return client.getJsonWebKeySet(null).onFailure(OidcCommonUtils.oidcEndpointNotAvailable()) .retry() .withBackOff(OidcCommonUtils.CONNECTION_BACKOFF_DURATION, OidcCommonUtils.CONNECTION_BACKOFF_DURATION) .expireIn(connectionDelayInMillisecs) @@ -419,7 +418,7 @@ public Throwable apply(Throwable t) { .onFailure() .invoke(client::close); } else { - return client.getJsonWebKeySet(); + return client.getJsonWebKeySet(null); } } @@ -434,7 +433,7 @@ protected static Uni createOidcClientUni(OidcTenantConfig oi WebClient client = WebClient.create(new io.vertx.mutiny.core.Vertx(vertx), options); - List clientRequestFilters = OidcCommonUtils.getClientRequestCustomizer(); + List clientRequestFilters = OidcCommonUtils.getClientRequestCustomizer(); Uni metadataUni = null; if (!oidcConfig.discoveryEnabled.orElse(true)) { @@ -479,7 +478,7 @@ public Uni apply(OidcConfigurationMetadata metadata, Throwab + " Use 'quarkus.oidc.user-info-path' if the discovery is disabled.")); } return Uni.createFrom() - .item(new OidcProviderClient(client, metadata, oidcConfig, clientRequestFilters)); + .item(new OidcProviderClient(client, vertx, metadata, oidcConfig, clientRequestFilters)); } }); diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcUtils.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcUtils.java index 7b3d41938b0b50..8fcc88f5c15ce4 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcUtils.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcUtils.java @@ -208,6 +208,9 @@ private static List findClaimWithRoles(OidcTenantConfig.Roles rolesConfi return convertJsonArrayToList((JsonArray) claimValue); } else if (claimValue != null) { String sep = rolesConfig.getRoleClaimSeparator().isPresent() ? rolesConfig.getRoleClaimSeparator().get() : " "; + if (claimValue.toString().isBlank()) { + return Collections.emptyList(); + } return Arrays.asList(claimValue.toString().split(sep)); } else { return Collections.emptyList(); @@ -237,6 +240,10 @@ private static Object findClaimValue(String claimPath, JsonObject json, String[] private static List convertJsonArrayToList(JsonArray claimValue) { List list = new ArrayList<>(claimValue.size()); for (int i = 0; i < claimValue.size(); i++) { + String claimValueStr = claimValue.getString(i); + if (claimValueStr == null || claimValueStr.isBlank()) { + continue; + } list.add(claimValue.getString(i)); } return list; diff --git a/extensions/oidc/runtime/src/test/java/io/quarkus/oidc/runtime/MemoryCacheTest.java b/extensions/oidc/runtime/src/test/java/io/quarkus/oidc/runtime/MemoryCacheTest.java new file mode 100644 index 00000000000000..6e0d7f082f6c14 --- /dev/null +++ b/extensions/oidc/runtime/src/test/java/io/quarkus/oidc/runtime/MemoryCacheTest.java @@ -0,0 +1,101 @@ +package io.quarkus.oidc.runtime; + +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.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.time.Duration; +import java.util.Optional; +import java.util.concurrent.Callable; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; + +import io.vertx.core.Vertx; + +public class MemoryCacheTest { + + static Vertx vertx = Vertx.vertx(); + + @AfterAll + public static void closeVertxClient() { + if (vertx != null) { + vertx.close().toCompletionStage().toCompletableFuture().join(); + vertx = null; + } + } + + @Test + public void testCache() throws Exception { + + MemoryCache cache = new MemoryCache(vertx, + // timer interval + Optional.of(Duration.ofSeconds(1)), + // entry is valid for 3 seconds + Duration.ofSeconds(2), + // max cache size + 2); + cache.add("1", new Bean("1")); + cache.add("2", new Bean("2")); + assertEquals(2, cache.getCacheSize()); + + assertEquals("1", cache.get("1").name); + assertEquals("2", cache.get("2").name); + + assertEquals("1", cache.remove("1").name); + assertNull(cache.get("1")); + assertEquals("2", cache.get("2").name); + assertEquals(1, cache.getCacheSize()); + + assertTrue(cache.isTimerRunning()); + + await().atMost(Duration.ofSeconds(5)).until(new Callable() { + + @Override + public Boolean call() throws Exception { + return cache.getCacheSize() == 0; + } + + }); + + cache.stopTimer(vertx); + assertFalse(cache.isTimerRunning()); + } + + @Test + public void testAddWhenMaxCacheSizeIsReached() throws Exception { + + MemoryCache cache = new MemoryCache(vertx, + // timer interval + Optional.empty(), + // entry is valid for 3 seconds + Duration.ofSeconds(3), + // max cache size + 2); + assertFalse(cache.isTimerRunning()); + + cache.add("1", new Bean("1")); + cache.add("2", new Bean("2")); + assertEquals(2, cache.getCacheSize()); + + // Currently, if the cache is full and a new entry has to be added, then the whole cache is cleared + // It can be optimized to remove the oldest entry only in the future + + cache.add("3", new Bean("3")); + assertEquals(1, cache.getCacheSize()); + + assertNull(cache.get("1")); + assertNull(cache.get("2")); + assertEquals("3", cache.get("3").name); + } + + static class Bean { + String name; + + Bean(String name) { + this.name = name; + } + } +} diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/InvalidAttributesConfigTest.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/InvalidAttributesConfigTest.java new file mode 100644 index 00000000000000..f2d1f661158ec6 --- /dev/null +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/InvalidAttributesConfigTest.java @@ -0,0 +1,31 @@ +package io.quarkus.opentelemetry.deployment; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.NoSuchElementException; +import java.util.function.Consumer; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class InvalidAttributesConfigTest { + @RegisterExtension + static final QuarkusUnitTest TEST = new QuarkusUnitTest() + .overrideConfigKey("quarkus.otel.resource.attributes", "pod.id=${SOMETHING}") + .assertException(new Consumer() { + @Override + public void accept(final Throwable throwable) { + throwable.getCause().printStackTrace(); + assertTrue(throwable.getCause() instanceof NoSuchElementException); + assertEquals("SRCFG00011: Could not expand value SOMETHING in property quarkus.otel.resource.attributes", + throwable.getCause().getMessage()); + } + }); + + @Test + void test() { + } +} diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryResourceTest.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryResourceTest.java index c1e2c9bfef298e..92c919c44fe800 100644 --- a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryResourceTest.java +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryResourceTest.java @@ -4,6 +4,7 @@ import static io.quarkus.opentelemetry.deployment.common.TestSpanExporter.getSpanByKindAndParentId; import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import java.util.List; @@ -54,6 +55,7 @@ void resource() { assertEquals("authservice", server.getResource().getAttribute(AttributeKey.stringKey("service.name"))); assertEquals(config.getRawValue("quarkus.uuid"), server.getResource().getAttribute(AttributeKey.stringKey("service.instance.id"))); + assertNotNull(server.getResource().getAttribute(AttributeKey.stringKey("host.name"))); } @Path("/hello") diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/AutoConfiguredOpenTelemetrySdkBuilderCustomizer.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/AutoConfiguredOpenTelemetrySdkBuilderCustomizer.java index cc9666624cd97f..de7e1ee998d961 100644 --- a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/AutoConfiguredOpenTelemetrySdkBuilderCustomizer.java +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/AutoConfiguredOpenTelemetrySdkBuilderCustomizer.java @@ -3,6 +3,8 @@ import static java.lang.Boolean.TRUE; import static java.util.Collections.emptyList; +import java.net.InetAddress; +import java.net.UnknownHostException; import java.util.ArrayList; import java.util.List; import java.util.function.BiFunction; @@ -71,12 +73,21 @@ public Resource apply(Resource existingResource, ConfigProperties configProperti .filter(sn -> !sn.equals(appConfig.name.orElse("unset"))) .orElse(null); + // must be resolved at startup, once. + String hostname = null; + try { + hostname = InetAddress.getLocalHost().getHostName(); + } catch (UnknownHostException e) { + hostname = "unknown"; + } + // Merge resource instances with env attributes Resource resource = resources.stream() .reduce(Resource.empty(), Resource::merge) .merge(TracerUtil.mapResourceAttributes( oTelRuntimeConfig.resourceAttributes().orElse(emptyList()), - serviceName)); // from properties + serviceName, // from properties + hostname)); return consolidatedResource.merge(resource); } else { return Resource.builder().build(); diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/OpenTelemetryRecorder.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/OpenTelemetryRecorder.java index 9c57af1d76d2f1..547a40b10789d5 100644 --- a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/OpenTelemetryRecorder.java +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/OpenTelemetryRecorder.java @@ -10,6 +10,7 @@ import jakarta.enterprise.util.TypeLiteral; import org.eclipse.microprofile.config.ConfigProvider; +import org.eclipse.microprofile.config.spi.Converter; import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.api.OpenTelemetry; @@ -20,8 +21,6 @@ import io.quarkus.opentelemetry.runtime.config.runtime.OTelRuntimeConfig; import io.quarkus.runtime.annotations.Recorder; import io.quarkus.runtime.configuration.DurationConverter; -import io.smallrye.config.ConfigValue; -import io.smallrye.config.NameIterator; import io.smallrye.config.SmallRyeConfig; import io.vertx.core.Vertx; @@ -89,46 +88,58 @@ private Map getOtelConfigs() { // load new properties for (String propertyName : config.getPropertyNames()) { if (propertyName.startsWith("quarkus.otel.")) { - ConfigValue configValue = config.getConfigValue(propertyName); - if (configValue.getValue() != null) { - NameIterator name = new NameIterator(propertyName); - name.next(); - oTelConfigs.put(name.getName().substring(name.getPosition() + 1), getValue(configValue)); - } + String value = getValue(config, propertyName); + oTelConfigs.put(propertyName.substring(8), value); } } return oTelConfigs; } - /** - * Transforms the value to what OTel expects - * TODO: this is super simplistic, and should be more modular if needed - */ - private String getValue(ConfigValue configValue) { - String name = configValue.getName(); - if (name.endsWith("timeout") || name.endsWith("delay")) { - String value = configValue.getValue(); - if (DurationConverter.DIGITS.asPredicate().test(value)) { - // OTel regards values without a unit to me milliseconds instead of seconds - // that java.time.Duration assumes, so let's just not do any conversion and let OTel handle with it - return value; - } - Duration duration; - try { - duration = DurationConverter.parseDuration(value); - } catch (Exception ignored) { - // it's not a Duration, so we can't do much - return configValue.getValue(); - } - try { - return duration.toMillis() + "ms"; - } catch (Exception ignored) { - return duration.toSeconds() + "s"; - } + private String getValue(SmallRyeConfig config, String propertyName) { + if (propertyName.endsWith("timeout") || propertyName.endsWith("delay")) { + return config.getValue(propertyName, OTelDurationConverter.INSTANCE); + } else { + return config.getValue(propertyName, String.class); } - return configValue.getValue(); } }; } + /** + * Transforms the value to what OTel expects + * TODO: this is super simplistic, and should be more modular if needed + */ + private static class OTelDurationConverter implements Converter { + static OTelDurationConverter INSTANCE = new OTelDurationConverter(); + + @Override + public String convert(final String value) throws IllegalArgumentException, NullPointerException { + if (value == null) { + throw new NullPointerException(); + } + + if (DurationConverter.DIGITS.asPredicate().test(value)) { + // OTel regards values without a unit to me milliseconds instead of seconds + // that java.time.Duration assumes, so let's just not do any conversion and let OTel handle with it + return value; + } + Duration duration; + try { + duration = DurationConverter.parseDuration(value); + } catch (Exception ignored) { + // it's not a Duration, so we can't do much + return value; + } + + if (duration == null) { + return value; + } + + try { + return duration.toMillis() + "ms"; + } catch (Exception ignored) { + return duration.toSeconds() + "s"; + } + } + } } 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 84ebd239996fa2..9c8e915904a04d 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,5 +1,6 @@ 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 java.util.List; @@ -14,7 +15,7 @@ public class TracerUtil { private TracerUtil() { } - public static Resource mapResourceAttributes(List resourceAttributes, String serviceName) { + public static Resource mapResourceAttributes(List resourceAttributes, String serviceName, String hostname) { final AttributesBuilder attributesBuilder = Attributes.builder(); if (!resourceAttributes.isEmpty()) { @@ -27,6 +28,10 @@ public static Resource mapResourceAttributes(List resourceAttributes, St attributesBuilder.put(SERVICE_NAME.getKey(), serviceName); } + if (hostname != null) { + attributesBuilder.put(HOST_NAME, hostname); + } + return Resource.create(attributesBuilder.build()); } } 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 c3b27767970ef7..99c005ce5ed91a 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 @@ -19,7 +19,7 @@ public void testMapResourceAttributes() { "service.namespace=mynamespace", "service.version=1.0", "deployment.environment=production"); - Resource resource = TracerUtil.mapResourceAttributes(resourceAttributes, null); + Resource resource = TracerUtil.mapResourceAttributes(resourceAttributes, null, null); Attributes attributes = resource.getAttributes(); Assertions.assertThat(attributes.size()).isEqualTo(4); Assertions.assertThat(attributes.get(ResourceAttributes.SERVICE_NAME)).isEqualTo("myservice"); 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 d4e52487d5ecd1..adc3c78166a5c9 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 @@ -104,7 +104,6 @@ import io.quarkus.qute.ErrorCode; import io.quarkus.qute.Expression; import io.quarkus.qute.Expression.VirtualMethodPart; -import io.quarkus.qute.Expressions; import io.quarkus.qute.LoopSectionHelper; import io.quarkus.qute.NamespaceResolver; import io.quarkus.qute.ParameterDeclaration; @@ -121,7 +120,6 @@ import io.quarkus.qute.TemplateGlobal; import io.quarkus.qute.TemplateInstance; import io.quarkus.qute.TemplateLocator; -import io.quarkus.qute.TemplateNode; import io.quarkus.qute.UserTagSectionHelper; import io.quarkus.qute.ValueResolver; import io.quarkus.qute.Variant; @@ -162,7 +160,7 @@ public class QuteProcessor { private static final String CHECKED_TEMPLATE_BASE_PATH = "basePath"; private static final String CHECKED_TEMPLATE_DEFAULT_NAME = "defaultName"; private static final String IGNORE_FRAGMENTS = "ignoreFragments"; - private static final String BASE_PATH = "templates"; + private static final String DEFAULT_ROOT_PATH = "templates"; private static final Set ITERATION_METADATA_KEYS = Set.of("count", "index", "indexParity", "hasNext", "odd", "isOdd", "even", "isEven", "isLast", "isFirst"); @@ -186,6 +184,20 @@ FeatureBuildItem feature() { return new FeatureBuildItem(Feature.QUTE); } + @BuildStep + TemplateRootBuildItem defaultTemplateRoot() { + return new TemplateRootBuildItem(DEFAULT_ROOT_PATH); + } + + @BuildStep + TemplateRootsBuildItem collectTemplateRoots(List templateRoots) { + Set roots = new HashSet<>(); + for (TemplateRootBuildItem root : templateRoots) { + roots.add(root.getPath()); + } + return new TemplateRootsBuildItem(Set.copyOf(roots)); + } + @BuildStep List beanDefiningAnnotations() { return List.of( @@ -657,49 +669,9 @@ public Optional getVariant() { } }); - // It's a file-based template - // We need to find out whether the parsed template represents a checked template - Map pathToPathWithoutSuffix = new HashMap<>(); - for (String path : filePaths.getFilePaths()) { - for (String suffix : config.suffixes) { - if (path.endsWith(suffix)) { - // Remove the suffix and add to Map - pathToPathWithoutSuffix.put(path, path.substring(0, path.length() - (suffix.length() + 1))); - break; - } - } - - // Path has already no suffix - if (!pathToPathWithoutSuffix.containsKey(path)) { - pathToPathWithoutSuffix.put(path, path); - } - } - - // Checked Template id -> method parameter declaration - Map> checkedTemplateIdToParamDecl = new HashMap<>(); - for (CheckedTemplateBuildItem checkedTemplate : checkedTemplates) { - if (checkedTemplate.isFragment()) { - continue; - } - for (Entry entry : checkedTemplate.bindings.entrySet()) { - checkedTemplateIdToParamDecl - .computeIfAbsent(checkedTemplate.templateId, s -> new HashMap<>()) - .put(entry.getKey(), new MethodParameterDeclaration(entry.getValue(), entry.getKey())); - } - } - - // Message Bundle Template id -> method parameter declaration - Map> msgBundleTemplateIdToParamDecl = new HashMap<>(); - for (MessageBundleMethodBuildItem messageBundleMethod : messageBundleMethods) { - MethodInfo method = messageBundleMethod.getMethod(); - for (ListIterator it = method.parameterTypes().listIterator(); it.hasNext();) { - Type paramType = it.next(); - String name = MessageBundleProcessor.getParameterName(method, it.previousIndex()); - msgBundleTemplateIdToParamDecl - .computeIfAbsent(messageBundleMethod.getTemplateId(), s -> new HashMap<>()) - .put(name, new MethodParameterDeclaration(getCheckedTemplateParameterTypeName(paramType), name)); - } - } + Map messageBundleMethodsMap = messageBundleMethods.stream() + .filter(MessageBundleMethodBuildItem::isValidatable) + .collect(Collectors.toMap(MessageBundleMethodBuildItem::getTemplateId, Function.identity())); builder.addParserHook(new ParserHook() { @@ -715,8 +687,17 @@ public void beforeParsing(ParserHelper parserHelper) { getCheckedTemplateParameterTypeName(global.getVariableType()).toString()); } - addMethodParamsToParserHelper(parserHelper, pathToPathWithoutSuffix.get(templateId), - checkedTemplateIdToParamDecl); + // It's a file-based template + // We need to find out whether the parsed template represents a checked template + String path = templatePathWithoutSuffix(templateId, config); + for (CheckedTemplateBuildItem checkedTemplate : checkedTemplates) { + if (checkedTemplate.templateId.equals(path)) { + for (Entry entry : checkedTemplate.bindings.entrySet()) { + parserHelper.addParameter(entry.getKey(), entry.getValue()); + } + break; + } + } if (templateId.startsWith(TemplatePathBuildItem.TAGS)) { parserHelper.addParameter(UserTagSectionHelper.Factory.ARGS, @@ -724,7 +705,16 @@ public void beforeParsing(ParserHelper parserHelper) { } } - addMethodParamsToParserHelper(parserHelper, templateId, msgBundleTemplateIdToParamDecl); + // If needed add params to message bundle templates + MessageBundleMethodBuildItem messageBundleMethod = messageBundleMethodsMap.get(templateId); + if (messageBundleMethod != null) { + MethodInfo method = messageBundleMethod.getMethod(); + for (ListIterator it = method.parameterTypes().listIterator(); it.hasNext();) { + Type paramType = it.next(); + String name = MessageBundleProcessor.getParameterName(method, it.previousIndex()); + parserHelper.addParameter(name, getCheckedTemplateParameterTypeName(paramType)); + } + } } }).build(); @@ -736,17 +726,7 @@ public void beforeParsing(ParserHelper parserHelper) { for (TemplatePathBuildItem path : templatePaths) { Template template = dummyEngine.getTemplate(path.getPath()); if (template != null) { - String templateIdWithoutSuffix = pathToPathWithoutSuffix.get(template.getId()); - - final List parameterDeclarations; - if (checkedTemplateIdToParamDecl.isEmpty()) { - parameterDeclarations = template.getParameterDeclarations(); - } else { - // Add method parameter declarations if they were not overridden in the template - parameterDeclarations = mergeParamDeclarations( - template.getParameterDeclarations(), - checkedTemplateIdToParamDecl.get(templateIdWithoutSuffix)); - } + String templateIdWithoutSuffix = templatePathWithoutSuffix(template.getId(), config); if (!checkedFragments.isEmpty()) { for (CheckedTemplateBuildItem checkedFragment : checkedFragments) { @@ -766,21 +746,15 @@ public void beforeParsing(ParserHelper parserHelper) { } analysis.add(new TemplateAnalysis(null, template.getGeneratedId(), template.getExpressions(), - parameterDeclarations, path.getPath(), template.getFragmentIds())); + template.getParameterDeclarations(), path.getPath(), template.getFragmentIds())); } } // Message bundle templates for (MessageBundleMethodBuildItem messageBundleMethod : messageBundleMethods) { Template template = dummyEngine.parse(messageBundleMethod.getTemplate(), null, messageBundleMethod.getTemplateId()); - - // Add method parameter declarations if they were not overridden in the template - List paramDeclarations = mergeParamDeclarations( - template.getParameterDeclarations(), - msgBundleTemplateIdToParamDecl.get(messageBundleMethod.getTemplateId())); - analysis.add(new TemplateAnalysis(messageBundleMethod.getTemplateId(), template.getGeneratedId(), - template.getExpressions(), paramDeclarations, + template.getExpressions(), template.getParameterDeclarations(), messageBundleMethod.getMethod().declaringClass().name() + "#" + messageBundleMethod.getMethod().name() + "()", template.getFragmentIds())); @@ -791,6 +765,17 @@ public void beforeParsing(ParserHelper parserHelper) { return new TemplatesAnalysisBuildItem(analysis); } + private String templatePathWithoutSuffix(String path, QuteConfig config) { + for (String suffix : config.suffixes) { + if (path.endsWith(suffix)) { + // Remove the suffix + path = path.substring(0, path.length() - (suffix.length() + 1)); + break; + } + } + return path; + } + @BuildStep void validateCheckedFragments(List validations, List expressionMatches, @@ -925,29 +910,6 @@ private static String getCheckedTemplateParameterParameterizedTypeName(Parameter return builder.toString(); } - private List mergeParamDeclarations(List parameterDeclarations, - Map paramNameToDeclaration) { - if (paramNameToDeclaration != null) { - Map mergeResult = new HashMap<>(paramNameToDeclaration); - for (ParameterDeclaration paramDeclaration : parameterDeclarations) { - // Template parameter declarations override method parameter declarations - mergeResult.put(paramDeclaration.getKey(), paramDeclaration); - } - return List.copyOf(mergeResult.values()); - } - return parameterDeclarations; - } - - private void addMethodParamsToParserHelper(ParserHelper parserHelper, String templateId, - Map> templateIdToParamDecl) { - var paramNameToDeclaration = templateIdToParamDecl.get(templateId); - if (paramNameToDeclaration != null) { - for (MethodParameterDeclaration parameterDeclaration : paramNameToDeclaration.values()) { - parserHelper.addParameter(parameterDeclaration.getKey(), parameterDeclaration.getParamType()); - } - } - } - @BuildStep void validateExpressions(TemplatesAnalysisBuildItem templatesAnalysis, BeanArchiveIndexBuildItem beanArchiveIndex, @@ -1481,17 +1443,24 @@ private static NamespaceResult processNamespace(Expression expression, MatchResu // data: Expression.Part firstPart = expression.getParts().get(0); String firstPartName = firstPart.getName(); - for (ParameterDeclaration paramDeclaration : templateAnalysis.parameterDeclarations) { - if (paramDeclaration.getKey().equals(firstPartName)) { - // Data Namespace expression has bounded parameter declaration - dataNamespaceTypeInfo = TypeInfos - .create(paramDeclaration.getTypeInfo(), firstPart, index, templateIdToPathFun, - expression.getOrigin()) - .asTypeInfo(); + // FIXME This is not entirely correct + // First we try to find a non-synthetic param declaration that matches the given name, + // and then we try the synthetic ones. + // However, this might result in confusing behavior when type-safe templates are used together with type-safe expressions. + // But this should not be a common use case. + ParameterDeclaration paramDeclaration = null; + for (ParameterDeclaration pd : templateAnalysis.getSortedParameterDeclarations()) { + if (pd.getKey().equals(firstPartName)) { + paramDeclaration = pd; break; } } - if (dataNamespaceTypeInfo == null) { + if (paramDeclaration != null) { + dataNamespaceTypeInfo = TypeInfos + .create(paramDeclaration.getTypeInfo(), firstPart, index, templateIdToPathFun, + expression.getOrigin()) + .asTypeInfo(); + } else { putResult(match, results, expression); ignored = true; } @@ -2090,9 +2059,9 @@ void collectTemplates(ApplicationArchivesBuildItem applicationArchives, BuildProducer watchedPaths, BuildProducer templatePaths, BuildProducer nativeImageResources, - QuteConfig config) + QuteConfig config, + TemplateRootsBuildItem templateRoots) throws IOException { - Set basePaths = new HashSet<>(); Set allApplicationArchives = applicationArchives.getAllApplicationArchives(); List extensionArtifacts = curateOutcome.getApplicationModel().getDependencies().stream() .filter(Dependency::isRuntimeExtensionArtifact).collect(Collectors.toList()); @@ -2101,7 +2070,12 @@ void collectTemplates(ApplicationArchivesBuildItem applicationArchives, watchedPaths.produce(HotDeploymentWatchedFileBuildItem.builder().setLocationPredicate(new Predicate() { @Override public boolean test(String path) { - return path.startsWith(BASE_PATH); + for (String rootPath : templateRoots) { + if (path.startsWith(rootPath)) { + return true; + } + } + return false; } }).build()); @@ -2110,54 +2084,67 @@ public boolean test(String path) { // Skip extension archives that are also application archives continue; } - for (Path path : artifact.getResolvedPaths()) { - if (Files.isDirectory(path)) { - // Try to find the templates in the root dir - try (Stream paths = Files.list(path)) { - Path basePath = paths.filter(QuteProcessor::isBasePath).findFirst().orElse(null); - if (basePath != null) { - LOGGER.debugf("Found extension templates dir: %s", path); - scan(basePath, basePath, BASE_PATH + "/", watchedPaths, templatePaths, nativeImageResources, - config); - break; - } - } + for (Path resolvedPath : artifact.getResolvedPaths()) { + if (Files.isDirectory(resolvedPath)) { + scanPath(resolvedPath, resolvedPath, config, templateRoots, watchedPaths, templatePaths, + nativeImageResources); } else { - try (FileSystem artifactFs = ZipUtils.newFileSystem(path)) { - Path basePath = artifactFs.getPath(BASE_PATH); - if (Files.exists(basePath)) { - LOGGER.debugf("Found extension templates in: %s", path); - scan(basePath, basePath, BASE_PATH + "/", watchedPaths, templatePaths, nativeImageResources, - config); + try (FileSystem artifactFs = ZipUtils.newFileSystem(resolvedPath)) { + for (String templateRoot : templateRoots) { + Path artifactBasePath = artifactFs.getPath(templateRoot); + if (Files.exists(artifactBasePath)) { + LOGGER.debugf("Found extension templates in: %s", resolvedPath); + scan(artifactBasePath, artifactBasePath, templateRoot + "/", watchedPaths, templatePaths, + nativeImageResources, + config); + } } } catch (IOException e) { - LOGGER.warnf(e, "Unable to create the file system from the path: %s", path); + LOGGER.warnf(e, "Unable to create the file system from the path: %s", resolvedPath); } } } } for (ApplicationArchive archive : allApplicationArchives) { archive.accept(tree -> { - for (Path rootDir : tree.getRoots()) { + for (Path root : tree.getRoots()) { // Note that we cannot use ApplicationArchive.getChildPath(String) here because we would not be able to detect // a wrong directory name on case-insensitive file systems - try (Stream rootDirPaths = Files.list(rootDir)) { - Path basePath = rootDirPaths.filter(QuteProcessor::isBasePath).findFirst().orElse(null); - if (basePath != null) { - LOGGER.debugf("Found templates dir: %s", basePath); - basePaths.add(basePath); - scan(basePath, basePath, BASE_PATH + "/", watchedPaths, templatePaths, nativeImageResources, - config); - break; - } - } catch (IOException e) { - throw new UncheckedIOException(e); - } + scanPath(root, root, config, templateRoots, watchedPaths, templatePaths, nativeImageResources); } }); } } + private void scanPath(Path rootPath, Path path, QuteConfig config, TemplateRootsBuildItem templateRoots, + BuildProducer watchedPaths, + BuildProducer templatePaths, + BuildProducer nativeImageResources) { + if (!Files.isDirectory(path)) { + return; + } + try (Stream paths = Files.list(path)) { + for (Path file : paths.collect(Collectors.toList())) { + if (Files.isDirectory(file)) { + // Iterate over the directories in the root + // "/io", "/META-INF", "/templates", "/web", etc. + Path relativePath = rootPath.relativize(file); + if (templateRoots.isRoot(relativePath)) { + LOGGER.debugf("Found templates dir: %s", file); + scan(file, file, file.getFileName() + "/", watchedPaths, templatePaths, + nativeImageResources, + config); + } else if (templateRoots.maybeRoot(relativePath)) { + // Scan the path recursively because the template root may be nested, for example "/web/public" + scanPath(rootPath, file, config, templateRoots, watchedPaths, templatePaths, nativeImageResources); + } + } + } + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + @BuildStep TemplateFilePathsBuildItem collectTemplateFilePaths(QuteConfig config, List templatePaths) { Set filePaths = new HashSet(); @@ -2412,7 +2399,8 @@ 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<>(); List tags = new ArrayList<>(); @@ -2436,7 +2424,8 @@ 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(GeneratedTemplateInitializerBuildItem::getClassName).collect(Collectors.toList()), + templateRoots.getPaths().stream().map(p -> p + "/").collect(Collectors.toSet()))) .done()); } @@ -3398,10 +3387,6 @@ private static boolean isExcluded(TypeCheck check, Iterable return false; } - private static boolean isBasePath(Path path) { - return path.getFileName().toString().equals(BASE_PATH); - } - private void checkDuplicatePaths(List templatePaths) { Map> duplicates = templatePaths.stream() .collect(Collectors.groupingBy(TemplatePathBuildItem::getPath)); @@ -3538,39 +3523,4 @@ public String getName() { } - private static final class MethodParameterDeclaration implements ParameterDeclaration { - - private final String paramType; - private final String paramName; - - private MethodParameterDeclaration(String paramType, String paramName) { - this.paramType = paramType; - this.paramName = paramName; - } - - public String getParamType() { - return paramType; - } - - @Override - public String getTypeInfo() { - return Expressions.typeInfoFrom(paramType); - } - - @Override - public String getKey() { - return paramName; - } - - @Override - public Expression getDefaultValue() { - return null; - } - - @Override - public TemplateNode.Origin getOrigin() { - return null; - } - } - } diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplateRootBuildItem.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplateRootBuildItem.java new file mode 100644 index 00000000000000..4ce1258e48ac37 --- /dev/null +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplateRootBuildItem.java @@ -0,0 +1,36 @@ +package io.quarkus.qute.deployment; + +import io.quarkus.builder.item.MultiBuildItem; + +/** + * This build item represents a source of template files. + *

+ * By default, the templates are found in the {@code templates} directory. However, an extension can produce this build item to + * register an additional root path. + *

+ * The path is relative to the artifact/project root and OS-agnostic, i.e. {@code /} is used as a path separator. + */ +public final class TemplateRootBuildItem extends MultiBuildItem { + + private final String path; + + public TemplateRootBuildItem(String path) { + this.path = normalize(path); + } + + public String getPath() { + return path; + } + + static String normalize(String path) { + path = path.strip(); + if (path.startsWith("/")) { + path = path.substring(1); + } + if (path.endsWith("/")) { + path = path.substring(0, path.length() - 1); + } + return path; + } + +} diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplateRootsBuildItem.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplateRootsBuildItem.java new file mode 100644 index 00000000000000..a6750aa200976d --- /dev/null +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplateRootsBuildItem.java @@ -0,0 +1,72 @@ +package io.quarkus.qute.deployment; + +import java.io.File; +import java.nio.file.Path; +import java.util.Iterator; +import java.util.Set; + +import io.quarkus.builder.item.SimpleBuildItem; + +/** + * The set of template root paths. + */ +public final class TemplateRootsBuildItem extends SimpleBuildItem implements Iterable { + + private Set rootPaths; + + public TemplateRootsBuildItem(Set paths) { + this.rootPaths = paths; + } + + public Set getPaths() { + return rootPaths; + } + + @Override + public Iterator iterator() { + return rootPaths.iterator(); + } + + /** + * The path must be relative to the resource root. + * + * @param path + * @return {@code true} is the given path represents a template root, {@code false} otherwise + */ + public boolean isRoot(Path path) { + String pathStr = normalize(path); + for (String rootPath : rootPaths) { + if (pathStr.equals(rootPath)) { + return true; + } + } + return false; + } + + /** + * The path must be relative to the resource root. + * + * @param path + * @return {@code true} is the given path may represent a template root, {@code false} otherwise + */ + public boolean maybeRoot(Path path) { + String pathStr = normalize(path); + for (String rootPath : rootPaths) { + if ((rootPath.contains("/") && rootPath.startsWith(pathStr)) + || rootPath.equals(pathStr)) { + return true; + } + } + return false; + } + + private static String normalize(Path path) { + String pathStr = path.toString(); + if (File.separatorChar != '/') { + // \foo\bar\templates -> /foo/bar/templates + pathStr = pathStr.replace(File.separatorChar, '/'); + } + return TemplateRootBuildItem.normalize(pathStr); + } + +} diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplatesAnalysisBuildItem.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplatesAnalysisBuildItem.java index 830588305c4eca..d67efdf86ab795 100644 --- a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplatesAnalysisBuildItem.java +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplatesAnalysisBuildItem.java @@ -1,5 +1,7 @@ package io.quarkus.qute.deployment; +import java.util.ArrayList; +import java.util.Comparator; import java.util.List; import java.util.Objects; import java.util.Set; @@ -62,6 +64,23 @@ Expression findExpression(int id) { return null; } + /** + * Non-synthetic declarations go first, then sorted by the line. + * + * @return the sorted list of parameter declarations + */ + public List getSortedParameterDeclarations() { + List ret = new ArrayList<>(parameterDeclarations); + ret.sort(new Comparator() { + @Override + public int compare(ParameterDeclaration pd1, ParameterDeclaration pd2) { + int ret = Boolean.compare(pd1.getOrigin().isSynthetic(), pd2.getOrigin().isSynthetic()); + return ret == 0 ? Integer.compare(pd1.getOrigin().getLine(), pd2.getOrigin().getLine()) : ret; + } + }); + return ret; + } + @Override public int hashCode() { final int prime = 31; diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/TemplateAnalysisTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/TemplateAnalysisTest.java new file mode 100644 index 00000000000000..143d778592b4fe --- /dev/null +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/TemplateAnalysisTest.java @@ -0,0 +1,87 @@ +package io.quarkus.qute.deployment; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.Optional; + +import org.junit.jupiter.api.Test; + +import io.quarkus.qute.Expression; +import io.quarkus.qute.ParameterDeclaration; +import io.quarkus.qute.TemplateNode.Origin; +import io.quarkus.qute.Variant; +import io.quarkus.qute.deployment.TemplatesAnalysisBuildItem.TemplateAnalysis; + +public class TemplateAnalysisTest { + + @Test + public void testSortedParamDeclarations() { + TemplateAnalysis analysis = new TemplateAnalysis(null, null, null, List.of(paramDeclaration("foo", -1), + paramDeclaration("bar", -1), paramDeclaration("qux", 10), paramDeclaration("baz", 1)), null, null); + List sorted = analysis.getSortedParameterDeclarations(); + assertEquals(4, sorted.size()); + assertEquals("baz", sorted.get(0).getKey()); + assertEquals("qux", sorted.get(1).getKey()); + assertTrue(sorted.get(2).getKey().equals("foo") || sorted.get(2).getKey().equals("bar")); + assertTrue(sorted.get(3).getKey().equals("foo") || sorted.get(3).getKey().equals("bar")); + } + + ParameterDeclaration paramDeclaration(String key, int line) { + return new ParameterDeclaration() { + + @Override + public String getTypeInfo() { + return null; + } + + @Override + public Origin getOrigin() { + return new Origin() { + + @Override + public Optional getVariant() { + return Optional.empty(); + } + + @Override + public String getTemplateId() { + return null; + } + + @Override + public String getTemplateGeneratedId() { + return null; + } + + @Override + public int getLineCharacterStart() { + return 0; + } + + @Override + public int getLineCharacterEnd() { + return 0; + } + + @Override + public int getLine() { + return line; + } + }; + } + + @Override + public String getKey() { + return key; + } + + @Override + public Expression getDefaultValue() { + return null; + } + }; + } + +} diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/templateroot/AdditionalTemplateRootTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/templateroot/AdditionalTemplateRootTest.java new file mode 100644 index 00000000000000..202cdbfc8b7e46 --- /dev/null +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/templateroot/AdditionalTemplateRootTest.java @@ -0,0 +1,57 @@ +package io.quarkus.qute.deployment.templateroot; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.function.Consumer; + +import jakarta.inject.Inject; + +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.builder.BuildChainBuilder; +import io.quarkus.builder.BuildContext; +import io.quarkus.builder.BuildStep; +import io.quarkus.qute.Engine; +import io.quarkus.qute.Template; +import io.quarkus.qute.deployment.TemplateRootBuildItem; +import io.quarkus.test.QuarkusUnitTest; + +public class AdditionalTemplateRootTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot(root -> root + .addAsResource(new StringAsset("Hi {name}!"), "templates/hi.txt") + .addAsResource(new StringAsset("Hello {name}!"), "web/public/hello.txt")) + .addBuildChainCustomizer(buildCustomizer()); + + static Consumer buildCustomizer() { + return new Consumer() { + @Override + public void accept(BuildChainBuilder builder) { + builder.addBuildStep(new BuildStep() { + @Override + public void execute(BuildContext context) { + context.produce(new TemplateRootBuildItem("web/public")); + } + }).produces(TemplateRootBuildItem.class) + .build(); + } + }; + } + + @Inject + Template hello; + + @Inject + Engine engine; + + @Test + public void testTemplate() { + assertEquals("Hi M!", engine.getTemplate("hi").data("name", "M").render()); + assertEquals("Hello M!", hello.data("name", "M").render()); + } + +} diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/templateroot/TemplateRootBuildItemTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/templateroot/TemplateRootBuildItemTest.java new file mode 100644 index 00000000000000..5e7a0a1152ed43 --- /dev/null +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/templateroot/TemplateRootBuildItemTest.java @@ -0,0 +1,18 @@ +package io.quarkus.qute.deployment.templateroot; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +import io.quarkus.qute.deployment.TemplateRootBuildItem; + +public class TemplateRootBuildItemTest { + + @Test + public void testNormalizedName() { + assertEquals("foo", new TemplateRootBuildItem("/foo/ ").getPath()); + assertEquals("foo/bar", new TemplateRootBuildItem("/foo/bar").getPath()); + assertEquals("baz", new TemplateRootBuildItem(" baz/").getPath()); + } + +} diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/templateroot/TemplateRootsBuildItemTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/templateroot/TemplateRootsBuildItemTest.java new file mode 100644 index 00000000000000..f2171a2b1bd176 --- /dev/null +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/templateroot/TemplateRootsBuildItemTest.java @@ -0,0 +1,39 @@ +package io.quarkus.qute.deployment.templateroot; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.File; +import java.nio.file.Path; +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import io.quarkus.qute.deployment.TemplateRootsBuildItem; + +public class TemplateRootsBuildItemTest { + + @Test + public void testIsRoot() { + TemplateRootsBuildItem buildItem = new TemplateRootsBuildItem(Set.of("templates", "public/web")); + assertTrue(buildItem.isRoot(Path.of("/templates"))); + assertTrue(buildItem.isRoot(Path.of("public/web"))); + assertTrue(buildItem.isRoot(Path.of("/templates/"))); + assertTrue(buildItem.isRoot(Path.of("public/web/"))); + assertFalse(buildItem.isRoot(Path.of("/foo/templates"))); + assertFalse(buildItem.isRoot(Path.of("/web"))); + assertFalse(buildItem.isRoot(Path.of("public"))); + assertFalse(buildItem.isRoot(Path.of("baz/web"))); + assertFalse(buildItem.isRoot(Path.of("baz/template"))); + } + + @Test + public void testMaybeRoot() { + TemplateRootsBuildItem buildItem = new TemplateRootsBuildItem(Set.of("templates", "public/web")); + assertTrue(buildItem.maybeRoot(Path.of("public"))); + assertTrue(buildItem.maybeRoot(Path.of("templates"))); + assertTrue(buildItem.maybeRoot(Path.of(File.separatorChar + "public" + File.separatorChar))); + assertFalse(buildItem.maybeRoot(Path.of("template"))); + assertFalse(buildItem.maybeRoot(Path.of("foo"))); + } +} 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 c4633c9028a563..9bcf522449dc66 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 @@ -14,6 +14,7 @@ import java.util.Map; import java.util.Map.Entry; import java.util.Optional; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.regex.Pattern; @@ -75,8 +76,7 @@ public class EngineProducer { private final ContentTypes contentTypes; private final List tags; private final List suffixes; - private final String basePath; - private final String tagPath; + private final Set templateRoots; private final Pattern templatePathExclude; private final Locale defaultLocale; private final Charset defaultCharset; @@ -89,8 +89,7 @@ public EngineProducer(QuteContext context, QuteConfig config, QuteRuntimeConfig @All List namespaceResolvers) { this.contentTypes = contentTypes; this.suffixes = config.suffixes; - this.basePath = "templates/"; - this.tagPath = basePath + TAGS; + this.templateRoots = context.getTemplateRoots(); this.tags = context.getTags(); this.templatePathExclude = config.templatePathExclude; this.defaultLocale = locales.defaultLocale; @@ -300,14 +299,6 @@ void onShutdown(@Observes ShutdownEvent event) { Qute.clearCache(); } - String getBasePath() { - return basePath; - } - - String getTagPath() { - return tagPath; - } - private Resolver createResolver(String resolverClassName) { try { Class resolverClazz = Thread.currentThread() @@ -337,29 +328,31 @@ private TemplateInstance.Initializer createInitializer(String initializerClassNa } private Optional locate(String path) { - URL resource = null; - String templatePath = basePath + path; - LOGGER.debugf("Locate template for %s", templatePath); if (templatePathExclude.matcher(path).matches()) { return Optional.empty(); } - resource = locatePath(templatePath); - if (resource == null) { - // Try path with suffixes - for (String suffix : suffixes) { - String pathWithSuffix = path + "." + suffix; - if (templatePathExclude.matcher(pathWithSuffix).matches()) { - continue; - } - templatePath = basePath + pathWithSuffix; - resource = locatePath(templatePath); - if (resource != null) { - break; + for (String templateRoot : templateRoots) { + URL resource = null; + String templatePath = templateRoot + path; + LOGGER.debugf("Locate template for %s", templatePath); + resource = locatePath(templatePath); + if (resource == null) { + // Try path with suffixes + for (String suffix : suffixes) { + String pathWithSuffix = path + "." + suffix; + if (templatePathExclude.matcher(pathWithSuffix).matches()) { + continue; + } + templatePath = templateRoot + pathWithSuffix; + resource = locatePath(templatePath); + if (resource != null) { + break; + } } } - } - if (resource != null) { - return Optional.of(new ResourceTemplateLocation(resource, createVariant(templatePath))); + if (resource != null) { + return Optional.of(new ResourceTemplateLocation(resource, createVariant(templatePath))); + } } return Optional.empty(); } 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 be9e699c827daa..17a2caee2ddba9 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 @@ -2,6 +2,7 @@ import java.util.List; import java.util.Map; +import java.util.Set; import java.util.function.Supplier; import io.quarkus.runtime.annotations.Recorder; @@ -11,7 +12,7 @@ public class QuteRecorder { public Supplier createContext(List resolverClasses, List templatePaths, List tags, Map> variants, - List templateInstanceInitializerClasses) { + List templateInstanceInitializerClasses, Set templateRoots) { return new Supplier() { @Override @@ -43,6 +44,10 @@ public List getTemplateInstanceInitializerClasses() { return templateInstanceInitializerClasses; } + @Override + public Set getTemplateRoots() { + return templateRoots; + } }; } }; @@ -60,6 +65,8 @@ public interface QuteContext { List getTemplateInstanceInitializerClasses(); + Set getTemplateRoots(); + } } diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/RedisCommandExtraArguments.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/RedisCommandExtraArguments.java index 3d58d049a4dc37..9a76064c18f83e 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/RedisCommandExtraArguments.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/RedisCommandExtraArguments.java @@ -8,17 +8,17 @@ public interface RedisCommandExtraArguments { /** - * @return the list of arguments, encoded as a list of String. + * @return the list of arguments. */ - default List toArgs() { + default List toArgs() { return toArgs(null); } /** * @param encoder an optional encoder to encode some of the values - * @return the list of arguments, encoded as a list of String. + * @return the list of arguments. */ - default List toArgs(Codec encoder) { + default List toArgs(Codec encoder) { return Collections.emptyList(); } diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/SortArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/SortArgs.java index 840366dd0b463a..cc61d4f32be2ea 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/SortArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/SortArgs.java @@ -131,8 +131,8 @@ public SortArgs get(String get) { return this; } - public List toArgs() { - List args = new ArrayList<>(); + public List toArgs() { + List args = new ArrayList<>(); if (by != null && !by.isBlank()) { args.add("BY"); args.add(by); diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/autosuggest/GetArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/autosuggest/GetArgs.java index bbfeab15e1406f..4eb4fdd6345b0d 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/autosuggest/GetArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/autosuggest/GetArgs.java @@ -47,8 +47,8 @@ public GetArgs withScores() { } @Override - public List toArgs() { - List list = new ArrayList<>(); + public List toArgs() { + List list = new ArrayList<>(); if (fuzzy) { list.add("FUZZY"); } diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/bitmap/BitFieldArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/bitmap/BitFieldArgs.java index 8137802e1fa826..4fa49b16200bf6 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/bitmap/BitFieldArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/bitmap/BitFieldArgs.java @@ -88,7 +88,7 @@ public String toString() { } - private final List commands = new ArrayList<>(); + private final List commands = new ArrayList<>(); private BitFieldType previousBitFieldType; /** @@ -351,7 +351,7 @@ private BitFieldType getPreviousFieldType() { } } - public List toArgs() { + public List toArgs() { return commands; } diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/bloom/BfInsertArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/bloom/BfInsertArgs.java index 47c65d954c8daa..95cdd00ca4e699 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/bloom/BfInsertArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/bloom/BfInsertArgs.java @@ -82,8 +82,8 @@ public BfInsertArgs expansion(int expansion) { } @Override - public List toArgs() { - List list = new ArrayList<>(); + public List toArgs() { + List list = new ArrayList<>(); if (capacity > 0) { list.add("CAPACITY"); diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/bloom/BfReserveArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/bloom/BfReserveArgs.java index 16df8a2b402672..ff8b0e3905c88c 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/bloom/BfReserveArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/bloom/BfReserveArgs.java @@ -41,8 +41,8 @@ public BfReserveArgs expansion(int expansion) { } @Override - public List toArgs() { - List list = new ArrayList<>(); + public List toArgs() { + List list = new ArrayList<>(); if (expansion > 0) { list.add("EXPANSION"); list.add(Integer.toString(expansion)); diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/cuckoo/CfInsertArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/cuckoo/CfInsertArgs.java index 2470caabc36e95..e1d9ae7b1b0b0e 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/cuckoo/CfInsertArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/cuckoo/CfInsertArgs.java @@ -36,8 +36,8 @@ public CfInsertArgs nocreate() { } @Override - public List toArgs() { - List list = new ArrayList<>(); + public List toArgs() { + List list = new ArrayList<>(); if (capacity > 0) { list.add("CAPACITY"); list.add(Long.toString(capacity)); diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/cuckoo/CfReserveArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/cuckoo/CfReserveArgs.java index 5765a3615447d6..4310b59b26fcbb 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/cuckoo/CfReserveArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/cuckoo/CfReserveArgs.java @@ -51,8 +51,8 @@ public CfReserveArgs expansion(int expansion) { } @Override - public List toArgs() { - List list = new ArrayList<>(); + public List toArgs() { + List list = new ArrayList<>(); if (bucketSize > 0) { list.add("BUCKETSIZE"); list.add(Long.toString(bucketSize)); diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/geo/GeoAddArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/geo/GeoAddArgs.java index 5e05387ed47e7f..e54eb16166499d 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/geo/GeoAddArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/geo/GeoAddArgs.java @@ -42,11 +42,11 @@ public GeoAddArgs ch() { return this; } - public List toArgs() { + public List toArgs() { if (xx && nx) { throw new IllegalArgumentException("Cannot set XX and NX together"); } - List args = new ArrayList<>(); + List args = new ArrayList<>(); if (xx) { args.add("XX"); } diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/geo/GeoRadiusArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/geo/GeoRadiusArgs.java index b8aa8263dea52d..3746e11df44e1d 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/geo/GeoRadiusArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/geo/GeoRadiusArgs.java @@ -101,13 +101,13 @@ public GeoRadiusArgs any() { } @Override - public List toArgs() { + public List toArgs() { // Validation if (any && count == -1) { throw new IllegalArgumentException("ANY can only be used if COUNT is also set"); } - List list = new ArrayList<>(); + List list = new ArrayList<>(); if (withDistance) { list.add("WITHDIST"); } diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/geo/GeoRadiusStoreArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/geo/GeoRadiusStoreArgs.java index e1e5b61a6328f3..5320e00c3f2ac8 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/geo/GeoRadiusStoreArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/geo/GeoRadiusStoreArgs.java @@ -127,7 +127,7 @@ public GeoRadiusStoreArgs storeDistKey(K storeDistKey) { } @Override - public List toArgs(Codec codec) { + public List toArgs(Codec codec) { // Validation if (any && count == -1) { throw new IllegalArgumentException("ANY can only be used if COUNT is also set"); @@ -136,7 +136,7 @@ public List toArgs(Codec codec) { throw new IllegalArgumentException("At least `STORE` or `STOREDIST` must be set"); } - List list = new ArrayList<>(); + List list = new ArrayList<>(); if (withDistance) { list.add("WITHDIST"); } diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/geo/GeoSearchArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/geo/GeoSearchArgs.java index 8d0c9f851c1211..9a41d13b8f6012 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/geo/GeoSearchArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/geo/GeoSearchArgs.java @@ -184,7 +184,7 @@ public GeoSearchArgs any() { } @Override - public List toArgs(Codec codec) { + public List toArgs(Codec codec) { // Validation if (any && count == -1) { throw new IllegalArgumentException("ANY can only be used if COUNT is also set"); @@ -198,7 +198,7 @@ public List toArgs(Codec codec) { throw new IllegalArgumentException("FROMMEMBER and FROMLONLAT cannot be used together"); } - List list = new ArrayList<>(); + List list = new ArrayList<>(); if (member != null) { list.add("FROMMEMBER"); list.add(new String(codec.encode(member), StandardCharsets.UTF_8)); @@ -249,7 +249,7 @@ public boolean hasCoordinates() { return withCoordinates; } - private void putFlag(List list, boolean flag, String value) { + private void putFlag(List list, boolean flag, String value) { if (flag) { list.add(value); } diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/geo/GeoSearchStoreArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/geo/GeoSearchStoreArgs.java index fe9057eff57f88..550fd8b5db5553 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/geo/GeoSearchStoreArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/geo/GeoSearchStoreArgs.java @@ -146,7 +146,7 @@ public GeoSearchStoreArgs any() { } @Override - public List toArgs(Codec codec) { + public List toArgs(Codec codec) { // Validation if (any && count == -1) { throw new IllegalArgumentException("ANY can only be used if COUNT is also set"); @@ -160,7 +160,7 @@ public List toArgs(Codec codec) { throw new IllegalArgumentException("FROMMEMBER and FROMLONLAT cannot be used together"); } - List list = new ArrayList<>(); + List list = new ArrayList<>(); if (member != null) { list.add("FROMMEMBER"); list.add(new String(codec.encode(member), StandardCharsets.UTF_8)); diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/json/JsonSetArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/json/JsonSetArgs.java index cb5f37c0dd8db8..8f88f3f947f227 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/json/JsonSetArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/json/JsonSetArgs.java @@ -31,11 +31,11 @@ public JsonSetArgs xx() { } @Override - public List toArgs() { + public List toArgs() { if (xx && nx) { throw new IllegalArgumentException("Cannot set XX and NX together"); } - List args = new ArrayList<>(); + List args = new ArrayList<>(); if (xx) { args.add("XX"); } diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/keys/CopyArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/keys/CopyArgs.java index fb21c77d6ef0aa..155d2dfd1ae06f 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/keys/CopyArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/keys/CopyArgs.java @@ -37,8 +37,8 @@ public CopyArgs replace(boolean replace) { } @Override - public List toArgs() { - List args = new ArrayList<>(); + public List toArgs() { + List args = new ArrayList<>(); if (destinationDb != -1) { args.add("DB"); args.add(Long.toString(destinationDb)); diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/keys/ExpireArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/keys/ExpireArgs.java index 36491aaff5f89a..30284cced7cbbb 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/keys/ExpireArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/keys/ExpireArgs.java @@ -55,8 +55,8 @@ public ExpireArgs gt() { return this; } - public List toArgs() { - List args = new ArrayList<>(); + public List toArgs() { + List args = new ArrayList<>(); boolean exclusion = false; if (nx) { diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/list/LPosArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/list/LPosArgs.java index 4958815119bd0f..ad10fd8aefb410 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/list/LPosArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/list/LPosArgs.java @@ -40,8 +40,8 @@ public LPosArgs maxlen(long max) { } @Override - public List toArgs() { - List list = new ArrayList<>(); + public List toArgs() { + List list = new ArrayList<>(); if (rank != 0) { list.add("RANK"); list.add(Long.toString(rank)); diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/AggregateArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/AggregateArgs.java index dfb7a055a0a54f..51aa61b154ba9e 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/AggregateArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/AggregateArgs.java @@ -242,8 +242,8 @@ public AggregateArgs cursorMaxIdleTime(Duration maxIdleDuration) { } @Override - public List toArgs() { - List list = new ArrayList<>(); + public List toArgs() { + List list = new ArrayList<>(); if (verbatim) { list.add("VERBATIM"); } diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/CreateArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/CreateArgs.java index 619944032d1e5b..c4910e7ddec713 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/CreateArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/CreateArgs.java @@ -288,8 +288,8 @@ public CreateArgs indexedField(String field, String alias, FieldType type) { } @Override - public List toArgs() { - List list = new ArrayList<>(); + public List toArgs() { + List list = new ArrayList<>(); if (onHash) { if (onJson) { diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/DistanceMetric.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/DistanceMetric.java new file mode 100644 index 00000000000000..97ffea48e689b4 --- /dev/null +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/DistanceMetric.java @@ -0,0 +1,10 @@ +package io.quarkus.redis.datasource.search; + +/** + * Metric for computing the distance between two vectors. + */ +public enum DistanceMetric { + L2, + IP, + COSINE +} diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/FieldOptions.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/FieldOptions.java index f640ee22ad3d3e..f19720735286cd 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/FieldOptions.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/FieldOptions.java @@ -17,6 +17,12 @@ public class FieldOptions { private char separator; private boolean caseSensitive; private boolean withSuffixTrie; + private VectorAlgorithm vectorAlgorithm; + private VectorType vectorType; + private Integer dimension; + private DistanceMetric distanceMetric; + private Integer initialCap; + private Integer blockSize; /** * Numeric, tag (not supported with JSON) or text attributes can have the optional SORTABLE argument. @@ -125,8 +131,98 @@ public FieldOptions withSuffixTrie() { return this; } + /** + * For vector fields, specifies the vector algorithm to use when searching k most similar vectors in an index. + * + * @param vectorAlgorithm the vector algorithm + * @return the current {@code FieldOptions} + */ + public FieldOptions vectorAlgorithm(VectorAlgorithm vectorAlgorithm) { + this.vectorAlgorithm = vectorAlgorithm; + return this; + } + + /** + * For vector fields, specifies the vector type. + * + * @param vectorType the vector type + * @return the current {@code FieldOptions} + */ + public FieldOptions vectorType(VectorType vectorType) { + this.vectorType = vectorType; + return this; + } + + /** + * For vector fields, specifies the dimension. + * + * @param dimension the dimension + * @return the current {@code FieldOptions} + */ + public FieldOptions dimension(int dimension) { + this.dimension = dimension; + return this; + } + + /** + * For vector fields, specifies the distance metric. + * + * @param distanceMetric the distance metric + * @return the current {@code FieldOptions} + */ + public FieldOptions distanceMetric(DistanceMetric distanceMetric) { + this.distanceMetric = distanceMetric; + return this; + } + + /** + * For vector fields, specifies the initial vector capacity in the index. + * + * @param initialCap the initial capacity + * @return the current {@code FieldOptions} + */ + public FieldOptions initialCap(int initialCap) { + this.initialCap = initialCap; + return this; + } + + /** + * For vector fields, specifies the block size (the amount of vectors to store in a contiguous array). + * + * @param blockSize the block size + * @return the current {@code FieldOptions} + */ + public FieldOptions blockSize(int blockSize) { + this.blockSize = blockSize; + return this; + } + public List toArgs() { List list = new ArrayList<>(); + if (vectorAlgorithm != null) { + list.add(vectorAlgorithm.name()); + list.add(String.valueOf(vectorSimilarityArgumentsCount())); + } + if (vectorType != null) { + list.add("TYPE"); + list.add(vectorType.name()); + } + if (dimension != null) { + list.add("DIM"); + list.add(dimension.toString()); + } + if (distanceMetric != null) { + list.add("DISTANCE_METRIC"); + list.add(distanceMetric.name()); + } + if (initialCap != null) { + list.add("INITIAL_CAP"); + list.add(initialCap.toString()); + } + if (blockSize != null) { + list.add("BLOCK_SIZE"); + list.add(blockSize.toString()); + } if (sortable) { list.add("SORTABLE"); } @@ -162,4 +258,24 @@ public List toArgs() { } return list; } + + private int vectorSimilarityArgumentsCount() { + int count = 0; + if (vectorType != null) { + count += 2; + } + if (dimension != null) { + count += 2; + } + if (distanceMetric != null) { + count += 2; + } + if (initialCap != null) { + count += 2; + } + if (blockSize != null) { + count += 2; + } + return count; + } } diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/HighlightArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/HighlightArgs.java index 40dc79dca260c7..a9441b9a067544 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/HighlightArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/HighlightArgs.java @@ -49,8 +49,8 @@ public HighlightArgs tags(String open, String close) { } @Override - public List toArgs() { - List list = new ArrayList<>(); + public List toArgs() { + List list = new ArrayList<>(); list.add("HIGHLIGHT"); if (fields != null && fields.length > 0) { list.add("FIELDS"); diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/IndexedField.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/IndexedField.java index 7b1f6cb63d7ffe..fc762b3fea51c8 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/IndexedField.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/IndexedField.java @@ -40,8 +40,8 @@ public static IndexedField from(String field, String alias, FieldType type) { this.options = options; } - public List toArgs() { - List list = new ArrayList<>(); + public List toArgs() { + List list = new ArrayList<>(); list.add(field); if (alias != null) { list.add("AS"); diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/QueryArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/QueryArgs.java index 76d229440d0014..18f1b65106cb92 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/QueryArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/QueryArgs.java @@ -8,6 +8,8 @@ import static io.smallrye.mutiny.helpers.ParameterValidation.positiveOrZero; import static io.smallrye.mutiny.helpers.ParameterValidation.validate; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; @@ -53,6 +55,7 @@ public class QueryArgs implements RedisCommandExtraArguments { private int count = -1; private Duration timeout; private final Map params = new HashMap<>(); + private final Map byteArrayParams = new HashMap<>(); private int dialect = -1; /** @@ -359,6 +362,66 @@ public QueryArgs param(String name, String value) { return this; } + /** + * Defines a parameter with a byte array value. + * + * @param name the parameter name + * @param value the parameter value as byte array + * @return the current {@code QueryArgs} + */ + public QueryArgs param(String name, byte[] value) { + this.byteArrayParams.put(notNullOrBlank(name, "name"), notNullOrEmpty(value, "value")); + return this; + } + + /** + * Defines a parameter with a float array value. + * + * @param name the parameter name + * @param value the parameter value as array of floats + * @return the current {@code QueryArgs} + */ + public QueryArgs param(String name, float[] value) { + this.byteArrayParams.put(notNullOrBlank(name, "name"), toByteArray(notNullOrEmpty(value, "value"))); + return this; + } + + /** + * Defines a parameter with a double array value. + * + * @param name the parameter name + * @param value the parameter value as array of doubles + * @return the current {@code QueryArgs} + */ + public QueryArgs param(String name, double[] value) { + this.byteArrayParams.put(notNullOrBlank(name, "name"), toByteArray(notNullOrEmpty(value, "value"))); + return this; + } + + /** + * Defines a parameter with an int array value. + * + * @param name the parameter name + * @param value the parameter value as array of ints + * @return the current {@code QueryArgs} + */ + public QueryArgs param(String name, int[] value) { + this.byteArrayParams.put(notNullOrBlank(name, "name"), toByteArray(notNullOrEmpty(value, "value"))); + return this; + } + + /** + * Defines a parameter with a long array value. + * + * @param name the parameter name + * @param value the parameter value as array of longs + * @return the current {@code QueryArgs} + */ + public QueryArgs param(String name, long[] value) { + this.byteArrayParams.put(notNullOrBlank(name, "name"), toByteArray(notNullOrEmpty(value, "value"))); + return this; + } + /** * Selects the dialect version under which to execute the query. * If not specified, the query will execute under the default dialect version set during module initial loading. @@ -372,8 +435,8 @@ public QueryArgs dialect(int version) { } @Override - public List toArgs(Codec encoder) { - List list = new ArrayList<>(); + public List toArgs(Codec encoder) { + List list = new ArrayList<>(); if (nocontent) { list.add("NOCONTENT"); @@ -480,9 +543,13 @@ public List toArgs(Codec encoder) { list.add(Long.toString(timeout.toMillis())); } - if (!params.isEmpty()) { + if (!params.isEmpty() || !byteArrayParams.isEmpty()) { list.add("PARAMS"); - list.add(Integer.toString(params.size())); + list.add(Integer.toString(params.size() + byteArrayParams.size())); + for (Map.Entry entry : byteArrayParams.entrySet()) { + list.add(entry.getKey()); + list.add(entry.getValue()); + } for (Map.Entry entry : params.entrySet()) { list.add(entry.getKey()); list.add(entry.getValue()); @@ -527,4 +594,29 @@ public boolean containsPayload() { public boolean containsSortKeys() { return withSortKeys; } + + private byte[] toByteArray(float[] input) { + byte[] bytes = new byte[Float.BYTES * input.length]; + ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN).asFloatBuffer().put(input); + return bytes; + } + + private byte[] toByteArray(double[] input) { + byte[] bytes = new byte[Double.BYTES * input.length]; + ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN).asDoubleBuffer().put(input); + return bytes; + } + + private byte[] toByteArray(int[] input) { + byte[] bytes = new byte[Integer.BYTES * input.length]; + ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN).asIntBuffer().put(input); + return bytes; + } + + private byte[] toByteArray(long[] input) { + byte[] bytes = new byte[Long.BYTES * input.length]; + ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN).asLongBuffer().put(input); + return bytes; + } + } diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/SpellCheckArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/SpellCheckArgs.java index 9eaba0373fa692..2f35553d347c8e 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/SpellCheckArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/SpellCheckArgs.java @@ -68,8 +68,8 @@ public SpellCheckArgs dialect(int dialect) { } @Override - public List toArgs() { - List list = new ArrayList<>(); + public List toArgs() { + List list = new ArrayList<>(); if (distance != 0) { list.add("DISTANCE"); list.add(Integer.toString(distance)); diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/SummarizeArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/SummarizeArgs.java index d2630e606ed4d0..4d309145093f4b 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/SummarizeArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/SummarizeArgs.java @@ -75,8 +75,8 @@ public SummarizeArgs separator(String separator) { } @Override - public List toArgs() { - List list = new ArrayList<>(); + public List toArgs() { + List list = new ArrayList<>(); list.add("SUMMARIZE"); if (fields != null && fields.length > 0) { list.add("FIELDS"); diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/VectorAlgorithm.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/VectorAlgorithm.java new file mode 100644 index 00000000000000..6c77a872aa5492 --- /dev/null +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/VectorAlgorithm.java @@ -0,0 +1,18 @@ +package io.quarkus.redis.datasource.search; + +/** + * The vector algorithm to use when searching k most similar vectors in an index. + */ +public enum VectorAlgorithm { + + /** + * Brute force algorithm. + */ + FLAT, + + /** + * Hierarchical Navigable Small World Graph algorithm. + */ + HNSW + +} diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/VectorType.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/VectorType.java new file mode 100644 index 00000000000000..c5fb2d049c3597 --- /dev/null +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/search/VectorType.java @@ -0,0 +1,15 @@ +package io.quarkus.redis.datasource.search; + +/** + * Type of vector stored in a vector field. + */ +public enum VectorType { + /** + * A 32-bit floating point number. + */ + FLOAT32, + /** + * A 64-bit floating point number. + */ + FLOAT64 +} diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/sortedset/ZAddArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/sortedset/ZAddArgs.java index eb51d95ae732fa..eedcc0d66f85a1 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/sortedset/ZAddArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/sortedset/ZAddArgs.java @@ -69,7 +69,7 @@ public ZAddArgs gt() { } @Override - public List toArgs() { + public List toArgs() { if (xx && nx) { throw new IllegalArgumentException("Cannot use XX and NX together"); } @@ -77,7 +77,7 @@ public List toArgs() { throw new IllegalArgumentException("Cannot use LT and GT together"); } - List args = new ArrayList<>(); + List args = new ArrayList<>(); putFlag(args, nx, "NX"); putFlag(args, xx, "XX"); putFlag(args, lt, "LT"); @@ -86,7 +86,7 @@ public List toArgs() { return args; } - public void putFlag(List args, boolean value, String flag) { + public void putFlag(List args, boolean value, String flag) { if (value) { args.add(flag); } diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/sortedset/ZAggregateArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/sortedset/ZAggregateArgs.java index 4a151a5568411a..1f12ec6c8bf3fa 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/sortedset/ZAggregateArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/sortedset/ZAggregateArgs.java @@ -80,8 +80,8 @@ public ZAggregateArgs max() { } @Override - public List toArgs() { - List args = new ArrayList<>(); + public List toArgs() { + List args = new ArrayList<>(); if (!weights.isEmpty()) { args.add("WEIGHTS"); for (double w : weights) { diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/sortedset/ZMpopArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/sortedset/ZMpopArgs.java index 887cfa7a766c7f..5878b25ae02219 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/sortedset/ZMpopArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/sortedset/ZMpopArgs.java @@ -45,12 +45,12 @@ public ZMpopArgs count(int count) { } @Override - public List toArgs() { + public List toArgs() { if (min && max) { throw new IllegalArgumentException("Cannot use MIN and MAX together"); } - List args = new ArrayList<>(); + List args = new ArrayList<>(); if (min) { args.add("MIN"); } diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/sortedset/ZRangeArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/sortedset/ZRangeArgs.java index 3102660a70ddb7..8a60c3d695090f 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/sortedset/ZRangeArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/sortedset/ZRangeArgs.java @@ -37,8 +37,8 @@ public ZRangeArgs limit(long offset, int count) { } @Override - public List toArgs() { - List list = new ArrayList<>(); + public List toArgs() { + List list = new ArrayList<>(); if (rev) { list.add("REV"); } diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/StreamRange.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/StreamRange.java index 1bf69911cef026..9958a008eb6704 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/StreamRange.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/StreamRange.java @@ -25,8 +25,8 @@ public static StreamRange of(String lowerBound, String higherBound) { } @Override - public List toArgs() { - List list = new ArrayList<>(); + public List toArgs() { + List list = new ArrayList<>(); list.add(lowerBound); list.add(higherBound); return list; diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XAddArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XAddArgs.java index 196ebb8a091440..76c61ef5665079 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XAddArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XAddArgs.java @@ -93,8 +93,8 @@ public XAddArgs limit(long limit) { } @Override - public List toArgs() { - List args = new ArrayList<>(); + public List toArgs() { + List args = new ArrayList<>(); if (nomkstream) { args.add("NOMKSTREAM"); } diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XClaimArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XClaimArgs.java index 3538cc9c816878..af3a1da6ec7ec1 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XClaimArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XClaimArgs.java @@ -97,8 +97,8 @@ public XClaimArgs lastId(String lastId) { } @Override - public List toArgs() { - List args = new ArrayList<>(); + public List toArgs() { + List args = new ArrayList<>(); if (idle != null) { args.add("IDLE"); diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XGroupCreateArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XGroupCreateArgs.java index db51b9b4743641..9656562661840b 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XGroupCreateArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XGroupCreateArgs.java @@ -40,8 +40,8 @@ public XGroupCreateArgs entriesRead(String id) { } @Override - public List toArgs() { - List args = new ArrayList<>(); + public List toArgs() { + List args = new ArrayList<>(); if (mkstream) { args.add("MKSTREAM"); } diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XGroupSetIdArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XGroupSetIdArgs.java index f5a00766e58f4b..4913bfbc3ced5b 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XGroupSetIdArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XGroupSetIdArgs.java @@ -29,8 +29,8 @@ public XGroupSetIdArgs entriesRead(long id) { } @Override - public List toArgs() { - List args = new ArrayList<>(); + public List toArgs() { + List args = new ArrayList<>(); if (entriesRead > 0) { args.add("ENTRIESREAD"); args.add(Long.toString(entriesRead)); diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XPendingArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XPendingArgs.java index e5c1db2afadbfd..5b5b30a41112fe 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XPendingArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XPendingArgs.java @@ -39,8 +39,8 @@ public Duration idle() { } @Override - public List toArgs() { - List args = new ArrayList<>(); + public List toArgs() { + List args = new ArrayList<>(); if (owner != null) { args.add(owner); diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XReadArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XReadArgs.java index 630b89b22f4eaa..151a9e6629a9ea 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XReadArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XReadArgs.java @@ -38,8 +38,8 @@ public XReadArgs block(Duration block) { } @Override - public List toArgs() { - List args = new ArrayList<>(); + public List toArgs() { + List args = new ArrayList<>(); if (count > 0) { args.add("COUNT"); args.add(Integer.toString(count)); diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XReadGroupArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XReadGroupArgs.java index 7fc78998e4d502..26be832fcaeb25 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XReadGroupArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XReadGroupArgs.java @@ -51,8 +51,8 @@ public XReadGroupArgs noack() { } @Override - public List toArgs() { - List args = new ArrayList<>(); + public List toArgs() { + List args = new ArrayList<>(); if (count > 0) { args.add("COUNT"); args.add(Integer.toString(count)); diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XTrimArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XTrimArgs.java index 81ef8a74f097dc..3d33cc56a2e8ed 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XTrimArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/stream/XTrimArgs.java @@ -63,8 +63,8 @@ public XTrimArgs limit(long limit) { } @Override - public List toArgs() { - List args = new ArrayList<>(); + public List toArgs() { + List args = new ArrayList<>(); if (maxlen > 0) { if (minid != null) { diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/string/GetExArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/string/GetExArgs.java index 85a743b0e01c34..2e82f9dc3afec4 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/string/GetExArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/string/GetExArgs.java @@ -128,8 +128,8 @@ public GetExArgs persist() { return this; } - public List toArgs() { - List args = new ArrayList<>(); + public List toArgs() { + List args = new ArrayList<>(); if (ex >= 0) { args.add("EX"); args.add(Long.toString(ex)); diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/string/SetArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/string/SetArgs.java index 7d23e48f075a69..8d9100e15eaab3 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/string/SetArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/string/SetArgs.java @@ -168,8 +168,8 @@ public SetArgs get() { return this; } - public List toArgs() { - List args = new ArrayList<>(); + public List toArgs() { + List args = new ArrayList<>(); if (ex >= 0) { args.add("EX"); args.add(Long.toString(ex)); diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/timeseries/AddArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/timeseries/AddArgs.java index 3063daff7cfcef..15386a16a66570 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/timeseries/AddArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/timeseries/AddArgs.java @@ -97,8 +97,8 @@ public AddArgs label(String label, Object value) { } @Override - public List toArgs() { - List list = new ArrayList<>(); + public List toArgs() { + List list = new ArrayList<>(); if (retention != null) { list.add("RETENTION"); if (retention == Duration.ZERO) { diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/timeseries/AlterArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/timeseries/AlterArgs.java index d3268d4b8ed3a1..25664e594b395f 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/timeseries/AlterArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/timeseries/AlterArgs.java @@ -92,8 +92,8 @@ public AlterArgs label(String label, Object value) { } @Override - public List toArgs() { - List list = new ArrayList<>(); + public List toArgs() { + List list = new ArrayList<>(); if (retention != null) { list.add("RETENTION"); if (retention == Duration.ZERO) { diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/timeseries/CreateArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/timeseries/CreateArgs.java index d908c15de00590..48903a55d32c96 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/timeseries/CreateArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/timeseries/CreateArgs.java @@ -123,8 +123,8 @@ public CreateArgs label(String label, Object value) { } @Override - public List toArgs() { - List list = new ArrayList<>(); + public List toArgs() { + List list = new ArrayList<>(); if (retention != null) { list.add("RETENTION"); if (retention == Duration.ZERO) { diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/timeseries/IncrementArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/timeseries/IncrementArgs.java index 33204a20c344d8..51442aaef1a900 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/timeseries/IncrementArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/timeseries/IncrementArgs.java @@ -95,8 +95,8 @@ public IncrementArgs label(String label, Object value) { } @Override - public List toArgs() { - List list = new ArrayList<>(); + public List toArgs() { + List list = new ArrayList<>(); if (timestamp >= 0) { list.add("TIMESTAMP"); list.add(Long.toString(timestamp)); diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/timeseries/MGetArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/timeseries/MGetArgs.java index 77a26768b54173..142e3ea077fe0e 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/timeseries/MGetArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/timeseries/MGetArgs.java @@ -56,8 +56,8 @@ public MGetArgs selectedLabel(String label) { } @Override - public List toArgs() { - List list = new ArrayList<>(); + public List toArgs() { + List list = new ArrayList<>(); if (latest) { list.add("LATEST"); } diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/timeseries/MRangeArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/timeseries/MRangeArgs.java index 6b70a1ca52f5d0..f0735aff1b8ee9 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/timeseries/MRangeArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/timeseries/MRangeArgs.java @@ -203,8 +203,8 @@ public MRangeArgs groupBy(String label, Reducer reducer) { } @Override - public List toArgs() { - List list = new ArrayList<>(); + public List toArgs() { + List list = new ArrayList<>(); if (latest) { list.add("LATEST"); diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/timeseries/RangeArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/timeseries/RangeArgs.java index 9cded0a3d0d4e0..71627f97a179c1 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/timeseries/RangeArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/timeseries/RangeArgs.java @@ -154,8 +154,8 @@ public RangeArgs empty() { } @Override - public List toArgs() { - List list = new ArrayList<>(); + public List toArgs() { + List list = new ArrayList<>(); if (latest) { list.add("LATEST"); diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/value/GetExArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/value/GetExArgs.java index 5966a68c43d41a..5bcdafdd942d84 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/value/GetExArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/value/GetExArgs.java @@ -125,8 +125,8 @@ public GetExArgs persist() { return this; } - public List toArgs() { - List args = new ArrayList<>(); + public List toArgs() { + List args = new ArrayList<>(); if (ex >= 0) { args.add("EX"); args.add(Long.toString(ex)); diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/value/SetArgs.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/value/SetArgs.java index c03af517867a49..e659cd0e25121c 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/value/SetArgs.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/datasource/value/SetArgs.java @@ -165,8 +165,8 @@ public SetArgs get() { return this; } - public List toArgs() { - List args = new ArrayList<>(); + public List toArgs() { + List args = new ArrayList<>(); if (ex >= 0) { args.add("EX"); args.add(Long.toString(ex)); diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/Validation.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/Validation.java index d58075205f90b4..402784fe8e9491 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/Validation.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/Validation.java @@ -19,6 +19,56 @@ public static X[] notNullOrEmpty(X[] array, String name) { return array; } + public static float[] notNullOrEmpty(float[] array, String name) { + if (array == null) { + throw new IllegalArgumentException("`" + name + "` must not be `null`"); + } + if (array.length == 0) { + throw new IllegalArgumentException("`" + name + "` must not be empty"); + } + return array; + } + + public static double[] notNullOrEmpty(double[] array, String name) { + if (array == null) { + throw new IllegalArgumentException("`" + name + "` must not be `null`"); + } + if (array.length == 0) { + throw new IllegalArgumentException("`" + name + "` must not be empty"); + } + return array; + } + + public static int[] notNullOrEmpty(int[] array, String name) { + if (array == null) { + throw new IllegalArgumentException("`" + name + "` must not be `null`"); + } + if (array.length == 0) { + throw new IllegalArgumentException("`" + name + "` must not be empty"); + } + return array; + } + + public static long[] notNullOrEmpty(long[] array, String name) { + if (array == null) { + throw new IllegalArgumentException("`" + name + "` must not be `null`"); + } + if (array.length == 0) { + throw new IllegalArgumentException("`" + name + "` must not be empty"); + } + return array; + } + + public static byte[] notNullOrEmpty(byte[] array, String name) { + if (array == null) { + throw new IllegalArgumentException("`" + name + "` must not be `null`"); + } + if (array.length == 0) { + throw new IllegalArgumentException("`" + name + "` must not be empty"); + } + return array; + } + public static String notNullOrBlank(String v, String name) { if (v == null) { throw new IllegalArgumentException("`" + name + "` must not be `null`"); diff --git a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/SearchCommandsTest.java b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/SearchCommandsTest.java index f4f124f9164723..cb3b5bbefa17e2 100644 --- a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/SearchCommandsTest.java +++ b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/SearchCommandsTest.java @@ -4,8 +4,10 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.fail; import static org.awaitility.Awaitility.await; +import static org.junit.jupiter.api.Assertions.assertEquals; import java.time.Duration; +import java.util.HashMap; import java.util.Map; import org.assertj.core.data.Offset; @@ -16,6 +18,8 @@ import io.quarkus.redis.datasource.hash.HashCommands; import io.quarkus.redis.datasource.search.AggregateArgs; import io.quarkus.redis.datasource.search.CreateArgs; +import io.quarkus.redis.datasource.search.DistanceMetric; +import io.quarkus.redis.datasource.search.Document; import io.quarkus.redis.datasource.search.FieldOptions; import io.quarkus.redis.datasource.search.FieldType; import io.quarkus.redis.datasource.search.HighlightArgs; @@ -23,7 +27,10 @@ import io.quarkus.redis.datasource.search.NumericFilter; import io.quarkus.redis.datasource.search.QueryArgs; import io.quarkus.redis.datasource.search.SearchCommands; +import io.quarkus.redis.datasource.search.SearchQueryResponse; import io.quarkus.redis.datasource.search.SpellCheckArgs; +import io.quarkus.redis.datasource.search.VectorAlgorithm; +import io.quarkus.redis.datasource.search.VectorType; import io.quarkus.redis.runtime.datasource.BlockingRedisDataSourceImpl; import io.vertx.core.json.JsonArray; import io.vertx.core.json.JsonObject; @@ -843,4 +850,74 @@ void testSynonymsInQueries() { assertThat(res.documents()).anySatisfy(d -> assertThat(d.property("t").asString()).isEqualTo("hello")); assertThat(res.documents()).anySatisfy(d -> assertThat(d.property("t").asString()).isEqualTo("world")); } + + @Test + void testKNearestNeighborsDouble() { + ds.search().ftCreate("IDX:double", + new CreateArgs() + .onJson() + .prefixes("indexed:") + .indexedField("$.vector", "vector", FieldType.VECTOR, + new FieldOptions() + .vectorAlgorithm(VectorAlgorithm.HNSW) + .dimension(6) + .distanceMetric(DistanceMetric.COSINE) + .vectorType(VectorType.FLOAT64))); + + double[] queryVector = new double[] { 0.0, 0.0, 1.0, 0.0, 0.0, 0.0 }; + + ds.json().jsonSet("indexed:1", "$", createDocument(new double[] { 1.0, 0.0, 0.0, 0.0, 0.0, 0.0 })); + ds.json().jsonSet("indexed:2", "$", createDocument(new double[] { 0.0, 1.0, 0.0, 0.0, 0.0, 0.0 })); + ds.json().jsonSet("indexed:3", "$", createDocument(new double[] { 0.0, 0.0, 1.0, 0.0, 0.0, 0.0 })); + ds.json().jsonSet("indexed:4", "$", createDocument(new double[] { 0.0, 0.0, 0.0, 1.0, 0.0, 0.0 })); + + String query = "*=>[ KNN 1 @vector $BLOB AS vector_score ]"; + + QueryArgs args = new QueryArgs() + .sortByAscending("vector_score") + .param("DIALECT", "2") + .param("BLOB", queryVector); + SearchQueryResponse response = ds.search().ftSearch("IDX:double", query, args); + assertEquals(1, response.count()); + Document foundEntry = response.documents().get(0); + assertEquals("indexed:3", foundEntry.key()); + } + + @Test + void testKNearestNeighborsFloat() { + ds.search().ftCreate("IDX:float", + new CreateArgs() + .onJson() + .prefixes("indexed:") + .indexedField("$.vector", "vector", FieldType.VECTOR, + new FieldOptions() + .vectorAlgorithm(VectorAlgorithm.HNSW) + .dimension(6) + .distanceMetric(DistanceMetric.COSINE) + .vectorType(VectorType.FLOAT32))); + + float[] queryVector = new float[] { 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f }; + + ds.json().jsonSet("indexed:1", "$", createDocument(new float[] { 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f })); + ds.json().jsonSet("indexed:2", "$", createDocument(new float[] { 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f })); + ds.json().jsonSet("indexed:3", "$", createDocument(new float[] { 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f })); + ds.json().jsonSet("indexed:4", "$", createDocument(new float[] { 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f })); + + String query = "*=>[ KNN 1 @vector $BLOB AS vector_score ]"; + QueryArgs args = new QueryArgs() + .sortByAscending("vector_score") + .param("DIALECT", "2") + .param("BLOB", queryVector); + + SearchQueryResponse response = ds.search().ftSearch("IDX:float", query, args); + assertEquals(1, response.count()); + Document foundEntry = response.documents().get(0); + assertEquals("indexed:3", foundEntry.key()); + } + + private Map createDocument(Object embedding) { + Map fields = new HashMap<>(); + fields.put("vector", embedding); + return fields; + } } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/CustomSerializerTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/CustomSerializerTest.java new file mode 100644 index 00000000000000..d8ba0287c1dd42 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/CustomSerializerTest.java @@ -0,0 +1,120 @@ +package io.quarkus.resteasy.reactive.jackson.deployment.test; + +import java.io.IOException; +import java.time.OffsetDateTime; +import java.util.Comparator; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.ext.ContextResolver; +import jakarta.ws.rs.ext.Provider; + +import org.assertj.core.api.Assertions; +import org.jboss.resteasy.reactive.RestResponse.StatusCode; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; + +import io.quarkus.arc.Unremovable; +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; +import io.restassured.internal.mapping.Jackson2Mapper; +import io.restassured.response.Response; + +public class CustomSerializerTest { + private static final OffsetDateTime FIXED_TIME = OffsetDateTime.now(); + private static final Jackson2Mapper MAPPER = new Jackson2Mapper((type, charset) -> { + final ObjectMapper mapper = new ObjectMapper(); + mapper.registerModule(new JavaTimeModule()); + return mapper; + }); + + @RegisterExtension + static QuarkusUnitTest test = new QuarkusUnitTest().withEmptyApplication(); + + @Test + void shouldUseModulesInCustomSerializer() { + final Response response = RestAssured.given().get("custom-serializer"); + Assertions.assertThat(response.statusCode()).isEqualTo(StatusCode.OK); + + final CustomData actual = response.as(CustomData.class, MAPPER); + final CustomData expected = new CustomData("test-data", FIXED_TIME); + Assertions.assertThat(actual) + .usingComparatorForType(Comparator.comparing(OffsetDateTime::toInstant), OffsetDateTime.class) + .usingRecursiveComparison() + .isEqualTo(expected); + } + + @Path("custom-serializer") + @Produces(MediaType.APPLICATION_JSON) + static class CustomJacksonEndpoint { + + @GET + public CustomData getCustom() { + return new CustomData("test-data", FIXED_TIME); + } + } + + static class CustomData { + private final String name; + private final OffsetDateTime time; + + @JsonCreator + CustomData(@JsonProperty("name") final String name, @JsonProperty("time") final OffsetDateTime time) { + this.name = name; + this.time = time; + } + + public String getName() { + return this.name; + } + + public OffsetDateTime getTime() { + return this.time; + } + } + + static class CustomDataSerializer extends StdSerializer { + CustomDataSerializer() { + super(CustomData.class); + } + + @Override + public void serialize(final CustomData customData, final JsonGenerator jsonGenerator, + final SerializerProvider serializerProvider) + throws IOException { + jsonGenerator.writeStartObject(); + jsonGenerator.writeStringField("name", customData.getName()); + if (customData.getTime() != null) { + jsonGenerator.writeObjectField("time", customData.getTime()); + } + jsonGenerator.writeEndObject(); + } + } + + @Provider + @Unremovable + public static class CustomObjectMapperContextResolver implements ContextResolver { + + @Override + public ObjectMapper getContext(final Class type) { + final ObjectMapper objectMapper = new ObjectMapper(); + final SimpleModule simpleModule = new SimpleModule("custom-data"); + simpleModule.addSerializer(new CustomDataSerializer()); + objectMapper.registerModule(simpleModule); + objectMapper.registerModule(new JavaTimeModule()); + return objectMapper; + } + } + +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jsonb/deployment/src/test/java/io/quarkus/resteasy/reactive/jsonb/deployment/test/sse/SseParserTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jsonb/deployment/src/test/java/io/quarkus/resteasy/reactive/jsonb/deployment/test/sse/SseParserTest.java index 7f8bd584a02447..36cca2e23779e2 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jsonb/deployment/src/test/java/io/quarkus/resteasy/reactive/jsonb/deployment/test/sse/SseParserTest.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jsonb/deployment/src/test/java/io/quarkus/resteasy/reactive/jsonb/deployment/test/sse/SseParserTest.java @@ -37,14 +37,14 @@ public void testParser() { testParser("data:foo\ndata:\ndata:bar\n\n", "foo\n\nbar", null, null, null, SseEvent.RECONNECT_NOT_SET); // no data: no event - testParser("\n", null, null, null, null, SseEvent.RECONNECT_NOT_SET); - testParser("data:\n\n", null, null, null, null, SseEvent.RECONNECT_NOT_SET); - testParser("data\n\n", null, null, null, null, SseEvent.RECONNECT_NOT_SET); + testParser("\n", "", null, null, null, SseEvent.RECONNECT_NOT_SET); + testParser("data:\n\n", "", null, null, null, SseEvent.RECONNECT_NOT_SET); + testParser("data\n\n", "", null, null, null, SseEvent.RECONNECT_NOT_SET); // all fields testParser("data:DATA\nid:ID\n:COMMENT\nretry:23\nevent:NAME\n\n", "DATA", "COMMENT", "ID", "NAME", 23); // all fields and no data - testParser("id:ID\n:COMMENT\nretry:23\nevent:NAME\n\n", null, "COMMENT", "ID", "NAME", 23); + testParser("id:ID\n:COMMENT\nretry:23\nevent:NAME\n\n", "", "COMMENT", "ID", "NAME", 23); // optional space after colon testParser("data:foo\n\n", "foo", null, null, null, SseEvent.RECONNECT_NOT_SET); diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/stream/StreamTestCase.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/stream/StreamTestCase.java index 7a2135bd216cd0..9da6a2fa1a778a 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/stream/StreamTestCase.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/stream/StreamTestCase.java @@ -264,7 +264,7 @@ public void testSseForMultiWithOutboundSseEvent() throws InterruptedException { }); sse.open(); Assertions.assertTrue(latch.await(20, TimeUnit.SECONDS)); - org.assertj.core.api.Assertions.assertThat(results).containsExactly(null, "uno", "dos", "tres"); + org.assertj.core.api.Assertions.assertThat(results).containsExactly("", "uno", "dos", "tres"); org.assertj.core.api.Assertions.assertThat(ids).containsExactly(null, "one", "two", "three"); org.assertj.core.api.Assertions.assertThat(names).containsExactly(null, "eins", "zwei", "drei"); org.assertj.core.api.Assertions.assertThat(comments).containsExactly("dummy", null, null, null); diff --git a/extensions/resteasy-reactive/rest-client-reactive/deployment/src/main/java/io/quarkus/rest/client/reactive/deployment/RestClientReactiveProcessor.java b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/main/java/io/quarkus/rest/client/reactive/deployment/RestClientReactiveProcessor.java index 04fd0a2d54494a..99f2c23cdcbcd3 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 @@ -310,18 +310,20 @@ void registerProvidersFromAnnotations(CombinedIndexBuildItem indexBuildItem, continue; } } - DotName providerDotName = providerClass.name(); + + List providerInterfaceNames = providerClass.interfaceNames(); // don't register server specific types - if (providerDotName.equals(ResteasyReactiveDotNames.CONTAINER_REQUEST_FILTER) - || providerDotName.equals(ResteasyReactiveDotNames.CONTAINER_RESPONSE_FILTER) - || providerDotName.equals(ResteasyReactiveDotNames.EXCEPTION_MAPPER)) { + if (providerInterfaceNames.contains(ResteasyReactiveDotNames.CONTAINER_REQUEST_FILTER) + || providerInterfaceNames.contains(ResteasyReactiveDotNames.CONTAINER_RESPONSE_FILTER) + || providerInterfaceNames.contains(ResteasyReactiveDotNames.EXCEPTION_MAPPER)) { continue; } - if (providerClass.interfaceNames().contains(ResteasyReactiveDotNames.FEATURE)) { + if (providerInterfaceNames.contains(ResteasyReactiveDotNames.FEATURE)) { continue; // features should not be automatically registered for the client, see javadoc for Feature } + DotName providerDotName = providerClass.name(); int priority = getAnnotatedPriority(index, providerDotName.toString(), Priorities.USER); constructor.invokeVirtualMethod( diff --git a/extensions/smallrye-health/deployment/src/main/java/io/quarkus/smallrye/health/deployment/SmallRyeHealthProcessor.java b/extensions/smallrye-health/deployment/src/main/java/io/quarkus/smallrye/health/deployment/SmallRyeHealthProcessor.java index 69d292bd816340..3899c03b691693 100644 --- a/extensions/smallrye-health/deployment/src/main/java/io/quarkus/smallrye/health/deployment/SmallRyeHealthProcessor.java +++ b/extensions/smallrye-health/deployment/src/main/java/io/quarkus/smallrye/health/deployment/SmallRyeHealthProcessor.java @@ -5,6 +5,7 @@ import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.util.Collection; +import java.util.HashSet; import java.util.List; import java.util.Optional; import java.util.Set; @@ -216,6 +217,7 @@ public void defineHealthRoutes(BuildProducer routes, .routeConfigKey("quarkus.smallrye-health.root-path") .handler(new SmallRyeHealthHandler()) .displayOnNotFoundPage() + .blockingRoute() .build()); // Register the liveness handler @@ -224,6 +226,7 @@ public void defineHealthRoutes(BuildProducer routes, .nestedRoute(healthConfig.rootPath, healthConfig.livenessPath) .handler(new SmallRyeLivenessHandler()) .displayOnNotFoundPage() + .blockingRoute() .build()); // Register the readiness handler @@ -232,14 +235,29 @@ public void defineHealthRoutes(BuildProducer routes, .nestedRoute(healthConfig.rootPath, healthConfig.readinessPath) .handler(new SmallRyeReadinessHandler()) .displayOnNotFoundPage() + .blockingRoute() .build()); + // Find all health groups + Set healthGroups = new HashSet<>(); + // with simple @HealthGroup annotations + for (AnnotationInstance healthGroupAnnotation : index.getAnnotations(HEALTH_GROUP)) { + healthGroups.add(healthGroupAnnotation.value().asString()); + } + // with @HealthGroups repeatable annotations + for (AnnotationInstance healthGroupsAnnotation : index.getAnnotations(HEALTH_GROUPS)) { + for (AnnotationInstance healthGroupAnnotation : healthGroupsAnnotation.value().asNestedArray()) { + healthGroups.add(healthGroupAnnotation.value().asString()); + } + } + // Register the health group handlers routes.produce(nonApplicationRootPathBuildItem.routeBuilder() .management("quarkus.smallrye-health.management.enabled") .nestedRoute(healthConfig.rootPath, healthConfig.groupPath) .handler(new SmallRyeHealthGroupHandler()) .displayOnNotFoundPage() + .blockingRoute() .build()); SmallRyeIndividualHealthGroupHandler handler = new SmallRyeIndividualHealthGroupHandler(); @@ -248,6 +266,7 @@ public void defineHealthRoutes(BuildProducer routes, .nestedRoute(healthConfig.rootPath, healthConfig.groupPath + "/*") .handler(handler) .displayOnNotFoundPage() + .blockingRoute() .build()); // Register the wellness handler @@ -256,6 +275,7 @@ public void defineHealthRoutes(BuildProducer routes, .nestedRoute(healthConfig.rootPath, healthConfig.wellnessPath) .handler(new SmallRyeWellnessHandler()) .displayOnNotFoundPage() + .blockingRoute() .build()); // Register the startup handler @@ -264,6 +284,7 @@ public void defineHealthRoutes(BuildProducer routes, .nestedRoute(healthConfig.rootPath, healthConfig.startupPath) .handler(new SmallRyeStartupHandler()) .displayOnNotFoundPage() + .blockingRoute() .build()); } diff --git a/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeHealthGroupHandler.java b/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeHealthGroupHandler.java index 95b87746c1b088..84c5c6fa62d0cb 100644 --- a/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeHealthGroupHandler.java +++ b/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeHealthGroupHandler.java @@ -2,13 +2,12 @@ import io.smallrye.health.SmallRyeHealth; import io.smallrye.health.SmallRyeHealthReporter; -import io.smallrye.mutiny.Uni; import io.vertx.ext.web.RoutingContext; public class SmallRyeHealthGroupHandler extends SmallRyeHealthHandlerBase { @Override - protected Uni getHealth(SmallRyeHealthReporter reporter, RoutingContext ctx) { - return reporter.getHealthGroupsAsync(); + protected SmallRyeHealth getHealth(SmallRyeHealthReporter reporter, RoutingContext ctx) { + return reporter.getHealthGroups(); } } diff --git a/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeHealthHandler.java b/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeHealthHandler.java index 6d9d33066e8fbd..6960bb284bce9b 100644 --- a/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeHealthHandler.java +++ b/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeHealthHandler.java @@ -2,13 +2,12 @@ import io.smallrye.health.SmallRyeHealth; import io.smallrye.health.SmallRyeHealthReporter; -import io.smallrye.mutiny.Uni; import io.vertx.ext.web.RoutingContext; public class SmallRyeHealthHandler extends SmallRyeHealthHandlerBase { @Override - protected Uni getHealth(SmallRyeHealthReporter reporter, RoutingContext ctx) { - return reporter.getHealthAsync(); + protected SmallRyeHealth getHealth(SmallRyeHealthReporter reporter, RoutingContext ctx) { + return reporter.getHealth(); } } diff --git a/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeHealthHandlerBase.java b/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeHealthHandlerBase.java index e9993754187690..fff1485398fbc1 100644 --- a/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeHealthHandlerBase.java +++ b/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeHealthHandlerBase.java @@ -10,11 +10,7 @@ import io.quarkus.vertx.http.runtime.security.QuarkusHttpUser; import io.smallrye.health.SmallRyeHealth; import io.smallrye.health.SmallRyeHealthReporter; -import io.smallrye.mutiny.Uni; -import io.smallrye.mutiny.vertx.MutinyHelper; -import io.vertx.core.Context; import io.vertx.core.Handler; -import io.vertx.core.Vertx; import io.vertx.core.buffer.Buffer; import io.vertx.core.http.HttpHeaders; import io.vertx.core.http.HttpServerResponse; @@ -22,7 +18,7 @@ abstract class SmallRyeHealthHandlerBase implements Handler { - protected abstract Uni getHealth(SmallRyeHealthReporter reporter, RoutingContext routingContext); + protected abstract SmallRyeHealth getHealth(SmallRyeHealthReporter reporter, RoutingContext routingContext); @Override public void handle(RoutingContext ctx) { @@ -45,21 +41,19 @@ private void doHandle(RoutingContext ctx) { Arc.container().instance(CurrentIdentityAssociation.class).get().setIdentity(user.getSecurityIdentity()); } SmallRyeHealthReporter reporter = Arc.container().instance(SmallRyeHealthReporter.class).get(); - Context context = Vertx.currentContext(); - getHealth(reporter, ctx).emitOn(MutinyHelper.executor(context)) - .subscribe().with(health -> { - HttpServerResponse resp = ctx.response(); - if (health.isDown()) { - resp.setStatusCode(503); - } - resp.headers().set(HttpHeaders.CONTENT_TYPE, "application/json; charset=UTF-8"); - Buffer buffer = Buffer.buffer(256); // this size seems to cover the basic health checks - try (BufferOutputStream outputStream = new BufferOutputStream(buffer);) { - reporter.reportHealth(outputStream, health); - resp.end(buffer); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - }); + SmallRyeHealth health = getHealth(reporter, ctx); + HttpServerResponse resp = ctx.response(); + if (health.isDown()) { + resp.setStatusCode(503); + } + resp.headers().set(HttpHeaders.CONTENT_TYPE, "application/json; charset=UTF-8"); + Buffer buffer = Buffer.buffer(256); // this size seems to cover the basic health checks + try (BufferOutputStream outputStream = new BufferOutputStream(buffer);) { + reporter.reportHealth(outputStream, health); + resp.end(buffer); + } catch (IOException e) { + throw new UncheckedIOException(e); + } } + } diff --git a/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeIndividualHealthGroupHandler.java b/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeIndividualHealthGroupHandler.java index e0c7ba38744399..66f960791ad8d8 100644 --- a/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeIndividualHealthGroupHandler.java +++ b/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeIndividualHealthGroupHandler.java @@ -2,14 +2,13 @@ import io.smallrye.health.SmallRyeHealth; import io.smallrye.health.SmallRyeHealthReporter; -import io.smallrye.mutiny.Uni; import io.vertx.ext.web.RoutingContext; public class SmallRyeIndividualHealthGroupHandler extends SmallRyeHealthHandlerBase { @Override - protected Uni getHealth(SmallRyeHealthReporter reporter, RoutingContext ctx) { + protected SmallRyeHealth getHealth(SmallRyeHealthReporter reporter, RoutingContext ctx) { String group = ctx.normalizedPath().substring(ctx.normalizedPath().lastIndexOf("/") + 1); - return reporter.getHealthGroupAsync(group); + return reporter.getHealthGroup(group); } } diff --git a/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeLivenessHandler.java b/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeLivenessHandler.java index ad33e824ff3d71..a5cf3dd904cbe9 100644 --- a/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeLivenessHandler.java +++ b/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeLivenessHandler.java @@ -2,13 +2,12 @@ import io.smallrye.health.SmallRyeHealth; import io.smallrye.health.SmallRyeHealthReporter; -import io.smallrye.mutiny.Uni; import io.vertx.ext.web.RoutingContext; public class SmallRyeLivenessHandler extends SmallRyeHealthHandlerBase { @Override - protected Uni getHealth(SmallRyeHealthReporter reporter, RoutingContext ctx) { - return reporter.getLivenessAsync(); + protected SmallRyeHealth getHealth(SmallRyeHealthReporter reporter, RoutingContext ctx) { + return reporter.getLiveness(); } } diff --git a/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeReadinessHandler.java b/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeReadinessHandler.java index 18c652bd673bd7..a23a3e1f9d5383 100644 --- a/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeReadinessHandler.java +++ b/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeReadinessHandler.java @@ -2,13 +2,12 @@ import io.smallrye.health.SmallRyeHealth; import io.smallrye.health.SmallRyeHealthReporter; -import io.smallrye.mutiny.Uni; import io.vertx.ext.web.RoutingContext; public class SmallRyeReadinessHandler extends SmallRyeHealthHandlerBase { @Override - protected Uni getHealth(SmallRyeHealthReporter reporter, RoutingContext ctx) { - return reporter.getReadinessAsync(); + protected SmallRyeHealth getHealth(SmallRyeHealthReporter reporter, RoutingContext routingContext) { + return reporter.getReadiness(); } } diff --git a/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeStartupHandler.java b/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeStartupHandler.java index cd1ae14846cc97..c450430735ecb8 100644 --- a/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeStartupHandler.java +++ b/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeStartupHandler.java @@ -2,13 +2,12 @@ import io.smallrye.health.SmallRyeHealth; import io.smallrye.health.SmallRyeHealthReporter; -import io.smallrye.mutiny.Uni; import io.vertx.ext.web.RoutingContext; public class SmallRyeStartupHandler extends SmallRyeHealthHandlerBase { @Override - protected Uni getHealth(SmallRyeHealthReporter reporter, RoutingContext ctx) { - return reporter.getStartupAsync(); + protected SmallRyeHealth getHealth(SmallRyeHealthReporter reporter, RoutingContext routingContext) { + return reporter.getStartup(); } } diff --git a/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeWellnessHandler.java b/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeWellnessHandler.java index e2131f51de416e..84ca3860c1caed 100644 --- a/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeWellnessHandler.java +++ b/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeWellnessHandler.java @@ -2,13 +2,12 @@ import io.smallrye.health.SmallRyeHealth; import io.smallrye.health.SmallRyeHealthReporter; -import io.smallrye.mutiny.Uni; import io.vertx.ext.web.RoutingContext; public class SmallRyeWellnessHandler extends SmallRyeHealthHandlerBase { @Override - protected Uni getHealth(SmallRyeHealthReporter reporter, RoutingContext ctx) { - return reporter.getWellnessAsync(); + protected SmallRyeHealth getHealth(SmallRyeHealthReporter reporter, RoutingContext routingContext) { + return reporter.getWellness(); } } diff --git a/extensions/smallrye-reactive-messaging/deployment/src/main/resources/dev-ui/qwc-smallrye-reactive-messaging-channels.js b/extensions/smallrye-reactive-messaging/deployment/src/main/resources/dev-ui/qwc-smallrye-reactive-messaging-channels.js index f382cbdf19ce2a..2f7abd6e6fcd58 100644 --- a/extensions/smallrye-reactive-messaging/deployment/src/main/resources/dev-ui/qwc-smallrye-reactive-messaging-channels.js +++ b/extensions/smallrye-reactive-messaging/deployment/src/main/resources/dev-ui/qwc-smallrye-reactive-messaging-channels.js @@ -61,7 +61,7 @@ export class QwcSmallryeReactiveMessagingChannels extends LitElement { > @@ -95,9 +95,19 @@ export class QwcSmallryeReactiveMessagingChannels extends LitElement { } _channelPublisherRenderer(channel) { - const publisher = channel.publisher; - if (publisher) { - return this._renderComponent(publisher); + const publishers = channel.publishers; + if (publishers) { + if (publishers.length === 1) { + return this._renderComponent(publishers[0]); + } else if (publishers.length > 1) { + return html` +
    + ${publishers.map(item => html`
  • ${this._renderComponent(item)}
  • `)} +
+ `; + } else { + return html`No publishers` + } } } diff --git a/extensions/smallrye-reactive-messaging/runtime/src/main/java/io/quarkus/smallrye/reactivemessaging/runtime/devui/DevReactiveMessagingInfos.java b/extensions/smallrye-reactive-messaging/runtime/src/main/java/io/quarkus/smallrye/reactivemessaging/runtime/devui/DevReactiveMessagingInfos.java index 6f79ad41b86e17..3fc7884e69be0a 100644 --- a/extensions/smallrye-reactive-messaging/runtime/src/main/java/io/quarkus/smallrye/reactivemessaging/runtime/devui/DevReactiveMessagingInfos.java +++ b/extensions/smallrye-reactive-messaging/runtime/src/main/java/io/quarkus/smallrye/reactivemessaging/runtime/devui/DevReactiveMessagingInfos.java @@ -34,21 +34,24 @@ public List get() { .get(); // collect all channels - Map publishers = new HashMap<>(); + Map> publishers = new HashMap<>(); Map> consumers = new HashMap<>(); Function> fun = e -> new ArrayList<>(); // Unfortunately, there is no easy way to obtain the connectors metadata Connectors connectors = container.instance(Connectors.class).get(); - publishers.putAll(connectors.outgoingConnectors); + for (Entry entry : connectors.outgoingConnectors.entrySet()) { + publishers.computeIfAbsent(entry.getKey(), fun) + .add(entry.getValue()); + } for (Entry entry : connectors.incomingConnectors.entrySet()) { consumers.computeIfAbsent(entry.getKey(), fun) .add(entry.getValue()); } for (EmitterConfiguration emitter : context.getEmitterConfigurations()) { - publishers.put(emitter.name(), - new Component(ComponentType.EMITTER, + publishers.computeIfAbsent(emitter.name(), fun) + .add(new Component(ComponentType.EMITTER, emitter.broadcast() ? "@Broadcast " : "" + asCode(DevConsoleRecorder.EMITTERS.get(emitter.name())))); } @@ -58,23 +61,27 @@ public List get() { asCode(DevConsoleRecorder.CHANNELS.get(channel.channelName)))); } for (MediatorConfiguration mediator : context.getMediatorConfigurations()) { - boolean isProcessor = !mediator.getIncoming().isEmpty() && mediator.getOutgoing() != null; + boolean isProcessor = !mediator.getIncoming().isEmpty() && !mediator.getOutgoings().isEmpty(); if (isProcessor) { - publishers.put(mediator.getOutgoing(), - new Component(ComponentType.PROCESSOR, asMethod(mediator.methodAsString()))); + for (String outgoing : mediator.getOutgoings()) { + publishers.computeIfAbsent(outgoing, fun) + .add(new Component(ComponentType.PROCESSOR, asMethod(mediator.methodAsString()))); + } for (String incoming : mediator.getIncoming()) { consumers.computeIfAbsent(incoming, fun) .add(new Component(ComponentType.PROCESSOR, asMethod(mediator.methodAsString()))); } - } else if (mediator.getOutgoing() != null) { - StringBuilder builder = new StringBuilder(); - builder.append(asMethod(mediator.methodAsString())); - if (mediator.getBroadcast()) { - builder.append("[broadcast: true]"); + } else if (!mediator.getOutgoings().isEmpty()) { + for (String outgoing : mediator.getOutgoings()) { + StringBuilder builder = new StringBuilder(); + builder.append(asMethod(mediator.methodAsString())); + if (mediator.getBroadcast()) { + builder.append("[broadcast: true]"); + } + publishers.computeIfAbsent(outgoing, fun) + .add(new Component(ComponentType.PUBLISHER, builder.toString())); } - publishers.put(mediator.getOutgoing(), - new Component(ComponentType.PUBLISHER, builder.toString())); } else if (!mediator.getIncoming().isEmpty()) { for (String incoming : mediator.getIncoming()) { consumers.computeIfAbsent(incoming, fun) @@ -113,12 +120,12 @@ public List getChannels() { public static class DevChannelInfo implements Comparable { private final String name; - private final Component publisher; + private final List publishers; private final List consumers; - public DevChannelInfo(String name, Component publisher, List consumers) { + public DevChannelInfo(String name, List publishers, List consumers) { this.name = name; - this.publisher = publisher; + this.publishers = publishers != null ? publishers : Collections.emptyList(); this.consumers = consumers != null ? consumers : Collections.emptyList(); } @@ -126,8 +133,8 @@ public String getName() { return name; } - public Component getPublisher() { - return publisher; + public List getPublishers() { + return publishers; } public List getConsumers() { @@ -136,17 +143,11 @@ public List getConsumers() { @Override public int compareTo(DevChannelInfo other) { - if (publisher != other.publisher) { - if (other.publisher == null) { - return -1; - } - if (publisher == null) { - return 1; - } - // publisher connectors first - if (publisher.type != other.publisher.type) { - return publisher.type == ComponentType.CONNECTOR ? -1 : 1; - } + // publisher connectors last + long publisherConnectors = publishers.stream().filter(Component::isConnector).count(); + long otherPublisherConnectors = other.publishers.stream().filter(Component::isConnector).count(); + if (publisherConnectors != otherPublisherConnectors) { + return Long.compare(otherPublisherConnectors, publisherConnectors); } // consumer connectors last long consumerConnectors = consumers.stream().filter(Component::isConnector).count(); @@ -154,10 +155,6 @@ public int compareTo(DevChannelInfo other) { if (consumerConnectors != otherConsumersConnectors) { return Long.compare(otherConsumersConnectors, consumerConnectors); } - if (publisher != other.publisher && publisher.type == ComponentType.CONNECTOR - && other.publisher.type != ComponentType.CONNECTOR) { - return 1; - } // alphabetically return name.compareTo(other.name); } diff --git a/extensions/smallrye-reactive-messaging/runtime/src/main/java/io/quarkus/smallrye/reactivemessaging/runtime/devui/ReactiveMessagingJsonRpcService.java b/extensions/smallrye-reactive-messaging/runtime/src/main/java/io/quarkus/smallrye/reactivemessaging/runtime/devui/ReactiveMessagingJsonRpcService.java index 3e83d74354348d..57554fb7e17853 100644 --- a/extensions/smallrye-reactive-messaging/runtime/src/main/java/io/quarkus/smallrye/reactivemessaging/runtime/devui/ReactiveMessagingJsonRpcService.java +++ b/extensions/smallrye-reactive-messaging/runtime/src/main/java/io/quarkus/smallrye/reactivemessaging/runtime/devui/ReactiveMessagingJsonRpcService.java @@ -24,7 +24,7 @@ public JsonArray getInfo() { private JsonObject toJson(DevReactiveMessagingInfos.DevChannelInfo channel) { JsonObject json = new JsonObject(); json.put("name", channel.getName()); - json.put("publisher", toJson(channel.getPublisher())); + json.put("publishers", toJson(channel.getPublishers())); json.put("consumers", toJson(channel.getConsumers())); return json; } diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/DevUIProcessor.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/DevUIProcessor.java index f8996848d6af63..b2468365265e9f 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/DevUIProcessor.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/DevUIProcessor.java @@ -201,6 +201,13 @@ void registerDevUiHandlers( .build()); } + Handler endpointInfoHandler = recorder.endpointInfoHandler(basepath); + + routeProducer.produce( + nonApplicationRootPathBuildItem.routeBuilder().route(DEVUI + SLASH + "endpoints.json") + .handler(endpointInfoHandler) + .build()); + // For the Vaadin router (So that bookmarks/url refreshes work) for (DevUIRoutesBuildItem devUIRoutesBuildItem : devUIRoutesBuildItems) { String route = devUIRoutesBuildItem.getPath(); diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/menu/EndpointsProcessor.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/menu/EndpointsProcessor.java new file mode 100644 index 00000000000000..634bdf50620323 --- /dev/null +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/menu/EndpointsProcessor.java @@ -0,0 +1,57 @@ +package io.quarkus.devui.deployment.menu; + +import static io.quarkus.deployment.annotations.ExecutionTime.STATIC_INIT; + +import java.util.List; +import java.util.stream.Collectors; + +import io.quarkus.deployment.IsDevelopment; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.Record; +import io.quarkus.devui.deployment.InternalPageBuildItem; +import io.quarkus.devui.runtime.DevUIRecorder; +import io.quarkus.devui.runtime.EndpointInfo; +import io.quarkus.devui.spi.page.Page; +import io.quarkus.vertx.http.deployment.HttpRootPathBuildItem; +import io.quarkus.vertx.http.deployment.NonApplicationRootPathBuildItem; +import io.quarkus.vertx.http.deployment.devmode.NotFoundPageDisplayableEndpointBuildItem; + +/** + * This creates Endpoints Page + */ +public class EndpointsProcessor { + private static final String DEVUI = "dev-ui"; + + @Record(STATIC_INIT) + @BuildStep(onlyIf = IsDevelopment.class) + void addEndpointInfos(List displayableEndpoints, + DevUIRecorder recorder, HttpRootPathBuildItem httpRoot) { + + List endpoints = displayableEndpoints + .stream() + .map(v -> new EndpointInfo(v.getEndpoint(httpRoot), v.getDescription())) + .sorted() + .collect(Collectors.toList()); + + recorder.setEndpoints(endpoints); + } + + @BuildStep(onlyIf = IsDevelopment.class) + InternalPageBuildItem createEndpointsPage(NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem) { + + String basepath = nonApplicationRootPathBuildItem.resolvePath(DEVUI); + + InternalPageBuildItem endpointsPage = new InternalPageBuildItem("Endpoints", 25); + + endpointsPage.addBuildTimeData("basepath", basepath); + + // Page + endpointsPage.addPage(Page.webComponentPageBuilder() + .namespace("devui-endpoints") + .title("Endpoints") + .icon("font-awesome-solid:plug") + .componentLink("qwc-endpoints.js")); + + return endpointsPage; + } +} diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/HttpSecurityPolicyBuildItem.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/HttpSecurityPolicyBuildItem.java index cfa973812af249..eea7841fecdb92 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/HttpSecurityPolicyBuildItem.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/HttpSecurityPolicyBuildItem.java @@ -5,6 +5,11 @@ import io.quarkus.builder.item.MultiBuildItem; import io.quarkus.vertx.http.runtime.security.HttpSecurityPolicy; +/** + * @deprecated Define {@link io.quarkus.vertx.http.runtime.security.HttpSecurityPolicy} CDI bean with {@link #name} + * set as the {@link HttpSecurityPolicy#name()}. + */ +@Deprecated public final class HttpSecurityPolicyBuildItem extends MultiBuildItem { final String name; diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/HttpSecurityProcessor.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/HttpSecurityProcessor.java index 84b2aaca206dcd..e414b69b3f7a99 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/HttpSecurityProcessor.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/HttpSecurityProcessor.java @@ -1,226 +1,116 @@ package io.quarkus.vertx.http.deployment; import static io.quarkus.arc.processor.DotNames.APPLICATION_SCOPED; +import static io.quarkus.arc.processor.DotNames.DEFAULT_BEAN; +import static io.quarkus.arc.processor.DotNames.SINGLETON; -import java.security.Permission; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.function.BiFunction; +import java.util.function.BooleanSupplier; import java.util.function.Consumer; -import java.util.function.Function; -import java.util.function.Supplier; import java.util.stream.Collectors; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Singleton; -import org.jboss.jandex.ClassInfo; -import org.jboss.jandex.IndexView; +import org.jboss.jandex.DotName; import org.jboss.jandex.MethodInfo; -import org.jboss.jandex.Type; import io.quarkus.arc.deployment.AdditionalBeanBuildItem; -import io.quarkus.arc.deployment.BeanContainerListenerBuildItem; +import io.quarkus.arc.deployment.AnnotationsTransformerBuildItem; import io.quarkus.arc.deployment.SyntheticBeanBuildItem; +import io.quarkus.arc.processor.AnnotationsTransformer; import io.quarkus.deployment.Capabilities; import io.quarkus.deployment.Capability; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.annotations.ExecutionTime; import io.quarkus.deployment.annotations.Record; -import io.quarkus.deployment.builditem.CombinedIndexBuildItem; -import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; import io.quarkus.runtime.RuntimeValue; -import io.quarkus.runtime.configuration.ConfigurationException; -import io.quarkus.security.StringPermission; import io.quarkus.security.spi.runtime.MethodDescription; import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig; -import io.quarkus.vertx.http.runtime.PolicyConfig; import io.quarkus.vertx.http.runtime.management.ManagementInterfaceBuildTimeConfig; -import io.quarkus.vertx.http.runtime.security.AuthenticatedHttpSecurityPolicy; import io.quarkus.vertx.http.runtime.security.BasicAuthenticationMechanism; -import io.quarkus.vertx.http.runtime.security.DenySecurityPolicy; import io.quarkus.vertx.http.runtime.security.EagerSecurityInterceptorStorage; import io.quarkus.vertx.http.runtime.security.FormAuthenticationMechanism; -import io.quarkus.vertx.http.runtime.security.HttpAuthenticationMechanism; import io.quarkus.vertx.http.runtime.security.HttpAuthenticator; import io.quarkus.vertx.http.runtime.security.HttpAuthorizer; import io.quarkus.vertx.http.runtime.security.HttpSecurityPolicy; import io.quarkus.vertx.http.runtime.security.HttpSecurityRecorder; import io.quarkus.vertx.http.runtime.security.MtlsAuthenticationMechanism; import io.quarkus.vertx.http.runtime.security.PathMatchingHttpSecurityPolicy; -import io.quarkus.vertx.http.runtime.security.PermitSecurityPolicy; -import io.quarkus.vertx.http.runtime.security.RolesAllowedHttpSecurityPolicy; -import io.quarkus.vertx.http.runtime.security.SupplierImpl; import io.quarkus.vertx.http.runtime.security.VertxBlockingSecurityExecutor; import io.vertx.core.http.ClientAuth; import io.vertx.ext.web.RoutingContext; public class HttpSecurityProcessor { - @BuildStep - @Record(ExecutionTime.STATIC_INIT) - public void builtins(BuildProducer producer, - BuildProducer reflectiveClassProducer, - CombinedIndexBuildItem combinedIndexBuildItem, - HttpBuildTimeConfig buildTimeConfig, HttpSecurityRecorder recorder, - BuildProducer beanProducer) { - producer.produce(new HttpSecurityPolicyBuildItem("deny", new SupplierImpl<>(new DenySecurityPolicy()))); - producer.produce(new HttpSecurityPolicyBuildItem("permit", new SupplierImpl<>(new PermitSecurityPolicy()))); - producer.produce( - new HttpSecurityPolicyBuildItem("authenticated", new SupplierImpl<>(new AuthenticatedHttpSecurityPolicy()))); - if (!buildTimeConfig.auth.permissions.isEmpty()) { - beanProducer.produce(AdditionalBeanBuildItem.unremovableOf(PathMatchingHttpSecurityPolicy.class)); - } - Map> permClassToCreator = new HashMap<>(); - for (Map.Entry e : buildTimeConfig.auth.rolePolicy.entrySet()) { - PolicyConfig policyConfig = e.getValue(); - if (policyConfig.permissions.isEmpty()) { - producer.produce(new HttpSecurityPolicyBuildItem(e.getKey(), - new SupplierImpl<>(new RolesAllowedHttpSecurityPolicy(e.getValue().rolesAllowed)))); - } else { - // create HTTP Security policy that checks allowed roles and grants SecurityIdentity permissions to - // requests that this policy allows to proceed - var permissionCreator = permClassToCreator.computeIfAbsent(policyConfig.permissionClass, - new Function>() { - @Override - public BiFunction apply(String s) { - if (StringPermission.class.getName().equals(s)) { - return recorder.stringPermissionCreator(); - } - boolean constructorAcceptsActions = validateConstructor(combinedIndexBuildItem.getIndex(), - policyConfig.permissionClass); - return recorder.customPermissionCreator(s, constructorAcceptsActions); - } - }); - var policy = recorder.createRolesAllowedPolicy(policyConfig.rolesAllowed, policyConfig.permissions, - permissionCreator); - producer.produce(new HttpSecurityPolicyBuildItem(e.getKey(), policy)); - } - } - - if (!permClassToCreator.isEmpty()) { - // we need to register Permission classes for reflection as strictly speaking - // they might not exactly match classes defined via `PermissionsAllowed#permission` - var permissionClassesArr = permClassToCreator.keySet().toArray(new String[0]); - reflectiveClassProducer - .produce(ReflectiveClassBuildItem.builder(permissionClassesArr).constructors().fields().methods().build()); - } - } - - private static boolean validateConstructor(IndexView index, String permissionClass) { - ClassInfo classInfo = index.getClassByName(permissionClass); - - if (classInfo == null) { - throw new ConfigurationException(String.format("Permission class '%s' is missing", permissionClass)); - } - - // must have exactly one constructor - if (classInfo.constructors().size() != 1) { - throw new ConfigurationException( - String.format("Permission class '%s' must have exactly one constructor", permissionClass)); - } - MethodInfo constructor = classInfo.constructors().get(0); - - // first parameter must be permission name (String) - if (constructor.parametersCount() == 0 || !isString(constructor.parameterType(0))) { - throw new ConfigurationException( - String.format("Permission class '%s' constructor first parameter must be '%s' (permission name)", - permissionClass, String.class.getName())); - } + private static final DotName BASIC_AUTH_MECH_NAME = DotName.createSimple(BasicAuthenticationMechanism.class); - // second parameter (actions) is optional - if (constructor.parametersCount() == 1) { - // permission constructor accepts just name, no actions - return false; - } - - if (constructor.parametersCount() == 2) { - if (!isStringArray(constructor.parameterType(1))) { - throw new ConfigurationException( - String.format("Permission class '%s' constructor second parameter must be '%s' array", permissionClass, - String.class.getName())); - } - return true; + @Record(ExecutionTime.STATIC_INIT) + @BuildStep + void produceNamedHttpSecurityPolicies(List httpSecurityPolicyBuildItems, + BuildProducer syntheticBeanProducer, + HttpSecurityRecorder recorder) { + if (!httpSecurityPolicyBuildItems.isEmpty()) { + httpSecurityPolicyBuildItems.forEach(item -> syntheticBeanProducer + .produce(SyntheticBeanBuildItem + .configure(HttpSecurityPolicy.class) + .named(HttpSecurityPolicy.class.getName() + "." + item.getName()) + .runtimeValue(recorder.createNamedHttpSecurityPolicy(item.getPolicySupplier(), item.getName())) + .addType(HttpSecurityPolicy.class) + .scope(Singleton.class) + .unremovable() + .done())); } - - throw new ConfigurationException(String.format( - "Permission class '%s' constructor must accept either one parameter (String permissionName), or two parameters (String permissionName, String[] actions)", - permissionClass)); - } - - private static boolean isStringArray(Type type) { - return type.kind() == Type.Kind.ARRAY && isString(type.asArrayType().constituent()); - } - - private static boolean isString(Type type) { - return type.kind() == Type.Kind.CLASS && type.name().toString().equals(String.class.getName()); } @BuildStep - @Record(ExecutionTime.RUNTIME_INIT) - SyntheticBeanBuildItem initFormAuth( + @Record(ExecutionTime.STATIC_INIT) + AdditionalBeanBuildItem initFormAuth( HttpSecurityRecorder recorder, HttpBuildTimeConfig buildTimeConfig, BuildProducer filterBuildItemBuildProducer) { - if (!buildTimeConfig.auth.proactive) { - filterBuildItemBuildProducer.produce(RouteBuildItem.builder().route(buildTimeConfig.auth.form.postLocation) - .handler(recorder.formAuthPostHandler()).build()); - } if (buildTimeConfig.auth.form.enabled) { - return SyntheticBeanBuildItem.configure(FormAuthenticationMechanism.class) - .types(HttpAuthenticationMechanism.class) - .setRuntimeInit() - .scope(Singleton.class) - .supplier(recorder.setupFormAuth()).done(); + if (!buildTimeConfig.auth.proactive) { + filterBuildItemBuildProducer.produce(RouteBuildItem.builder().route(buildTimeConfig.auth.form.postLocation) + .handler(recorder.formAuthPostHandler()).build()); + } + return AdditionalBeanBuildItem.builder().setUnremovable().addBeanClass(FormAuthenticationMechanism.class) + .setDefaultScope(SINGLETON).build(); } return null; } @BuildStep - @Record(ExecutionTime.RUNTIME_INIT) - SyntheticBeanBuildItem initMtlsClientAuth( - HttpSecurityRecorder recorder, - HttpBuildTimeConfig buildTimeConfig) { + AdditionalBeanBuildItem initMtlsClientAuth(HttpBuildTimeConfig buildTimeConfig) { if (isMtlsClientAuthenticationEnabled(buildTimeConfig)) { - return SyntheticBeanBuildItem.configure(MtlsAuthenticationMechanism.class) - .types(HttpAuthenticationMechanism.class) - .setRuntimeInit() - .scope(Singleton.class) - .supplier(recorder.setupMtlsClientAuth()).done(); + return AdditionalBeanBuildItem.builder().setUnremovable().addBeanClass(MtlsAuthenticationMechanism.class) + .setDefaultScope(SINGLETON).build(); } return null; } - @BuildStep - @Record(ExecutionTime.RUNTIME_INIT) - SyntheticBeanBuildItem initBasicAuth( - HttpSecurityRecorder recorder, - HttpBuildTimeConfig buildTimeConfig, - ManagementInterfaceBuildTimeConfig managementInterfaceBuildTimeConfig, + @BuildStep(onlyIf = IsApplicationBasicAuthRequired.class) + AdditionalBeanBuildItem initBasicAuth(HttpBuildTimeConfig buildTimeConfig, + BuildProducer annotationsTransformerProducer, BuildProducer securityInformationProducer) { - if (!applicationBasicAuthRequired(buildTimeConfig, managementInterfaceBuildTimeConfig)) { - return null; - } - SyntheticBeanBuildItem.ExtendedBeanConfigurator configurator = SyntheticBeanBuildItem - .configure(BasicAuthenticationMechanism.class) - .types(HttpAuthenticationMechanism.class) - .setRuntimeInit() - .scope(Singleton.class) - .supplier(recorder.setupBasicAuth(buildTimeConfig)); if (!buildTimeConfig.auth.form.enabled && !isMtlsClientAuthenticationEnabled(buildTimeConfig) && !buildTimeConfig.auth.basic.orElse(false)) { //if not explicitly enabled we make this a default bean, so it is the fallback if nothing else is defined - configurator.defaultBean(); - if (buildTimeConfig.auth.basic.isPresent() && buildTimeConfig.auth.basic.get()) { - securityInformationProducer.produce(SecurityInformationBuildItem.BASIC()); - } + annotationsTransformerProducer.produce(new AnnotationsTransformerBuildItem(AnnotationsTransformer + .appliedToClass() + .whenClass(cl -> BASIC_AUTH_MECH_NAME.equals(cl.name())) + .thenTransform(t -> t.add(DEFAULT_BEAN)))); + } + + if (buildTimeConfig.auth.basic.isPresent() && buildTimeConfig.auth.basic.get()) { + securityInformationProducer.produce(SecurityInformationBuildItem.BASIC()); } - return configurator.done(); + return AdditionalBeanBuildItem.builder().setUnremovable().addBeanClass(BasicAuthenticationMechanism.class).build(); } public static boolean applicationBasicAuthRequired(HttpBuildTimeConfig buildTimeConfig, @@ -247,18 +137,8 @@ void setupAuthenticationMechanisms( BuildProducer filterBuildItemBuildProducer, BuildProducer beanProducer, Capabilities capabilities, - BuildProducer beanContainerListenerBuildItemBuildProducer, HttpBuildTimeConfig buildTimeConfig, - List httpSecurityPolicyBuildItemList, BuildProducer securityInformationProducer) { - Map> policyMap = new HashMap<>(); - for (HttpSecurityPolicyBuildItem e : httpSecurityPolicyBuildItemList) { - if (policyMap.containsKey(e.getName())) { - throw new RuntimeException("Multiple HTTP security policies defined with name " + e.getName()); - } - policyMap.put(e.getName(), e.policySupplier); - } - if (!buildTimeConfig.auth.form.enabled && buildTimeConfig.auth.basic.orElse(false)) { securityInformationProducer.produce(SecurityInformationBuildItem.BASIC()); } @@ -270,21 +150,13 @@ void setupAuthenticationMechanisms( beanProducer .produce(AdditionalBeanBuildItem.builder().setUnremovable().addBeanClass(HttpAuthenticator.class) .addBeanClass(HttpAuthorizer.class).build()); + beanProducer.produce(AdditionalBeanBuildItem.unremovableOf(PathMatchingHttpSecurityPolicy.class)); filterBuildItemBuildProducer .produce(new FilterBuildItem( recorder.authenticationMechanismHandler(buildTimeConfig.auth.proactive), FilterBuildItem.AUTHENTICATION)); filterBuildItemBuildProducer .produce(new FilterBuildItem(recorder.permissionCheckHandler(), FilterBuildItem.AUTHORIZATION)); - - if (!buildTimeConfig.auth.permissions.isEmpty()) { - beanContainerListenerBuildItemBuildProducer - .produce(new BeanContainerListenerBuildItem(recorder.initPermissions(buildTimeConfig, policyMap))); - } - } else { - if (!buildTimeConfig.auth.permissions.isEmpty()) { - throw new IllegalStateException("HTTP permissions have been set however security is not enabled"); - } } } @@ -325,4 +197,18 @@ void produceEagerSecurityInterceptorStorage(HttpSecurityRecorder recorder, private static boolean isMtlsClientAuthenticationEnabled(HttpBuildTimeConfig buildTimeConfig) { return !ClientAuth.NONE.equals(buildTimeConfig.tlsClientAuth); } + + static class IsApplicationBasicAuthRequired implements BooleanSupplier { + private final boolean required; + + public IsApplicationBasicAuthRequired(HttpBuildTimeConfig httpBuildTimeConfig, + ManagementInterfaceBuildTimeConfig managementInterfaceBuildTimeConfig) { + required = applicationBasicAuthRequired(httpBuildTimeConfig, managementInterfaceBuildTimeConfig); + } + + @Override + public boolean getAsBoolean() { + return required; + } + } } diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/ManagementInterfaceSecurityProcessor.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/ManagementInterfaceSecurityProcessor.java index 07d36632f97ae4..fc7d1e31a0b6b2 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/ManagementInterfaceSecurityProcessor.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/ManagementInterfaceSecurityProcessor.java @@ -1,13 +1,8 @@ package io.quarkus.vertx.http.deployment; -import java.util.HashMap; -import java.util.Map; -import java.util.function.Supplier; - import jakarta.inject.Singleton; import io.quarkus.arc.deployment.AdditionalBeanBuildItem; -import io.quarkus.arc.deployment.BeanContainerListenerBuildItem; import io.quarkus.arc.deployment.SyntheticBeanBuildItem; import io.quarkus.deployment.Capabilities; import io.quarkus.deployment.Capability; @@ -15,47 +10,26 @@ import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.annotations.ExecutionTime; import io.quarkus.deployment.annotations.Record; -import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig; -import io.quarkus.vertx.http.runtime.PolicyConfig; +import io.quarkus.vertx.http.deployment.HttpSecurityProcessor.IsApplicationBasicAuthRequired; import io.quarkus.vertx.http.runtime.management.ManagementInterfaceBuildTimeConfig; import io.quarkus.vertx.http.runtime.management.ManagementInterfaceSecurityRecorder; -import io.quarkus.vertx.http.runtime.security.AuthenticatedHttpSecurityPolicy; import io.quarkus.vertx.http.runtime.security.BasicAuthenticationMechanism; -import io.quarkus.vertx.http.runtime.security.DenySecurityPolicy; import io.quarkus.vertx.http.runtime.security.HttpAuthenticationMechanism; import io.quarkus.vertx.http.runtime.security.HttpAuthenticator; -import io.quarkus.vertx.http.runtime.security.HttpSecurityPolicy; import io.quarkus.vertx.http.runtime.security.ManagementInterfaceHttpAuthorizer; import io.quarkus.vertx.http.runtime.security.ManagementPathMatchingHttpSecurityPolicy; -import io.quarkus.vertx.http.runtime.security.PermitSecurityPolicy; -import io.quarkus.vertx.http.runtime.security.RolesAllowedHttpSecurityPolicy; -import io.quarkus.vertx.http.runtime.security.SupplierImpl; public class ManagementInterfaceSecurityProcessor { - @BuildStep - public void builtins(ManagementInterfaceBuildTimeConfig buildTimeConfig, - BuildProducer beanProducer) { - if (!buildTimeConfig.auth.permissions.isEmpty()) { - beanProducer.produce(AdditionalBeanBuildItem.unremovableOf(ManagementPathMatchingHttpSecurityPolicy.class)); - } - } - - @BuildStep - @Record(ExecutionTime.RUNTIME_INIT) + @BuildStep(onlyIfNot = IsApplicationBasicAuthRequired.class) + @Record(ExecutionTime.STATIC_INIT) SyntheticBeanBuildItem initBasicAuth( - HttpBuildTimeConfig httpBuildTimeConfig, ManagementInterfaceSecurityRecorder recorder, ManagementInterfaceBuildTimeConfig managementInterfaceBuildTimeConfig) { - if (HttpSecurityProcessor.applicationBasicAuthRequired(httpBuildTimeConfig, managementInterfaceBuildTimeConfig)) { - return null; - } - if (managementInterfaceBuildTimeConfig.auth.basic.orElse(false)) { SyntheticBeanBuildItem.ExtendedBeanConfigurator configurator = SyntheticBeanBuildItem .configure(BasicAuthenticationMechanism.class) .types(HttpAuthenticationMechanism.class) - .setRuntimeInit() .scope(Singleton.class) .supplier(recorder.setupBasicAuth()); return configurator.done(); @@ -71,39 +45,21 @@ void setupAuthenticationMechanisms( BuildProducer filterBuildItemBuildProducer, BuildProducer beanProducer, Capabilities capabilities, - BuildProducer beanContainerListenerBuildItemBuildProducer, ManagementInterfaceBuildTimeConfig buildTimeConfig) { - - Map> policyMap = new HashMap<>(); - for (Map.Entry e : buildTimeConfig.auth.rolePolicy.entrySet()) { - policyMap.put(e.getKey(), - new SupplierImpl<>(new RolesAllowedHttpSecurityPolicy(e.getValue().rolesAllowed))); - } - policyMap.put("deny", new SupplierImpl<>(new DenySecurityPolicy())); - policyMap.put("permit", new SupplierImpl<>(new PermitSecurityPolicy())); - policyMap.put("authenticated", new SupplierImpl<>(new AuthenticatedHttpSecurityPolicy())); - if (buildTimeConfig.auth.basic.orElse(false) && capabilities.isPresent(Capability.SECURITY)) { beanProducer .produce(AdditionalBeanBuildItem.builder().setUnremovable() .addBeanClass(HttpAuthenticator.class) + .addBeanClass(ManagementPathMatchingHttpSecurityPolicy.class) .addBeanClass(ManagementInterfaceHttpAuthorizer.class).build()); filterBuildItemBuildProducer .produce(new ManagementInterfaceFilterBuildItem( recorder.authenticationMechanismHandler(buildTimeConfig.auth.proactive), ManagementInterfaceFilterBuildItem.AUTHENTICATION)); filterBuildItemBuildProducer - .produce(new ManagementInterfaceFilterBuildItem(recorder.permissionCheckHandler(buildTimeConfig, policyMap), + .produce(new ManagementInterfaceFilterBuildItem(recorder.permissionCheckHandler(), ManagementInterfaceFilterBuildItem.AUTHORIZATION)); - if (!buildTimeConfig.auth.permissions.isEmpty()) { - beanContainerListenerBuildItemBuildProducer - .produce(new BeanContainerListenerBuildItem(recorder.initPermissions(buildTimeConfig, policyMap))); - } - } else { - if (!buildTimeConfig.auth.permissions.isEmpty()) { - throw new IllegalStateException("HTTP permissions have been set however security is not enabled"); - } } } } diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/CustomNamedHttpSecPolicy.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/CustomNamedHttpSecPolicy.java new file mode 100644 index 00000000000000..61d8213fd9e2ba --- /dev/null +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/CustomNamedHttpSecPolicy.java @@ -0,0 +1,29 @@ +package io.quarkus.vertx.http.security; + +import jakarta.enterprise.context.ApplicationScoped; + +import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.vertx.http.runtime.security.HttpSecurityPolicy; +import io.smallrye.mutiny.Uni; +import io.vertx.ext.web.RoutingContext; + +@ApplicationScoped +public class CustomNamedHttpSecPolicy implements HttpSecurityPolicy { + @Override + public Uni checkPermission(RoutingContext request, Uni identity, + AuthorizationRequestContext requestContext) { + if (isRequestAuthorized(request)) { + return Uni.createFrom().item(CheckResult.PERMIT); + } + return Uni.createFrom().item(CheckResult.DENY); + } + + private static boolean isRequestAuthorized(RoutingContext request) { + return request.request().headers().contains("hush-hush"); + } + + @Override + public String name() { + return "custom123"; + } +} diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/CustomNamedHttpSecPolicyTest.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/CustomNamedHttpSecPolicyTest.java new file mode 100644 index 00000000000000..5e5aae1dfb2df1 --- /dev/null +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/CustomNamedHttpSecPolicyTest.java @@ -0,0 +1,82 @@ +package io.quarkus.vertx.http.security; + +import java.util.function.Supplier; + +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.BeforeAll; +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; + +public class CustomNamedHttpSecPolicyTest { + + @BeforeAll + public static void setup() { + TestIdentityController.resetRoles().add("test", "test", "test"); + } + + private static final String APP_PROPS = "" + + "quarkus.http.auth.permission.authenticated.paths=admin\n" + + "quarkus.http.auth.permission.authenticated.policy=custom123\n"; + + @RegisterExtension + static QuarkusUnitTest test = new QuarkusUnitTest().setArchiveProducer(new Supplier<>() { + @Override + public JavaArchive get() { + return ShrinkWrap.create(JavaArchive.class) + .addClasses(TestIdentityController.class, TestIdentityProvider.class, AdminPathHandler.class, + CustomNamedHttpSecPolicy.class) + .addAsResource(new StringAsset(APP_PROPS), "application.properties"); + } + }); + + @Test + public void testAdminPath() { + RestAssured + .given() + .when() + .get("/admin") + .then() + .assertThat() + .statusCode(401); + RestAssured + .given() + .when() + .header("hush-hush", "ignored") + .get("/admin") + .then() + .assertThat() + .statusCode(200) + .body(Matchers.equalTo(":/admin")); + RestAssured + .given() + .auth() + .preemptive() + .basic("test", "test") + .when() + .header("hush-hush", "ignored") + .get("/admin") + .then() + .assertThat() + .statusCode(200) + .body(Matchers.equalTo("test:/admin")); + RestAssured + .given() + .auth() + .preemptive() + .basic("test", "test") + .when() + .get("/admin") + .then() + .assertThat() + .statusCode(403); + } + +} diff --git a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-endpoints.js b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-endpoints.js new file mode 100644 index 00000000000000..9b0ded9264cbb3 --- /dev/null +++ b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-endpoints.js @@ -0,0 +1,101 @@ +import { LitElement, html, css} from 'lit'; +import { basepath } from 'devui-data'; +import '@vaadin/progress-bar'; +import '@vaadin/grid'; +import { columnBodyRenderer } from '@vaadin/grid/lit.js'; +import '@vaadin/grid/vaadin-grid-sort-column.js'; + +/** + * This component show all available endpoints + */ +export class QwcEndpoints extends LitElement { + + static styles = css` + .infogrid { + width: 99%; + height: 99%; + } + a { + cursor: pointer; + color: var(--lumo-body-text-color); + } + a:link { + text-decoration: none; + color: var(--lumo-body-text-color); + } + a:visited { + text-decoration: none; + color: var(--lumo-body-text-color); + } + a:active { + text-decoration: none; + color: var(--lumo-body-text-color); + } + a:hover { + color: var(--quarkus-red); + } + `; + + static properties = { + _info: {state: true}, + } + + constructor() { + super(); + this._info = null; + } + + async connectedCallback() { + super.connectedCallback(); + await this.load(); + } + + async load() { + const response = await fetch(basepath + "/endpoints.json"); + const data = await response.json(); + this._info = data; + } + + render() { + if (this._info) { + const items = []; + for (const [key, value] of Object.entries(this._info)) { + items.push({"uri" : key, "description": value}); + } + + return html` + + + + + + `; + }else{ + return html` +
+
Fetching information...
+ +
+ `; + } + } + + _uriRenderer(endpoint) { + if (endpoint.uri) { + return html`${endpoint.uri}`; + } + } + + _descriptionRenderer(endpoint) { + if (endpoint.description) { + return html`${endpoint.description}`; + } + } + +} +customElements.define('qwc-endpoints', QwcEndpoints); diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/DevUIRecorder.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/DevUIRecorder.java index 57fc606b1b702d..9b9336d19253df 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/DevUIRecorder.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/DevUIRecorder.java @@ -87,6 +87,14 @@ public Handler buildTimeStaticHandler(String basePath, Map endpointInfoHandler(String basePath) { + return new EndpointInfoHandler(basePath); + } + + public void setEndpoints(List endpointInfos) { + EndpointInfoHandler.setEndpoints(endpointInfos); + } + public Handler vaadinRouterHandler(String basePath) { return new VaadinRouterHandler(basePath); } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/EndpointInfo.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/EndpointInfo.java new file mode 100644 index 00000000000000..73c9c3f4353bf6 --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/EndpointInfo.java @@ -0,0 +1,36 @@ +package io.quarkus.devui.runtime; + +public class EndpointInfo implements Comparable { + private String uri; + private String description; + + // for bytecode recorder + public EndpointInfo() { + } + + public EndpointInfo(String uri, String description) { + this.uri = uri; + this.description = description; + } + + public String getDescription() { + return description; + } + + public String getUri() { + return uri; + } + + public void setDescription(String description) { + this.description = description; + } + + public void setUri(String uri) { + this.uri = uri; + } + + @Override + public int compareTo(EndpointInfo o) { + return uri.compareTo(o.uri); + } +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/EndpointInfoHandler.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/EndpointInfoHandler.java new file mode 100644 index 00000000000000..b171187fdd22e6 --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/EndpointInfoHandler.java @@ -0,0 +1,79 @@ +package io.quarkus.devui.runtime; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import io.vertx.core.Handler; +import io.vertx.core.json.Json; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.web.RoutingContext; + +/** + * Handler to return the endpoint info + */ +public class EndpointInfoHandler implements Handler { + private static volatile List endpointInfos; + + static void setEndpoints(List endpointInfos) { + EndpointInfoHandler.endpointInfos = endpointInfos; + } + + private String basePath; // Like /q/dev-ui + + public EndpointInfoHandler() { + + } + + public EndpointInfoHandler(String basePath) { + this.basePath = basePath; + } + + public String getBasePath() { + return basePath; + } + + public void setBasePath(String basePath) { + this.basePath = basePath; + } + + @Override + public void handle(RoutingContext event) { + String normalizedPath = event.normalizedPath(); + if (normalizedPath.contains(SLASH)) { + int si = normalizedPath.lastIndexOf(SLASH) + 1; + String path = normalizedPath.substring(0, si); + String fileName = normalizedPath.substring(si); + if (path.startsWith(basePath) && fileName.equals("endpoints.json")) { + + event.response() + .setStatusCode(STATUS) + .setStatusMessage(OK) + .putHeader(CONTENT_TYPE, "application/json") + .end(Json.encodePrettily(getContent())); + + } else { + event.next(); + } + } else { + event.next(); + } + } + + private JsonObject getContent() { + + Map info = new HashMap<>(); + + for (EndpointInfo endpoint : EndpointInfoHandler.endpointInfos) { + info.put(endpoint.getUri(), endpoint.getDescription()); + } + + return new JsonObject(info); + + } + + private static final int STATUS = 200; + private static final String OK = "OK"; + private static final String SLASH = "/"; + private static final String CONTENT_TYPE = "Content-Type"; +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/AuthConfig.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/AuthConfig.java index 3e609d66f98257..e4ff3dec29715a 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/AuthConfig.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/AuthConfig.java @@ -1,6 +1,5 @@ package io.quarkus.vertx.http.runtime; -import java.util.Map; import java.util.Optional; import io.quarkus.runtime.annotations.ConfigGroup; @@ -26,24 +25,6 @@ public class AuthConfig { @ConfigItem public FormAuthConfig form; - /** - * The authentication realm - */ - @ConfigItem - public Optional realm; - - /** - * The HTTP permissions - */ - @ConfigItem(name = "permission") - public Map permissions; - - /** - * The HTTP role based policies - */ - @ConfigItem(name = "policy") - public Map rolePolicy; - /** * If this is true and credentials are present then a user will always be authenticated * before the request progresses. diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/AuthRuntimeConfig.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/AuthRuntimeConfig.java new file mode 100644 index 00000000000000..8552e30ef6b6bb --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/AuthRuntimeConfig.java @@ -0,0 +1,38 @@ +package io.quarkus.vertx.http.runtime; + +import java.util.Map; +import java.util.Optional; + +import io.quarkus.runtime.annotations.ConfigGroup; +import io.quarkus.runtime.annotations.ConfigItem; + +/** + * Authentication mechanism information used for configuring HTTP auth instance for the deployment. + */ +@ConfigGroup +public class AuthRuntimeConfig { + + /** + * The HTTP permissions + */ + @ConfigItem(name = "permission") + public Map permissions; + + /** + * The HTTP role based policies + */ + @ConfigItem(name = "policy") + public Map rolePolicy; + + /** + * The authentication realm + */ + @ConfigItem + public Optional realm; + + /** + * Form Auth config + */ + @ConfigItem + public FormAuthRuntimeConfig form; +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/FormAuthConfig.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/FormAuthConfig.java index e08e6595129ba6..2e6d38250c8da3 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/FormAuthConfig.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/FormAuthConfig.java @@ -1,8 +1,5 @@ package io.quarkus.vertx.http.runtime; -import java.time.Duration; -import java.util.Optional; - import io.quarkus.runtime.annotations.ConfigGroup; import io.quarkus.runtime.annotations.ConfigItem; @@ -11,14 +8,6 @@ */ @ConfigGroup public class FormAuthConfig { - /** - * SameSite attribute values for the session and location cookies. - */ - public enum CookieSameSite { - STRICT, - LAX, - NONE - } /** * If form authentication is enabled. @@ -26,109 +15,10 @@ public enum CookieSameSite { @ConfigItem public boolean enabled; - /** - * The login page. Redirect to login page can be disabled by setting `quarkus.http.auth.form.login-page=`. - */ - @ConfigItem(defaultValue = "/login.html") - public Optional loginPage; - /** * The post location. */ @ConfigItem(defaultValue = "/j_security_check") public String postLocation; - /** - * The username field name. - */ - @ConfigItem(defaultValue = "j_username") - public String usernameParameter; - - /** - * The password field name. - */ - @ConfigItem(defaultValue = "j_password") - public String passwordParameter; - - /** - * The error page. Redirect to error page can be disabled by setting `quarkus.http.auth.form.error-page=`. - */ - @ConfigItem(defaultValue = "/error.html") - public Optional errorPage; - - /** - * The landing page to redirect to if there is no saved page to redirect back to. - * Redirect to landing page can be disabled by setting `quarkus.http.auth.form.landing-page=`. - */ - @ConfigItem(defaultValue = "/index.html") - public Optional landingPage; - - /** - * Option to disable redirect to landingPage if there is no saved page to redirect back to. Form Auth POST is followed - * by redirect to landingPage by default. - * - * @deprecated redirect to landingPage can be disabled by removing default landing page - * (via `quarkus.http.auth.form.landing-page=`). Quarkus will ignore this configuration property - * if there is no landing page. - */ - @ConfigItem(defaultValue = "true") - @Deprecated - public boolean redirectAfterLogin; - - /** - * Option to control the name of the cookie used to redirect the user back - * to where he wants to get access to. - */ - @ConfigItem(defaultValue = "quarkus-redirect-location") - public String locationCookie; - - /** - * The inactivity (idle) timeout - * - * When inactivity timeout is reached, cookie is not renewed and a new login is enforced. - */ - @ConfigItem(defaultValue = "PT30M") - public Duration timeout; - - /** - * How old a cookie can get before it will be replaced with a new cookie with an updated timeout, also - * 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 - * 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 - * 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. - */ - @ConfigItem(defaultValue = "PT1M") - public Duration newCookieInterval; - - /** - * The cookie that is used to store the persistent session - */ - @ConfigItem(defaultValue = "quarkus-credential") - public String cookieName; - - /** - * The cookie path for the session and location cookies. - */ - @ConfigItem(defaultValue = "/") - public Optional cookiePath = Optional.of("/"); - - /** - * Set the HttpOnly attribute to prevent access to the cookie via JavaScript. - */ - @ConfigItem(defaultValue = "false") - public boolean httpOnlyCookie; - - /** - * SameSite attribute for the session and location cookies. - */ - @ConfigItem(defaultValue = "strict") - public CookieSameSite cookieSameSite = CookieSameSite.STRICT; } 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 new file mode 100644 index 00000000000000..d4152fe4a9b133 --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/FormAuthRuntimeConfig.java @@ -0,0 +1,122 @@ +package io.quarkus.vertx.http.runtime; + +import java.time.Duration; +import java.util.Optional; + +import io.quarkus.runtime.annotations.ConfigGroup; +import io.quarkus.runtime.annotations.ConfigItem; + +/** + * config for the form authentication mechanism + */ +@ConfigGroup +public class FormAuthRuntimeConfig { + /** + * SameSite attribute values for the session and location cookies. + */ + public enum CookieSameSite { + STRICT, + LAX, + NONE + } + + /** + * The login page. Redirect to login page can be disabled by setting `quarkus.http.auth.form.login-page=`. + */ + @ConfigItem(defaultValue = "/login.html") + public Optional loginPage; + + /** + * The username field name. + */ + @ConfigItem(defaultValue = "j_username") + public String usernameParameter; + + /** + * The password field name. + */ + @ConfigItem(defaultValue = "j_password") + public String passwordParameter; + + /** + * The error page. Redirect to error page can be disabled by setting `quarkus.http.auth.form.error-page=`. + */ + @ConfigItem(defaultValue = "/error.html") + public Optional errorPage; + + /** + * The landing page to redirect to if there is no saved page to redirect back to. + * Redirect to landing page can be disabled by setting `quarkus.http.auth.form.landing-page=`. + */ + @ConfigItem(defaultValue = "/index.html") + public Optional landingPage; + + /** + * Option to disable redirect to landingPage if there is no saved page to redirect back to. Form Auth POST is followed + * by redirect to landingPage by default. + * + * @deprecated redirect to landingPage can be disabled by removing default landing page + * (via `quarkus.http.auth.form.landing-page=`). Quarkus will ignore this configuration property + * if there is no landing page. + */ + @ConfigItem(defaultValue = "true") + @Deprecated + public boolean redirectAfterLogin; + + /** + * Option to control the name of the cookie used to redirect the user back + * to where he wants to get access to. + */ + @ConfigItem(defaultValue = "quarkus-redirect-location") + public String locationCookie; + + /** + * The inactivity (idle) timeout + * + * When inactivity timeout is reached, cookie is not renewed and a new login is enforced. + */ + @ConfigItem(defaultValue = "PT30M") + public Duration timeout; + + /** + * How old a cookie can get before it will be replaced with a new cookie with an updated timeout, also + * 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 + * 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 + * 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. + */ + @ConfigItem(defaultValue = "PT1M") + public Duration newCookieInterval; + + /** + * The cookie that is used to store the persistent session + */ + @ConfigItem(defaultValue = "quarkus-credential") + public String cookieName; + + /** + * The cookie path for the session and location cookies. + */ + @ConfigItem(defaultValue = "/") + public Optional cookiePath = Optional.of("/"); + + /** + * Set the HttpOnly attribute to prevent access to the cookie via JavaScript. + */ + @ConfigItem(defaultValue = "false") + public boolean httpOnlyCookie; + + /** + * SameSite attribute for the session and location cookies. + */ + @ConfigItem(defaultValue = "strict") + public CookieSameSite cookieSameSite = CookieSameSite.STRICT; +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpConfiguration.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpConfiguration.java index 89ffdf53d0c198..e726692e1952b5 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpConfiguration.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpConfiguration.java @@ -14,6 +14,11 @@ @ConfigRoot(phase = ConfigPhase.RUN_TIME) public class HttpConfiguration { + /** + * Authentication configuration + */ + public AuthRuntimeConfig auth; + /** * Enable the CORS filter. */ diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/PolicyConfig.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/PolicyConfig.java index 7d16bc2f4e392d..6977b99f08770b 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/PolicyConfig.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/PolicyConfig.java @@ -36,6 +36,7 @@ public class PolicyConfig { * Permissions granted by this policy will be created with a `java.security.Permission` implementation * specified by this configuration property. The permission class must declare exactly one constructor * that accepts permission name (`String`) or permission name and actions (`String`, `String[]`). + * Permission class must be registered for reflection if you run your application in a native mode. */ @ConfigItem(defaultValue = "io.quarkus.security.StringPermission") public String permissionClass = StringPermission.class.getName(); diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/management/ManagementAuthConfig.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/management/ManagementAuthConfig.java index 017fcfe953a666..a22db7e1393599 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/management/ManagementAuthConfig.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/management/ManagementAuthConfig.java @@ -1,12 +1,9 @@ package io.quarkus.vertx.http.runtime.management; -import java.util.Map; import java.util.Optional; import io.quarkus.runtime.annotations.ConfigGroup; import io.quarkus.runtime.annotations.ConfigItem; -import io.quarkus.vertx.http.runtime.PolicyConfig; -import io.quarkus.vertx.http.runtime.PolicyMappingConfig; /** * Authentication for the management interface. @@ -20,18 +17,6 @@ public class ManagementAuthConfig { @ConfigItem public Optional basic; - /** - * The HTTP permissions - */ - @ConfigItem(name = "permission") - public Map permissions; - - /** - * The HTTP role based policies - */ - @ConfigItem(name = "policy") - public Map rolePolicy; - /** * If this is true and credentials are present then a user will always be authenticated * before the request progresses. diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/management/ManagementInterfaceConfiguration.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/management/ManagementInterfaceConfiguration.java index 9d77f458d1c901..7a2236a17f240a 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/management/ManagementInterfaceConfiguration.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/management/ManagementInterfaceConfiguration.java @@ -23,6 +23,11 @@ @ConfigRoot(phase = ConfigPhase.RUN_TIME, name = "management") public class ManagementInterfaceConfiguration { + /** + * Authentication configuration + */ + public ManagementRuntimeAuthConfig auth; + /** * The HTTP port */ diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/management/ManagementInterfaceSecurityRecorder.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/management/ManagementInterfaceSecurityRecorder.java index 03e6be2d83742e..f3b1b9f5a3c331 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/management/ManagementInterfaceSecurityRecorder.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/management/ManagementInterfaceSecurityRecorder.java @@ -1,18 +1,13 @@ package io.quarkus.vertx.http.runtime.management; -import java.util.Map; import java.util.function.Supplier; import jakarta.enterprise.inject.Instance; import jakarta.enterprise.inject.spi.CDI; -import io.quarkus.arc.runtime.BeanContainer; -import io.quarkus.arc.runtime.BeanContainerListener; -import io.quarkus.runtime.RuntimeValue; import io.quarkus.runtime.annotations.Recorder; import io.quarkus.vertx.http.runtime.security.AbstractPathMatchingHttpSecurityPolicy; import io.quarkus.vertx.http.runtime.security.BasicAuthenticationMechanism; -import io.quarkus.vertx.http.runtime.security.HttpSecurityPolicy; import io.quarkus.vertx.http.runtime.security.HttpSecurityRecorder.AbstractAuthenticationHandler; import io.quarkus.vertx.http.runtime.security.ManagementInterfaceHttpAuthorizer; import io.quarkus.vertx.http.runtime.security.ManagementPathMatchingHttpSecurityPolicy; @@ -22,21 +17,11 @@ @Recorder public class ManagementInterfaceSecurityRecorder { - final RuntimeValue httpConfiguration; - final ManagementInterfaceBuildTimeConfig buildTimeConfig; - - public ManagementInterfaceSecurityRecorder(RuntimeValue httpConfiguration, - ManagementInterfaceBuildTimeConfig buildTimeConfig) { - this.httpConfiguration = httpConfiguration; - this.buildTimeConfig = buildTimeConfig; - } - public Handler authenticationMechanismHandler(boolean proactiveAuthentication) { return new ManagementAuthenticationHandler(proactiveAuthentication); } - public Handler permissionCheckHandler(ManagementInterfaceBuildTimeConfig buildTimeConfig, - Map> policies) { + public Handler permissionCheckHandler() { return new Handler() { volatile ManagementInterfaceHttpAuthorizer authorizer; @@ -52,17 +37,6 @@ public void handle(RoutingContext event) { }; } - public BeanContainerListener initPermissions(ManagementInterfaceBuildTimeConfig buildTimeConfig, - Map> policies) { - return new BeanContainerListener() { - @Override - public void created(BeanContainer container) { - container.beanInstance(ManagementPathMatchingHttpSecurityPolicy.class) - .init(buildTimeConfig.auth.permissions, policies, buildTimeConfig.rootPath); - } - }; - } - public Supplier setupBasicAuth() { return new Supplier() { @Override @@ -91,5 +65,10 @@ protected void setPathMatchingPolicy(RoutingContext event) { event.put(AbstractPathMatchingHttpSecurityPolicy.class.getName(), pathMatchingPolicy); } } + + @Override + protected boolean httpPermissionsEmpty() { + return CDI.current().select(ManagementInterfaceConfiguration.class).get().auth.permissions.isEmpty(); + } } } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/management/ManagementRuntimeAuthConfig.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/management/ManagementRuntimeAuthConfig.java new file mode 100644 index 00000000000000..f9002d619a0815 --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/management/ManagementRuntimeAuthConfig.java @@ -0,0 +1,27 @@ +package io.quarkus.vertx.http.runtime.management; + +import java.util.Map; + +import io.quarkus.runtime.annotations.ConfigGroup; +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.vertx.http.runtime.PolicyConfig; +import io.quarkus.vertx.http.runtime.PolicyMappingConfig; + +/** + * Authentication for the management interface. + */ +@ConfigGroup +public class ManagementRuntimeAuthConfig { + + /** + * The HTTP permissions + */ + @ConfigItem(name = "permission") + public Map permissions; + + /** + * The HTTP role based policies + */ + @ConfigItem(name = "policy") + public Map rolePolicy; +} 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 14aa4d607cd29e..304688d56f51d8 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 @@ -1,6 +1,11 @@ package io.quarkus.vertx.http.runtime.security; +import static io.quarkus.security.PermissionsAllowed.PERMISSION_TO_ACTION_SEPARATOR; + +import java.lang.reflect.InvocationTargetException; +import java.security.Permission; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -8,9 +13,13 @@ import java.util.Map; import java.util.Set; import java.util.function.Function; -import java.util.function.Supplier; +import jakarta.enterprise.inject.Instance; + +import io.quarkus.runtime.configuration.ConfigurationException; +import io.quarkus.security.StringPermission; import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.vertx.http.runtime.PolicyConfig; 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; @@ -26,6 +35,11 @@ public class AbstractPathMatchingHttpSecurityPolicy { private final PathMatcher> pathMatcher = new PathMatcher<>(); + AbstractPathMatchingHttpSecurityPolicy(Map permissions, + Map rolePolicy, String rootPath, Instance installedPolicies) { + init(permissions, toNamedHttpSecPolicies(rolePolicy, installedPolicies), rootPath); + } + public String getAuthMechanismName(RoutingContext routingContext) { PathMatcher.PathMatch> toCheck = pathMatcher.match(routingContext.normalizedPath()); if (toCheck.getValue() == null || toCheck.getValue().isEmpty()) { @@ -79,13 +93,8 @@ public Uni apply(CheckResult checkResult) { }); } - public void init(Map permissions, - Map> supplierMap, String rootPath) { - Map permissionCheckers = new HashMap<>(); - for (Map.Entry> i : supplierMap.entrySet()) { - permissionCheckers.put(i.getKey(), i.getValue().get()); - } - + private void init(Map permissions, + Map permissionCheckers, String rootPath) { Map> tempMap = new HashMap<>(); for (Map.Entry entry : permissions.entrySet()) { HttpSecurityPolicy checker = permissionCheckers.get(entry.getValue().policy); @@ -150,6 +159,167 @@ public List findPermissionCheckers(RoutingContext context) { } + private static Map toNamedHttpSecPolicies(Map rolePolicies, + Instance installedPolicies) { + Map namedPolicies = new HashMap<>(); + for (Instance.Handle handle : installedPolicies.handles()) { + if (handle.getBean().getBeanClass().getSuperclass() == AbstractPathMatchingHttpSecurityPolicy.class) { + continue; + } + var policy = handle.get(); + if (policy.name() != null) { + if (policy.name().isBlank()) { + throw new ConfigurationException("HTTP Security policy '" + policy + "' name must not be blank"); + } + namedPolicies.put(policy.name(), policy); + } + } + + for (Map.Entry e : rolePolicies.entrySet()) { + PolicyConfig policyConfig = e.getValue(); + if (policyConfig.permissions.isEmpty()) { + namedPolicies.put(e.getKey(), new RolesAllowedHttpSecurityPolicy(policyConfig.rolesAllowed)); + } else { + final Map> roleToPermissions = new HashMap<>(); + for (Map.Entry> roleToPermissionStr : policyConfig.permissions.entrySet()) { + + // collect permission actions + // perm1:action1,perm2:action2,perm1:action3 -> perm1:action1,action3 and perm2:action2 + Map cache = new HashMap<>(); + final String role = roleToPermissionStr.getKey(); + for (String permissionToAction : roleToPermissionStr.getValue()) { + // parse permission to actions and add it to cache + addPermissionToAction(cache, role, permissionToAction); + } + + // create permissions + var permissions = new HashSet(); + for (PermissionToActions helper : cache.values()) { + if (StringPermission.class.getName().equals(policyConfig.permissionClass)) { + permissions.add(new StringPermission(helper.permissionName, helper.actions.toArray(new String[0]))); + } else { + permissions.add(customPermissionCreator(policyConfig, helper)); + } + } + + roleToPermissions.put(role, Set.copyOf(permissions)); + } + namedPolicies.put(e.getKey(), + new RolesAllowedHttpSecurityPolicy(policyConfig.rolesAllowed, Map.copyOf(roleToPermissions))); + } + } + namedPolicies.put("deny", new DenySecurityPolicy()); + namedPolicies.put("permit", new PermitSecurityPolicy()); + namedPolicies.put("authenticated", new AuthenticatedHttpSecurityPolicy()); + return namedPolicies; + } + + private static boolean acceptsActions(String permissionClassStr) { + var permissionClass = loadClass(permissionClassStr); + if (permissionClass.getConstructors().length != 1) { + throw new ConfigurationException( + String.format("Permission class '%s' must have exactly one constructor", permissionClass)); + } + var constructor = permissionClass.getConstructors()[0]; + // first parameter must be permission name (String) + if (constructor.getParameterCount() == 0 || !(constructor.getParameterTypes()[0] == String.class)) { + throw new ConfigurationException( + String.format("Permission class '%s' constructor first parameter must be '%s' (permission name)", + permissionClass, String.class.getName())); + } + final boolean acceptsActions; + if (constructor.getParameterCount() == 1) { + acceptsActions = false; + } else { + if (constructor.getParameterCount() == 2) { + if (constructor.getParameterTypes()[1] != String[].class) { + throw new ConfigurationException( + String.format("Permission class '%s' constructor second parameter must be '%s' array", + permissionClass, + String.class.getName())); + } + } else { + throw new ConfigurationException(String.format( + "Permission class '%s' constructor must accept either one parameter (String permissionName), or two parameters (String permissionName, String[] actions)", + permissionClass)); + } + acceptsActions = true; + } + return acceptsActions; + } + + private static void addPermissionToAction(Map cache, String role, String permissionToAction) { + final String permissionName; + final String action; + // incoming value is either in format perm1:action1 or perm1 (with or withot action) + if (permissionToAction.contains(PERMISSION_TO_ACTION_SEPARATOR)) { + // perm1:action1 + var permToActions = permissionToAction.split(PERMISSION_TO_ACTION_SEPARATOR); + if (permToActions.length != 2) { + throw new ConfigurationException( + String.format("Invalid permission format '%s', please use exactly one permission to action separator", + permissionToAction)); + } + permissionName = permToActions[0].trim(); + action = permToActions[1].trim(); + } else { + // perm1 + permissionName = permissionToAction.trim(); + action = null; + } + + if (permissionName.isEmpty()) { + throw new ConfigurationException( + String.format("Invalid permission name '%s' for role '%s'", permissionToAction, role)); + } + + cache.computeIfAbsent(permissionName, new Function() { + @Override + public PermissionToActions apply(String s) { + return new PermissionToActions(s); + } + }).addAction(action); + } + + private static Class loadClass(String className) { + try { + return Thread.currentThread().getContextClassLoader().loadClass(className); + } catch (ClassNotFoundException e) { + throw new RuntimeException("Unable to load class '" + className + "' for creating permission", e); + } + } + + private static Permission customPermissionCreator(PolicyConfig policyConfig, PermissionToActions helper) { + try { + var constructor = loadClass(policyConfig.permissionClass).getConstructors()[0]; + if (acceptsActions(policyConfig.permissionClass)) { + return (Permission) constructor.newInstance(helper.permissionName, helper.actions.toArray(new String[0])); + } else { + return (Permission) constructor.newInstance(helper.permissionName); + } + } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(String.format("Failed to create Permission - class '%s', name '%s', actions '%s'", + policyConfig.permissionClass, helper.permissionName, + Arrays.toString(helper.actions.toArray(new String[0]))), e); + } + } + + private static final class PermissionToActions { + private final String permissionName; + private final Set actions; + + private PermissionToActions(String permissionName) { + this.permissionName = permissionName; + this.actions = new HashSet<>(); + } + + private void addAction(String action) { + if (action != null) { + this.actions.add(action); + } + } + } + static class HttpMatcher { final String authMechanism; diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/BasicAuthenticationMechanism.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/BasicAuthenticationMechanism.java index 62bc6b8ae8ef54..7c16ecd8d00c6c 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/BasicAuthenticationMechanism.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/BasicAuthenticationMechanism.java @@ -29,6 +29,7 @@ import java.util.Set; import java.util.regex.Pattern; +import jakarta.inject.Inject; import jakarta.inject.Singleton; import org.jboss.logging.Logger; @@ -41,6 +42,8 @@ import io.quarkus.security.identity.SecurityIdentity; import io.quarkus.security.identity.request.AuthenticationRequest; import io.quarkus.security.identity.request.UsernamePasswordAuthenticationRequest; +import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig; +import io.quarkus.vertx.http.runtime.HttpConfiguration; import io.smallrye.mutiny.Uni; import io.vertx.ext.web.RoutingContext; @@ -53,19 +56,6 @@ public class BasicAuthenticationMechanism implements HttpAuthenticationMechanism private static final Logger log = Logger.getLogger(BasicAuthenticationMechanism.class); - public static final String SILENT = "silent"; - public static final String CHARSET = "charset"; - /** - * A comma separated list of patterns and charsets. The pattern is a regular expression. - * - * Because different browsers use different encodings this allows for the correct encoding to be selected based - * on the current browser. In general though it is recommended that BASIC auth not be used when passwords contain - * characters outside ASCII, as some browsers use the current locate to determine encoding. - * - * This list must have an even number of elements, as it is interpreted as pattern,charset,pattern,charset,... - */ - public static final String USER_AGENT_CHARSETS = "user-agent-charsets"; - private final String challenge; private static final String BASIC = "basic"; @@ -85,6 +75,11 @@ public class BasicAuthenticationMechanism implements HttpAuthenticationMechanism private final Charset charset; private final Map userAgentCharsets; + @Inject + BasicAuthenticationMechanism(HttpConfiguration runtimeConfig, HttpBuildTimeConfig buildTimeConfig) { + this(runtimeConfig.auth.realm.orElse(null), buildTimeConfig.auth.form.enabled); + } + public BasicAuthenticationMechanism(final String realmName) { this(realmName, false); } @@ -101,25 +96,6 @@ public BasicAuthenticationMechanism(final String realmName, final boolean silent this.userAgentCharsets = Collections.unmodifiableMap(new LinkedHashMap<>(userAgentCharsets)); } - @Deprecated - public BasicAuthenticationMechanism(final String realmName, final String mechanismName) { - this(realmName, mechanismName, false); - } - - @Deprecated - public BasicAuthenticationMechanism(final String realmName, final String mechanismName, final boolean silent) { - this(realmName, mechanismName, silent, StandardCharsets.UTF_8, Collections.emptyMap()); - } - - @Deprecated - public BasicAuthenticationMechanism(final String realmName, final String mechanismName, final boolean silent, - Charset charset, Map userAgentCharsets) { - this.challenge = BASIC_PREFIX + "realm=\"" + realmName + "\""; - this.silent = silent; - this.charset = charset; - this.userAgentCharsets = Collections.unmodifiableMap(new LinkedHashMap<>(userAgentCharsets)); - } - @Override public Uni authenticate(RoutingContext context, IdentityProviderManager identityProviderManager) { diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/FormAuthenticationMechanism.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/FormAuthenticationMechanism.java index 85927ed2f89229..639565c632ad2b 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/FormAuthenticationMechanism.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/FormAuthenticationMechanism.java @@ -1,12 +1,16 @@ package io.quarkus.vertx.http.runtime.security; import java.net.URI; +import java.security.SecureRandom; import java.util.Arrays; +import java.util.Base64; import java.util.HashSet; import java.util.Optional; import java.util.Set; import java.util.function.Consumer; +import jakarta.inject.Inject; + import org.jboss.logging.Logger; import io.netty.handler.codec.http.HttpHeaderNames; @@ -18,6 +22,10 @@ import io.quarkus.security.identity.request.AuthenticationRequest; import io.quarkus.security.identity.request.TrustedAuthenticationRequest; import io.quarkus.security.identity.request.UsernamePasswordAuthenticationRequest; +import io.quarkus.vertx.http.runtime.FormAuthConfig; +import io.quarkus.vertx.http.runtime.FormAuthRuntimeConfig; +import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig; +import io.quarkus.vertx.http.runtime.HttpConfiguration; import io.smallrye.mutiny.Uni; import io.smallrye.mutiny.subscription.UniEmitter; import io.vertx.core.Handler; @@ -47,6 +55,45 @@ public class FormAuthenticationMechanism implements HttpAuthenticationMechanism private final PersistentLoginManager loginManager; + //the temp encryption key, persistent across dev mode restarts + static volatile String encryptionKey; + + @Inject + FormAuthenticationMechanism(HttpConfiguration httpConfiguration, HttpBuildTimeConfig buildTimeConfig) { + String key; + if (!httpConfiguration.encryptionKey.isPresent()) { + if (encryptionKey != null) { + //persist across dev mode restarts + key = encryptionKey; + } else { + byte[] data = new byte[32]; + new SecureRandom().nextBytes(data); + key = encryptionKey = Base64.getEncoder().encodeToString(data); + log.warn("Encryption key was not specified for persistent FORM auth, using temporary key " + key); + } + } else { + key = httpConfiguration.encryptionKey.get(); + } + FormAuthConfig form = buildTimeConfig.auth.form; + FormAuthRuntimeConfig runtimeForm = httpConfiguration.auth.form; + this.loginManager = new PersistentLoginManager(key, runtimeForm.cookieName, runtimeForm.timeout.toMillis(), + runtimeForm.newCookieInterval.toMillis(), runtimeForm.httpOnlyCookie, runtimeForm.cookieSameSite.name(), + runtimeForm.cookiePath.orElse(null)); + this.loginPage = startWithSlash(runtimeForm.loginPage.orElse(null)); + this.errorPage = startWithSlash(runtimeForm.errorPage.orElse(null)); + this.landingPage = startWithSlash(runtimeForm.landingPage.orElse(null)); + this.postLocation = startWithSlash(form.postLocation); + this.usernameParameter = runtimeForm.usernameParameter; + this.passwordParameter = runtimeForm.passwordParameter; + this.locationCookie = runtimeForm.locationCookie; + this.cookiePath = runtimeForm.cookiePath.orElse(null); + boolean redirectAfterLogin = runtimeForm.redirectAfterLogin; + this.redirectToLandingPage = landingPage != null && redirectAfterLogin; + this.redirectToLoginPage = loginPage != null; + this.redirectToErrorPage = errorPage != null; + this.cookieSameSite = CookieSameSite.valueOf(runtimeForm.cookieSameSite.name()); + } + public FormAuthenticationMechanism(String loginPage, String postLocation, String usernameParameter, String passwordParameter, String errorPage, String landingPage, boolean redirectAfterLogin, String locationCookie, String cookieSameSite, String cookiePath, @@ -240,4 +287,11 @@ public Set> getCredentialTypes() { public Uni getCredentialTransport(RoutingContext context) { return Uni.createFrom().item(new HttpCredentialTransport(HttpCredentialTransport.Type.POST, postLocation, FORM)); } + + private static String startWithSlash(String page) { + if (page == null) { + return null; + } + return page.startsWith("/") ? page : "/" + page; + } } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpAuthorizer.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpAuthorizer.java index 190a7774888738..6f4529041ab3b4 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpAuthorizer.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpAuthorizer.java @@ -23,10 +23,12 @@ public class HttpAuthorizer extends AbstractHttpAuthorizer { } private static List toList(Instance installedPolicies) { - List policies = new ArrayList<>(); + List globalPolicies = new ArrayList<>(); for (HttpSecurityPolicy i : installedPolicies) { - policies.add(i); + if (i.name() == null) { + globalPolicies.add(i); + } } - return policies; + return globalPolicies; } } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityPolicy.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityPolicy.java index 88a10a5da19324..f0c9062360111e 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityPolicy.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityPolicy.java @@ -8,18 +8,27 @@ /** * An HTTP Security policy, that controls which requests are allowed to proceed. - * - * There are two different ways these policies can be installed. The easiest is to just create a CDI bean, in which - * case the policy will be invoked on every request. - * - * Alternatively HttpSecurityPolicyBuildItem can be used to create a named policy. This policy can then be referenced - * in the application.properties path matching rules, which allows this policy to be applied to specific requests. + * CDI beans implementing this interface are invoked on every request unless they define {@link #name()}. + * The policy with {@link #name()} can then be referenced in the application.properties path matching rules, + * which allows this policy to be applied only to specific requests. */ public interface HttpSecurityPolicy { Uni checkPermission(RoutingContext request, Uni identity, AuthorizationRequestContext requestContext); + /** + * HTTP Security policy name referenced in the application.properties path matching rules, which allows this + * policy to be applied to specific requests. The name must not be blank. When the name is {@code null}, policy + * will be applied to every request. + * + * @return policy name + */ + default String name() { + // null == global policy + return null; + } + /** * The results of a permission check */ diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityRecorder.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityRecorder.java index 84b3ef16064ade..65d25a92394a8c 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityRecorder.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityRecorder.java @@ -1,20 +1,9 @@ package io.quarkus.vertx.http.runtime.security; -import static io.quarkus.security.PermissionsAllowed.PERMISSION_TO_ACTION_SEPARATOR; - -import java.lang.reflect.InvocationTargetException; -import java.security.Permission; -import java.security.SecureRandom; -import java.util.Arrays; -import java.util.Base64; import java.util.HashMap; -import java.util.HashSet; -import java.util.List; import java.util.Map; -import java.util.Set; import java.util.concurrent.CompletionException; import java.util.function.BiConsumer; -import java.util.function.BiFunction; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; @@ -24,20 +13,14 @@ import org.jboss.logging.Logger; -import io.quarkus.arc.runtime.BeanContainer; -import io.quarkus.arc.runtime.BeanContainerListener; import io.quarkus.runtime.RuntimeValue; import io.quarkus.runtime.annotations.Recorder; -import io.quarkus.runtime.configuration.ConfigurationException; import io.quarkus.security.AuthenticationCompletionException; import io.quarkus.security.AuthenticationFailedException; import io.quarkus.security.AuthenticationRedirectException; -import io.quarkus.security.StringPermission; import io.quarkus.security.identity.SecurityIdentity; import io.quarkus.security.identity.request.AnonymousAuthenticationRequest; import io.quarkus.security.spi.runtime.MethodDescription; -import io.quarkus.vertx.http.runtime.FormAuthConfig; -import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig; import io.quarkus.vertx.http.runtime.HttpConfiguration; import io.smallrye.mutiny.CompositeException; import io.smallrye.mutiny.Uni; @@ -52,23 +35,6 @@ public class HttpSecurityRecorder { private static final Logger log = Logger.getLogger(HttpSecurityRecorder.class); - protected static final Consumer NOOP_CALLBACK = new Consumer() { - @Override - public void accept(Throwable throwable) { - - } - }; - - final RuntimeValue httpConfiguration; - final HttpBuildTimeConfig buildTimeConfig; - - //the temp encryption key, persistent across dev mode restarts - static volatile String encryptionKey; - - public HttpSecurityRecorder(RuntimeValue httpConfiguration, HttpBuildTimeConfig buildTimeConfig) { - this.httpConfiguration = httpConfiguration; - this.buildTimeConfig = buildTimeConfig; - } public Handler authenticationMechanismHandler(boolean proactiveAuthentication) { return new HttpAuthenticationHandler(proactiveAuthentication); @@ -88,83 +54,6 @@ public void handle(RoutingContext event) { }; } - public BeanContainerListener initPermissions(HttpBuildTimeConfig buildTimeConfig, - Map> policies) { - return new BeanContainerListener() { - @Override - public void created(BeanContainer container) { - container.beanInstance(PathMatchingHttpSecurityPolicy.class) - .init(buildTimeConfig.auth.permissions, policies, buildTimeConfig.rootPath); - } - }; - } - - public Supplier setupFormAuth() { - - return new Supplier() { - - @Override - public FormAuthenticationMechanism get() { - String key; - if (!httpConfiguration.getValue().encryptionKey.isPresent()) { - if (encryptionKey != null) { - //persist across dev mode restarts - key = encryptionKey; - } else { - byte[] data = new byte[32]; - new SecureRandom().nextBytes(data); - key = encryptionKey = Base64.getEncoder().encodeToString(data); - log.warn("Encryption key was not specified for persistent FORM auth, using temporary key " + key); - } - } else { - key = httpConfiguration.getValue().encryptionKey.get(); - } - FormAuthConfig form = buildTimeConfig.auth.form; - PersistentLoginManager loginManager = new PersistentLoginManager(key, form.cookieName, form.timeout.toMillis(), - form.newCookieInterval.toMillis(), form.httpOnlyCookie, form.cookieSameSite.name(), - form.cookiePath.orElse(null)); - String loginPage = startWithSlash(form.loginPage.orElse(null)); - String errorPage = startWithSlash(form.errorPage.orElse(null)); - String landingPage = startWithSlash(form.landingPage.orElse(null)); - String postLocation = startWithSlash(form.postLocation); - String usernameParameter = form.usernameParameter; - String passwordParameter = form.passwordParameter; - String locationCookie = form.locationCookie; - String cookiePath = form.cookiePath.orElse(null); - boolean redirectAfterLogin = form.redirectAfterLogin; - return new FormAuthenticationMechanism(loginPage, postLocation, usernameParameter, passwordParameter, - errorPage, landingPage, redirectAfterLogin, locationCookie, form.cookieSameSite.name(), cookiePath, - loginManager); - } - }; - } - - private static String startWithSlash(String page) { - if (page == null) { - return null; - } - return page.startsWith("/") ? page : "/" + page; - } - - public Supplier setupBasicAuth(HttpBuildTimeConfig buildTimeConfig) { - return new Supplier() { - @Override - public BasicAuthenticationMechanism get() { - return new BasicAuthenticationMechanism(buildTimeConfig.auth.realm.orElse(null), - buildTimeConfig.auth.form.enabled); - } - }; - } - - public Supplier setupMtlsClientAuth() { - return new Supplier() { - @Override - public MtlsAuthenticationMechanism get() { - return new MtlsAuthenticationMechanism(); - } - }; - } - /** * This handler resolves the identity, and will be mapped to the post location. Otherwise, * for lazy auth the post will not be evaluated if there is no security rule for the post location. @@ -194,36 +83,6 @@ public void onFailure(Throwable throwable) { }; } - public BiFunction stringPermissionCreator() { - return StringPermission::new; - } - - public BiFunction customPermissionCreator(String clazz, boolean acceptsActions) { - return new BiFunction() { - @Override - public Permission apply(String name, String[] actions) { - try { - if (acceptsActions) { - return (Permission) loadClass(clazz).getConstructors()[0].newInstance(name, actions); - } else { - return (Permission) loadClass(clazz).getConstructors()[0].newInstance(name); - } - } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { - throw new RuntimeException( - String.format("Failed to create Permission - class '%s', name '%s', actions '%s'", clazz, - name, Arrays.toString(actions)), - e); - } - } - }; - } - - public Supplier createRolesAllowedPolicy(List rolesAllowed, - Map> roleToPermissionsStr, BiFunction permissionCreator) { - final Map> roleToPermissions = createPermissions(roleToPermissionsStr, permissionCreator); - return new SupplierImpl<>(new RolesAllowedHttpSecurityPolicy(rolesAllowed, roleToPermissions)); - } - public Supplier createSecurityInterceptorStorage( Map, Consumer> endpointRuntimeValToInterceptor) { @@ -240,63 +99,22 @@ public EagerSecurityInterceptorStorage get() { }; } - private static Map> createPermissions(Map> roleToPermissions, - BiFunction permissionCreator) { - // role -> created permissions - Map> result = new HashMap<>(); - for (Map.Entry> e : roleToPermissions.entrySet()) { - - // collect permission actions - // perm1:action1,perm2:action2,perm1:action3 -> perm1:action1,action3 and perm2:action2 - Map cache = new HashMap<>(); - final String role = e.getKey(); - for (String permissionToAction : e.getValue()) { - // parse permission to actions and add it to cache - addPermissionToAction(cache, role, permissionToAction); - } - - // create permissions - var permissions = new HashSet(); - for (PermissionToActions permission : cache.values()) { - permissions.add(permission.create(permissionCreator)); - } + public RuntimeValue createNamedHttpSecurityPolicy(Supplier policySupplier, + String name) { + return new RuntimeValue<>(new HttpSecurityPolicy() { + private final HttpSecurityPolicy delegate = policySupplier.get(); - result.put(role, Set.copyOf(permissions)); - } - return Map.copyOf(result); - } - - private static void addPermissionToAction(Map cache, String role, String permissionToAction) { - final String permissionName; - final String action; - // incoming value is either in format perm1:action1 or perm1 (with or withot action) - if (permissionToAction.contains(PERMISSION_TO_ACTION_SEPARATOR)) { - // perm1:action1 - var permToActions = permissionToAction.split(PERMISSION_TO_ACTION_SEPARATOR); - if (permToActions.length != 2) { - throw new ConfigurationException( - String.format("Invalid permission format '%s', please use exactly one permission to action separator", - permissionToAction)); + @Override + public Uni checkPermission(RoutingContext request, Uni identity, + AuthorizationRequestContext requestContext) { + return delegate.checkPermission(request, identity, requestContext); } - permissionName = permToActions[0].trim(); - action = permToActions[1].trim(); - } else { - // perm1 - permissionName = permissionToAction.trim(); - action = null; - } - - if (permissionName.isEmpty()) { - throw new ConfigurationException( - String.format("Invalid permission name '%s' for role '%s'", permissionToAction, role)); - } - cache.computeIfAbsent(permissionName, new Function() { @Override - public PermissionToActions apply(String s) { - return new PermissionToActions(s); + public String name() { + return name; } - }).addAction(action); + }); } public static abstract class DefaultAuthFailureHandler implements BiConsumer { @@ -380,10 +198,16 @@ protected void setPathMatchingPolicy(RoutingContext event) { event.put(AbstractPathMatchingHttpSecurityPolicy.class.getName(), pathMatchingPolicy); } } + + @Override + protected boolean httpPermissionsEmpty() { + return CDI.current().select(HttpConfiguration.class).get().auth.permissions.isEmpty(); + } } public static abstract class AbstractAuthenticationHandler implements Handler { volatile HttpAuthenticator authenticator; + volatile Boolean patchMatchingPolicyEnabled = null; final boolean proactiveAuthentication; public AbstractAuthenticationHandler(boolean proactiveAuthentication) { @@ -397,7 +221,12 @@ public void handle(RoutingContext event) { } //we put the authenticator into the routing context so it can be used by other systems event.put(HttpAuthenticator.class.getName(), authenticator); - setPathMatchingPolicy(event); + if (patchMatchingPolicyEnabled == null) { + setPatchMatchingPolicyEnabled(); + } + if (patchMatchingPolicyEnabled) { + setPathMatchingPolicy(event); + } //register the default auth failure handler if (proactiveAuthentication) { @@ -523,34 +352,14 @@ public void accept(SecurityIdentity identity, Throwable throwable, Boolean aBool } } - protected abstract void setPathMatchingPolicy(RoutingContext event); - } - - private static final class PermissionToActions { - private final String permissionName; - private final Set actions; - - private PermissionToActions(String permissionName) { - this.permissionName = permissionName; - this.actions = new HashSet<>(); - } - - private void addAction(String action) { - if (action != null) { - this.actions.add(action); + private synchronized void setPatchMatchingPolicyEnabled() { + if (patchMatchingPolicyEnabled == null) { + patchMatchingPolicyEnabled = !httpPermissionsEmpty(); } } - private Permission create(BiFunction permissionCreator) { - return permissionCreator.apply(permissionName, actions.toArray(new String[0])); - } - } + protected abstract void setPathMatchingPolicy(RoutingContext event); - private static Class loadClass(String className) { - try { - return Thread.currentThread().getContextClassLoader().loadClass(className); - } catch (ClassNotFoundException e) { - throw new RuntimeException("Unable to load class '" + className + "' for creating permission", e); - } + protected abstract boolean httpPermissionsEmpty(); } } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/ManagementPathMatchingHttpSecurityPolicy.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/ManagementPathMatchingHttpSecurityPolicy.java index fc0258841adf17..037c3ceed32bb7 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/ManagementPathMatchingHttpSecurityPolicy.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/ManagementPathMatchingHttpSecurityPolicy.java @@ -1,12 +1,24 @@ package io.quarkus.vertx.http.runtime.security; +import jakarta.enterprise.inject.Instance; import jakarta.inject.Singleton; +import io.quarkus.runtime.Startup; +import io.quarkus.vertx.http.runtime.management.ManagementInterfaceBuildTimeConfig; +import io.quarkus.vertx.http.runtime.management.ManagementInterfaceConfiguration; + /** * A security policy that allows for matching of other security policies based on paths. * * This is used for the default path/method based RBAC. */ +@Startup // do not initialize path matcher during first HTTP request @Singleton public class ManagementPathMatchingHttpSecurityPolicy extends AbstractPathMatchingHttpSecurityPolicy { + + ManagementPathMatchingHttpSecurityPolicy(ManagementInterfaceBuildTimeConfig buildTimeConfig, + ManagementInterfaceConfiguration runTimeConfig, Instance installedPolicies) { + super(runTimeConfig.auth.permissions, runTimeConfig.auth.rolePolicy, buildTimeConfig.rootPath, installedPolicies); + } + } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/PathMatchingHttpSecurityPolicy.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/PathMatchingHttpSecurityPolicy.java index 2132ab9532dd35..b258e4fa0be0ee 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/PathMatchingHttpSecurityPolicy.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/PathMatchingHttpSecurityPolicy.java @@ -1,13 +1,24 @@ package io.quarkus.vertx.http.runtime.security; +import jakarta.enterprise.inject.Instance; import jakarta.inject.Singleton; +import io.quarkus.runtime.Startup; +import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig; +import io.quarkus.vertx.http.runtime.HttpConfiguration; + /** * A security policy that allows for matching of other security policies based on paths. * * This is used for the default path/method based RBAC. */ +@Startup // do not initialize path matcher during first HTTP request @Singleton -public class PathMatchingHttpSecurityPolicy extends AbstractPathMatchingHttpSecurityPolicy - implements HttpSecurityPolicy { +public class PathMatchingHttpSecurityPolicy extends AbstractPathMatchingHttpSecurityPolicy implements HttpSecurityPolicy { + + PathMatchingHttpSecurityPolicy(HttpConfiguration httpConfig, HttpBuildTimeConfig buildTimeConfig, + Instance installedPolicies) { + super(httpConfig.auth.permissions, httpConfig.auth.rolePolicy, buildTimeConfig.rootPath, installedPolicies); + } + } diff --git a/independent-projects/arc/pom.xml b/independent-projects/arc/pom.xml index 6ac1bfd41d8c68..465857a577aa50 100644 --- a/independent-projects/arc/pom.xml +++ b/independent-projects/arc/pom.xml @@ -50,7 +50,7 @@ 1.7.0 3.1.5 3.5.3.Final - 2.2.0 + 2.5.1 1.6.Final 3.24.2 diff --git a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/app/QuarkusBootstrap.java b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/app/QuarkusBootstrap.java index ea8d53f0c959a0..70ba824e22dce7 100644 --- a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/app/QuarkusBootstrap.java +++ b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/app/QuarkusBootstrap.java @@ -39,7 +39,7 @@ public class QuarkusBootstrap implements Serializable { private final PathCollection applicationRoot; /** - * The root of the project. This may be different to the application root for tests that + * The root of the project. This may be different from the application root for tests that * run in a different directory. */ private final Path projectRoot; diff --git a/independent-projects/bootstrap/pom.xml b/independent-projects/bootstrap/pom.xml index 9bee854eb5e9ed..0db20683a71fe6 100644 --- a/independent-projects/bootstrap/pom.xml +++ b/independent-projects/bootstrap/pom.xml @@ -62,7 +62,7 @@ 2.0.1 1.16.0 2.15.0 - 3.12.0 + 3.13.0 32.1.3-jre 1.0.1 2.8 diff --git a/independent-projects/extension-maven-plugin/pom.xml b/independent-projects/extension-maven-plugin/pom.xml index 8e928fdce58dc9..165bdf86493a83 100644 --- a/independent-projects/extension-maven-plugin/pom.xml +++ b/independent-projects/extension-maven-plugin/pom.xml @@ -42,7 +42,7 @@ 3.2.1 3.1.2 3.8.1 - 2.15.2 + 2.15.3 1.3.2 5.10.0 diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/ParameterDeclaration.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/ParameterDeclaration.java index 6a6da4bce112b5..8373f9f3cd1d8b 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/ParameterDeclaration.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/ParameterDeclaration.java @@ -4,7 +4,7 @@ import io.quarkus.qute.TemplateNode.Origin; /** - * Represents a parameter declaration, i.e. {@org.acme.Foo foo}. + * Represents a type parameter declaration, i.e. {@org.acme.Foo foo}. */ public interface ParameterDeclaration { diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/Parser.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/Parser.java index dad999d56c2112..b6eba251f7d9bc 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/Parser.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/Parser.java @@ -126,9 +126,19 @@ private Template currentTemplate() { Template parse() { - sectionStack.addFirst(SectionNode.builder(ROOT_HELPER_NAME, origin(0), this, this) + SectionNode.Builder rootBuilder = SectionNode.builder(ROOT_HELPER_NAME, syntheticOrigin(), this, this) .setEngine(engine) - .setHelperFactory(ROOT_SECTION_HELPER_FACTORY)); + .setHelperFactory(ROOT_SECTION_HELPER_FACTORY); + sectionStack.addFirst(rootBuilder); + + // Add synthetic nodes for param declarations added by parser hooks + Map bindings = scopeStack.peek().getBindings(); + if (bindings != null && !bindings.isEmpty()) { + for (Entry e : bindings.entrySet()) { + rootBuilder.currentBlock().addNode( + new ParameterDeclarationNode(e.getValue(), e.getKey(), null, syntheticOrigin())); + } + } long start = System.nanoTime(); Reader r = reader; @@ -1066,6 +1076,10 @@ Origin origin(int lineCharacterOffset) { return new OriginImpl(line, lineCharacter - lineCharacterOffset, lineCharacter, templateId, generatedId, variant); } + Origin syntheticOrigin() { + return new OriginImpl(-1, -1, -1, templateId, generatedId, variant); + } + private List> readLines(SectionNode rootNode) { List> lines = new ArrayList<>(); // Add the last line manually - there is no line separator to trigger flush diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/ParserHelper.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/ParserHelper.java index 9d25886f270b27..d44fb05de3c85d 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/ParserHelper.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/ParserHelper.java @@ -16,8 +16,7 @@ public interface ParserHelper { /** * Adds an implicit parameter declaration. This is an alternative approach to explicit parameter - * declarations - * used directly in the templates, e.g. {@org.acme.Foo foo}. + * declarations used directly in the templates, e.g. {@org.acme.Foo foo}. * * @param name * @param type diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/Scope.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/Scope.java index b60e10c5691d65..3760d45e2bac7e 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/Scope.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/Scope.java @@ -38,6 +38,10 @@ public String getBinding(String binding) { return parentScope != null ? parentScope.getBinding(binding) : null; } + Map getBindings() { + return bindings; + } + public String getBindingTypeOrDefault(String binding, String defaultValue) { String type = getBinding(binding); return type != null ? type : defaultValue; diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/Template.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/Template.java index 3f9008fa2f998b..615d72b3da5308 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/Template.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/Template.java @@ -155,9 +155,12 @@ default String render() { Optional getVariant(); /** + * Returns all type parameter declarations of the template, including the declarations added by a + * {@link io.quarkus.qute.ParserHook}. + *

* If invoked upon a fragment instance then delegate to the defining template. * - * @return an immutable list of all parameter declarations defined in the template + * @return an immutable list of all type parameter declarations defined in the template */ List getParameterDeclarations(); diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateNode.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateNode.java index af05f6c74d283b..23096cb61698cf 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateNode.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateNode.java @@ -107,6 +107,13 @@ default void appendTo(StringBuilder builder) { } } + /** + * @return {@code true} if the template node was not part of the original template, {@code false} otherwise + */ + default boolean isSynthetic() { + return getLine() == -1; + } + } } diff --git a/independent-projects/qute/core/src/test/java/io/quarkus/qute/ParserTest.java b/independent-projects/qute/core/src/test/java/io/quarkus/qute/ParserTest.java index 350b092e20513e..6337cadc49d36f 100644 --- a/independent-projects/qute/core/src/test/java/io/quarkus/qute/ParserTest.java +++ b/independent-projects/qute/core/src/test/java/io/quarkus/qute/ParserTest.java @@ -3,6 +3,7 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType; 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.assertTrue; import static org.junit.jupiter.api.Assertions.fail; @@ -165,6 +166,10 @@ public void testLines() { assertEquals(8, itemNameOrigin.getLine()); assertEquals(1, itemNameOrigin.getLineCharacterStart()); assertEquals(11, itemNameOrigin.getLineCharacterEnd()); + ParameterDeclaration pd = template.getParameterDeclarations().get(0); + assertEquals(1, pd.getOrigin().getLine()); + assertEquals("foo", pd.getKey()); + assertNull(pd.getDefaultValue()); } @Test @@ -316,9 +321,16 @@ public void testParserHook() { public void beforeParsing(ParserHelper parserHelper) { parserHelper.addContentFilter(contents -> contents.replace("bard", "bar")); parserHelper.addContentFilter(contents -> contents.replace("${", "$\\{")); + parserHelper.addParameter("foo", String.class.getName()); } }).build().parse("${foo}::{bard}"); assertEquals("${foo}::true", template.data("bar", true).render()); + List paramDeclarations = template.getParameterDeclarations(); + assertEquals(1, paramDeclarations.size()); + ParameterDeclaration pd = paramDeclarations.get(0); + assertEquals("foo", pd.getKey()); + assertEquals("|java.lang.String|", pd.getTypeInfo()); + assertTrue(pd.getOrigin().isSynthetic()); } @Test diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/SseParser.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/SseParser.java index 08525139dc0e98..46bb82858514ab 100644 --- a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/SseParser.java +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/SseParser.java @@ -158,7 +158,7 @@ private void dispatchEvent() { event.setComment(commentBuffer.length() == 0 ? null : commentBuffer.toString()); // SSE spec says empty string is the default, but JAX-RS says null if not specified event.setId(lastEventId); - event.setData(dataBuffer.length() == 0 ? null : dataBuffer.toString()); + event.setData(dataBuffer.length() == 0 ? "" : dataBuffer.toString()); // SSE spec says "message" is the default, but JAX-RS says null if not specified event.setName(eventType); event.setReconnectDelay(eventReconnectTime); diff --git a/independent-projects/resteasy-reactive/pom.xml b/independent-projects/resteasy-reactive/pom.xml index 1954af34fec2cd..82ecd9308c3376 100644 --- a/independent-projects/resteasy-reactive/pom.xml +++ b/independent-projects/resteasy-reactive/pom.xml @@ -41,7 +41,7 @@ 2.1.2 3.1.0 4.0.1 - 1.1.4 + 1.1.5 UTF-8 11 11 @@ -66,7 +66,7 @@ 4.4.6 5.3.2 1.0.0.Final - 2.15.2 + 2.15.3 2.4.0 3.0.2 3.0.3 diff --git a/independent-projects/resteasy-reactive/server/jackson/src/main/java/org/jboss/resteasy/reactive/server/jackson/JacksonMessageBodyWriterUtil.java b/independent-projects/resteasy-reactive/server/jackson/src/main/java/org/jboss/resteasy/reactive/server/jackson/JacksonMessageBodyWriterUtil.java index d5e9f631a2b17a..3b004d2236ae93 100644 --- a/independent-projects/resteasy-reactive/server/jackson/src/main/java/org/jboss/resteasy/reactive/server/jackson/JacksonMessageBodyWriterUtil.java +++ b/independent-projects/resteasy-reactive/server/jackson/src/main/java/org/jboss/resteasy/reactive/server/jackson/JacksonMessageBodyWriterUtil.java @@ -28,6 +28,7 @@ public static ObjectWriter createDefaultWriter(ObjectMapper mapper) { if (JacksonMessageBodyWriterUtil.needsNewFactory(jsonFactory)) { jsonFactory = jsonFactory.copy(); JacksonMessageBodyWriterUtil.setNecessaryJsonFactoryConfig(jsonFactory); + jsonFactory.setCodec(mapper); return mapper.writer().with(jsonFactory); } else { return mapper.writer(); diff --git a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/buildtool/gradle-kotlin-dsl/base/build-layout.include.qute b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/buildtool/gradle-kotlin-dsl/base/build-layout.include.qute index 3b643510d09a4d..4b0d7655a7a459 100644 --- a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/buildtool/gradle-kotlin-dsl/base/build-layout.include.qute +++ b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/buildtool/gradle-kotlin-dsl/base/build-layout.include.qute @@ -50,13 +50,8 @@ version = "{project.version}" {#insert java} java { - {#if java.version == "17"} - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 - {#else} - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 - {/if} + sourceCompatibility = JavaVersion.VERSION_{java.version} + targetCompatibility = JavaVersion.VERSION_{java.version} } {/} diff --git a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/buildtool/gradle-kotlin-dsl/scala/build.tpl.qute.gradle.kts b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/buildtool/gradle-kotlin-dsl/scala/build.tpl.qute.gradle.kts index a6c4baa92a1a30..035088350b60fe 100644 --- a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/buildtool/gradle-kotlin-dsl/scala/build.tpl.qute.gradle.kts +++ b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/buildtool/gradle-kotlin-dsl/scala/build.tpl.qute.gradle.kts @@ -9,11 +9,6 @@ plugins { tasks.withType { scalaCompileOptions.encoding = "UTF-8" - {#if java.version == "11"} - sourceCompatibility = JavaVersion.VERSION_11.toString() - targetCompatibility = JavaVersion.VERSION_11.toString() - {#else} - sourceCompatibility = JavaVersion.VERSION_1_8.toString() - targetCompatibility = JavaVersion.VERSION_1_8.toString() - {/if} + sourceCompatibility = JavaVersion.VERSION_{java.version}.toString() + targetCompatibility = JavaVersion.VERSION_{java.version}.toString() } diff --git a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/buildtool/gradle/base/build-layout.include.qute b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/buildtool/gradle/base/build-layout.include.qute index 53c267137122b8..dd292c9d2fbd69 100644 --- a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/buildtool/gradle/base/build-layout.include.qute +++ b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/buildtool/gradle/base/build-layout.include.qute @@ -46,13 +46,8 @@ version '{project.version}' {#insert java} java { - {#if java.version == "17"} - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 - {#else} - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 - {/if} + sourceCompatibility = JavaVersion.VERSION_{java.version} + targetCompatibility = JavaVersion.VERSION_{java.version} } {/} diff --git a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/buildtool/gradle/scala/build.tpl.qute.gradle b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/buildtool/gradle/scala/build.tpl.qute.gradle index 6119eea5168af0..835921cebee0b2 100644 --- a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/buildtool/gradle/scala/build.tpl.qute.gradle +++ b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/buildtool/gradle/scala/build.tpl.qute.gradle @@ -9,11 +9,6 @@ plugins { compileScala { scalaCompileOptions.encoding = 'UTF-8' - {#if java.version == "11"} - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 - {#else} - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 - {/if} + sourceCompatibility = JavaVersion.VERSION_{java.version} + targetCompatibility = JavaVersion.VERSION_{java.version} } diff --git a/independent-projects/tools/pom.xml b/independent-projects/tools/pom.xml index 624be6575399f4..48814923bfe7a6 100644 --- a/independent-projects/tools/pom.xml +++ b/independent-projects/tools/pom.xml @@ -52,7 +52,7 @@ 3.24.2 - 2.15.2 + 2.15.3 4.0.1 5.10.0 1.24.0 diff --git a/integration-tests/devmode/src/test/java/io/quarkus/test/devui/DevUIReactiveMessagingJsonRPCTest.java b/integration-tests/devmode/src/test/java/io/quarkus/test/devui/DevUIReactiveMessagingJsonRPCTest.java index 14a743cdac5564..c96ccd7b1ffc7b 100644 --- a/integration-tests/devmode/src/test/java/io/quarkus/test/devui/DevUIReactiveMessagingJsonRPCTest.java +++ b/integration-tests/devmode/src/test/java/io/quarkus/test/devui/DevUIReactiveMessagingJsonRPCTest.java @@ -43,9 +43,9 @@ public void testProcessor() throws Exception { consumerExists = typeAndDescriptionExist(consumers, "CHANNEL", "io.quarkus.test.devui.MyProcessor#channel"); } - JsonNode publisher = channel.get("publisher"); - if (publisher != null) { - publisherExists = typeAndDescriptionExist(publisher, "PROCESSOR", + JsonNode publishers = channel.get("publishers"); + if (publishers != null) { + publisherExists = typeAndDescriptionExist(publishers, "PROCESSOR", "io.quarkus.test.devui.MyProcessor#process()"); } } diff --git a/integration-tests/gradle/src/main/resources/grpc-descriptor-set-alternate-output-dir/build.gradle b/integration-tests/gradle/src/main/resources/grpc-descriptor-set-alternate-output-dir/build.gradle new file mode 100644 index 00000000000000..57535418f2507b --- /dev/null +++ b/integration-tests/gradle/src/main/resources/grpc-descriptor-set-alternate-output-dir/build.gradle @@ -0,0 +1,33 @@ +plugins { + id 'java' + id 'io.quarkus' +} + +repositories { + mavenLocal { + content { + includeGroupByRegex 'io.quarkus.*' + includeGroup 'org.hibernate.orm' + } + } + mavenCentral() + gradlePluginPortal() +} + +dependencies { + implementation enforcedPlatform("${quarkusPlatformGroupId}:${quarkusPlatformArtifactId}:${quarkusPlatformVersion}") + implementation 'io.quarkus:quarkus-resteasy' + implementation 'io.quarkus:quarkus-grpc' +} + +group 'org.acme' +version '1.0.0-SNAPSHOT' + +java { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 +} + +test { + systemProperty "java.util.logging.manager", "org.jboss.logmanager.LogManager" +} \ No newline at end of file diff --git a/integration-tests/gradle/src/main/resources/grpc-descriptor-set-alternate-output-dir/gradle.properties b/integration-tests/gradle/src/main/resources/grpc-descriptor-set-alternate-output-dir/gradle.properties new file mode 100644 index 00000000000000..ec2b6ef199c2ca --- /dev/null +++ b/integration-tests/gradle/src/main/resources/grpc-descriptor-set-alternate-output-dir/gradle.properties @@ -0,0 +1,2 @@ +quarkusPlatformArtifactId=quarkus-bom +quarkusPlatformGroupId=io.quarkus diff --git a/integration-tests/gradle/src/main/resources/grpc-descriptor-set-alternate-output-dir/settings.gradle b/integration-tests/gradle/src/main/resources/grpc-descriptor-set-alternate-output-dir/settings.gradle new file mode 100644 index 00000000000000..662c07020134c5 --- /dev/null +++ b/integration-tests/gradle/src/main/resources/grpc-descriptor-set-alternate-output-dir/settings.gradle @@ -0,0 +1,16 @@ +pluginManagement { + repositories { + mavenLocal { + content { + includeGroupByRegex 'io.quarkus.*' + includeGroup 'org.hibernate.orm' + } + } + mavenCentral() + gradlePluginPortal() + } + plugins { + id 'io.quarkus' version "${quarkusPluginVersion}" + } +} +rootProject.name = 'grpc-descriptor-set-alternate-output-dir' \ No newline at end of file diff --git a/integration-tests/gradle/src/main/resources/grpc-descriptor-set-alternate-output-dir/src/main/java/org/acme/quarkus/sample/HelloResource.java b/integration-tests/gradle/src/main/resources/grpc-descriptor-set-alternate-output-dir/src/main/java/org/acme/quarkus/sample/HelloResource.java new file mode 100644 index 00000000000000..e5c864ff6be0e9 --- /dev/null +++ b/integration-tests/gradle/src/main/resources/grpc-descriptor-set-alternate-output-dir/src/main/java/org/acme/quarkus/sample/HelloResource.java @@ -0,0 +1,21 @@ +package org.acme.quarkus.sample; + +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +import io.quarkus.example.HelloMsg; + +@Path("/hello") +public class HelloResource { + + @GET + @Produces(MediaType.TEXT_PLAIN) + public String hello() { + Integer number = HelloMsg.Status.TEST_ONE.getNumber(); + // return a thing from proto file (for devmode test) + return "hello " + number; + } +} diff --git a/integration-tests/gradle/src/main/resources/grpc-descriptor-set-alternate-output-dir/src/main/proto/hello.proto b/integration-tests/gradle/src/main/resources/grpc-descriptor-set-alternate-output-dir/src/main/proto/hello.proto new file mode 100644 index 00000000000000..4ebcaf36db77ab --- /dev/null +++ b/integration-tests/gradle/src/main/resources/grpc-descriptor-set-alternate-output-dir/src/main/proto/hello.proto @@ -0,0 +1,23 @@ +syntax = "proto3"; +package io.quarkus.example; + +option java_multiple_files = true; +option java_package = "io.quarkus.example"; + +import "google/protobuf/timestamp.proto"; + + +message HelloMsg { + enum Status { + UNKNOWN = 0; + NOT_SERVING = 1; + TEST_ONE = 2; + } + string message = 1; + google.protobuf.Timestamp date_time = 2; + Status status = 3; +} + +service DevModeService { + rpc Check(HelloMsg) returns (HelloMsg); +} \ No newline at end of file diff --git a/integration-tests/gradle/src/main/resources/grpc-descriptor-set-alternate-output-dir/src/main/resources/application.properties b/integration-tests/gradle/src/main/resources/grpc-descriptor-set-alternate-output-dir/src/main/resources/application.properties new file mode 100644 index 00000000000000..f4576b12d2123d --- /dev/null +++ b/integration-tests/gradle/src/main/resources/grpc-descriptor-set-alternate-output-dir/src/main/resources/application.properties @@ -0,0 +1,3 @@ +quarkus.generate-code.grpc.descriptor-set.generate=true +quarkus.generate-code.grpc.descriptor-set.name=hello.dsc +quarkus.generate-code.grpc.descriptor-set.output-dir=proto \ No newline at end of file diff --git a/integration-tests/gradle/src/main/resources/grpc-descriptor-set-alternate-output/build.gradle b/integration-tests/gradle/src/main/resources/grpc-descriptor-set-alternate-output/build.gradle new file mode 100644 index 00000000000000..57535418f2507b --- /dev/null +++ b/integration-tests/gradle/src/main/resources/grpc-descriptor-set-alternate-output/build.gradle @@ -0,0 +1,33 @@ +plugins { + id 'java' + id 'io.quarkus' +} + +repositories { + mavenLocal { + content { + includeGroupByRegex 'io.quarkus.*' + includeGroup 'org.hibernate.orm' + } + } + mavenCentral() + gradlePluginPortal() +} + +dependencies { + implementation enforcedPlatform("${quarkusPlatformGroupId}:${quarkusPlatformArtifactId}:${quarkusPlatformVersion}") + implementation 'io.quarkus:quarkus-resteasy' + implementation 'io.quarkus:quarkus-grpc' +} + +group 'org.acme' +version '1.0.0-SNAPSHOT' + +java { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 +} + +test { + systemProperty "java.util.logging.manager", "org.jboss.logmanager.LogManager" +} \ No newline at end of file diff --git a/integration-tests/gradle/src/main/resources/grpc-descriptor-set-alternate-output/gradle.properties b/integration-tests/gradle/src/main/resources/grpc-descriptor-set-alternate-output/gradle.properties new file mode 100644 index 00000000000000..ec2b6ef199c2ca --- /dev/null +++ b/integration-tests/gradle/src/main/resources/grpc-descriptor-set-alternate-output/gradle.properties @@ -0,0 +1,2 @@ +quarkusPlatformArtifactId=quarkus-bom +quarkusPlatformGroupId=io.quarkus diff --git a/integration-tests/gradle/src/main/resources/grpc-descriptor-set-alternate-output/settings.gradle b/integration-tests/gradle/src/main/resources/grpc-descriptor-set-alternate-output/settings.gradle new file mode 100644 index 00000000000000..687590ad969809 --- /dev/null +++ b/integration-tests/gradle/src/main/resources/grpc-descriptor-set-alternate-output/settings.gradle @@ -0,0 +1,16 @@ +pluginManagement { + repositories { + mavenLocal { + content { + includeGroupByRegex 'io.quarkus.*' + includeGroup 'org.hibernate.orm' + } + } + mavenCentral() + gradlePluginPortal() + } + plugins { + id 'io.quarkus' version "${quarkusPluginVersion}" + } +} +rootProject.name = 'grpc-descriptor-set-alternate-output' \ No newline at end of file diff --git a/integration-tests/gradle/src/main/resources/grpc-descriptor-set-alternate-output/src/main/java/org/acme/quarkus/sample/HelloResource.java b/integration-tests/gradle/src/main/resources/grpc-descriptor-set-alternate-output/src/main/java/org/acme/quarkus/sample/HelloResource.java new file mode 100644 index 00000000000000..e5c864ff6be0e9 --- /dev/null +++ b/integration-tests/gradle/src/main/resources/grpc-descriptor-set-alternate-output/src/main/java/org/acme/quarkus/sample/HelloResource.java @@ -0,0 +1,21 @@ +package org.acme.quarkus.sample; + +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +import io.quarkus.example.HelloMsg; + +@Path("/hello") +public class HelloResource { + + @GET + @Produces(MediaType.TEXT_PLAIN) + public String hello() { + Integer number = HelloMsg.Status.TEST_ONE.getNumber(); + // return a thing from proto file (for devmode test) + return "hello " + number; + } +} diff --git a/integration-tests/gradle/src/main/resources/grpc-descriptor-set-alternate-output/src/main/proto/hello.proto b/integration-tests/gradle/src/main/resources/grpc-descriptor-set-alternate-output/src/main/proto/hello.proto new file mode 100644 index 00000000000000..4ebcaf36db77ab --- /dev/null +++ b/integration-tests/gradle/src/main/resources/grpc-descriptor-set-alternate-output/src/main/proto/hello.proto @@ -0,0 +1,23 @@ +syntax = "proto3"; +package io.quarkus.example; + +option java_multiple_files = true; +option java_package = "io.quarkus.example"; + +import "google/protobuf/timestamp.proto"; + + +message HelloMsg { + enum Status { + UNKNOWN = 0; + NOT_SERVING = 1; + TEST_ONE = 2; + } + string message = 1; + google.protobuf.Timestamp date_time = 2; + Status status = 3; +} + +service DevModeService { + rpc Check(HelloMsg) returns (HelloMsg); +} \ No newline at end of file diff --git a/integration-tests/gradle/src/main/resources/grpc-descriptor-set-alternate-output/src/main/resources/application.properties b/integration-tests/gradle/src/main/resources/grpc-descriptor-set-alternate-output/src/main/resources/application.properties new file mode 100644 index 00000000000000..8181681cb49514 --- /dev/null +++ b/integration-tests/gradle/src/main/resources/grpc-descriptor-set-alternate-output/src/main/resources/application.properties @@ -0,0 +1,2 @@ +quarkus.generate-code.grpc.descriptor-set.generate=true +quarkus.generate-code.grpc.descriptor-set.name=hello.dsc \ No newline at end of file diff --git a/integration-tests/gradle/src/main/resources/grpc-descriptor-set/build.gradle b/integration-tests/gradle/src/main/resources/grpc-descriptor-set/build.gradle new file mode 100644 index 00000000000000..57535418f2507b --- /dev/null +++ b/integration-tests/gradle/src/main/resources/grpc-descriptor-set/build.gradle @@ -0,0 +1,33 @@ +plugins { + id 'java' + id 'io.quarkus' +} + +repositories { + mavenLocal { + content { + includeGroupByRegex 'io.quarkus.*' + includeGroup 'org.hibernate.orm' + } + } + mavenCentral() + gradlePluginPortal() +} + +dependencies { + implementation enforcedPlatform("${quarkusPlatformGroupId}:${quarkusPlatformArtifactId}:${quarkusPlatformVersion}") + implementation 'io.quarkus:quarkus-resteasy' + implementation 'io.quarkus:quarkus-grpc' +} + +group 'org.acme' +version '1.0.0-SNAPSHOT' + +java { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 +} + +test { + systemProperty "java.util.logging.manager", "org.jboss.logmanager.LogManager" +} \ No newline at end of file diff --git a/integration-tests/gradle/src/main/resources/grpc-descriptor-set/gradle.properties b/integration-tests/gradle/src/main/resources/grpc-descriptor-set/gradle.properties new file mode 100644 index 00000000000000..ec2b6ef199c2ca --- /dev/null +++ b/integration-tests/gradle/src/main/resources/grpc-descriptor-set/gradle.properties @@ -0,0 +1,2 @@ +quarkusPlatformArtifactId=quarkus-bom +quarkusPlatformGroupId=io.quarkus diff --git a/integration-tests/gradle/src/main/resources/grpc-descriptor-set/settings.gradle b/integration-tests/gradle/src/main/resources/grpc-descriptor-set/settings.gradle new file mode 100644 index 00000000000000..46dc110045b06e --- /dev/null +++ b/integration-tests/gradle/src/main/resources/grpc-descriptor-set/settings.gradle @@ -0,0 +1,16 @@ +pluginManagement { + repositories { + mavenLocal { + content { + includeGroupByRegex 'io.quarkus.*' + includeGroup 'org.hibernate.orm' + } + } + mavenCentral() + gradlePluginPortal() + } + plugins { + id 'io.quarkus' version "${quarkusPluginVersion}" + } +} +rootProject.name = 'grpc-descriptor-set' \ No newline at end of file diff --git a/integration-tests/gradle/src/main/resources/grpc-descriptor-set/src/main/java/org/acme/quarkus/sample/HelloResource.java b/integration-tests/gradle/src/main/resources/grpc-descriptor-set/src/main/java/org/acme/quarkus/sample/HelloResource.java new file mode 100644 index 00000000000000..e5c864ff6be0e9 --- /dev/null +++ b/integration-tests/gradle/src/main/resources/grpc-descriptor-set/src/main/java/org/acme/quarkus/sample/HelloResource.java @@ -0,0 +1,21 @@ +package org.acme.quarkus.sample; + +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +import io.quarkus.example.HelloMsg; + +@Path("/hello") +public class HelloResource { + + @GET + @Produces(MediaType.TEXT_PLAIN) + public String hello() { + Integer number = HelloMsg.Status.TEST_ONE.getNumber(); + // return a thing from proto file (for devmode test) + return "hello " + number; + } +} diff --git a/integration-tests/gradle/src/main/resources/grpc-descriptor-set/src/main/proto/hello.proto b/integration-tests/gradle/src/main/resources/grpc-descriptor-set/src/main/proto/hello.proto new file mode 100644 index 00000000000000..4ebcaf36db77ab --- /dev/null +++ b/integration-tests/gradle/src/main/resources/grpc-descriptor-set/src/main/proto/hello.proto @@ -0,0 +1,23 @@ +syntax = "proto3"; +package io.quarkus.example; + +option java_multiple_files = true; +option java_package = "io.quarkus.example"; + +import "google/protobuf/timestamp.proto"; + + +message HelloMsg { + enum Status { + UNKNOWN = 0; + NOT_SERVING = 1; + TEST_ONE = 2; + } + string message = 1; + google.protobuf.Timestamp date_time = 2; + Status status = 3; +} + +service DevModeService { + rpc Check(HelloMsg) returns (HelloMsg); +} \ No newline at end of file diff --git a/integration-tests/gradle/src/main/resources/grpc-descriptor-set/src/main/resources/application.properties b/integration-tests/gradle/src/main/resources/grpc-descriptor-set/src/main/resources/application.properties new file mode 100644 index 00000000000000..b39cd85e72737a --- /dev/null +++ b/integration-tests/gradle/src/main/resources/grpc-descriptor-set/src/main/resources/application.properties @@ -0,0 +1 @@ +quarkus.generate-code.grpc.descriptor-set.generate=true \ No newline at end of file diff --git a/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/application/build.gradle b/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/application/build.gradle new file mode 100644 index 00000000000000..556e19daf29830 --- /dev/null +++ b/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/application/build.gradle @@ -0,0 +1,32 @@ +plugins{ + id "java" + id "io.quarkus" +} + + + +group 'io.quarkus.test.application' +version '1.0-SNAPSHOT' + + +repositories { + mavenLocal() + mavenCentral() +} + +dependencies { + implementation enforcedPlatform("${quarkusPlatformGroupId}:${quarkusPlatformArtifactId}:${quarkusPlatformVersion}") + implementation 'io.quarkus:quarkus-resteasy-reactive' + implementation ('org.acme.extensions:example-extension') + + testImplementation 'io.quarkus:quarkus-junit5' + testImplementation 'io.rest-assured:rest-assured' +} + +test { + useJUnitPlatform() +} + +quarkusIntTest { + environment "MY_RT_NAME", "genadiy" +} \ No newline at end of file diff --git a/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/application/gradle.properties b/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/application/gradle.properties new file mode 100644 index 00000000000000..ec2b6ef199c2ca --- /dev/null +++ b/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/application/gradle.properties @@ -0,0 +1,2 @@ +quarkusPlatformArtifactId=quarkus-bom +quarkusPlatformGroupId=io.quarkus diff --git a/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/application/settings.gradle b/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/application/settings.gradle new file mode 100644 index 00000000000000..7eeaae22f27fc8 --- /dev/null +++ b/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/application/settings.gradle @@ -0,0 +1,20 @@ +pluginManagement { + repositories { + mavenLocal { + content { + includeGroupByRegex 'io.quarkus.*' + } + } + mavenCentral() + gradlePluginPortal() + } + //noinspection GroovyAssignabilityCheck + plugins { + id 'io.quarkus' version "${quarkusPluginVersion}" + } +} + +includeBuild('../extensions/example-extension'){ + +} +rootProject.name='application' \ No newline at end of file diff --git a/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/application/src/integrationTest/java/org/acme/ExampleResourceIT.java b/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/application/src/integrationTest/java/org/acme/ExampleResourceIT.java new file mode 100644 index 00000000000000..6219887bad7f46 --- /dev/null +++ b/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/application/src/integrationTest/java/org/acme/ExampleResourceIT.java @@ -0,0 +1,29 @@ +package org.acme; + +import io.quarkus.test.junit.QuarkusIntegrationTest; +import org.junit.jupiter.api.Test; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.CoreMatchers.is; + +@QuarkusIntegrationTest +public class ExampleResourceIT { + + @Test + public void testHelloEndpoint() { + given() + .when().get("/hello") + .then() + .statusCode(200) + .body(is("hello cheburashka")); + } + + @Test + public void testRuntimeName() { + given() + .when().get("/hello/runtime-name") + .then() + .statusCode(200) + .body(is("genadiy")); + } +} \ No newline at end of file diff --git a/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/application/src/main/java/org/acme/quarkus/sample/HelloResource.java b/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/application/src/main/java/org/acme/quarkus/sample/HelloResource.java new file mode 100644 index 00000000000000..50a4aced5c534b --- /dev/null +++ b/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/application/src/main/java/org/acme/quarkus/sample/HelloResource.java @@ -0,0 +1,33 @@ +package org.acme.quarkus.sample; + +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +import org.acme.example.extension.runtime.ExampleBuildOptions; +import org.acme.example.extension.runtime.ExampleRuntimeConfig; + +@Path("/hello") +public class HelloResource { + + @Inject + ExampleBuildOptions buildOptions; + + @Inject + ExampleRuntimeConfig rtConfig; + + @GET + @Produces(MediaType.TEXT_PLAIN) + public String hello() { + return "hello " + buildOptions.name; + } + + @GET + @Produces(MediaType.TEXT_PLAIN) + @Path("/runtime-name") + public String runtimeName() { + return rtConfig.runtimeName; + } +} diff --git a/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/application/src/main/resources/application.properties b/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/application/src/main/resources/application.properties new file mode 100644 index 00000000000000..39fdee9f38d1e7 --- /dev/null +++ b/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/application/src/main/resources/application.properties @@ -0,0 +1 @@ +quarkus.example.runtime-name=${MY_RT_NAME:none} \ No newline at end of file diff --git a/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/extensions/example-extension/build.gradle b/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/extensions/example-extension/build.gradle new file mode 100644 index 00000000000000..464a421fce2a9a --- /dev/null +++ b/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/extensions/example-extension/build.gradle @@ -0,0 +1,34 @@ +plugins{ + id 'java-library' + id 'maven-publish' +} +subprojects {subProject-> + apply plugin: 'java-library' + apply plugin: 'maven-publish' + + group 'org.acme.extensions' + version '1.0-SNAPSHOT' + publishing { + publications { + maven(MavenPublication) { + groupId = 'org.acme.extensions' + artifactId = subProject.name + version = '1.0-SNAPSHOT' + from components.java + } + } + } +} + +publishing { + publications { + maven(MavenPublication) { + groupId = 'org.acme.extensions' + artifactId = rootProject.name + version = '1.0-SNAPSHOT' + from components.java + } + } +} +group 'org.acme.extensions' +version '1.0-SNAPSHOT' \ No newline at end of file diff --git a/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/extensions/example-extension/deployment/build.gradle b/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/extensions/example-extension/deployment/build.gradle new file mode 100644 index 00000000000000..3ce19bef01ea11 --- /dev/null +++ b/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/extensions/example-extension/deployment/build.gradle @@ -0,0 +1,18 @@ +plugins { + id 'java' + id 'java-library' +} +repositories { + mavenLocal() + mavenCentral() +} + +dependencies { + implementation platform("${quarkusPlatformGroupId}:${quarkusPlatformArtifactId}:${quarkusPlatformVersion}") + annotationProcessor "io.quarkus:quarkus-extension-processor:${quarkusPlatformVersion}" + + + api project(':example-extension') + implementation 'io.quarkus:quarkus-arc-deployment' +} + diff --git a/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/extensions/example-extension/deployment/src/main/java/org/acme/example/extension/deployment/ExampleConfig.java b/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/extensions/example-extension/deployment/src/main/java/org/acme/example/extension/deployment/ExampleConfig.java new file mode 100644 index 00000000000000..d92ed03d4757e1 --- /dev/null +++ b/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/extensions/example-extension/deployment/src/main/java/org/acme/example/extension/deployment/ExampleConfig.java @@ -0,0 +1,14 @@ +package org.acme.example.extension.deployment; + +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConfigRoot; + +@ConfigRoot +public class ExampleConfig { + + /** + * name + */ + @ConfigItem(defaultValue = "none") + String name; +} \ No newline at end of file diff --git a/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/extensions/example-extension/deployment/src/main/java/org/acme/example/extension/deployment/ExampleProcessor.java b/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/extensions/example-extension/deployment/src/main/java/org/acme/example/extension/deployment/ExampleProcessor.java new file mode 100644 index 00000000000000..7368451542ce07 --- /dev/null +++ b/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/extensions/example-extension/deployment/src/main/java/org/acme/example/extension/deployment/ExampleProcessor.java @@ -0,0 +1,30 @@ +package org.acme.example.extension.deployment; + +import jakarta.inject.Singleton; + +import org.acme.example.extension.runtime.ExampleBuildOptions; +import org.acme.example.extension.runtime.ExampleRecorder; +import io.quarkus.arc.deployment.SyntheticBeanBuildItem; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.ExecutionTime; +import io.quarkus.deployment.annotations.Record; +import io.quarkus.deployment.builditem.FeatureBuildItem; + +class ExampleProcessor { + + private static final String FEATURE = "example"; + + @BuildStep + FeatureBuildItem feature() { + return new FeatureBuildItem(FEATURE); + } + + @BuildStep + @Record(ExecutionTime.STATIC_INIT) + SyntheticBeanBuildItem syntheticBean(ExampleRecorder recorder, ExampleConfig config) { + return SyntheticBeanBuildItem.configure(ExampleBuildOptions.class) + .scope(Singleton.class) + .runtimeValue(recorder.buildOptions(config.name)) + .done(); + } +} diff --git a/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/extensions/example-extension/gradle.properties b/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/extensions/example-extension/gradle.properties new file mode 100644 index 00000000000000..ec2b6ef199c2ca --- /dev/null +++ b/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/extensions/example-extension/gradle.properties @@ -0,0 +1,2 @@ +quarkusPlatformArtifactId=quarkus-bom +quarkusPlatformGroupId=io.quarkus diff --git a/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/extensions/example-extension/runtime/build.gradle b/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/extensions/example-extension/runtime/build.gradle new file mode 100644 index 00000000000000..29ba93c67b5d6e --- /dev/null +++ b/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/extensions/example-extension/runtime/build.gradle @@ -0,0 +1,20 @@ + +plugins { + id 'io.quarkus.extension' +} + +quarkusExtension { + deploymentModule = 'example-extension-deployment' +} + +repositories { + mavenLocal() + mavenCentral() +} + +dependencies { + implementation platform("${quarkusPlatformGroupId}:${quarkusPlatformArtifactId}:${quarkusPlatformVersion}") + annotationProcessor "io.quarkus:quarkus-extension-processor:${quarkusPlatformVersion}" + implementation 'io.quarkus:quarkus-arc' +} + diff --git a/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/extensions/example-extension/runtime/src/main/java/org/acme/example/extension/runtime/ExampleBuildOptions.java b/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/extensions/example-extension/runtime/src/main/java/org/acme/example/extension/runtime/ExampleBuildOptions.java new file mode 100644 index 00000000000000..79c18d4929b67d --- /dev/null +++ b/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/extensions/example-extension/runtime/src/main/java/org/acme/example/extension/runtime/ExampleBuildOptions.java @@ -0,0 +1,10 @@ +package org.acme.example.extension.runtime; + +public class ExampleBuildOptions { + + public final String name; + + public ExampleBuildOptions(String name) { + this.name = name; + } +} diff --git a/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/extensions/example-extension/runtime/src/main/java/org/acme/example/extension/runtime/ExampleRecorder.java b/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/extensions/example-extension/runtime/src/main/java/org/acme/example/extension/runtime/ExampleRecorder.java new file mode 100644 index 00000000000000..3c41ac8f05ddc9 --- /dev/null +++ b/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/extensions/example-extension/runtime/src/main/java/org/acme/example/extension/runtime/ExampleRecorder.java @@ -0,0 +1,13 @@ +package org.acme.example.extension.runtime; + +import io.quarkus.runtime.RuntimeValue; +import io.quarkus.runtime.annotations.Recorder; + +@Recorder +public class ExampleRecorder { + + public RuntimeValue buildOptions(String name) { + System.out.println("ExampleRecorder.buildOptions " + name); + return new RuntimeValue<>(new ExampleBuildOptions(name)); + } +} \ No newline at end of file diff --git a/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/extensions/example-extension/runtime/src/main/java/org/acme/example/extension/runtime/ExampleRuntimeConfig.java b/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/extensions/example-extension/runtime/src/main/java/org/acme/example/extension/runtime/ExampleRuntimeConfig.java new file mode 100644 index 00000000000000..8e4890e928ce54 --- /dev/null +++ b/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/extensions/example-extension/runtime/src/main/java/org/acme/example/extension/runtime/ExampleRuntimeConfig.java @@ -0,0 +1,16 @@ +package org.acme.example.extension.runtime; + +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.annotations.ConfigRoot; + +@ConfigRoot(phase = ConfigPhase.RUN_TIME) +public class ExampleRuntimeConfig { + + /** + * Whether the banner will be displayed + */ + @ConfigItem(defaultValue = "none") + public String runtimeName; + +} \ No newline at end of file diff --git a/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/extensions/example-extension/runtime/src/main/resources/META-INF/quarkus-extension.properties b/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/extensions/example-extension/runtime/src/main/resources/META-INF/quarkus-extension.properties new file mode 100644 index 00000000000000..2e1a6326847e11 --- /dev/null +++ b/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/extensions/example-extension/runtime/src/main/resources/META-INF/quarkus-extension.properties @@ -0,0 +1 @@ +deployment-artifact=org.acme.extensions\:example-extension-deployment\:1.0 \ No newline at end of file diff --git a/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/extensions/example-extension/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/extensions/example-extension/runtime/src/main/resources/META-INF/quarkus-extension.yaml new file mode 100644 index 00000000000000..12a5c710c9e823 --- /dev/null +++ b/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/extensions/example-extension/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -0,0 +1,12 @@ +--- +name: Quarkus Example Extension +artifact: ${project.groupId}:${project.artifactId}:${project.version} +metadata: + config: + - "quarkus.example.extension." + keywords: + - "logzio" + - "logging" + categories: + - "logging" +description: "Quarkus example extension" \ No newline at end of file diff --git a/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/extensions/example-extension/settings.gradle b/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/extensions/example-extension/settings.gradle new file mode 100644 index 00000000000000..ca835fae5d2dbb --- /dev/null +++ b/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/extensions/example-extension/settings.gradle @@ -0,0 +1,21 @@ +pluginManagement { + repositories { + gradlePluginPortal() + mavenLocal() + } + plugins { + id 'io.quarkus.extension' version "${quarkusPluginVersion}" + } +} +dependencyResolutionManagement { + repositories { + mavenLocal() + mavenCentral() + } + +} +rootProject.name = 'example-extension-parent' +include(':deployment') +include(':runtime') +project(':deployment').name='example-extension-deployment' +project(':runtime').name='example-extension' \ No newline at end of file diff --git a/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/gradle.properties b/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/gradle.properties new file mode 100644 index 00000000000000..8f063b7d88ba4a --- /dev/null +++ b/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/gradle.properties @@ -0,0 +1,2 @@ +quarkusPlatformArtifactId=quarkus-bom +quarkusPlatformGroupId=io.quarkus \ No newline at end of file diff --git a/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/settings.gradle b/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/settings.gradle new file mode 100644 index 00000000000000..53eb6f0795e295 --- /dev/null +++ b/integration-tests/gradle/src/main/resources/system-props-as-build-time-config-source/settings.gradle @@ -0,0 +1,18 @@ +pluginManagement { + repositories { + mavenLocal { + content { + includeGroupByRegex 'io.quarkus.*' + } + } + mavenCentral() + gradlePluginPortal() + } + //noinspection GroovyAssignabilityCheck + plugins { + id 'io.quarkus' version "${quarkusPluginVersion}" + } +} + +includeBuild('extensions/example-extension') +includeBuild('application') \ No newline at end of file diff --git a/integration-tests/gradle/src/test/java/io/quarkus/gradle/CustomManifestArgumentsTest.java b/integration-tests/gradle/src/test/java/io/quarkus/gradle/CustomManifestArgumentsTest.java index d7aafae527d76a..7b7ae3c97e2141 100644 --- a/integration-tests/gradle/src/test/java/io/quarkus/gradle/CustomManifestArgumentsTest.java +++ b/integration-tests/gradle/src/test/java/io/quarkus/gradle/CustomManifestArgumentsTest.java @@ -11,10 +11,13 @@ import java.util.jar.Manifest; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledOnOs; +import org.junit.jupiter.api.condition.OS; public class CustomManifestArgumentsTest extends QuarkusGradleWrapperTestBase { @Test + @DisabledOnOs(value = OS.WINDOWS, disabledReason = "for whatever reason, this is not working anymore on Widndows, the customSection is null...") public void shouldContainsSpecificManifestProperty() throws Exception { File projectDir = getProjectDir("custom-config-java-module"); diff --git a/integration-tests/gradle/src/test/java/io/quarkus/gradle/GrpcDescriptorSetAlternateOutputBuildTest.java b/integration-tests/gradle/src/test/java/io/quarkus/gradle/GrpcDescriptorSetAlternateOutputBuildTest.java new file mode 100644 index 00000000000000..e108c207370d74 --- /dev/null +++ b/integration-tests/gradle/src/test/java/io/quarkus/gradle/GrpcDescriptorSetAlternateOutputBuildTest.java @@ -0,0 +1,27 @@ +package io.quarkus.gradle; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +public class GrpcDescriptorSetAlternateOutputBuildTest extends QuarkusGradleWrapperTestBase { + + @Test + public void testGrpcDescriptorSetAlternateOutput() throws Exception { + var projectDir = getProjectDir("grpc-descriptor-set-alternate-output"); + var buildResult = runGradleWrapper(projectDir, "clean", "build"); + assertThat(BuildResult.isSuccessful(buildResult.getTasks().get(":quarkusGenerateCode"))).isTrue(); + + var expectedOutputDir = projectDir.toPath() + .resolve("build") + .resolve("classes") + .resolve("java") + .resolve("quarkus-generated-sources") + .resolve("grpc"); + + assertThat(expectedOutputDir).exists(); + assertThat(expectedOutputDir.resolve("hello.dsc")) + .exists() + .isNotEmptyFile(); + } +} diff --git a/integration-tests/gradle/src/test/java/io/quarkus/gradle/GrpcDescriptorSetAlternateOutputDirBuildTest.java b/integration-tests/gradle/src/test/java/io/quarkus/gradle/GrpcDescriptorSetAlternateOutputDirBuildTest.java new file mode 100644 index 00000000000000..579042e321d110 --- /dev/null +++ b/integration-tests/gradle/src/test/java/io/quarkus/gradle/GrpcDescriptorSetAlternateOutputDirBuildTest.java @@ -0,0 +1,24 @@ +package io.quarkus.gradle; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +public class GrpcDescriptorSetAlternateOutputDirBuildTest extends QuarkusGradleWrapperTestBase { + + @Test + public void testGrpcDescriptorSetAlternateOutputDir() throws Exception { + var projectDir = getProjectDir("grpc-descriptor-set-alternate-output-dir"); + var buildResult = runGradleWrapper(projectDir, "clean", "build"); + assertThat(BuildResult.isSuccessful(buildResult.getTasks().get(":quarkusGenerateCode"))).isTrue(); + + var expectedOutputDir = projectDir.toPath() + .resolve("build") + .resolve("proto"); + + assertThat(expectedOutputDir).exists(); + assertThat(expectedOutputDir.resolve("hello.dsc")) + .exists() + .isNotEmptyFile(); + } +} diff --git a/integration-tests/gradle/src/test/java/io/quarkus/gradle/GrpcDescriptorSetBuildTest.java b/integration-tests/gradle/src/test/java/io/quarkus/gradle/GrpcDescriptorSetBuildTest.java new file mode 100644 index 00000000000000..c5506309e0bc89 --- /dev/null +++ b/integration-tests/gradle/src/test/java/io/quarkus/gradle/GrpcDescriptorSetBuildTest.java @@ -0,0 +1,27 @@ +package io.quarkus.gradle; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +public class GrpcDescriptorSetBuildTest extends QuarkusGradleWrapperTestBase { + + @Test + public void testGrpcDescriptorSet() throws Exception { + var projectDir = getProjectDir("grpc-descriptor-set"); + var buildResult = runGradleWrapper(projectDir, "clean", "build"); + assertThat(BuildResult.isSuccessful(buildResult.getTasks().get(":quarkusGenerateCode"))).isTrue(); + + var expectedOutputDir = projectDir.toPath() + .resolve("build") + .resolve("classes") + .resolve("java") + .resolve("quarkus-generated-sources") + .resolve("grpc"); + + assertThat(expectedOutputDir).exists(); + assertThat(expectedOutputDir.resolve("descriptor_set.dsc")) + .exists() + .isNotEmptyFile(); + } +} diff --git a/integration-tests/gradle/src/test/java/io/quarkus/gradle/SystemPropsAsBuildTimeConfigSourceTest.java b/integration-tests/gradle/src/test/java/io/quarkus/gradle/SystemPropsAsBuildTimeConfigSourceTest.java new file mode 100644 index 00000000000000..a526a346c65800 --- /dev/null +++ b/integration-tests/gradle/src/test/java/io/quarkus/gradle/SystemPropsAsBuildTimeConfigSourceTest.java @@ -0,0 +1,52 @@ +package io.quarkus.gradle; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.Properties; + +import org.junit.jupiter.api.Test; + +public class SystemPropsAsBuildTimeConfigSourceTest extends QuarkusGradleWrapperTestBase { + + @Test + public void testBasicMultiModuleBuild() throws Exception { + + final File projectDir = getProjectDir("system-props-as-build-time-config-source"); + + final File appProperties = new File(projectDir, "application/gradle.properties"); + final File extensionProperties = new File(projectDir, "extensions/example-extension/gradle.properties"); + + final Path projectProperties = projectDir.toPath().resolve("gradle.properties"); + + try { + Files.copy(projectProperties, appProperties.toPath(), StandardCopyOption.REPLACE_EXISTING); + Files.copy(projectProperties, extensionProperties.toPath(), StandardCopyOption.REPLACE_EXISTING); + } catch (IOException e) { + throw new IllegalStateException("Unable to copy gradle.properties file", e); + } + + gradleConfigurationCache(false); + runGradleWrapper(projectDir, + "-Dquarkus.example.name=cheburashka", + "-Dquarkus.example.runtime-name=crocodile", + "-Dquarkus.package.type=mutable-jar", + ":example-extension:example-extension-deployment:build", + // this quarkusIntTest will make sure runtime config properties passed as env vars when launching the app are effective + ":application:quarkusIntTest"); + + final Path buildSystemPropsPath = projectDir.toPath().resolve("application").resolve("build").resolve("quarkus-app") + .resolve("quarkus").resolve("build-system.properties"); + assertThat(buildSystemPropsPath).exists(); + var props = new Properties(); + try (var reader = Files.newBufferedReader(buildSystemPropsPath)) { + props.load(reader); + } + assertThat(props).doesNotContainKey("quarkus.example.name"); + assertThat(props).doesNotContainKey("quarkus.example.runtime-name"); + } +} diff --git a/integration-tests/gradle/src/test/java/io/quarkus/gradle/nativeimage/BasicJavaNativeBuildIT.java b/integration-tests/gradle/src/test/java/io/quarkus/gradle/nativeimage/BasicJavaNativeBuildIT.java index 28c01e9c43934c..cf7904ca9b6a02 100644 --- a/integration-tests/gradle/src/test/java/io/quarkus/gradle/nativeimage/BasicJavaNativeBuildIT.java +++ b/integration-tests/gradle/src/test/java/io/quarkus/gradle/nativeimage/BasicJavaNativeBuildIT.java @@ -20,7 +20,7 @@ public class BasicJavaNativeBuildIT extends QuarkusNativeGradleITBase { public void shouldBuildNativeImage() throws Exception { final File projectDir = getProjectDir("basic-java-native-module"); - final BuildResult build = runGradleWrapper(projectDir, "clean", "buildNative", "-Dquarkus.package.type=fast-jar"); + final BuildResult build = runGradleWrapper(projectDir, "clean", "buildNative"); assertThat(build.getTasks().get(":quarkusBuild")).isEqualTo(BuildResult.SUCCESS_OUTCOME); final String buildOutput = build.getOutput(); @@ -48,7 +48,7 @@ public void shouldBuildNativeImage() throws Exception { public void shouldBuildNativeImageWithCustomName() throws Exception { final File projectDir = getProjectDir("basic-java-native-module"); - final BuildResult build = runGradleWrapper(projectDir, "clean", "buildNative", "-Dquarkus.package.type=fast-jar", + final BuildResult build = runGradleWrapper(projectDir, "clean", "buildNative", "-Dquarkus.package.output-name=test"); assertThat(build.getTasks().get(":quarkusBuild")).isEqualTo(BuildResult.SUCCESS_OUTCOME); @@ -78,7 +78,7 @@ public void shouldBuildNativeImageWithCustomName() throws Exception { public void shouldBuildNativeImageWithCustomNameWithoutSuffix() throws Exception { final File projectDir = getProjectDir("basic-java-native-module"); - final BuildResult build = runGradleWrapper(projectDir, "clean", "buildNative", "-Dquarkus.package.type=fast-jar", + final BuildResult build = runGradleWrapper(projectDir, "clean", "buildNative", "-Dquarkus.package.output-name=test", "-Dquarkus.package.add-runner-suffix=false"); assertThat(build.getTasks().get(":quarkusBuild")).isEqualTo(BuildResult.SUCCESS_OUTCOME); diff --git a/integration-tests/grpc-descriptor-sets/grpc-descriptor-set-alternate-output-dir/pom.xml b/integration-tests/grpc-descriptor-sets/grpc-descriptor-set-alternate-output-dir/pom.xml new file mode 100644 index 00000000000000..8f819511f53e69 --- /dev/null +++ b/integration-tests/grpc-descriptor-sets/grpc-descriptor-set-alternate-output-dir/pom.xml @@ -0,0 +1,28 @@ + + + 4.0.0 + + + quarkus-integration-test-grpc-descriptor-sets-parent + io.quarkus + 999-SNAPSHOT + + + quarkus-integration-test-grpc-descriptor-sets-alternate-output-dir + Quarkus - Integration Tests - gRPC - Descriptor Sets - Alternate Output Dir + + + + + maven-surefire-plugin + + + ${project.build.directory} + + + + + + diff --git a/integration-tests/grpc-descriptor-sets/grpc-descriptor-set-alternate-output-dir/src/main/java/io/quarkus/grpc/examples/hello/HelloWorldEndpoint.java b/integration-tests/grpc-descriptor-sets/grpc-descriptor-set-alternate-output-dir/src/main/java/io/quarkus/grpc/examples/hello/HelloWorldEndpoint.java new file mode 100644 index 00000000000000..4922f5e81e09ea --- /dev/null +++ b/integration-tests/grpc-descriptor-sets/grpc-descriptor-set-alternate-output-dir/src/main/java/io/quarkus/grpc/examples/hello/HelloWorldEndpoint.java @@ -0,0 +1,41 @@ +package io.quarkus.grpc.examples.hello; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; + +import examples.GreeterGrpc; +import examples.HelloReply; +import examples.HelloRequest; +import examples.MutinyGreeterGrpc; +import io.quarkus.grpc.GrpcClient; +import io.smallrye.mutiny.Uni; + +@Path("/hello") +public class HelloWorldEndpoint { + + @GrpcClient("hello") + GreeterGrpc.GreeterBlockingStub blockingHelloService; + + @GrpcClient("hello") + MutinyGreeterGrpc.MutinyGreeterStub mutinyHelloService; + + @GET + @Path("/blocking/{name}") + public String helloBlocking(@PathParam("name") String name) { + HelloReply reply = blockingHelloService.sayHello(HelloRequest.newBuilder().setName(name).build()); + return generateResponse(reply); + + } + + @GET + @Path("/mutiny/{name}") + public Uni helloMutiny(@PathParam("name") String name) { + return mutinyHelloService.sayHello(HelloRequest.newBuilder().setName(name).build()) + .onItem().transform((reply) -> generateResponse(reply)); + } + + public String generateResponse(HelloReply reply) { + return String.format("%s! HelloWorldService has been called %d number of times.", reply.getMessage(), reply.getCount()); + } +} diff --git a/integration-tests/grpc-descriptor-sets/grpc-descriptor-set-alternate-output-dir/src/main/java/io/quarkus/grpc/examples/hello/HelloWorldService.java b/integration-tests/grpc-descriptor-sets/grpc-descriptor-set-alternate-output-dir/src/main/java/io/quarkus/grpc/examples/hello/HelloWorldService.java new file mode 100644 index 00000000000000..6b13fdf54462da --- /dev/null +++ b/integration-tests/grpc-descriptor-sets/grpc-descriptor-set-alternate-output-dir/src/main/java/io/quarkus/grpc/examples/hello/HelloWorldService.java @@ -0,0 +1,23 @@ +package io.quarkus.grpc.examples.hello; + +import java.util.concurrent.atomic.AtomicInteger; + +import examples.HelloReply; +import examples.HelloRequest; +import examples.MutinyGreeterGrpc; +import io.quarkus.grpc.GrpcService; +import io.smallrye.mutiny.Uni; + +@GrpcService +public class HelloWorldService extends MutinyGreeterGrpc.GreeterImplBase { + + AtomicInteger counter = new AtomicInteger(); + + @Override + public Uni sayHello(HelloRequest request) { + int count = counter.incrementAndGet(); + String name = request.getName(); + return Uni.createFrom().item("Hello " + name) + .map(res -> HelloReply.newBuilder().setMessage(res).setCount(count).build()); + } +} diff --git a/integration-tests/grpc-descriptor-sets/grpc-descriptor-set-alternate-output-dir/src/main/proto/helloworld.proto b/integration-tests/grpc-descriptor-sets/grpc-descriptor-set-alternate-output-dir/src/main/proto/helloworld.proto new file mode 100644 index 00000000000000..c50ba71b3b4fa4 --- /dev/null +++ b/integration-tests/grpc-descriptor-sets/grpc-descriptor-set-alternate-output-dir/src/main/proto/helloworld.proto @@ -0,0 +1,21 @@ +syntax = "proto2"; + +option java_multiple_files = true; +option java_package = "examples"; +option java_outer_classname = "HelloWorldProto"; +option objc_class_prefix = "HLW"; + +package helloworld; + +service Greeter { + rpc SayHello (HelloRequest) returns (HelloReply) {} +} + +message HelloRequest { + required string name = 1; +} + +message HelloReply { + required string message = 1; + optional int32 count = 2; +} diff --git a/integration-tests/grpc-descriptor-sets/grpc-descriptor-set-alternate-output-dir/src/main/resources/application.properties b/integration-tests/grpc-descriptor-sets/grpc-descriptor-set-alternate-output-dir/src/main/resources/application.properties new file mode 100644 index 00000000000000..214afae1821d51 --- /dev/null +++ b/integration-tests/grpc-descriptor-sets/grpc-descriptor-set-alternate-output-dir/src/main/resources/application.properties @@ -0,0 +1,12 @@ +quarkus.generate-code.grpc.descriptor-set.generate=true +quarkus.generate-code.grpc.descriptor-set.name=hello.dsc +quarkus.generate-code.grpc.descriptor-set.output-dir=proto + +quarkus.grpc.server.port=9001 + +quarkus.grpc.clients.hello.host=localhost +quarkus.grpc.clients.hello.port=9001 + +%vertx.quarkus.grpc.clients.hello.port=8081 +%vertx.quarkus.grpc.clients.hello.use-quarkus-grpc-client=true +%vertx.quarkus.grpc.server.use-separate-server=false diff --git a/integration-tests/grpc-descriptor-sets/grpc-descriptor-set-alternate-output-dir/src/test/java/io/quarkus/grpc/examples/hello/DescriptorSetExistsTest.java b/integration-tests/grpc-descriptor-sets/grpc-descriptor-set-alternate-output-dir/src/test/java/io/quarkus/grpc/examples/hello/DescriptorSetExistsTest.java new file mode 100644 index 00000000000000..a982554fb6d758 --- /dev/null +++ b/integration-tests/grpc-descriptor-sets/grpc-descriptor-set-alternate-output-dir/src/test/java/io/quarkus/grpc/examples/hello/DescriptorSetExistsTest.java @@ -0,0 +1,21 @@ +package io.quarkus.grpc.examples.hello; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.nio.file.Path; + +import org.junit.jupiter.api.Test; + +public class DescriptorSetExistsTest { + + @Test + public void descriptorSetExists() { + var expectedOutputDir = Path.of(System.getProperty("build.dir")) + .resolve("proto"); + + assertThat(expectedOutputDir).exists(); + assertThat(expectedOutputDir.resolve("hello.dsc")) + .exists() + .isNotEmptyFile(); + } +} diff --git a/integration-tests/grpc-descriptor-sets/grpc-descriptor-set-alternate-output/pom.xml b/integration-tests/grpc-descriptor-sets/grpc-descriptor-set-alternate-output/pom.xml new file mode 100644 index 00000000000000..91203496baddea --- /dev/null +++ b/integration-tests/grpc-descriptor-sets/grpc-descriptor-set-alternate-output/pom.xml @@ -0,0 +1,28 @@ + + + 4.0.0 + + + quarkus-integration-test-grpc-descriptor-sets-parent + io.quarkus + 999-SNAPSHOT + + + quarkus-integration-test-grpc-descriptor-sets-alternate-output + Quarkus - Integration Tests - gRPC - Descriptor Sets - Alternate Output + + + + + maven-surefire-plugin + + + ${project.build.directory} + + + + + + diff --git a/integration-tests/grpc-descriptor-sets/grpc-descriptor-set-alternate-output/src/main/java/io/quarkus/grpc/examples/hello/HelloWorldEndpoint.java b/integration-tests/grpc-descriptor-sets/grpc-descriptor-set-alternate-output/src/main/java/io/quarkus/grpc/examples/hello/HelloWorldEndpoint.java new file mode 100644 index 00000000000000..4922f5e81e09ea --- /dev/null +++ b/integration-tests/grpc-descriptor-sets/grpc-descriptor-set-alternate-output/src/main/java/io/quarkus/grpc/examples/hello/HelloWorldEndpoint.java @@ -0,0 +1,41 @@ +package io.quarkus.grpc.examples.hello; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; + +import examples.GreeterGrpc; +import examples.HelloReply; +import examples.HelloRequest; +import examples.MutinyGreeterGrpc; +import io.quarkus.grpc.GrpcClient; +import io.smallrye.mutiny.Uni; + +@Path("/hello") +public class HelloWorldEndpoint { + + @GrpcClient("hello") + GreeterGrpc.GreeterBlockingStub blockingHelloService; + + @GrpcClient("hello") + MutinyGreeterGrpc.MutinyGreeterStub mutinyHelloService; + + @GET + @Path("/blocking/{name}") + public String helloBlocking(@PathParam("name") String name) { + HelloReply reply = blockingHelloService.sayHello(HelloRequest.newBuilder().setName(name).build()); + return generateResponse(reply); + + } + + @GET + @Path("/mutiny/{name}") + public Uni helloMutiny(@PathParam("name") String name) { + return mutinyHelloService.sayHello(HelloRequest.newBuilder().setName(name).build()) + .onItem().transform((reply) -> generateResponse(reply)); + } + + public String generateResponse(HelloReply reply) { + return String.format("%s! HelloWorldService has been called %d number of times.", reply.getMessage(), reply.getCount()); + } +} diff --git a/integration-tests/grpc-descriptor-sets/grpc-descriptor-set-alternate-output/src/main/java/io/quarkus/grpc/examples/hello/HelloWorldService.java b/integration-tests/grpc-descriptor-sets/grpc-descriptor-set-alternate-output/src/main/java/io/quarkus/grpc/examples/hello/HelloWorldService.java new file mode 100644 index 00000000000000..6b13fdf54462da --- /dev/null +++ b/integration-tests/grpc-descriptor-sets/grpc-descriptor-set-alternate-output/src/main/java/io/quarkus/grpc/examples/hello/HelloWorldService.java @@ -0,0 +1,23 @@ +package io.quarkus.grpc.examples.hello; + +import java.util.concurrent.atomic.AtomicInteger; + +import examples.HelloReply; +import examples.HelloRequest; +import examples.MutinyGreeterGrpc; +import io.quarkus.grpc.GrpcService; +import io.smallrye.mutiny.Uni; + +@GrpcService +public class HelloWorldService extends MutinyGreeterGrpc.GreeterImplBase { + + AtomicInteger counter = new AtomicInteger(); + + @Override + public Uni sayHello(HelloRequest request) { + int count = counter.incrementAndGet(); + String name = request.getName(); + return Uni.createFrom().item("Hello " + name) + .map(res -> HelloReply.newBuilder().setMessage(res).setCount(count).build()); + } +} diff --git a/integration-tests/grpc-descriptor-sets/grpc-descriptor-set-alternate-output/src/main/proto/helloworld.proto b/integration-tests/grpc-descriptor-sets/grpc-descriptor-set-alternate-output/src/main/proto/helloworld.proto new file mode 100644 index 00000000000000..c50ba71b3b4fa4 --- /dev/null +++ b/integration-tests/grpc-descriptor-sets/grpc-descriptor-set-alternate-output/src/main/proto/helloworld.proto @@ -0,0 +1,21 @@ +syntax = "proto2"; + +option java_multiple_files = true; +option java_package = "examples"; +option java_outer_classname = "HelloWorldProto"; +option objc_class_prefix = "HLW"; + +package helloworld; + +service Greeter { + rpc SayHello (HelloRequest) returns (HelloReply) {} +} + +message HelloRequest { + required string name = 1; +} + +message HelloReply { + required string message = 1; + optional int32 count = 2; +} diff --git a/integration-tests/grpc-descriptor-sets/grpc-descriptor-set-alternate-output/src/main/resources/application.properties b/integration-tests/grpc-descriptor-sets/grpc-descriptor-set-alternate-output/src/main/resources/application.properties new file mode 100644 index 00000000000000..20a1d5d62db45c --- /dev/null +++ b/integration-tests/grpc-descriptor-sets/grpc-descriptor-set-alternate-output/src/main/resources/application.properties @@ -0,0 +1,11 @@ +quarkus.generate-code.grpc.descriptor-set.generate=true +quarkus.generate-code.grpc.descriptor-set.name=hello.dsc + +quarkus.grpc.server.port=9001 + +quarkus.grpc.clients.hello.host=localhost +quarkus.grpc.clients.hello.port=9001 + +%vertx.quarkus.grpc.clients.hello.port=8081 +%vertx.quarkus.grpc.clients.hello.use-quarkus-grpc-client=true +%vertx.quarkus.grpc.server.use-separate-server=false diff --git a/integration-tests/grpc-descriptor-sets/grpc-descriptor-set-alternate-output/src/test/java/io/quarkus/grpc/examples/hello/DescriptorSetExistsTest.java b/integration-tests/grpc-descriptor-sets/grpc-descriptor-set-alternate-output/src/test/java/io/quarkus/grpc/examples/hello/DescriptorSetExistsTest.java new file mode 100644 index 00000000000000..0c1a586f0ec381 --- /dev/null +++ b/integration-tests/grpc-descriptor-sets/grpc-descriptor-set-alternate-output/src/test/java/io/quarkus/grpc/examples/hello/DescriptorSetExistsTest.java @@ -0,0 +1,22 @@ +package io.quarkus.grpc.examples.hello; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.nio.file.Path; + +import org.junit.jupiter.api.Test; + +public class DescriptorSetExistsTest { + + @Test + public void descriptorSetExists() { + var expectedOutputDir = Path.of(System.getProperty("build.dir")) + .resolve("generated-sources") + .resolve("grpc"); + + assertThat(expectedOutputDir).exists(); + assertThat(expectedOutputDir.resolve("hello.dsc")) + .exists() + .isNotEmptyFile(); + } +} diff --git a/integration-tests/grpc-descriptor-sets/grpc-descriptor-set/pom.xml b/integration-tests/grpc-descriptor-sets/grpc-descriptor-set/pom.xml new file mode 100644 index 00000000000000..cc417c76e19b9c --- /dev/null +++ b/integration-tests/grpc-descriptor-sets/grpc-descriptor-set/pom.xml @@ -0,0 +1,28 @@ + + + 4.0.0 + + + quarkus-integration-test-grpc-descriptor-sets-parent + io.quarkus + 999-SNAPSHOT + + + quarkus-integration-test-grpc-descriptor-sets-descriptor-set + Quarkus - Integration Tests - gRPC - Descriptor Sets - Descriptor Set + + + + + maven-surefire-plugin + + + ${project.build.directory} + + + + + + diff --git a/integration-tests/grpc-descriptor-sets/grpc-descriptor-set/src/main/java/io/quarkus/grpc/examples/hello/HelloWorldEndpoint.java b/integration-tests/grpc-descriptor-sets/grpc-descriptor-set/src/main/java/io/quarkus/grpc/examples/hello/HelloWorldEndpoint.java new file mode 100644 index 00000000000000..4922f5e81e09ea --- /dev/null +++ b/integration-tests/grpc-descriptor-sets/grpc-descriptor-set/src/main/java/io/quarkus/grpc/examples/hello/HelloWorldEndpoint.java @@ -0,0 +1,41 @@ +package io.quarkus.grpc.examples.hello; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; + +import examples.GreeterGrpc; +import examples.HelloReply; +import examples.HelloRequest; +import examples.MutinyGreeterGrpc; +import io.quarkus.grpc.GrpcClient; +import io.smallrye.mutiny.Uni; + +@Path("/hello") +public class HelloWorldEndpoint { + + @GrpcClient("hello") + GreeterGrpc.GreeterBlockingStub blockingHelloService; + + @GrpcClient("hello") + MutinyGreeterGrpc.MutinyGreeterStub mutinyHelloService; + + @GET + @Path("/blocking/{name}") + public String helloBlocking(@PathParam("name") String name) { + HelloReply reply = blockingHelloService.sayHello(HelloRequest.newBuilder().setName(name).build()); + return generateResponse(reply); + + } + + @GET + @Path("/mutiny/{name}") + public Uni helloMutiny(@PathParam("name") String name) { + return mutinyHelloService.sayHello(HelloRequest.newBuilder().setName(name).build()) + .onItem().transform((reply) -> generateResponse(reply)); + } + + public String generateResponse(HelloReply reply) { + return String.format("%s! HelloWorldService has been called %d number of times.", reply.getMessage(), reply.getCount()); + } +} diff --git a/integration-tests/grpc-descriptor-sets/grpc-descriptor-set/src/main/java/io/quarkus/grpc/examples/hello/HelloWorldService.java b/integration-tests/grpc-descriptor-sets/grpc-descriptor-set/src/main/java/io/quarkus/grpc/examples/hello/HelloWorldService.java new file mode 100644 index 00000000000000..6b13fdf54462da --- /dev/null +++ b/integration-tests/grpc-descriptor-sets/grpc-descriptor-set/src/main/java/io/quarkus/grpc/examples/hello/HelloWorldService.java @@ -0,0 +1,23 @@ +package io.quarkus.grpc.examples.hello; + +import java.util.concurrent.atomic.AtomicInteger; + +import examples.HelloReply; +import examples.HelloRequest; +import examples.MutinyGreeterGrpc; +import io.quarkus.grpc.GrpcService; +import io.smallrye.mutiny.Uni; + +@GrpcService +public class HelloWorldService extends MutinyGreeterGrpc.GreeterImplBase { + + AtomicInteger counter = new AtomicInteger(); + + @Override + public Uni sayHello(HelloRequest request) { + int count = counter.incrementAndGet(); + String name = request.getName(); + return Uni.createFrom().item("Hello " + name) + .map(res -> HelloReply.newBuilder().setMessage(res).setCount(count).build()); + } +} diff --git a/integration-tests/grpc-descriptor-sets/grpc-descriptor-set/src/main/proto/helloworld.proto b/integration-tests/grpc-descriptor-sets/grpc-descriptor-set/src/main/proto/helloworld.proto new file mode 100644 index 00000000000000..c50ba71b3b4fa4 --- /dev/null +++ b/integration-tests/grpc-descriptor-sets/grpc-descriptor-set/src/main/proto/helloworld.proto @@ -0,0 +1,21 @@ +syntax = "proto2"; + +option java_multiple_files = true; +option java_package = "examples"; +option java_outer_classname = "HelloWorldProto"; +option objc_class_prefix = "HLW"; + +package helloworld; + +service Greeter { + rpc SayHello (HelloRequest) returns (HelloReply) {} +} + +message HelloRequest { + required string name = 1; +} + +message HelloReply { + required string message = 1; + optional int32 count = 2; +} diff --git a/integration-tests/grpc-descriptor-sets/grpc-descriptor-set/src/main/resources/application.properties b/integration-tests/grpc-descriptor-sets/grpc-descriptor-set/src/main/resources/application.properties new file mode 100644 index 00000000000000..b63c49d1fd2612 --- /dev/null +++ b/integration-tests/grpc-descriptor-sets/grpc-descriptor-set/src/main/resources/application.properties @@ -0,0 +1,10 @@ +quarkus.generate-code.grpc.descriptor-set.generate=true + +quarkus.grpc.server.port=9001 + +quarkus.grpc.clients.hello.host=localhost +quarkus.grpc.clients.hello.port=9001 + +%vertx.quarkus.grpc.clients.hello.port=8081 +%vertx.quarkus.grpc.clients.hello.use-quarkus-grpc-client=true +%vertx.quarkus.grpc.server.use-separate-server=false diff --git a/integration-tests/grpc-descriptor-sets/grpc-descriptor-set/src/test/java/io/quarkus/grpc/examples/hello/DescriptorSetExistsTest.java b/integration-tests/grpc-descriptor-sets/grpc-descriptor-set/src/test/java/io/quarkus/grpc/examples/hello/DescriptorSetExistsTest.java new file mode 100644 index 00000000000000..4b64f9caccae66 --- /dev/null +++ b/integration-tests/grpc-descriptor-sets/grpc-descriptor-set/src/test/java/io/quarkus/grpc/examples/hello/DescriptorSetExistsTest.java @@ -0,0 +1,22 @@ +package io.quarkus.grpc.examples.hello; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.nio.file.Path; + +import org.junit.jupiter.api.Test; + +public class DescriptorSetExistsTest { + + @Test + public void descriptorSetExists() { + var expectedOutputDir = Path.of(System.getProperty("build.dir")) + .resolve("generated-sources") + .resolve("grpc"); + + assertThat(expectedOutputDir).exists(); + assertThat(expectedOutputDir.resolve("descriptor_set.dsc")) + .exists() + .isNotEmptyFile(); + } +} diff --git a/integration-tests/grpc-descriptor-sets/pom.xml b/integration-tests/grpc-descriptor-sets/pom.xml new file mode 100644 index 00000000000000..45a21572d641dd --- /dev/null +++ b/integration-tests/grpc-descriptor-sets/pom.xml @@ -0,0 +1,105 @@ + + + 4.0.0 + + + quarkus-integration-tests-parent + io.quarkus + 999-SNAPSHOT + + + quarkus-integration-test-grpc-descriptor-sets-parent + Quarkus - Integration Tests - gRPC - Descriptor Sets - Parent + pom + + + grpc-descriptor-set + grpc-descriptor-set-alternate-output + grpc-descriptor-set-alternate-output-dir + + + + + io.quarkus + quarkus-resteasy-reactive + + + io.quarkus + quarkus-grpc + + + io.quarkus + quarkus-junit5 + test + + + io.rest-assured + rest-assured + test + + + org.awaitility + awaitility + test + + + org.assertj + assertj-core + test + + + io.quarkus + quarkus-test-grpc + ${project.version} + test + + + + + io.quarkus + quarkus-grpc-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-resteasy-reactive-deployment + ${project.version} + pom + test + + + * + * + + + + + + + + + io.quarkus + quarkus-maven-plugin + + + + generate-code + build + + + + + + + + diff --git a/integration-tests/kubernetes-client-hack-extension/deployment/pom.xml b/integration-tests/kubernetes-client-hack-extension/deployment/pom.xml new file mode 100644 index 00000000000000..913462707b124c --- /dev/null +++ b/integration-tests/kubernetes-client-hack-extension/deployment/pom.xml @@ -0,0 +1,58 @@ + + + 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 new file mode 100644 index 00000000000000..91372ea46589e1 --- /dev/null +++ b/integration-tests/kubernetes-client-hack-extension/deployment/src/main/java/io/quarkus/kubernetes/client/deployment/NativeOverrides.java @@ -0,0 +1,12 @@ +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 new file mode 100644 index 00000000000000..c29e01f59239c3 --- /dev/null +++ b/integration-tests/kubernetes-client-hack-extension/pom.xml @@ -0,0 +1,21 @@ + + + 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 new file mode 100644 index 00000000000000..a0278c43dafaa9 --- /dev/null +++ b/integration-tests/kubernetes-client-hack-extension/runtime/pom.xml @@ -0,0 +1,48 @@ + + + 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 new file mode 100644 index 00000000000000..42eda1b64d7c59 --- /dev/null +++ b/integration-tests/kubernetes-client-hack-extension/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -0,0 +1,11 @@ +--- +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 e0d02c37608926..a08bd4306d3977 100644 --- a/integration-tests/kubernetes-client/pom.xml +++ b/integration-tests/kubernetes-client/pom.xml @@ -22,9 +22,24 @@ 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 + + + io.fabric8 + openshift-model-operator-hub + + org.bouncycastle @@ -75,6 +90,19 @@ + + 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/maven/src/test/java/io/quarkus/maven/it/CreateProjectMojoIT.java b/integration-tests/maven/src/test/java/io/quarkus/maven/it/CreateProjectMojoIT.java index 39ba3756cf3aff..8374d41fe2a172 100644 --- a/integration-tests/maven/src/test/java/io/quarkus/maven/it/CreateProjectMojoIT.java +++ b/integration-tests/maven/src/test/java/io/quarkus/maven/it/CreateProjectMojoIT.java @@ -494,6 +494,24 @@ public void testProjectGenerationFromScratchWithJava17() throws MavenInvocationE .contains("maven.compiler.release>17<"); } + @Test + public void testProjectGenerationFromScratchWithJava21() throws MavenInvocationException, IOException { + testDir = initEmptyProject("projects/project-generation-with-java21"); + assertThat(testDir).isDirectory(); + invoker = initInvoker(testDir); + + Properties properties = new Properties(); + properties.put("javaVersion", "21"); + + InvocationResult result = setup(properties); + assertThat(result.getExitCode()).isZero(); + + testDir = new File(testDir, "code-with-quarkus"); + assertThat(new File(testDir, "pom.xml")).isFile(); + assertThat(FileUtils.readFileToString(new File(testDir, "pom.xml"), "UTF-8")) + .contains("maven.compiler.release>21<"); + } + @Test public void testProjectGenerationFromScratchWithGradleJava11() throws MavenInvocationException, IOException { testDir = initEmptyProject("projects/project-generation-with-gradle-java11"); @@ -532,6 +550,25 @@ public void testProjectGenerationFromScratchWithGradleJava17() throws MavenInvoc .contains("sourceCompatibility = JavaVersion.VERSION_17"); } + @Test + public void testProjectGenerationFromScratchWithGradleJava21() throws MavenInvocationException, IOException { + testDir = initEmptyProject("projects/project-generation-with-gradle-java21"); + assertThat(testDir).isDirectory(); + invoker = initInvoker(testDir); + + Properties properties = new Properties(); + properties.put("javaVersion", "21"); + properties.put("buildTool", "gradle"); + + InvocationResult result = setup(properties); + assertThat(result.getExitCode()).isZero(); + + testDir = new File(testDir, "code-with-quarkus"); + assertThat(new File(testDir, "build.gradle")).isFile(); + assertThat(FileUtils.readFileToString(new File(testDir, "build.gradle"), "UTF-8")) + .contains("sourceCompatibility = JavaVersion.VERSION_21"); + } + /** * Reproducer for https://github.com/quarkusio/quarkus/issues/671 */ diff --git a/integration-tests/oidc-client-wiremock/src/main/java/io/quarkus/it/keycloak/OidcRequestCustomizer.java b/integration-tests/oidc-client-wiremock/src/main/java/io/quarkus/it/keycloak/OidcRequestCustomizer.java index 6f9df502631112..b0d0f2282c034d 100644 --- a/integration-tests/oidc-client-wiremock/src/main/java/io/quarkus/it/keycloak/OidcRequestCustomizer.java +++ b/integration-tests/oidc-client-wiremock/src/main/java/io/quarkus/it/keycloak/OidcRequestCustomizer.java @@ -3,16 +3,17 @@ import jakarta.enterprise.context.ApplicationScoped; import io.quarkus.arc.Unremovable; -import io.quarkus.oidc.common.OidcClientRequestFilter; +import io.quarkus.oidc.common.OidcRequestContextProperties; +import io.quarkus.oidc.common.OidcRequestFilter; import io.vertx.mutiny.core.buffer.Buffer; import io.vertx.mutiny.ext.web.client.HttpRequest; @ApplicationScoped @Unremovable -public class OidcRequestCustomizer implements OidcClientRequestFilter { +public class OidcRequestCustomizer implements OidcRequestFilter { @Override - public void filter(HttpRequest request, Buffer buffer) { + public void filter(HttpRequest request, Buffer buffer, OidcRequestContextProperties contextProps) { String uri = request.uri(); if (uri.endsWith("/non-standard-tokens")) { request.putHeader("GrantType", getGrantType(buffer.toString())); diff --git a/integration-tests/oidc-tenancy/src/main/java/io/quarkus/it/keycloak/OidcResource.java b/integration-tests/oidc-tenancy/src/main/java/io/quarkus/it/keycloak/OidcResource.java index 7082b5ca3c6b0d..76f67a46ae8141 100644 --- a/integration-tests/oidc-tenancy/src/main/java/io/quarkus/it/keycloak/OidcResource.java +++ b/integration-tests/oidc-tenancy/src/main/java/io/quarkus/it/keycloak/OidcResource.java @@ -237,6 +237,16 @@ public String testAccessToken(@QueryParam("kid") String kid, @QueryParam("sub") " \"expires_in\": 300 }"; } + @POST + @Path("accesstoken-empty-scope") + @Produces("application/json") + public String testAccessTokenWithEmptyScope(@QueryParam("kid") String kid, @QueryParam("sub") String subject) { + return "{\"access_token\": \"" + jwt(null, subject, kid, true) + "\"," + + " \"token_type\": \"Bearer\"," + + " \"refresh_token\": \"123456789\"," + + " \"expires_in\": 300 }"; + } + @POST @Path("opaque-token") @Produces("application/json") @@ -290,6 +300,10 @@ public boolean disableRotate() { } private String jwt(String audience, String subject, String kid) { + return jwt(audience, subject, kid, false); + } + + private String jwt(String audience, String subject, String kid, boolean withEmptyScope) { JwtClaimsBuilder builder = Jwt.claim("typ", "Bearer") .upn("alice") .preferredUserName("alice") @@ -302,6 +316,10 @@ private String jwt(String audience, String subject, String kid) { builder.subject(subject); } + if (withEmptyScope) { + builder.claim("scope", ""); + } + return builder.jws().keyId(kid) .sign(key.getPrivateKey()); } diff --git a/integration-tests/oidc-tenancy/src/test/java/io/quarkus/it/keycloak/BearerTokenAuthorizationTest.java b/integration-tests/oidc-tenancy/src/test/java/io/quarkus/it/keycloak/BearerTokenAuthorizationTest.java index 73d7f7b646591e..12fe6f454dd4e9 100644 --- a/integration-tests/oidc-tenancy/src/test/java/io/quarkus/it/keycloak/BearerTokenAuthorizationTest.java +++ b/integration-tests/oidc-tenancy/src/test/java/io/quarkus/it/keycloak/BearerTokenAuthorizationTest.java @@ -517,15 +517,24 @@ public void testJwtTokenIntrospectionOnlyAndUserInfo() { + "introspection_client_id:none,introspection_client_secret:none,active:true,userinfo:alice,cache-size:0")); } + // verifies empty scope claim makes no difference (e.g. doesn't cause NPE) + RestAssured.given().auth().oauth2(getAccessTokenWithEmptyScopeFromSimpleOidc("2")) + .when().get("/tenant/tenant-oidc-introspection-only/api/user") + .then() + .statusCode(200) + .body(equalTo( + "tenant-oidc-introspection-only:alice,client_id:client-introspection-only," + + "introspection_client_id:none,introspection_client_secret:none,active:true,userinfo:alice,cache-size:0")); + RestAssured.given().auth().oauth2(getAccessTokenFromSimpleOidc("987654321", "2")) .when().get("/tenant/tenant-oidc-introspection-only/api/user") .then() .statusCode(401); RestAssured.when().get("/oidc/jwk-endpoint-call-count").then().body(equalTo("0")); - RestAssured.when().get("/oidc/introspection-endpoint-call-count").then().body(equalTo("4")); + RestAssured.when().get("/oidc/introspection-endpoint-call-count").then().body(equalTo("5")); RestAssured.when().post("/oidc/disable-introspection").then().body(equalTo("false")); - RestAssured.when().get("/oidc/userinfo-endpoint-call-count").then().body(equalTo("4")); + RestAssured.when().get("/oidc/userinfo-endpoint-call-count").then().body(equalTo("5")); RestAssured.when().get("/cache/size").then().body(equalTo("0")); } @@ -694,13 +703,21 @@ private String getAccessTokenFromSimpleOidc(String kid) { } private String getAccessTokenFromSimpleOidc(String subject, String kid) { + return getAccessTokenFromSimpleOidc(subject, kid, "/oidc/accesstoken"); + } + + private String getAccessTokenWithEmptyScopeFromSimpleOidc(String kid) { + return getAccessTokenFromSimpleOidc("123456789", kid, "/oidc/accesstoken-empty-scope"); + } + + private static String getAccessTokenFromSimpleOidc(String subject, String kid, String tokenEndpoint) { String json = RestAssured .given() .queryParam("sub", subject) .queryParam("kid", kid) .formParam("grant_type", "authorization_code") .when() - .post("/oidc/accesstoken") + .post(tokenEndpoint) .body().asString(); JsonObject object = new JsonObject(json); return object.getString("access_token"); diff --git a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/OidcRequestCustomizer.java b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/OidcRequestCustomizer.java index 071409f1cea53a..0f76995ecd0ed4 100644 --- a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/OidcRequestCustomizer.java +++ b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/OidcRequestCustomizer.java @@ -3,21 +3,38 @@ import jakarta.enterprise.context.ApplicationScoped; import io.quarkus.arc.Unremovable; -import io.quarkus.oidc.common.OidcClientRequestFilter; +import io.quarkus.oidc.AccessTokenCredential; +import io.quarkus.oidc.common.OidcRequestContextProperties; +import io.quarkus.oidc.common.OidcRequestFilter; import io.vertx.core.http.HttpMethod; import io.vertx.mutiny.core.buffer.Buffer; import io.vertx.mutiny.ext.web.client.HttpRequest; @ApplicationScoped @Unremovable -public class OidcRequestCustomizer implements OidcClientRequestFilter { +public class OidcRequestCustomizer implements OidcRequestFilter { @Override - public void filter(HttpRequest request, Buffer buffer) { + public void filter(HttpRequest request, Buffer buffer, OidcRequestContextProperties contextProps) { HttpMethod method = request.method(); String uri = request.uri(); if (method == HttpMethod.GET && uri.endsWith("/auth/azure/jwk")) { - request.putHeader("Authorization", "ID token"); + String token = contextProps.getString(OidcRequestContextProperties.TOKEN); + AccessTokenCredential tokenCred = contextProps.get(OidcRequestContextProperties.TOKEN_CREDENTIAL, + AccessTokenCredential.class); + // or + // IdTokenCredential tokenCred = contextProps.get(OidcRequestContextProperties.TOKEN_CREDENTIAL, + // IdTokenCredential.class); + // or + // TokenCredential tokenCred = contextProps.get(OidcRequestContextProperties.TOKEN_CREDENTIAL, + // TokenCredential.class); + // if either access or ID token has to be verified and check is it an instanceof + // AccessTokenCredential or IdTokenCredential + // or simply + // String token = contextProps.getString(OidcRequestContextProperties.TOKEN); + if (token.equals(tokenCred.getToken())) { + request.putHeader("Authorization", "Access token: " + token); + } } } diff --git a/integration-tests/oidc-wiremock/src/main/resources/application.properties b/integration-tests/oidc-wiremock/src/main/resources/application.properties index f806a8948240c2..1a0e9556492dee 100644 --- a/integration-tests/oidc-wiremock/src/main/resources/application.properties +++ b/integration-tests/oidc-wiremock/src/main/resources/application.properties @@ -130,6 +130,7 @@ quarkus.oidc.bearer-azure.provider=microsoft quarkus.oidc.bearer-azure.application-type=service quarkus.oidc.bearer-azure.discovery-enabled=false quarkus.oidc.bearer-azure.jwks-path=${keycloak.url}/azure/jwk +quarkus.oidc.bearer-azure.jwks.resolve-early=false quarkus.oidc.bearer-azure.token.lifespan-grace=2147483647 quarkus.oidc.bearer-azure.token.customizer-name=azure-access-token-customizer 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 4c0b332ce82ad2..4e31443081776b 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 @@ -50,11 +50,11 @@ public void testSecureAccessSuccessPreferredUsername() { @Test public void testAccessResourceAzure() throws Exception { + String azureToken = readFile("token.txt"); String azureJwk = readFile("jwks.json"); wireMockServer.stubFor(WireMock.get("/auth/azure/jwk") - .withHeader("Authorization", matching("ID token")) + .withHeader("Authorization", matching("Access token: " + azureToken)) .willReturn(WireMock.aResponse().withBody(azureJwk))); - String azureToken = readFile("token.txt"); RestAssured.given().auth().oauth2(azureToken) .when().get("/api/admin/bearer-azure") .then() diff --git a/integration-tests/openshift-client/pom.xml b/integration-tests/openshift-client/pom.xml index a3d35c28736c74..eef764d55f5e64 100644 --- a/integration-tests/openshift-client/pom.xml +++ b/integration-tests/openshift-client/pom.xml @@ -13,15 +13,34 @@ quarkus-integration-test-openshift-client Quarkus - Integration Tests - OpenShift Client + + true + + io.quarkus quarkus-resteasy-jackson + + io.quarkus + quarkus-integration-test-kubernetes-client-hack-extension + ${project.version} + io.quarkus quarkus-openshift-client ${project.version} + + + io.fabric8 + openshift-model-operator + + + io.fabric8 + openshift-model-operator-hub + + io.quarkus @@ -51,6 +70,19 @@ + + 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/pom.xml b/integration-tests/pom.xml index b623b32388ee67..436687b87d0645 100644 --- a/integration-tests/pom.xml +++ b/integration-tests/pom.xml @@ -285,6 +285,7 @@ amazon-lambda-http-resteasy amazon-lambda-http-resteasy-reactive container-image + kubernetes-client-hack-extension kubernetes kubernetes-client kubernetes-client-devservices @@ -373,6 +374,7 @@ locales redis-devservices + grpc-descriptor-sets grpc-inprocess grpc-vertx grpc-tls diff --git a/pom.xml b/pom.xml index f7323a0c9aa8d6..8e7cfdf42099fd 100644 --- a/pom.xml +++ b/pom.xml @@ -55,7 +55,7 @@ 11 11 true - + ${env.GRAALVM_HOME} jdbc:postgresql:hibernate_orm_test @@ -69,14 +69,14 @@ 0.8.11 - 6.8.1 + 6.9.2 1.59.0 1.2.1 3.24.4 ${protoc.version} - 2.27.0 + 2.28.0 7.4.0