diff --git a/.github/develocity-preapproved-developers.json b/.github/develocity-preapproved-developers.json new file mode 100644 index 0000000000000..8747b70b304e1 --- /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" + ] +} diff --git a/.github/matrix-jvm-tests.json b/.github/matrix-jvm-tests.json index 23255ead23846..62eabe7448386 100644 --- a/.github/matrix-jvm-tests.json +++ b/.github/matrix-jvm-tests.json @@ -1,11 +1,4 @@ [ { - "name": "11", - "java-version": 11, - "maven_args": "$JVM_TEST_MAVEN_ARGS", - "maven_opts": "-Xmx2g -XX:MaxMetaspaceSize=1g", - "os-name": "ubuntu-latest" -} -, { "name": "17", "java-version": 17, "maven_args": "$JVM_TEST_MAVEN_ARGS", diff --git a/.github/native-tests.json b/.github/native-tests.json index 6542e8537d8b9..4308dea0d13c3 100644 --- a/.github/native-tests.json +++ b/.github/native-tests.json @@ -39,7 +39,7 @@ { "category": "Data6", "timeout": 95, - "test-modules": "elasticsearch-rest-client, elasticsearch-java-client, hibernate-search-orm-elasticsearch, hibernate-search-orm-elasticsearch-tenancy, hibernate-search-orm-opensearch, hibernate-search-orm-elasticsearch-coordination-outbox-polling", + "test-modules": "elasticsearch-rest-client, elasticsearch-java-client, hibernate-search-orm-elasticsearch, hibernate-search-orm-elasticsearch-tenancy, hibernate-search-orm-opensearch, hibernate-search-orm-elasticsearch-outbox-polling", "os-name": "ubuntu-latest" }, { diff --git a/.github/quarkus-github-bot.yml b/.github/quarkus-github-bot.yml index 3e580c63ce584..78f3ce633c3e3 100644 --- a/.github/quarkus-github-bot.yml +++ b/.github/quarkus-github-bot.yml @@ -68,6 +68,11 @@ triage: title: "dev.?ui" notify: [phillip-kruger, cescoffier] notifyInPullRequest: true + - id: mvnpm + labels: [area/dev-ui] + title: "(mvnpm|mvnpm.org)" + notify: [phillip-kruger] + notifyInPullRequest: true - labels: [area/gradle] title: "gradle" notify: [quarkusio/devtools, glefloch] diff --git a/.github/verify-tests-json.sh b/.github/verify-tests-json.sh new file mode 100755 index 0000000000000..22fa8cfa16364 --- /dev/null +++ b/.github/verify-tests-json.sh @@ -0,0 +1,41 @@ +#!/bin/bash + +# Purpose: Verify that a JSON file such as native-tests.json only contains valid references in its "test-modules" fields +# Note: This script is only for CI and does therefore not aim to be compatible with BSD/macOS. + +set -e -u -o pipefail +shopt -s failglob + +# path of this shell script +PRG_PATH=$( cd "$(dirname "$0")" ; pwd -P ) + +if (( $# != 2 )) +then + echo 'Invalid parameters' + echo 'Usage: $0 ' + exit 1 +fi + +JSON_PATH="${PRG_PATH}/$1" +IT_DIR_PATH="$2" + +echo "Checking JSON file $JSON_PATH against modules in directory $IT_DIR_PATH" + +INVALID_REFS=($( + # Print unmatched names from the JSON file + # Input 1: List all test modules from the JSON file, one per line, trimmed, sorted + # Input 2: List all Maven modules, one per line, sorted + join -v 1 \ + <(jq -r '.include[] | ."test-modules"' ${PRG_PATH}/$1 | tr ',' $'\n' | sed 's|^\s*||;s|\s*$||;' | grep -v '^$' | sort) \ + <(find "$IT_DIR_PATH" -mindepth 2 -name pom.xml -exec realpath --relative-to "$IT_DIR_PATH" '{}' \; | xargs -d $'\n' -n 1 dirname | sort) +)) + +if [[ ${#INVALID_REFS[@]} = 0 ]] +then + echo "$JSON_PATH is valid when checked against $IT_DIR_PATH" + exit 0 +else + echo "$JSON_PATH is invalid when checked against $IT_DIR_PATH" + echo "'test-modules' that cannot be resolved as paths to Maven modules relative to $IT_DIR_PATH: ${INVALID_REFS[*]}" + exit 1 +fi \ No newline at end of file diff --git a/.github/workflows/ci-actions-incremental.yml b/.github/workflows/ci-actions-incremental.yml index 92b758f0d09ef..25bc005e8cda9 100644 --- a/.github/workflows/ci-actions-incremental.yml +++ b/.github/workflows/ci-actions-incremental.yml @@ -83,8 +83,8 @@ jobs: steps: - name: Build run: sleep 30 - build-jdk11: - name: "Initial JDK 11 Build" + build-jdk17: + name: "Initial JDK 17 Build" runs-on: ubuntu-latest # Skip main in forks # Skip draft PRs and those with WIP in the subject, rerun as soon as its removed @@ -108,8 +108,8 @@ jobs: steps: - name: Gradle Enterprise environment run: | - echo "GE_TAGS=jdk-11" >> "$GITHUB_ENV" - echo "GE_CUSTOM_VALUES=gh-job-name=Initial JDK 11 Build" >> "$GITHUB_ENV" + echo "GE_TAGS=jdk-17" >> "$GITHUB_ENV" + echo "GE_CUSTOM_VALUES=gh-job-name=Initial JDK 17 Build" >> "$GITHUB_ENV" - uses: actions/checkout@v4 with: # this is important for GIB to work @@ -118,11 +118,11 @@ jobs: run: git remote show quarkusio &> /dev/null || git remote add quarkusio https://github.com/quarkusio/quarkus.git - name: Reclaim Disk Space run: .github/ci-prerequisites.sh - - name: Set up JDK 11 - uses: actions/setup-java@v3 + - name: Set up JDK 17 + uses: actions/setup-java@v4 with: distribution: temurin - java-version: 11 + java-version: 17 - name: Generate .m2 cache key id: m2-cache-key run: | @@ -134,7 +134,19 @@ jobs: path: ~/.m2/repository # refresh cache every week to avoid unlimited growth key: ${{ steps.m2-cache-key.outputs.key }} + - name: Verify native-tests.json + run: ./.github/verify-tests-json.sh native-tests.json integration-tests/ + - name: Verify virtual-threads-tests.json + run: ./.github/verify-tests-json.sh virtual-threads-tests.json integration-tests/virtual-threads/ + - name: Setup Develocity Build Scan capture + uses: gradle/github-actions/maven-build-scan-setup@v0.2 + with: + build-scan-capture-strategy: ON_DEMAND + job-name: "Initial JDK 17 Build" + wrapper-init: true - name: Build + env: + CAPTURE_BUILD_SCAN: true run: | ./mvnw -T1C $COMMON_MAVEN_ARGS -DskipTests -DskipITs -DskipDocs -Dinvoker.skip -Dno-format -Dtcks -Prelocations clean install - name: Verify extension dependencies @@ -193,26 +205,21 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ failure() || cancelled() }} with: - name: "build-reports-Initial JDK 11 Build" + name: "build-reports-Initial JDK 17 Build" path: | target/build-report.json 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 runs-on: ubuntu-latest # Skip main in forks if: "github.repository == 'quarkusio/quarkus' || !endsWith(github.ref, '/main')" - needs: build-jdk11 + needs: build-jdk17 env: - GIB_IMPACTED_MODULES: ${{ needs.build-jdk11.outputs.gib_impacted }} + GIB_IMPACTED_MODULES: ${{ needs.build-jdk17.outputs.gib_impacted }} outputs: native_matrix: ${{ steps.calc-native-matrix.outputs.matrix }} jvm_matrix: ${{ steps.calc-jvm-matrix.outputs.matrix }} @@ -276,7 +283,7 @@ jobs: jvm-tests: name: JVM Tests - JDK ${{matrix.java.name}} runs-on: ${{ matrix.java.os-name }} - needs: [build-jdk11, calculate-test-jobs] + needs: [build-jdk17, calculate-test-jobs] # Skip main in forks if: "needs.calculate-test-jobs.outputs.run_jvm == 'true' && (github.repository == 'quarkusio/quarkus' || !endsWith(github.ref, '/main'))" timeout-minutes: 400 @@ -317,7 +324,7 @@ jobs: - name: Set up JDK ${{ env.JAVA_VERSION_GRADLE }} for Gradle (if needed) if: ${{ env.JAVA_VERSION_GRADLE != matrix.java.java-version }} - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: temurin java-version: ${{ matrix.java.java-version-gradle }} @@ -331,7 +338,7 @@ jobs: echo "GRADLE_JAVA_HOME=${!GRADLE_JAVA_HOME_VARIABLE}" >> "$GITHUB_ENV" - name: Set up JDK ${{ matrix.java.java-version }} - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: ${{ matrix.java.java-distribution || 'temurin' }} java-version: ${{ matrix.java.java-version }} @@ -342,7 +349,7 @@ jobs: with: path: ~/.m2/repository # refresh cache every week to avoid unlimited growth - key: ${{ needs.build-jdk11.outputs.m2-cache-key }} + key: ${{ needs.build-jdk17.outputs.m2-cache-key }} - name: Download .m2/repository/io/quarkus uses: actions/download-artifact@v3 with: @@ -350,9 +357,17 @@ jobs: path: . - name: Extract .m2/repository/io/quarkus run: tar -xzf m2-io-quarkus.tgz -C ~ + - name: Setup Develocity Build Scan capture + uses: gradle/github-actions/maven-build-scan-setup@v0.2 + with: + build-scan-capture-strategy: ON_DEMAND + job-name: "JVM Tests - JDK ${{matrix.java.name}}" + wrapper-init: true - name: Build + env: + CAPTURE_BUILD_SCAN: true # 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 clean install -Dsurefire.timeout=1200 -pl !integration-tests/gradle -pl !integration-tests/maven -pl !integration-tests/devmode -pl !integration-tests/devtools -Dno-test-kubernetes -pl !docs ${{ matrix.java.maven_args }} ${{ needs.build-jdk11.outputs.gib_args }} + run: ./mvnw $COMMON_MAVEN_ARGS clean install -Dsurefire.timeout=1200 -pl !integration-tests/gradle -pl !integration-tests/maven -pl !integration-tests/devmode -pl !integration-tests/devtools -Dno-test-kubernetes -pl !docs ${{ matrix.java.maven_args }} ${{ needs.build-jdk17.outputs.gib_args }} - name: Clean Gradle temp directory if: always() run: devtools/gradle/gradlew --stop && rm -rf devtools/gradle/gradle-extension-plugin/build/tmp @@ -382,7 +397,7 @@ jobs: with: name: "GC log - JDK ${{matrix.java.name}}" path: | - **/windows-java-11.txt + **/windows-java-17.txt !**/build/tmp/** retention-days: 5 - name: Upload build.log (if build failed) @@ -393,22 +408,11 @@ 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}} runs-on: ${{ matrix.java.os-name }} - needs: [build-jdk11, calculate-test-jobs] + needs: [build-jdk17, calculate-test-jobs] env: MAVEN_OPTS: -Xmx2g -XX:MaxMetaspaceSize=1g # Skip main in forks @@ -419,13 +423,13 @@ jobs: matrix: java: - { - name: "11", - java-version: 11, + name: "17", + java-version: 17, os-name: "ubuntu-latest" } - { - name: "11 Windows", - java-version: 11, + name: "17 Windows", + java-version: 17, os-name: "windows-latest" } steps: @@ -447,7 +451,7 @@ jobs: with: path: ~/.m2/repository # refresh cache every week to avoid unlimited growth - key: ${{ needs.build-jdk11.outputs.m2-cache-key }} + key: ${{ needs.build-jdk17.outputs.m2-cache-key }} - name: Download .m2/repository/io/quarkus uses: actions/download-artifact@v3 with: @@ -456,14 +460,22 @@ jobs: - name: Extract .m2/repository/io/quarkus run: tar -xzf m2-io-quarkus.tgz -C ~ - name: Set up JDK ${{ matrix.java.java-version }} - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: temurin java-version: ${{ matrix.java.java-version }} + - name: Setup Develocity Build Scan capture + uses: gradle/github-actions/maven-build-scan-setup@v0.2 + with: + build-scan-capture-strategy: ON_DEMAND + job-name: "Maven Tests - JDK ${{matrix.java.name}}" + wrapper-init: true - name: Build + env: + CAPTURE_BUILD_SCAN: true # 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 }} + run: ./mvnw $COMMON_MAVEN_ARGS $JVM_TEST_MAVEN_ARGS clean install -pl 'integration-tests/maven' -pl 'integration-tests/devmode' ${{ needs.build-jdk17.outputs.gib_args }} - name: Prepare failure archive (if maven failed) if: failure() run: find . -name '*-reports' -type d -o -name '*.log' | tar -czf test-reports.tgz -T - @@ -492,16 +504,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}} runs-on: ${{matrix.java.os-name}} - needs: [build-jdk11, calculate-test-jobs] + needs: [build-jdk17, calculate-test-jobs] env: # leave more space for the actual gradle execution (which is just wrapped by maven) MAVEN_OPTS: -Xmx1g @@ -513,13 +520,13 @@ jobs: matrix: java: - { - name: "11", - java-version: 11, + name: "17", + java-version: 17, os-name: "ubuntu-latest" } - { - name: "11 Windows", - java-version: 11, + name: "17 Windows", + java-version: 17, os-name: "windows-latest" } steps: @@ -536,7 +543,7 @@ jobs: with: path: ~/.m2/repository # refresh cache every week to avoid unlimited growth - key: ${{ needs.build-jdk11.outputs.m2-cache-key }} + key: ${{ needs.build-jdk17.outputs.m2-cache-key }} - name: Download .m2/repository/io/quarkus uses: actions/download-artifact@v3 with: @@ -545,7 +552,7 @@ jobs: - name: Extract .m2/repository/io/quarkus run: tar -xzf m2-io-quarkus.tgz -C ~ - name: Set up JDK ${{ matrix.java.java-version }} - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: temurin java-version: ${{ matrix.java.java-version }} @@ -554,7 +561,15 @@ jobs: # runs on Windows as well but would require newline conversion, not worth it if: "!startsWith(matrix.java.os-name, 'windows')" run: ./integration-tests/gradle/update-dependencies.sh $COMMON_MAVEN_ARGS -Dscan=false + - name: Setup Develocity Build Scan capture + uses: gradle/github-actions/maven-build-scan-setup@v0.2 + with: + build-scan-capture-strategy: ON_DEMAND + job-name: "Gradle Tests - JDK ${{matrix.java.name}}" + wrapper-init: true - name: Build + env: + CAPTURE_BUILD_SCAN: true # Important: keep -pl ... in sync with "Calculate run flags"! run: ./mvnw $COMMON_MAVEN_ARGS $JVM_TEST_MAVEN_ARGS clean install -pl integration-tests/gradle - name: Upload build reports (if build failed) @@ -569,16 +584,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}} runs-on: ${{matrix.java.os-name}} - needs: [build-jdk11, calculate-test-jobs] + needs: [build-jdk17, calculate-test-jobs] # Skip main in forks if: "needs.calculate-test-jobs.outputs.run_devtools == 'true' && (github.repository == 'quarkusio/quarkus' || !endsWith(github.ref, '/main'))" timeout-minutes: 60 @@ -587,18 +597,18 @@ jobs: matrix: java: - { - name: "11", - java-version: 11, + name: "7", + java-version: 17, os-name: "ubuntu-latest" } - { - name: "17", - java-version: 17, + name: "21", + java-version: 21, os-name: "ubuntu-latest" } - { - name: "11 Windows", - java-version: 11, + name: "17 Windows", + java-version: 17, os-name: "windows-latest" } steps: @@ -615,7 +625,7 @@ jobs: with: path: ~/.m2/repository # refresh cache every week to avoid unlimited growth - key: ${{ needs.build-jdk11.outputs.m2-cache-key }} + key: ${{ needs.build-jdk17.outputs.m2-cache-key }} - name: Download .m2/repository/io/quarkus uses: actions/download-artifact@v3 with: @@ -624,11 +634,19 @@ jobs: - name: Extract .m2/repository/io/quarkus run: tar -xzf m2-io-quarkus.tgz -C ~ - name: Set up JDK ${{ matrix.java.java-version }} - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: temurin java-version: ${{ matrix.java.java-version }} + - name: Setup Develocity Build Scan capture + uses: gradle/github-actions/maven-build-scan-setup@v0.2 + with: + build-scan-capture-strategy: ON_DEMAND + job-name: "Devtools Tests - JDK ${{matrix.java.name}}" + wrapper-init: true - name: Build + env: + CAPTURE_BUILD_SCAN: true # Important: keep -pl ... in sync with "Calculate run flags"! run: ./mvnw $COMMON_MAVEN_ARGS $JVM_TEST_MAVEN_ARGS clean install -pl 'integration-tests/devtools' - name: Prepare failure archive (if maven failed) @@ -652,16 +670,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}} runs-on: ${{matrix.java.os-name}} - needs: [build-jdk11, calculate-test-jobs] + needs: [build-jdk17, calculate-test-jobs] # Skip main in forks if: "needs.calculate-test-jobs.outputs.run_kubernetes == 'true' && (github.repository == 'quarkusio/quarkus' || !endsWith(github.ref, '/main'))" timeout-minutes: 40 @@ -670,18 +683,18 @@ jobs: matrix: java: - { - name: "11", - java-version: 11, + name: "17", + java-version: 17, os-name: "ubuntu-latest" } - { - name: "17", - java-version: 17, + name: "21", + java-version: 21, os-name: "ubuntu-latest" } - { - name: "11 Windows", - java-version: 11, + name: "17 Windows", + java-version: 17, os-name: "windows-latest" } steps: @@ -698,7 +711,7 @@ jobs: with: path: ~/.m2/repository # refresh cache every week to avoid unlimited growth - key: ${{ needs.build-jdk11.outputs.m2-cache-key }} + key: ${{ needs.build-jdk17.outputs.m2-cache-key }} - name: Download .m2/repository/io/quarkus uses: actions/download-artifact@v3 with: @@ -707,11 +720,19 @@ jobs: - name: Extract .m2/repository/io/quarkus run: tar -xzf m2-io-quarkus.tgz -C ~ - name: Set up JDK ${{ matrix.java.java-version }} - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: temurin java-version: ${{ matrix.java.java-version }} + - name: Setup Develocity Build Scan capture + uses: gradle/github-actions/maven-build-scan-setup@v0.2 + with: + build-scan-capture-strategy: ON_DEMAND + job-name: "Kubernetes Tests - JDK ${{matrix.java.name}}" + wrapper-init: true - name: Build + env: + CAPTURE_BUILD_SCAN: true # Important: keep -pl ... in sync with "Calculate run flags"! run: ./mvnw $COMMON_MAVEN_ARGS $JVM_TEST_MAVEN_ARGS clean install -f 'integration-tests/kubernetes' - name: Prepare failure archive (if maven failed) @@ -735,16 +756,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: "Kubernetes Tests - JDK ${{matrix.java.name}}" quickstarts-tests: name: Quickstarts Compilation - JDK ${{matrix.java.name}} runs-on: ${{matrix.java.os-name}} - needs: [build-jdk11, calculate-test-jobs] + needs: [build-jdk17, calculate-test-jobs] # Skip main in forks if: "needs.calculate-test-jobs.outputs.run_quickstarts == 'true' && (github.repository == 'quarkusio/quarkus' || !endsWith(github.ref, '/main'))" timeout-minutes: 90 @@ -768,7 +784,7 @@ jobs: with: path: ~/.m2/repository # refresh cache every week to avoid unlimited growth - key: ${{ needs.build-jdk11.outputs.m2-cache-key }} + key: ${{ needs.build-jdk17.outputs.m2-cache-key }} - name: Download .m2/repository/io/quarkus uses: actions/download-artifact@v3 with: @@ -777,11 +793,19 @@ jobs: - name: Extract .m2/repository/io/quarkus run: tar -xzf m2-io-quarkus.tgz -C ~ - name: Set up JDK ${{ matrix.java.java-version }} - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: temurin java-version: ${{ matrix.java.java-version }} + - name: Setup Develocity Build Scan capture + uses: gradle/github-actions/maven-build-scan-setup@v0.2 + with: + build-scan-capture-strategy: ON_DEMAND + job-name: "Quickstarts Compilation - JDK ${{matrix.java.name}}" + wrapper-init: true - name: Compile Quickstarts + env: + CAPTURE_BUILD_SCAN: true run: | git clone https://github.com/quarkusio/quarkus-quickstarts.git && cd quarkus-quickstarts git checkout development @@ -796,16 +820,11 @@ 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}} - needs: [build-jdk11, calculate-test-jobs] + needs: [build-jdk17, calculate-test-jobs] # Skip main in forks if: "needs.calculate-test-jobs.outputs.virtual_threads_matrix != '{}' && (github.repository == 'quarkusio/quarkus' || !endsWith(github.ref, '/main'))" timeout-minutes: ${{matrix.timeout}} @@ -825,7 +844,7 @@ jobs: with: path: ~/.m2/repository # refresh cache every week to avoid unlimited growth - key: ${{ needs.build-jdk11.outputs.m2-cache-key }} + key: ${{ needs.build-jdk17.outputs.m2-cache-key }} - name: Download .m2/repository/io/quarkus uses: actions/download-artifact@v3 with: @@ -834,7 +853,7 @@ jobs: - name: Extract .m2/repository/io/quarkus run: tar -xzf m2-io-quarkus.tgz -C ~ - name: Set up JDK 21 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: temurin java-version: 21 @@ -842,10 +861,17 @@ jobs: - name: Update Docker Client User Agent run: | cat <<< $(jq '.HttpHeaders += {"User-Agent": "Quarkus-CI-Docker-Client"}' ~/.docker/config.json) > ~/.docker/config.json + - name: Setup Develocity Build Scan capture + uses: gradle/github-actions/maven-build-scan-setup@v0.2 + with: + build-scan-capture-strategy: ON_DEMAND + job-name: "Native Tests - Virtual Thread - ${{matrix.category}}" + wrapper-init: true - name: Build env: TEST_MODULES: ${{matrix.test-modules}} CONTAINER_BUILD: ${{startsWith(matrix.os-name, 'windows') && 'false' || 'true'}} + CAPTURE_BUILD_SCAN: true run: | export LANG=en_US && ./mvnw $COMMON_MAVEN_ARGS -f integration-tests/virtual-threads -pl "$TEST_MODULES" $NATIVE_TEST_MAVEN_ARGS -Dextra-args=--enable-preview -Dquarkus.native.container-build=true - name: Upload build reports (if build failed) @@ -858,15 +884,10 @@ 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] + needs: [build-jdk17, calculate-test-jobs] # Skip main in forks if: "needs.calculate-test-jobs.outputs.run_tcks == 'true' && (github.repository == 'quarkusio/quarkus' || !endsWith(github.ref, '/main'))" runs-on: ubuntu-latest @@ -874,7 +895,7 @@ jobs: steps: - name: Gradle Enterprise environment run: | - echo "GE_TAGS=jdk-11" >> "$GITHUB_ENV" + echo "GE_TAGS=jdk-17" >> "$GITHUB_ENV" echo "GE_CUSTOM_VALUES=gh-job-name=MicroProfile TCKs Tests" >> "$GITHUB_ENV" - uses: actions/checkout@v4 with: @@ -884,17 +905,17 @@ jobs: run: git remote show quarkusio &> /dev/null || git remote add quarkusio https://github.com/quarkusio/quarkus.git - name: Reclaim Disk Space run: .github/ci-prerequisites.sh - - name: Set up JDK 11 - uses: actions/setup-java@v3 + - name: Set up JDK 17 + uses: actions/setup-java@v4 with: distribution: temurin - java-version: 11 + java-version: 17 - name: Restore Maven Repository uses: actions/cache/restore@v3 with: path: ~/.m2/repository # refresh cache every week to avoid unlimited growth - key: ${{ needs.build-jdk11.outputs.m2-cache-key }} + key: ${{ needs.build-jdk17.outputs.m2-cache-key }} - name: Download .m2/repository/io/quarkus uses: actions/download-artifact@v3 with: @@ -902,10 +923,18 @@ jobs: path: . - name: Extract .m2/repository/io/quarkus run: tar -xzf m2-io-quarkus.tgz -C ~ + - name: Setup Develocity Build Scan capture + uses: gradle/github-actions/maven-build-scan-setup@v0.2 + with: + build-scan-capture-strategy: ON_DEMAND + job-name: "MicroProfile TCKs Tests" + wrapper-init: true - name: Verify + env: + CAPTURE_BUILD_SCAN: true # Important: keep -pl ... in sync with "Calculate run flags"! # Despite the pre-calculated run_tcks flag, GIB has to be re-run here to figure out the exact tcks submodules to build. - run: ./mvnw $COMMON_MAVEN_ARGS -Dtcks -pl tcks -amd clean install ${{ needs.build-jdk11.outputs.gib_args }} + run: ./mvnw $COMMON_MAVEN_ARGS -Dtcks -pl tcks -amd clean install ${{ needs.build-jdk17.outputs.gib_args }} - name: Verify resteasy-reative dependencies # note: ideally, this would be run _before_ mvnw but that would required building tcks/resteasy-reactive in two steps run: ./tcks/resteasy-reactive/update-dependencies.sh $COMMON_MAVEN_ARGS @@ -930,15 +959,10 @@ 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}} - needs: [build-jdk11, calculate-test-jobs] + needs: [build-jdk17, calculate-test-jobs] runs-on: ${{matrix.os-name}} env: # leave more space for the actual native compilation and execution @@ -964,11 +988,11 @@ jobs: - name: Reclaim Disk Space run: .github/ci-prerequisites.sh if: ${{ !startsWith(matrix.os-name, 'windows') }} - - name: Set up JDK 11 - uses: actions/setup-java@v3 + - name: Set up JDK 17 + uses: actions/setup-java@v4 with: distribution: temurin - java-version: 11 + java-version: 17 - name: Install cl.exe if: startsWith(matrix.os-name, 'windows') uses: ilammy/msvc-dev-cmd@v1 @@ -992,7 +1016,7 @@ jobs: with: path: ~/.m2/repository # refresh cache every week to avoid unlimited growth - key: ${{ needs.build-jdk11.outputs.m2-cache-key }} + key: ${{ needs.build-jdk17.outputs.m2-cache-key }} - name: Download .m2/repository/io/quarkus uses: actions/download-artifact@v3 with: @@ -1000,10 +1024,17 @@ jobs: path: . - name: Extract .m2/repository/io/quarkus run: tar -xzf m2-io-quarkus.tgz -C ~ + - name: Setup Develocity Build Scan capture + uses: gradle/github-actions/maven-build-scan-setup@v0.2 + with: + build-scan-capture-strategy: ON_DEMAND + job-name: "Native Tests - ${{matrix.category}}" + wrapper-init: true - name: Build env: TEST_MODULES: ${{matrix.test-modules}} CONTAINER_BUILD: ${{startsWith(matrix.os-name, 'windows') && 'false' || 'true'}} + CAPTURE_BUILD_SCAN: true run: ./mvnw $COMMON_MAVEN_ARGS -f integration-tests -pl "$TEST_MODULES" $NATIVE_TEST_MAVEN_ARGS -Dquarkus.native.container-build=$CONTAINER_BUILD - name: Prepare failure archive (if maven failed) if: failure() @@ -1027,23 +1058,18 @@ 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 name: Build report - needs: [build-jdk11,jvm-tests,maven-tests,gradle-tests,devtools-tests,kubernetes-tests,quickstarts-tests,tcks-test,native-tests,virtual-thread-native-tests] + needs: [build-jdk17,jvm-tests,maven-tests,gradle-tests,devtools-tests,kubernetes-tests,quickstarts-tests,tcks-test,native-tests,virtual-thread-native-tests] if: always() steps: - uses: actions/download-artifact@v3 with: path: build-reports-artifacts - name: Set up JDK 17 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: temurin java-version: 17 diff --git a/.github/workflows/ci-fork-mvn-cache.yml b/.github/workflows/ci-fork-mvn-cache.yml index 13f7c8d6ce21b..dd8140e97fc3c 100644 --- a/.github/workflows/ci-fork-mvn-cache.yml +++ b/.github/workflows/ci-fork-mvn-cache.yml @@ -29,8 +29,8 @@ concurrency: env: LANG: en_US.UTF-8 jobs: - build-jdk11: - name: "Quick JDK 11 Build" + build-jdk17: + name: "Quick JDK 17 Build" runs-on: ubuntu-latest # Skip in main repo if: github.repository != 'quarkusio/quarkus' @@ -38,11 +38,11 @@ jobs: - uses: actions/checkout@v4 - name: Reclaim Disk Space run: .github/ci-prerequisites.sh - - name: Set up JDK 11 - uses: actions/setup-java@v3 + - name: Set up JDK 17 + uses: actions/setup-java@v4 with: distribution: temurin - java-version: 11 + java-version: 17 - name: Get Date id: get-date run: | diff --git a/.github/workflows/ci-istio.yml b/.github/workflows/ci-istio.yml index 90fc13bb35cb7..8fc760b6d499e 100644 --- a/.github/workflows/ci-istio.yml +++ b/.github/workflows/ci-istio.yml @@ -16,7 +16,7 @@ jobs: if: "github.repository == 'quarkusio/quarkus' || github.event_name == 'workflow_dispatch'" steps: - uses: actions/checkout@v4 - - uses: actions/setup-java@v3 + - uses: actions/setup-java@v4 with: distribution: 'temurin' java-version: '17' @@ -44,7 +44,7 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 - - uses: actions/setup-java@v3 + - uses: actions/setup-java@v4 with: distribution: 'temurin' java-version: '17' diff --git a/.github/workflows/ci-kubernetes.yml b/.github/workflows/ci-kubernetes.yml index c609523620cc1..68caaeb5069cd 100644 --- a/.github/workflows/ci-kubernetes.yml +++ b/.github/workflows/ci-kubernetes.yml @@ -16,7 +16,7 @@ jobs: if: "github.repository == 'quarkusio/quarkus' || github.event_name == 'workflow_dispatch'" steps: - uses: actions/checkout@v4 - - uses: actions/setup-java@v3 + - uses: actions/setup-java@v4 with: distribution: 'temurin' java-version: '17' @@ -44,7 +44,7 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 - - uses: actions/setup-java@v3 + - uses: actions/setup-java@v4 with: distribution: 'temurin' java-version: '17' @@ -101,7 +101,7 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 - - uses: actions/setup-java@v3 + - uses: actions/setup-java@v4 with: distribution: 'temurin' java-version: '17' @@ -155,7 +155,7 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 - - uses: actions/setup-java@v3 + - uses: actions/setup-java@v4 with: distribution: 'temurin' java-version: '17' diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 885d9c663cd2c..2f7c5e36cda32 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -32,10 +32,10 @@ jobs: fetch-depth: 1 ref: main - name: Setup Java JDK - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: temurin - java-version: 11 + java-version: 17 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL @@ -43,14 +43,14 @@ jobs: with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. + # By default, queries listed here will override any specified in a config file. # Prefix the list here with "+" to use these queries and those in the config file. # queries: ./path/to/local/query, your-org/your-repo/queries@main # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - - if: matrix.language == 'java' + - if: matrix.language == 'java' name: Build Java run: ./mvnw -B --settings .github/mvn-settings.xml -Dquickly-ci install diff --git a/.github/workflows/deploy-snapshots.yml b/.github/workflows/deploy-snapshots.yml index 6572ca746670c..0f439b840b711 100644 --- a/.github/workflows/deploy-snapshots.yml +++ b/.github/workflows/deploy-snapshots.yml @@ -20,11 +20,11 @@ jobs: ref: main - name: Reclaim Disk Space run: .github/ci-prerequisites.sh - - name: Set up JDK 11 - uses: actions/setup-java@v3 + - name: Set up JDK 17 + uses: actions/setup-java@v4 with: distribution: temurin - java-version: 11 + java-version: 17 - name: Get Date id: get-date run: | diff --git a/.github/workflows/develocity-publish-build-scans.yml b/.github/workflows/develocity-publish-build-scans.yml index c391c6bc68608..2b51d80b75056 100644 --- a/.github/workflows/develocity-publish-build-scans.yml +++ b/.github/workflows/develocity-publish-build-scans.yml @@ -14,21 +14,37 @@ jobs: if: github.repository == 'quarkusio/quarkus' && github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion != 'cancelled' runs-on: ubuntu-latest permissions: + actions: write pull-requests: write checks: write steps: + - uses: actions/checkout@v4 + - name: Extract preapproved developers list + id: extract-preapproved-developers + run: | + echo "preapproved-developpers<> $GITHUB_OUTPUT + cat .github/develocity-preapproved-developers.json >> $GITHUB_OUTPUT + echo >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + - name: Setup Build Scan link capture + if: ${{ contains(fromJson(steps.extract-preapproved-developers.outputs.preapproved-developpers).preapproved-developers, github.event.workflow_run.actor.login) }} + uses: gradle/github-actions/maven-build-scan-setup@v0.2 - 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) }} + uses: gradle/github-actions/maven-build-scan-publish@v0.2 with: develocity-url: 'https://ge.quarkus.io' develocity-access-key: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} skip-comment: true + skip-summary: true - name: Inject build scans in reports + if: ${{ contains(fromJson(steps.extract-preapproved-developers.outputs.preapproved-developpers).preapproved-developers, github.event.workflow_run.actor.login) }} uses: quarkusio/action-helpers@main with: github-token: ${{ secrets.GITHUB_TOKEN }} action: inject-build-scans workflow-run-id: ${{ github.event.workflow_run.id }} - name: Output JSON file + if: ${{ contains(fromJson(steps.extract-preapproved-developers.outputs.preapproved-developpers).preapproved-developers, github.event.workflow_run.actor.login) }} run: | - if [ -f build-metadata.json ]; then jq '.' build-metadata.json >> $GITHUB_STEP_SUMMARY; fi + if [ -f $HOME/build-metadata.json ]; then jq '.' $HOME/build-metadata.json >> $GITHUB_STEP_SUMMARY; fi diff --git a/.github/workflows/doc-build.yml b/.github/workflows/doc-build.yml index 48cf245ea0c8c..80e6c455edb4e 100644 --- a/.github/workflows/doc-build.yml +++ b/.github/workflows/doc-build.yml @@ -34,11 +34,11 @@ jobs: if: "github.repository == 'quarkusio/quarkus' || !endsWith(github.ref, '/main')" steps: - uses: actions/checkout@v4 - - name: Set up JDK 11 - uses: actions/setup-java@v3 + - name: Set up JDK 17 + uses: actions/setup-java@v4 with: distribution: temurin - java-version: 11 + java-version: 17 - name: Get Date id: get-date run: | diff --git a/.github/workflows/jdk-early-access-build.yml b/.github/workflows/jdk-early-access-build.yml index 1de2069e26358..658d9c0d16b93 100644 --- a/.github/workflows/jdk-early-access-build.yml +++ b/.github/workflows/jdk-early-access-build.yml @@ -59,7 +59,7 @@ jobs: release: ${{ matrix.version }} - name: Set up JDK from other provider than jdk.java.net if: matrix.dist != 'jdk.java.net' - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: ${{ matrix.dist }} java-version: ${{ matrix.version }} diff --git a/.github/workflows/owasp-check.yml b/.github/workflows/owasp-check.yml index f432a94454e0e..a03900952b881 100644 --- a/.github/workflows/owasp-check.yml +++ b/.github/workflows/owasp-check.yml @@ -13,7 +13,7 @@ jobs: strategy: fail-fast: false - + steps: - name: Checkout repository uses: actions/checkout@v4 @@ -21,10 +21,10 @@ jobs: fetch-depth: 1 ref: main - name: Setup Java JDK - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: temurin - java-version: 11 + java-version: 17 - name: Build Java diff --git a/.github/workflows/podman-build.yml b/.github/workflows/podman-build.yml index 840933b3e146e..d781457c77fa4 100644 --- a/.github/workflows/podman-build.yml +++ b/.github/workflows/podman-build.yml @@ -52,7 +52,7 @@ jobs: if: "!startsWith(matrix.java.os-name, 'windows') && !startsWith(matrix.java.os-name, 'macos')" run: .github/ci-prerequisites.sh - name: Set up JDK 17 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: temurin java-version: 17 diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml index 6e4ea70aff830..33adbbdecaa23 100644 --- a/.github/workflows/release-build.yml +++ b/.github/workflows/release-build.yml @@ -19,11 +19,11 @@ jobs: ref: main - name: Reclaim Disk Space run: .github/ci-prerequisites.sh - - name: Set up JDK 11 - uses: actions/setup-java@v3 + - name: Set up JDK 17 + uses: actions/setup-java@v4 with: distribution: temurin - java-version: 11 + java-version: 17 - name: Create maven repo run: mkdir -p $HOME/release/repository - name: Build and Test diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index 37ba96684f104..6586a11df6a7a 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -1,11 +1,8 @@ name: Sonarcloud Analysis on: -# push: -# branches: -# - main -# pull_request_target: -# types: [opened, synchronize, reopened] workflow_dispatch: + schedule: + - cron: '30 5 * * 0' jobs: build: name: Build @@ -14,11 +11,11 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis - - name: Set up JDK 11 - uses: actions/setup-java@v3 + - name: Set up JDK 17 + uses: actions/setup-java@v4 with: distribution: temurin - java-version: 11 + java-version: 17 - name: Get Date id: get-date run: | diff --git a/.github/workflows/sonarcloud.yml.disabled b/.github/workflows/sonarcloud.yml.disabled deleted file mode 100644 index 6badee103d86a..0000000000000 --- a/.github/workflows/sonarcloud.yml.disabled +++ /dev/null @@ -1,40 +0,0 @@ -name: Sonarcloud Analysis -#on: -# push: -# branches: -# - main -# pull_request: -# types: [opened, synchronize, reopened] -jobs: - build: - name: Build - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - with: - fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis - - name: Set up JDK 11 - uses: actions/setup-java@v2 - with: - distribution: temurin - java-version: 11 - - name: Get Date - id: get-date - run: | - echo "date=$(/bin/date -u "+%Y-%m")" >> $GITHUB_OUTPUT - shell: bash - - name: Cache SonarCloud packages - uses: actions/cache@v2 - with: - path: ~/.sonar/cache - key: ${{ runner.os }}-sonar-${{ steps.get-date.outputs.date }} - - name: Cache Maven packages - uses: actions/cache@v2 - with: - path: ~/.m2 - key: ${{ runner.os }}-m2-${{ steps.get-date.outputs.date }} - - name: Build and analyze - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_API_TOKEN }} - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - run: ./mvnw -B -Dquickly-ci install org.sonarsource.scanner.maven:sonar-maven-plugin:sonar diff --git a/.mvn/gradle-enterprise.xml b/.mvn/gradle-enterprise.xml index 27ac5553de6a2..62ed9dd2327a0 100644 --- a/.mvn/gradle-enterprise.xml +++ b/.mvn/gradle-enterprise.xml @@ -23,11 +23,11 @@ - #{env['GRADLE_LOCAL_BUILD_CACHE'] != null} + #{env['GRADLE_LOCAL_BUILD_CACHE'] != null and env['RELEASE_GITHUB_TOKEN'] == null} - true - #{env['CI'] != null and env['GRADLE_ENTERPRISE_ACCESS_KEY'] != null and env['GRADLE_ENTERPRISE_ACCESS_KEY'] != ''} + #{env['RELEASE_GITHUB_TOKEN'] == null} + #{env['CI'] != null and env['GRADLE_ENTERPRISE_ACCESS_KEY'] != null and env['GRADLE_ENTERPRISE_ACCESS_KEY'] != '' and env['RELEASE_GITHUB_TOKEN'] == null} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e5265121ea1e8..e71961362a993 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -227,7 +227,7 @@ If you have not done so on this machine, you need to: * macOS: Use the `Disk Utility.app` to check. It also allows you to create a case-sensitive volume to store your code projects. See this [blog entry](https://karnsonline.com/case-sensitive-apfs/) for more. * Windows: [Enable case sensitive file names per directory](https://learn.microsoft.com/en-us/windows/wsl/case-sensitivity) * Install Git and configure your GitHub access -* Install Java SDK 11+ (OpenJDK recommended) +* Install Java SDK 17+ (OpenJDK recommended) * Install [GraalVM](https://quarkus.io/guides/building-native-image) * Install platform C developer tools: * Linux @@ -357,7 +357,7 @@ in `quarkus-parent` (root `pom.xml`). When contributing to Quarkus, it is recommended to respect the following rules. > **Note:** The `impsort-maven-plugin` uses the `.cache` directory on each module to speed up the build. -> Because we have configured the plugin to store in a versioned directory, you may notice over time that the `.cache` directory grows in size. You can safely delete the `.cache` directory in each module to reclaim the space. +> Because we have configured the plugin to store in a versioned directory, you may notice over time that the `.cache` directory grows in size. You can safely delete the `.cache` directory in each module to reclaim the space. > Running `./mvnw clean -Dclean-cache` automatically deletes that directory for you. **Contributing to an extension** @@ -748,7 +748,7 @@ This project is an open source project, please act responsibly, be nice, polite See section `IDEA Setup` as there are different possible solutions described. -* IntelliJ does not recognize the project as a Java 11 project +* IntelliJ does not recognize the project as a Java 17 project In the Maven pane, uncheck the `include-jdk-misc` and `compile-java8-release-flag` profiles @@ -787,5 +787,5 @@ This project is an open source project, please act responsibly, be nice, polite ... ``` * Tests fail with `Caused by: io.quarkus.runtime.QuarkusBindException: Port(s) already bound: 8080: Address already in use` - + Check that you do not have other Quarkus dev environments, apps or other web servers running on this default 8080 port. diff --git a/README.md b/README.md index 14f56b1a924a1..bd2037b525683 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ [![License](https://img.shields.io/github/license/quarkusio/quarkus?style=for-the-badge&logo=apache)](https://www.apache.org/licenses/LICENSE-2.0) [![Project Chat](https://img.shields.io/badge/zulip-join_chat-brightgreen.svg?style=for-the-badge&logo=zulip)](https://quarkusio.zulipchat.com/) [![Gitpod Ready-to-Code](https://img.shields.io/badge/Gitpod-Ready--to--Code-blue?style=for-the-badge&logo=gitpod&logoColor=white)](https://gitpod.io/#https://github.com/quarkusio/quarkus/-/tree/main/) -[![Supported JVM Versions](https://img.shields.io/badge/JVM-11--17--21-brightgreen.svg?style=for-the-badge&logo=openjdk)](https://github.com/quarkusio/quarkus/actions/runs/113853915/) +[![Supported JVM Versions](https://img.shields.io/badge/JVM-17--21-brightgreen.svg?style=for-the-badge&logo=openjdk)](https://github.com/quarkusio/quarkus/actions/runs/113853915/) [![Develocity](https://img.shields.io/badge/Revved%20up%20by-Develocity-06A0CE?style=for-the-badge&logo=gradle)](https://ge.quarkus.io/scans) [![GitHub Repo stars](https://img.shields.io/github/stars/quarkusio/quarkus?style=for-the-badge)](https://github.com/quarkusio/quarkus/stargazers) @@ -15,7 +15,7 @@ Quarkus is a Cloud Native, (Linux) Container First framework for writing Java applications. -* **Container First**: +* **Container First**: Minimal footprint Java applications optimal for running in containers. * **Cloud Native**: Embraces [12 factor architecture](https://12factor.net) in environments like Kubernetes. diff --git a/bom/application/pom.xml b/bom/application/pom.xml index 951c4bf03f1a7..4812c5c207d91 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -15,12 +15,12 @@ 2.0.1 - 1.76 - 1.0.2.3 + 1.77 + 1.0.2.4 1.0.17 5.0.0 3.0.2 - 3.1.5 + 3.1.6 1.3.2 1 1.1.5 @@ -55,14 +55,14 @@ 4.0.4 4.0.0 3.7.0 - 2.6.0 + 2.6.1 6.2.6 4.4.0 2.1.0 1.0.13 3.0.1 3.7.2 - 4.11.0 + 4.12.0 2.4.0 2.1.2 2.1.1 @@ -85,26 +85,28 @@ 4.0.1 4.0.4 9.6 - 2.15.0 + 2.15.1 16.0.0.Final 3.0-alpha-2 2.1.0 23.0.1 1.7.0 - 2.15.3 + 2.16.0 1.0.0.Final - 3.13.0 + 3.14.0 1.16.0 - 1.5.1 - - 6.2.13.Final + 1.6.0 + + 6.4.0.Final 1.14.7 6.0.6.Final - 2.0.6.Final + 2.2.0.Final 8.0.1.Final - 6.2.2.Final + 7.0.0.Final 7.0.0.Final 2.1 8.0.0.Final @@ -126,18 +128,18 @@ 9.2.1 2.3.2 2.2.224 - 42.6.0 - 3.2.0 + 42.7.1 + 3.3.1 8.0.33 - 12.4.0.jre11 + 12.4.2.jre11 1.6.7 23.3.0.23.09 10.14.2.0 11.5.8.0 1.2.6 - 5.3.2 + 5.4.0 2.2 - 5.10.0 + 5.10.1 1.5.0 14.0.21.Final 4.6.5.Final @@ -147,7 +149,7 @@ 1.0.4 3.5.3.Final 2.5.1 - 3.6.0 + 3.6.1 1.8.0 1.1.10.5 0.100.0 @@ -155,44 +157,44 @@ 2.13.12 1.2.3 3.11.3 - 2.14.0 + 2.15.0 2.2.0 1.0.0 - 1.9.10 + 1.9.21 1.7.3 0.27.0 - 1.6.0 - 4.1.0 + 1.6.2 + 4.1.1 3.2.0 4.2.0 3.0.2.Final 9.22.3 3.0.3 - 4.24.0 + 4.25.0 4.24.0 2.2 6.0.0 4.11.1 1.8.0 0.34.1 - 3.25.5 + 3.25.6 3.14.9 1.17.6 - 0.2.1 - 4.9.2 + 0.3.0 + 4.11.1 5.2.SP7 2.1.SP2 5.4.Final 2.1.SP1 - 5.3.1 + 5.8.0 5.8.0 - 4.10.1 + 4.13.0 2.0.2.Final - 22.0.5 + 23.0.1 1.15.1 - 3.38.0 - 2.22.0 + 3.41.0 + 2.23.0 0.25.0 1.43.3 2.1 @@ -201,14 +203,14 @@ 1.25.0 1.11.0 2.10.1 - 1.1.1.Final - 2.20.0 + 1.1.2.Final + 2.22.0 1.3.0.Final 1.11.3 - 2.4.14.Final + 2.5.3.Final 0.1.18.Final - 1.19.1 - 3.3.3 + 1.19.3 + 3.3.4 2.0.0 1.4.4 @@ -218,7 +220,7 @@ 6.7.0.202309050840-r 0.14.0 - 9.34 + 9.37.3 0.0.6 0.1.3 2.10.0 @@ -407,6 +409,15 @@ import + + + org.hibernate.search + hibernate-search-bom + ${hibernate-search.version} + pom + import + + io.smallrye.reactive @@ -443,7 +454,7 @@ org.jetbrains annotations - 24.0.1 + 24.1.0 @@ -1195,12 +1206,12 @@ io.quarkus - quarkus-hibernate-search-orm-coordination-outbox-polling + quarkus-hibernate-search-orm-outbox-polling ${project.version} io.quarkus - quarkus-hibernate-search-orm-coordination-outbox-polling-deployment + quarkus-hibernate-search-orm-outbox-polling-deployment ${project.version} @@ -2048,6 +2059,11 @@ quarkus-scheduler-api ${project.version} + + io.quarkus + quarkus-scheduler-spi + ${project.version} + io.quarkus quarkus-scheduler-common @@ -2218,6 +2234,11 @@ quarkus-funqy-amazon-lambda ${project.version} + + io.quarkus + quarkus-funqy-amazon-lambda-deployment + ${project.version} + io.quarkus quarkus-funqy-google-cloud-functions @@ -2573,16 +2594,6 @@ quarkus-container-image-jib-deployment ${project.version} - - io.quarkus - quarkus-container-image-s2i - ${project.version} - - - io.quarkus - quarkus-container-image-s2i-deployment - ${project.version} - io.quarkus quarkus-container-image-openshift @@ -3321,48 +3332,10 @@ com.github.docker-java - docker-java - ${docker-java.version} - - - com.github.docker-java - docker-java-api - ${docker-java.version} - - - com.github.docker-java - docker-java-core - ${docker-java.version} - - - com.github.docker-java - docker-java-transport - ${docker-java.version} - - - com.github.docker-java - docker-java-transport-httpclient5 - ${docker-java.version} - - - com.github.docker-java - docker-java-transport-jersey - ${docker-java.version} - - - com.github.docker-java - docker-java-transport-netty - ${docker-java.version} - - - com.github.docker-java - docker-java-transport-okhttp - ${docker-java.version} - - - com.github.docker-java - docker-java-transport-zerodep + docker-java-bom ${docker-java.version} + pom + import com.google.cloud.functions @@ -5047,8 +5020,9 @@ hibernate-core ${hibernate-orm.version} + - org.jboss + io.smallrye jandex @@ -5090,22 +5064,7 @@ org.hibernate.search - hibernate-search-backend-elasticsearch - ${hibernate-search.version} - - - org.hibernate.search - hibernate-search-backend-elasticsearch-aws - ${hibernate-search.version} - - - org.hibernate.search - hibernate-search-mapper-orm-coordination-outbox-polling-orm6 - ${hibernate-search.version} - - - org.hibernate.search - hibernate-search-mapper-orm-orm6 + hibernate-search-mapper-orm ${hibernate-search.version} @@ -5126,10 +5085,6 @@ ${hibernate-search.version} - - org.jboss - jandex - io.smallrye jandex @@ -5142,10 +5097,6 @@ ${hibernate-search.version} - - org.jboss - jandex - io.smallrye jandex @@ -5158,10 +5109,6 @@ ${hibernate-search.version} - - org.jboss - jandex - io.smallrye jandex diff --git a/build-parent/pom.xml b/build-parent/pom.xml index e22da92a4c227..e6604d6ce0d9d 100644 --- a/build-parent/pom.xml +++ b/build-parent/pom.xml @@ -20,9 +20,9 @@ 3.11.0 - 1.9.10 + 1.9.21 1.9.10 - 2.13.8 + 2.13.12 4.8.1 ${scala-maven-plugin.version} @@ -33,12 +33,12 @@ ${version.surefire.plugin} - 3.1.5 + 3.1.6 1.0.0 2.5.10 2.70.0 - 3.25.5 + 3.25.6 2.0.3.Final 6.0.1 @@ -62,17 +62,20 @@ [3.8.2,) - 3.9.5 + 3.9.6 3.2.0 - 8.4 + 8.5 ${project.version} ${project.version} 3.8.1 + + 6.4.0.Final - 4.10.1 + 4.13.0 :Z @@ -101,20 +104,20 @@ - 22.0.5 + 23.0.1 19.0.3 quay.io/keycloak/keycloak:${keycloak.version} quay.io/keycloak/keycloak:${keycloak.wildfly.version}-legacy - 6.0.10 + 6.0.11 3.24.2 - 3.2.0 + 3.3.1 7.3.0 - 2.31.0 + 2.31.2 @@ -131,7 +134,7 @@ 0.14.7 0.26.1 - 3.4.0 + 3.5.0 0.14.5 0.4.5 @@ -187,7 +190,7 @@ 1.7.0 5.4.3 2.1.0 - 1.8.1 + 1.8.2 2.4.0 @@ -295,6 +298,11 @@ ${project.version} test + + io.quarkus + quarkus-arc-test-supplement + ${project.version} + org.assertj assertj-core @@ -906,7 +914,7 @@ org.apache.groovy groovy - 4.0.15 + 4.0.16 @@ -1114,12 +1122,12 @@ - 11 + 17 html - https://docs.oracle.com/en/java/javase/11/docs/api/ + https://docs.oracle.com/en/java/javase/17/docs/api/ diff --git a/core/deployment/src/main/java/io/quarkus/deployment/Capability.java b/core/deployment/src/main/java/io/quarkus/deployment/Capability.java index 6fc7ea4006912..6cd1937771799 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/Capability.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/Capability.java @@ -32,10 +32,12 @@ public interface Capability { String LRA_PARTICIPANT = QUARKUS_PREFIX + ".lra.participant"; String JACKSON = QUARKUS_PREFIX + ".jackson"; + String JSONB = QUARKUS_PREFIX + ".jsonb"; - String KOTLIN = QUARKUS_PREFIX + ".kotlin"; + String JAXB = QUARKUS_PREFIX + ".jaxb"; + String JAXP = QUARKUS_PREFIX + ".jaxp"; - String JSONB = QUARKUS_PREFIX + ".jsonb"; + String KOTLIN = QUARKUS_PREFIX + ".kotlin"; String HAL = QUARKUS_PREFIX + ".hal"; @@ -97,7 +99,6 @@ public interface Capability { String METRICS = QUARKUS_PREFIX + ".metrics"; String CONTAINER_IMAGE_JIB = QUARKUS_PREFIX + ".container.image.jib"; String CONTAINER_IMAGE_DOCKER = QUARKUS_PREFIX + ".container.image.docker"; - String CONTAINER_IMAGE_S2I = QUARKUS_PREFIX + ".container.image.s2i"; String CONTAINER_IMAGE_OPENSHIFT = QUARKUS_PREFIX + ".container.image.openshift"; String CONTAINER_IMAGE_BUILDPACK = QUARKUS_PREFIX + ".container.image.buildpack"; String HIBERNATE_ORM = QUARKUS_PREFIX + ".hibernate.orm"; diff --git a/core/deployment/src/main/java/io/quarkus/deployment/CodeGenProvider.java b/core/deployment/src/main/java/io/quarkus/deployment/CodeGenProvider.java index e5910422f46af..c06f0277f2031 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/CodeGenProvider.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/CodeGenProvider.java @@ -3,6 +3,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.Map; +import java.util.Objects; import org.eclipse.microprofile.config.Config; import org.wildfly.common.annotation.NotNull; @@ -92,4 +93,14 @@ default void init(ApplicationModel model, Map properties) { default boolean shouldRun(Path sourceDir, Config config) { return Files.isDirectory(sourceDir); } + + /** + * Resolve path; e.g. symlinks, etc + * + * @param path the path to resolve + * @return resolved path + */ + static Path resolve(Path path) { + return Objects.requireNonNull(path).resolve("."); + } } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/IsDockerWorking.java b/core/deployment/src/main/java/io/quarkus/deployment/IsDockerWorking.java index 802c7f2308724..e739ca5a7c51f 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/IsDockerWorking.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/IsDockerWorking.java @@ -1,7 +1,7 @@ package io.quarkus.deployment; -import static io.quarkus.runtime.util.ContainerRuntimeUtil.ContainerRuntime.UNAVAILABLE; +import static io.quarkus.deployment.util.ContainerRuntimeUtil.ContainerRuntime.UNAVAILABLE; import java.io.IOException; import java.lang.reflect.Field; @@ -22,7 +22,7 @@ import org.jboss.logging.Logger; import io.quarkus.deployment.console.StartupLogCompressor; -import io.quarkus.runtime.util.ContainerRuntimeUtil; +import io.quarkus.deployment.util.ContainerRuntimeUtil; public class IsDockerWorking implements BooleanSupplier { diff --git a/core/deployment/src/main/java/io/quarkus/deployment/builditem/SetClassPathSystemPropBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/builditem/SetClassPathSystemPropBuildItem.java new file mode 100644 index 0000000000000..7461e22f7a73f --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/builditem/SetClassPathSystemPropBuildItem.java @@ -0,0 +1,12 @@ +package io.quarkus.deployment.builditem; + +import io.quarkus.builder.item.MultiBuildItem; + +/** + * A marker build item to make Quarkus set the {@code java.class.path} system property. + * This system property is used in rare by libraries (Truffle for example) to create their own ClassLoaders. + * The value of the system property is simply best effort, as there is no way to faithfully represent + * the Quarkus ClassLoader hierarchies in a system property value. + */ +public final class SetClassPathSystemPropBuildItem extends MultiBuildItem { +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/index/IndexWrapper.java b/core/deployment/src/main/java/io/quarkus/deployment/index/IndexWrapper.java index 904e38123913c..9b515259151ab 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/index/IndexWrapper.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/index/IndexWrapper.java @@ -26,14 +26,22 @@ import org.jboss.jandex.Type; import org.jboss.logging.Logger; -import io.quarkus.deployment.steps.CombinedIndexBuildStep; - /** * This wrapper is used to index JDK classes on demand. */ public class IndexWrapper implements IndexView { - private static final Logger LOGGER = Logger.getLogger(CombinedIndexBuildStep.class); + private static final Logger LOGGER = Logger.getLogger(IndexWrapper.class); + + private static final Set PRIMITIVE_TYPES = Set.of( + "boolean", + "byte", + "short", + "int", + "long", + "float", + "double", + "char"); private final IndexView index; private final ClassLoader deploymentClassLoader; @@ -300,6 +308,10 @@ private Optional computeAdditional(DotName className) { static boolean index(Indexer indexer, String className, ClassLoader classLoader) { boolean result = false; + if (PRIMITIVE_TYPES.contains(className) || className.startsWith("[")) { + // Ignore primitives and arrays + return false; + } try (InputStream stream = classLoader .getResourceAsStream(className.replace('.', '/') + ".class")) { if (stream != null) { diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/NativeConfig.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/NativeConfig.java index c973f8a449a91..9b193308ec9f6 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/NativeConfig.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/NativeConfig.java @@ -5,12 +5,12 @@ import java.util.Optional; import java.util.OptionalInt; +import io.quarkus.deployment.util.ContainerRuntimeUtil; import io.quarkus.runtime.annotations.ConfigDocDefault; import io.quarkus.runtime.annotations.ConfigGroup; import io.quarkus.runtime.annotations.ConfigPhase; import io.quarkus.runtime.annotations.ConfigRoot; import io.quarkus.runtime.configuration.TrimmedStringConverter; -import io.quarkus.runtime.util.ContainerRuntimeUtil; import io.smallrye.config.ConfigMapping; import io.smallrye.config.WithConverter; import io.smallrye.config.WithDefault; diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/builditem/CompiledJavaVersionBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/builditem/CompiledJavaVersionBuildItem.java index 88dadbcc63dd4..ee2ab28b42d4d 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/builditem/CompiledJavaVersionBuildItem.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/builditem/CompiledJavaVersionBuildItem.java @@ -76,7 +76,7 @@ final class Known implements JavaVersion { private static final int JAVA_11_MAJOR = 55; private static final int JAVA_17_MAJOR = 61; private static final int JAVA_19_MAJOR = 63; - private static final int JAVA_21_MAJOR = 66; + private static final int JAVA_21_MAJOR = 65; private final int determinedMajor; diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/AppCDSBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/AppCDSBuildStep.java index 2c7a26da98230..c9ca54d8d6262 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/AppCDSBuildStep.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/AppCDSBuildStep.java @@ -1,7 +1,7 @@ package io.quarkus.deployment.pkg.steps; import static io.quarkus.deployment.pkg.steps.LinuxIDUtil.getLinuxID; -import static io.quarkus.runtime.util.ContainerRuntimeUtil.detectContainerRuntime; +import static io.quarkus.deployment.util.ContainerRuntimeUtil.detectContainerRuntime; import java.io.File; import java.io.IOException; @@ -28,8 +28,8 @@ import io.quarkus.deployment.pkg.builditem.JarBuildItem; import io.quarkus.deployment.pkg.builditem.OutputTargetBuildItem; import io.quarkus.deployment.steps.MainClassBuildStep; +import io.quarkus.deployment.util.ContainerRuntimeUtil.ContainerRuntime; import io.quarkus.runtime.LaunchMode; -import io.quarkus.runtime.util.ContainerRuntimeUtil.ContainerRuntime; import io.quarkus.utilities.JavaBinFinder; public class AppCDSBuildStep { diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/GraalVM.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/GraalVM.java index d711928b67b39..6b9be123db6b3 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/GraalVM.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/GraalVM.java @@ -31,9 +31,9 @@ static final class VersionParseHelper { private static final String VENDOR_VERS = "(?.*)"; private static final String JDK_DEBUG = "[^\\)]*"; // zero or more of >anything not a ')'< - private static final String RUNTIME_NAME = "(?(?:OpenJDK|GraalVM) Runtime Environment) "; + private static final String RUNTIME_NAME = "(?(?:.*) Runtime Environment) "; private static final String BUILD_INFO = "(?.*)"; - private static final String VM_NAME = "(?(?:OpenJDK 64-Bit Server|Substrate) VM) "; + private static final String VM_NAME = "(?(?:.*) VM) "; private static final String FIRST_LINE_PATTERN = "native-image " + VSTR_FORMAT + " .*$"; private static final String SECOND_LINE_PATTERN = RUNTIME_NAME diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/JarResultBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/JarResultBuildStep.java index ad00129f0f466..8f18b874d6f2e 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/JarResultBuildStep.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/JarResultBuildStep.java @@ -1011,7 +1011,6 @@ private NativeImageSourceJarBuildItem buildNativeImageThinJar(CurateOutcomeBuild removedArtifacts.add("org.graalvm.sdk:nativeimage"); removedArtifacts.add("org.graalvm.sdk:word"); removedArtifacts.add("org.graalvm.sdk:collections"); - removedArtifacts.add("org.graalvm.polyglot:polyglot"); doLegacyThinJarGeneration(curateOutcomeBuildItem, outputTargetBuildItem, transformedClasses, applicationArchivesBuildItem, applicationInfo, packageConfig, generatedResources, libDir, allClasses, diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildContainerRunner.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildContainerRunner.java index d826c15c68113..6b5425c16078f 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildContainerRunner.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildContainerRunner.java @@ -14,8 +14,8 @@ import org.jboss.logging.Logger; import io.quarkus.deployment.pkg.NativeConfig; +import io.quarkus.deployment.util.ContainerRuntimeUtil; import io.quarkus.deployment.util.ProcessUtil; -import io.quarkus.runtime.util.ContainerRuntimeUtil; public abstract class NativeImageBuildContainerRunner extends NativeImageBuildRunner { @@ -49,8 +49,8 @@ public void setup(boolean processInheritIODisabled) { // will appear to block and no output will be shown String effectiveBuilderImage = nativeConfig.builderImage().getEffectiveImage(); var builderImagePull = nativeConfig.builderImage().pull(); - log.infof("Checking status of builder image '%s'", effectiveBuilderImage); if (builderImagePull != NativeConfig.ImagePullStrategy.ALWAYS) { + log.infof("Checking status of builder image '%s'", effectiveBuilderImage); Process imageInspectProcess = null; try { final ProcessBuilder pb = new ProcessBuilder( @@ -82,20 +82,37 @@ public void setup(boolean processInheritIODisabled) { } } } - Process pullProcess = null; + try { - final ProcessBuilder pb = new ProcessBuilder( - Arrays.asList(containerRuntime.getExecutableName(), "pull", effectiveBuilderImage)); - pullProcess = ProcessUtil.launchProcess(pb, processInheritIODisabled); - if (pullProcess.waitFor() != 0) { - throw new RuntimeException("Failed to pull builder image '" + effectiveBuilderImage + "'"); - } - } catch (IOException | InterruptedException e) { - throw new RuntimeException("Failed to pull builder image '" + effectiveBuilderImage + "'", e); - } finally { - if (pullProcess != null) { - pullProcess.destroy(); + log.infof("Pulling builder image '%s'", effectiveBuilderImage); + pull(effectiveBuilderImage, processInheritIODisabled); + } catch (Exception e) { + log.infof("Retrying in 5 seconds"); + try { + Thread.sleep(5_000L); + } catch (InterruptedException e1) { + throw new RuntimeException(e1); } + log.infof("Pulling builder image '%s' (take 2)", effectiveBuilderImage); + pull(effectiveBuilderImage, processInheritIODisabled); + } + } + } + + private void pull(String effectiveBuilderImage, boolean processInheritIODisabled) { + Process pullProcess = null; + try { + final ProcessBuilder pb = new ProcessBuilder( + Arrays.asList(containerRuntime.getExecutableName(), "pull", effectiveBuilderImage)); + pullProcess = ProcessUtil.launchProcess(pb, processInheritIODisabled); + if (pullProcess.waitFor() != 0) { + throw new RuntimeException("Failed to pull builder image '" + effectiveBuilderImage + "'"); + } + } catch (IOException | InterruptedException e) { + throw new RuntimeException("Failed to pull builder image '" + effectiveBuilderImage + "'"); + } finally { + if (pullProcess != null) { + pullProcess.destroy(); } } } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildLocalContainerRunner.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildLocalContainerRunner.java index 9d678efb5383b..1afb99e41f7fd 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildLocalContainerRunner.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildLocalContainerRunner.java @@ -19,6 +19,9 @@ public NativeImageBuildLocalContainerRunner(NativeConfig nativeConfig) { super(nativeConfig); if (SystemUtils.IS_OS_LINUX || SystemUtils.IS_OS_MAC) { final ArrayList containerRuntimeArgs = new ArrayList<>(Arrays.asList(baseContainerRuntimeArgs)); + if (containerRuntime.isInWindowsWSL()) { + containerRuntimeArgs.add("--interactive"); + } if (containerRuntime.isDocker() && containerRuntime.isRootless()) { Collections.addAll(containerRuntimeArgs, "--user", String.valueOf(0)); } else { diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/UpxCompressionBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/UpxCompressionBuildStep.java index 80b05436996a8..7ab2f19910dd1 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/UpxCompressionBuildStep.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/UpxCompressionBuildStep.java @@ -21,9 +21,9 @@ import io.quarkus.deployment.pkg.builditem.NativeImageBuildItem; import io.quarkus.deployment.pkg.builditem.NativeImageRunnerBuildItem; import io.quarkus.deployment.pkg.builditem.UpxCompressedBuildItem; +import io.quarkus.deployment.util.ContainerRuntimeUtil; import io.quarkus.deployment.util.FileUtil; import io.quarkus.deployment.util.ProcessUtil; -import io.quarkus.runtime.util.ContainerRuntimeUtil; public class UpxCompressionBuildStep { diff --git a/core/deployment/src/main/java/io/quarkus/deployment/steps/ClassPathSystemPropBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/steps/ClassPathSystemPropBuildStep.java new file mode 100644 index 0000000000000..feda25cbade14 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/steps/ClassPathSystemPropBuildStep.java @@ -0,0 +1,53 @@ +package io.quarkus.deployment.steps; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +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.SetClassPathSystemPropBuildItem; +import io.quarkus.deployment.pkg.builditem.CurateOutcomeBuildItem; +import io.quarkus.maven.dependency.ResolvedDependency; +import io.quarkus.runtime.ClassPathSystemPropertyRecorder; + +public class ClassPathSystemPropBuildStep { + + @BuildStep + public void produce(BuildProducer producer, CurateOutcomeBuildItem curateOutcome) { + boolean truffleUsed = curateOutcome.getApplicationModel().getDependencies().stream() + .anyMatch(d -> d.getGroupId().equals("org.graalvm.polyglot")); + if (truffleUsed) { + producer.produce(new SetClassPathSystemPropBuildItem()); + } + } + + @BuildStep + @Record(ExecutionTime.STATIC_INIT) + public void set(List setCPItems, + CurateOutcomeBuildItem curateOutcome, + ClassPathSystemPropertyRecorder recorder) { + if (setCPItems.isEmpty()) { + return; + } + Collection runtimeDependencies = curateOutcome.getApplicationModel().getRuntimeDependencies(); + List parentFirst = new ArrayList<>(); + List regular = new ArrayList<>(); + for (ResolvedDependency dependency : runtimeDependencies) { + if (dependency.isClassLoaderParentFirst()) { + parentFirst.addAll(dependency.getContentTree().getRoots()); + } else { + regular.addAll(dependency.getContentTree().getRoots()); + + } + } + String classPathValue = Stream.concat(parentFirst.stream(), regular.stream()).map(p -> p.toAbsolutePath().toString()) + .collect(Collectors.joining(":")); + recorder.set(classPathValue); + } +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/steps/MainClassBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/steps/MainClassBuildStep.java index de47b02e92602..d181e33c5a1f7 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/steps/MainClassBuildStep.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/steps/MainClassBuildStep.java @@ -268,8 +268,6 @@ void build(List staticInitTasks, startupContext, mv.getMethodParam(0)); mv.invokeStaticMethod(CONFIGURE_STEP_TIME_ENABLED); - ResultHandle profiles = mv - .invokeStaticMethod(ofMethod(ConfigUtils.class, "getProfiles", List.class)); tryBlock = mv.tryBlock(); tryBlock.invokeStaticMethod(CONFIGURE_STEP_TIME_START); @@ -298,7 +296,7 @@ void build(List staticInitTasks, tryBlock.load(applicationInfo.getVersion()), tryBlock.load(Version.getVersion()), featuresHandle, - profiles, + tryBlock.invokeStaticMethod(ofMethod(ConfigUtils.class, "getProfiles", List.class)), tryBlock.load(LaunchMode.DEVELOPMENT.equals(launchMode.getLaunchMode())), tryBlock.load(launchMode.isAuxiliaryApplication())); diff --git a/core/runtime/src/main/java/io/quarkus/runtime/util/ContainerRuntimeUtil.java b/core/deployment/src/main/java/io/quarkus/deployment/util/ContainerRuntimeUtil.java similarity index 84% rename from core/runtime/src/main/java/io/quarkus/runtime/util/ContainerRuntimeUtil.java rename to core/deployment/src/main/java/io/quarkus/deployment/util/ContainerRuntimeUtil.java index 607ead4f24980..ff6452d4199f2 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/util/ContainerRuntimeUtil.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/util/ContainerRuntimeUtil.java @@ -1,4 +1,4 @@ -package io.quarkus.runtime.util; +package io.quarkus.deployment.util; import java.io.BufferedReader; import java.io.IOException; @@ -10,6 +10,7 @@ import java.util.regex.Pattern; import java.util.stream.Collectors; +import org.apache.commons.lang3.SystemUtils; import org.eclipse.microprofile.config.ConfigProvider; import org.jboss.logging.Logger; @@ -27,6 +28,10 @@ public final class ContainerRuntimeUtil { * runtime and the container runtime would be detected again and again unnecessarily. */ private static final String CONTAINER_RUNTIME_SYS_PROP = "quarkus-local-container-runtime"; + /** + * Defines the maximum number of characters to read from the output of the `docker info` command. + */ + private static final int MAX_ANTICIPATED_CHARACTERS_IN_DOCKER_INFO = 3000; private ContainerRuntimeUtil() { } @@ -112,14 +117,69 @@ private static ContainerRuntime getContainerRuntimeEnvironment() { } private static ContainerRuntime fullyResolveContainerRuntime(ContainerRuntime containerRuntimeEnvironment) { - boolean rootless = getRootlessStateFor(containerRuntimeEnvironment); + boolean rootless = false; + boolean isInWindowsWSL = false; + Process rootlessProcess = null; + ProcessBuilder pb = null; + try { + pb = new ProcessBuilder(containerRuntimeEnvironment.getExecutableName(), "info").redirectErrorStream(true); + rootlessProcess = pb.start(); + int exitCode = rootlessProcess.waitFor(); + if (exitCode != 0) { + log.warnf("Command \"%s\" exited with error code %d. " + + "Rootless container runtime detection might not be reliable or the container service is not running at all.", + String.join(" ", pb.command()), exitCode); + } + try (InputStream inputStream = rootlessProcess.getInputStream(); + InputStreamReader inputStreamReader = new InputStreamReader(inputStream); + BufferedReader bufferedReader = new BufferedReader(inputStreamReader)) { + bufferedReader.mark(MAX_ANTICIPATED_CHARACTERS_IN_DOCKER_INFO); + if (exitCode != 0) { + log.debugf("Command \"%s\" output: %s", String.join(" ", pb.command()), + bufferedReader.lines().collect(Collectors.joining(System.lineSeparator()))); + } else { + Predicate stringPredicate; + // Docker includes just "rootless" under SecurityOptions, while podman includes "rootless: " + if (containerRuntimeEnvironment.isDocker()) { + // We also treat Docker Desktop as "rootless" since the way it binds mounts does not + // transparently map the host user ID and GID + // see https://docs.docker.com/desktop/faqs/linuxfaqs/#how-do-i-enable-file-sharing + stringPredicate = line -> line.trim().equals("rootless") || line.contains("Docker Desktop") + || line.contains("desktop-linux"); + } else { + stringPredicate = line -> line.trim().equals("rootless: true"); + } + rootless = bufferedReader.lines().anyMatch(stringPredicate); + + if (SystemUtils.IS_OS_LINUX && containerRuntimeEnvironment.isDocker()) { + stringPredicate = line -> line.trim().contains("WSL"); + bufferedReader.reset(); + isInWindowsWSL = bufferedReader.lines().anyMatch(stringPredicate); + } + } + } + } catch (IOException | InterruptedException e) { + // If an exception is thrown in the process, assume we are not running rootless (default docker installation) + log.debugf(e, "Failure to read info output from %s", String.join(" ", pb.command())); + } finally { + if (rootlessProcess != null) { + rootlessProcess.destroy(); + } + } - if (!rootless) { - return containerRuntimeEnvironment; + if (rootless) { + if (isInWindowsWSL) { + return ContainerRuntime.WSL_ROOTLESS; + } + return containerRuntimeEnvironment == ContainerRuntime.DOCKER ? ContainerRuntime.DOCKER_ROOTLESS + : ContainerRuntime.PODMAN_ROOTLESS; } - return containerRuntimeEnvironment == ContainerRuntime.DOCKER ? ContainerRuntime.DOCKER_ROOTLESS - : ContainerRuntime.PODMAN_ROOTLESS; + if (isInWindowsWSL) { + return ContainerRuntime.WSL; + } + + return containerRuntimeEnvironment; } private static ContainerRuntime loadContainerRuntimeFromSystemProperty() { @@ -168,56 +228,14 @@ private static String getVersionOutputFor(ContainerRuntime containerRuntime) { } } - private static boolean getRootlessStateFor(ContainerRuntime containerRuntime) { - Process rootlessProcess = null; - ProcessBuilder pb = null; - try { - pb = new ProcessBuilder(containerRuntime.getExecutableName(), "info").redirectErrorStream(true); - rootlessProcess = pb.start(); - int exitCode = rootlessProcess.waitFor(); - if (exitCode != 0) { - log.warnf("Command \"%s\" exited with error code %d. " + - "Rootless container runtime detection might not be reliable or the container service is not running at all.", - String.join(" ", pb.command()), exitCode); - } - try (InputStream inputStream = rootlessProcess.getInputStream(); - InputStreamReader inputStreamReader = new InputStreamReader(inputStream); - BufferedReader bufferedReader = new BufferedReader(inputStreamReader)) { - if (exitCode != 0) { - log.debugf("Command \"%s\" output: %s", String.join(" ", pb.command()), - bufferedReader.lines().collect(Collectors.joining(System.lineSeparator()))); - return false; - } else { - final Predicate stringPredicate; - // Docker includes just "rootless" under SecurityOptions, while podman includes "rootless: " - if (containerRuntime == ContainerRuntime.DOCKER) { - // We also treat Docker Desktop as "rootless" since the way it binds mounts does not - // transparently map the host user ID and GID - // see https://docs.docker.com/desktop/faqs/linuxfaqs/#how-do-i-enable-file-sharing - stringPredicate = line -> line.trim().equals("rootless") || line.contains("desktop-linux"); - } else { - stringPredicate = line -> line.trim().equals("rootless: true"); - } - return bufferedReader.lines().anyMatch(stringPredicate); - } - } - } catch (IOException | InterruptedException e) { - // If an exception is thrown in the process, assume we are not running rootless (default docker installation) - log.debugf(e, "Failure to read info output from %s", String.join(" ", pb.command())); - return false; - } finally { - if (rootlessProcess != null) { - rootlessProcess.destroy(); - } - } - } - /** * Supported Container runtimes */ public enum ContainerRuntime { DOCKER("docker", false), DOCKER_ROOTLESS("docker", true), + WSL("docker", false), + WSL_ROOTLESS("docker", false), PODMAN("podman", false), PODMAN_ROOTLESS("podman", true), UNAVAILABLE(null, false); @@ -240,13 +258,17 @@ public String getExecutableName() { } public boolean isDocker() { - return this == DOCKER || this == DOCKER_ROOTLESS; + return this.executableName.equals("docker"); } public boolean isPodman() { return this == PODMAN || this == PODMAN_ROOTLESS; } + public boolean isInWindowsWSL() { + return this == WSL || this == WSL_ROOTLESS; + } + public boolean isRootless() { return rootless; } diff --git a/core/deployment/src/test/java/io/quarkus/deployment/pkg/TestNativeConfig.java b/core/deployment/src/test/java/io/quarkus/deployment/pkg/TestNativeConfig.java index 900acdcef123c..fab7177f696d0 100644 --- a/core/deployment/src/test/java/io/quarkus/deployment/pkg/TestNativeConfig.java +++ b/core/deployment/src/test/java/io/quarkus/deployment/pkg/TestNativeConfig.java @@ -4,7 +4,7 @@ import java.util.List; import java.util.Optional; -import io.quarkus.runtime.util.ContainerRuntimeUtil; +import io.quarkus.deployment.util.ContainerRuntimeUtil; public class TestNativeConfig implements NativeConfig { diff --git a/core/deployment/src/test/java/io/quarkus/deployment/pkg/steps/GraalVMTest.java b/core/deployment/src/test/java/io/quarkus/deployment/pkg/steps/GraalVMTest.java index 7bb5a88fea6be..2914dfe0ee7cb 100644 --- a/core/deployment/src/test/java/io/quarkus/deployment/pkg/steps/GraalVMTest.java +++ b/core/deployment/src/test/java/io/quarkus/deployment/pkg/steps/GraalVMTest.java @@ -63,6 +63,16 @@ public void testGraalVMVersionDetected() { + "GraalVM Runtime Environment GraalVM CE (build 20+34-jvmci-23.0-b10)\n" + "Substrate VM GraalVM CE (build 20+34, serial gc)").split("\\n")))); + // Should also work for other unknown implementations of GraalVM + assertVersion(new Version("GraalVM 23.0", "23.0", GRAALVM), GRAALVM, + Version.of(Stream.of(("native-image 20 2023-07-30\n" + + "Foo Runtime Environment whatever (build 20+34-jvmci-23.0-b7)\n" + + "Foo VM whatever (build 20+34, serial gc)").split("\\n")))); + assertVersion(new Version("GraalVM 23.0", "23.0", GRAALVM), GRAALVM, + Version.of(Stream.of(("native-image 20 2023-07-30\n" + + "Another Runtime Environment whatever (build 20+34-jvmci-23.0-b7)\n" + + "Another VM whatever (build 20+34, serial gc)").split("\\n")))); + // Older version parsing assertVersion(new Version("GraalVM 20.1", "20.1", GRAALVM), GRAALVM, Version.of(Stream.of("GraalVM Version 20.1.0 (Java Version 11.0.7)"))); @@ -130,6 +140,19 @@ public void testGraalVM22DevVersionParser() { assertThat(graalVM22Dev.javaVersion.update()).isEqualTo(0); } + @Test + public void testGraalVMEE22DevVersionParser() { + Version graalVMEE22Dev = Version.of(Stream.of(("native-image 22 2024-03-19\n" + + "Java(TM) SE Runtime Environment Oracle GraalVM 22-dev+25.1 (build 22+25-jvmci-b01)\n" + + "Java HotSpot(TM) 64-Bit Server VM Oracle GraalVM 22-dev+25.1 (build 22+25-jvmci-b01, mixed mode, sharing)") + .split("\\n"))); + assertThat(graalVMEE22Dev.distribution.name()).isEqualTo("GRAALVM"); + assertThat(graalVMEE22Dev.getVersionAsString()).isEqualTo("24.0-dev"); + assertThat(graalVMEE22Dev.javaVersion.toString()).isEqualTo("22+25-jvmci-b01"); + assertThat(graalVMEE22Dev.javaVersion.feature()).isEqualTo(22); + assertThat(graalVMEE22Dev.javaVersion.update()).isEqualTo(0); + } + @Test public void testGraalVMVersionsOlderThan() { assertOlderThan("GraalVM Version 19.3.6 CE", "GraalVM Version 20.2.0 (Java Version 11.0.9)"); diff --git a/core/deployment/src/test/java/io/quarkus/deployment/pkg/steps/NativeImageBuildContainerRunnerTest.java b/core/deployment/src/test/java/io/quarkus/deployment/pkg/steps/NativeImageBuildContainerRunnerTest.java index bdbec6fe40318..695ab0b45af48 100644 --- a/core/deployment/src/test/java/io/quarkus/deployment/pkg/steps/NativeImageBuildContainerRunnerTest.java +++ b/core/deployment/src/test/java/io/quarkus/deployment/pkg/steps/NativeImageBuildContainerRunnerTest.java @@ -9,7 +9,7 @@ import io.quarkus.deployment.pkg.NativeConfig; import io.quarkus.deployment.pkg.TestNativeConfig; -import io.quarkus.runtime.util.ContainerRuntimeUtil; +import io.quarkus.deployment.util.ContainerRuntimeUtil; class NativeImageBuildContainerRunnerTest { diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/Constants.java b/core/processor/src/main/java/io/quarkus/annotation/processor/Constants.java index 9bd2f92f17720..ad550c1d9ddee 100644 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/Constants.java +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/Constants.java @@ -108,7 +108,7 @@ final public class Constants { ".About the Duration format\n" + "====\n" + "To write duration values, use the standard `java.time.Duration` format.\n" + - "See the link:https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/time/Duration.html#parse(java.lang.CharSequence)[Duration#parse() javadoc] for more information.\n" + "See the link:https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/time/Duration.html#parse(java.lang.CharSequence)[Duration#parse() Java API documentation] for more information.\n" + "\n" + "You can also use a simplified format, starting with a number:\n" + diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigDocItemFinder.java b/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigDocItemFinder.java index 004ecae8dfbaa..04cdd47aba0cd 100644 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigDocItemFinder.java +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigDocItemFinder.java @@ -495,7 +495,8 @@ private List extractEnumValues(TypeMirror realTypeMirror, boolean useHyp if (rawJavaDoc != null && !rawJavaDoc.isBlank()) { // Show enum constant description as a Tooltip String javaDoc = enumJavaDocParser.parseConfigDescription(rawJavaDoc); - acceptedValues.add(String.format(Constants.TOOLTIP, enumValue, javaDoc)); + acceptedValues.add(String.format(Constants.TOOLTIP, enumValue, + javaDoc.replace("

", Constants.EMPTY).replace("

", Constants.EMPTY))); } else { acceptedValues.add(Constants.CODE_DELIMITER + enumValue + Constants.CODE_DELIMITER); diff --git a/core/runtime/src/main/java/io/quarkus/runtime/ClassPathSystemPropertyRecorder.java b/core/runtime/src/main/java/io/quarkus/runtime/ClassPathSystemPropertyRecorder.java new file mode 100644 index 0000000000000..fdca4fcb0cb65 --- /dev/null +++ b/core/runtime/src/main/java/io/quarkus/runtime/ClassPathSystemPropertyRecorder.java @@ -0,0 +1,11 @@ +package io.quarkus.runtime; + +import io.quarkus.runtime.annotations.Recorder; + +@Recorder +public class ClassPathSystemPropertyRecorder { + + public void set(String value) { + System.setProperty("java.class.path", value); + } +} diff --git a/core/runtime/src/main/java/io/quarkus/runtime/configuration/ConfigDiagnostic.java b/core/runtime/src/main/java/io/quarkus/runtime/configuration/ConfigDiagnostic.java index 6a28d740a1713..574ae74cf169e 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/configuration/ConfigDiagnostic.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/configuration/ConfigDiagnostic.java @@ -184,7 +184,7 @@ public static Set configFiles(Path configFilesLocation) throws IOExcepti Set configFiles = new HashSet<>(); try (DirectoryStream candidates = Files.newDirectoryStream(configFilesLocation, CONFIG_FILES_FILTER)) { for (Path candidate : candidates) { - configFiles.add(candidate.toString()); + configFiles.add(candidate.toUri().toURL().toString()); } } return configFiles; diff --git a/devtools/bom-descriptor-json/pom.xml b/devtools/bom-descriptor-json/pom.xml index 02d144586d3ca..79f1e15561eed 100644 --- a/devtools/bom-descriptor-json/pom.xml +++ b/devtools/bom-descriptor-json/pom.xml @@ -382,19 +382,6 @@
- - io.quarkus - quarkus-container-image-s2i - ${project.version} - pom - test - - - * - * - - - io.quarkus quarkus-core @@ -839,7 +826,7 @@ io.quarkus - quarkus-hibernate-search-orm-coordination-outbox-polling + quarkus-hibernate-search-orm-elasticsearch ${project.version} pom test @@ -852,7 +839,7 @@ io.quarkus - quarkus-hibernate-search-orm-elasticsearch + quarkus-hibernate-search-orm-outbox-polling ${project.version} pom test diff --git a/devtools/cli/src/main/java/io/quarkus/cli/CreateExtension.java b/devtools/cli/src/main/java/io/quarkus/cli/CreateExtension.java index 9cbbc9889bd34..91f913675aab7 100644 --- a/devtools/cli/src/main/java/io/quarkus/cli/CreateExtension.java +++ b/devtools/cli/src/main/java/io/quarkus/cli/CreateExtension.java @@ -1,7 +1,9 @@ package io.quarkus.cli; +import java.util.ArrayList; import java.util.Map; import java.util.TreeMap; +import java.util.stream.Collectors; import io.quarkus.cli.common.OutputOptionMixin; import io.quarkus.cli.common.PropertiesOptions; @@ -13,6 +15,7 @@ import io.quarkus.devtools.commands.data.QuarkusCommandOutcome; import io.quarkus.devtools.commands.handlers.CreateExtensionCommandHandler; import io.quarkus.devtools.project.BuildTool; +import io.quarkus.devtools.project.JavaVersion; import io.quarkus.devtools.project.QuarkusProject; import io.quarkus.maven.dependency.ArtifactCoords; import io.quarkus.registry.catalog.ExtensionCatalog; @@ -68,6 +71,12 @@ }) public class CreateExtension extends BaseCreateCommand { + static class VersionCandidates extends ArrayList { + VersionCandidates() { + super(JavaVersion.JAVA_VERSIONS_LTS.stream().map(String::valueOf).collect(Collectors.toList())); + } + } + @CommandLine.Spec protected CommandLine.Model.CommandSpec spec; @@ -77,6 +86,11 @@ public class CreateExtension extends BaseCreateCommand { @CommandLine.ArgGroup(order = 1, heading = "%nQuarkus version:%n") TargetQuarkusPlatformGroup targetQuarkusVersion = new TargetQuarkusPlatformGroup(); + // Ideally we should use TargetLanguageGroup once we support creating extensions with Kotlin + @CommandLine.Option(names = { + "--java" }, description = "Target Java version.\n Valid values: ${COMPLETION-CANDIDATES}", completionCandidates = VersionCandidates.class, defaultValue = JavaVersion.DEFAULT_JAVA_VERSION_FOR_EXTENSION) + String javaVersion; + @CommandLine.ArgGroup(order = 2, exclusive = false, heading = "%nGenerated artifacts%n") ExtensionNameGenerationGroup nameGeneration = new ExtensionNameGenerationGroup(); @@ -117,6 +131,7 @@ public Integer call() throws Exception { .quarkusBomGroupId(quarkusBom.getGroupId()) .quarkusBomArtifactId(quarkusBom.getArtifactId()) .quarkusBomVersion(quarkusBom.getVersion()) + .javaVersion(javaVersion) .withCodestart(codeGeneration.withCodestart()) .withoutUnitTest(codeGeneration.skipUnitTest()) .withoutDevModeTest(codeGeneration.skipDevModeTest()) diff --git a/devtools/cli/src/main/java/io/quarkus/cli/ProjectExtensionsAdd.java b/devtools/cli/src/main/java/io/quarkus/cli/ProjectExtensionsAdd.java index 3b1266ecdae52..4d3e1572d771c 100644 --- a/devtools/cli/src/main/java/io/quarkus/cli/ProjectExtensionsAdd.java +++ b/devtools/cli/src/main/java/io/quarkus/cli/ProjectExtensionsAdd.java @@ -17,7 +17,7 @@ public class ProjectExtensionsAdd extends BaseBuildCommand implements Callable extensions; @Override diff --git a/devtools/cli/src/main/java/io/quarkus/cli/ProjectExtensionsRemove.java b/devtools/cli/src/main/java/io/quarkus/cli/ProjectExtensionsRemove.java index adf82223bccfc..2affcc921bf42 100644 --- a/devtools/cli/src/main/java/io/quarkus/cli/ProjectExtensionsRemove.java +++ b/devtools/cli/src/main/java/io/quarkus/cli/ProjectExtensionsRemove.java @@ -17,7 +17,7 @@ public class ProjectExtensionsRemove extends BaseBuildCommand implements Callabl @CommandLine.Mixin RunModeOption runMode; - @CommandLine.Parameters(arity = "1", paramLabel = "EXTENSION", description = "Extension(s) to remove from this project.") + @CommandLine.Parameters(arity = "1", paramLabel = "EXTENSION", description = "Extension(s) to remove from this project.", split = ",") Set extensions; @Override diff --git a/devtools/cli/src/test/java/io/quarkus/cli/CliDriver.java b/devtools/cli/src/test/java/io/quarkus/cli/CliDriver.java index 6bbbc63a26463..60af7b5aabb94 100644 --- a/devtools/cli/src/test/java/io/quarkus/cli/CliDriver.java +++ b/devtools/cli/src/test/java/io/quarkus/cli/CliDriver.java @@ -319,7 +319,7 @@ public static Result invokeExtensionRemoveQute(Path projectRoot, Path file) thro } public static Result invokeExtensionAddMultiple(Path projectRoot, Path file) throws Exception { - // add the qute extension + // add amazon-lambda-http and jackson extensions Result result = execute(projectRoot, "extension", "add", "amazon-lambda-http", "jackson", "-e", "-B", "--verbose"); Assertions.assertEquals(CommandLine.ExitCode.OK, result.exitCode, "Expected OK return code. Result:\n" + result); @@ -345,7 +345,7 @@ public static Result invokeExtensionAddMultiple(Path projectRoot, Path file) thr } public static Result invokeExtensionRemoveMultiple(Path projectRoot, Path file) throws Exception { - // add the qute extension + // remove amazon-lambda-http and jackson extensions Result result = execute(projectRoot, "extension", "remove", "amazon-lambda-http", "jackson", "-e", "-B", "--verbose"); Assertions.assertEquals(CommandLine.ExitCode.OK, result.exitCode, "Expected OK return code. Result:\n" + result); @@ -370,6 +370,52 @@ public static Result invokeExtensionRemoveMultiple(Path projectRoot, Path file) return result; } + public static Result invokeExtensionAddMultipleCommas(Path projectRoot, Path file) throws Exception { + Result result = execute(projectRoot, "extension", "add", + "quarkus-resteasy-reactive-jsonb,quarkus-resteasy-reactive-jackson", "-e", "-B", "--verbose"); + Assertions.assertEquals(CommandLine.ExitCode.OK, result.exitCode, + "Expected OK return code. Result:\n" + result); + + result = invokeValidateExtensionList(projectRoot); + Assertions.assertTrue(result.stdout.contains("quarkus-qute"), + "Expected quarkus-qute to be in the list of extensions. Result:\n" + result); + Assertions.assertTrue(result.stdout.contains("quarkus-resteasy-reactive-jsonb"), + "Expected quarkus-resteasy-reactive-jsonb to be in the list of extensions. Result:\n" + result); + Assertions.assertTrue(result.stdout.contains("quarkus-resteasy-reactive-jackson"), + "Expected quarkus-resteasy-reactive-jackson to be in the list of extensions. Result:\n" + result); + + String content = CliDriver.readFileAsString(file); + Assertions.assertTrue(content.contains("quarkus-qute"), + "quarkus-qute should still be listed as a dependency. Result:\n" + content); + Assertions.assertTrue(content.contains("quarkus-resteasy-reactive-jsonb"), + "quarkus-resteasy-reactive-jsonb should be listed as a dependency. Result:\n" + content); + Assertions.assertTrue(content.contains("quarkus-resteasy-reactive-jackson"), + "quarkus-resteasy-reactive-jackson should be listed as a dependency. Result:\n" + content); + + return result; + } + + public static Result invokeExtensionRemoveMultipleCommas(Path projectRoot, Path file) throws Exception { + Result result = execute(projectRoot, "extension", "remove", + "quarkus-resteasy-reactive-jsonb,quarkus-resteasy-reactive-jackson", "-e", "-B", "--verbose"); + Assertions.assertEquals(CommandLine.ExitCode.OK, result.exitCode, + "Expected OK return code. Result:\n" + result); + + result = invokeValidateExtensionList(projectRoot); + Assertions.assertFalse(result.stdout.contains("quarkus-resteasy-reactive-jsonb"), + "quarkus-resteasy-reactive-jsonb should not be in the list of extensions. Result:\n" + result); + Assertions.assertFalse(result.stdout.contains("quarkus-resteasy-reactive-jackson"), + "quarkus-resteasy-reactive-jackson should not be in the list of extensions. Result:\n" + result); + + String content = CliDriver.readFileAsString(file); + Assertions.assertFalse(content.contains("quarkus-resteasy-reactive-jsonb"), + "quarkus-resteasy-reactive-jsonb should not be listed as a dependency. Result:\n" + content); + Assertions.assertFalse(content.contains("quarkus-resteasy-reactive-jackson"), + "quarkus-resteasy-reactive-jackson should not be listed as a dependency. Result:\n" + content); + + return result; + } + public static Result invokeExtensionListInstallable(Path projectRoot) throws Exception { Result result = CliDriver.execute(projectRoot, "extension", "list", "-e", "-B", "--verbose", "-i"); Assertions.assertEquals(CommandLine.ExitCode.OK, result.exitCode, diff --git a/devtools/cli/src/test/java/io/quarkus/cli/CliProjectGradleTest.java b/devtools/cli/src/test/java/io/quarkus/cli/CliProjectGradleTest.java index a0ae6007a5de8..354ed4055f511 100644 --- a/devtools/cli/src/test/java/io/quarkus/cli/CliProjectGradleTest.java +++ b/devtools/cli/src/test/java/io/quarkus/cli/CliProjectGradleTest.java @@ -400,25 +400,26 @@ public void testCreateArgPassthrough() throws Exception { } @Test - public void testCreateArgJava11() throws Exception { + public void testCreateArgJava17() throws Exception { CliDriver.Result result = CliDriver.execute(workspaceRoot, "create", "app", "--gradle", "-e", "-B", "--verbose", - "--java", "11"); + "--java", "17"); // We don't need to retest this, just need to make sure all the arguments were passed through Assertions.assertEquals(CommandLine.ExitCode.OK, result.exitCode, "Expected OK return code." + result); Path buildGradle = project.resolve("build.gradle"); String buildGradleContent = CliDriver.readFileAsString(buildGradle); - Assertions.assertTrue(buildGradleContent.contains("sourceCompatibility = JavaVersion.VERSION_11"), - "Java 11 should be used when specified. Found:\n" + buildGradle); + + Assertions.assertTrue(buildGradleContent.contains("sourceCompatibility = JavaVersion.VERSION_17"), + "Java 17 should be used when specified. Found:\n" + buildGradleContent); } @Test - public void testCreateArgJava17() throws Exception { + public void testCreateArgJava21() throws Exception { CliDriver.Result result = CliDriver.execute(workspaceRoot, "create", "app", "--gradle", "-e", "-B", "--verbose", - "--java", "17"); + "--java", "21"); // We don't need to retest this, just need to make sure all the arguments were passed through Assertions.assertEquals(CommandLine.ExitCode.OK, result.exitCode, "Expected OK return code." + result); @@ -426,8 +427,8 @@ public void testCreateArgJava17() throws Exception { Path buildGradle = project.resolve("build.gradle"); String buildGradleContent = CliDriver.readFileAsString(buildGradle); - Assertions.assertTrue(buildGradleContent.contains("sourceCompatibility = JavaVersion.VERSION_17"), - "Java 17 should be used when specified. Found:\n" + buildGradleContent); + Assertions.assertTrue(buildGradleContent.contains("sourceCompatibility = JavaVersion.VERSION_21"), + "Java 21 should be used when specified. Found:\n" + buildGradleContent); } String validateBasicGradleGroovyIdentifiers(Path project, String group, String artifact, String version) throws Exception { diff --git a/devtools/cli/src/test/java/io/quarkus/cli/CliProjectJBangTest.java b/devtools/cli/src/test/java/io/quarkus/cli/CliProjectJBangTest.java index 5a48ea499205b..7b585873e9053 100644 --- a/devtools/cli/src/test/java/io/quarkus/cli/CliProjectJBangTest.java +++ b/devtools/cli/src/test/java/io/quarkus/cli/CliProjectJBangTest.java @@ -168,34 +168,33 @@ public void testBuildOptions() throws Exception { } @Test - public void testCreateArgJava11() throws Exception { + public void testCreateArgJava17() throws Exception { CliDriver.Result result = CliDriver.execute(workspaceRoot, "create", "app", "--jbang", "-e", "-B", "--verbose", - "--java", "11"); + "--java", "17"); // We don't need to retest this, just need to make sure all the arguments were passed through Assertions.assertEquals(CommandLine.ExitCode.OK, result.exitCode, "Expected OK return code." + result); Path javaMain = validateJBangSourcePackage(project, ""); // no package name - String source = CliDriver.readFileAsString(javaMain); - Assertions.assertTrue(source.contains("//JAVA 11"), - "Generated source should contain //JAVA 11. Found:\n" + source); + Assertions.assertTrue(source.contains("//JAVA 17"), + "Generated source should contain //JAVA 17. Found:\n" + source); } @Test - public void testCreateArgJava17() throws Exception { + public void testCreateArgJava21() throws Exception { CliDriver.Result result = CliDriver.execute(workspaceRoot, "create", "app", "--jbang", "-e", "-B", "--verbose", - "--java", "17"); + "--java", "21"); // We don't need to retest this, just need to make sure all the arguments were passed through Assertions.assertEquals(CommandLine.ExitCode.OK, result.exitCode, "Expected OK return code." + result); Path javaMain = validateJBangSourcePackage(project, ""); // no package name String source = CliDriver.readFileAsString(javaMain); - Assertions.assertTrue(source.contains("//JAVA 17"), - "Generated source should contain //JAVA 17. Found:\n" + source); + Assertions.assertTrue(source.contains("//JAVA 21"), + "Generated source should contain //JAVA 21. Found:\n" + source); } void validateBasicIdentifiers(Path project, String group, String artifact, String version) throws Exception { diff --git a/devtools/cli/src/test/java/io/quarkus/cli/CliProjectMavenTest.java b/devtools/cli/src/test/java/io/quarkus/cli/CliProjectMavenTest.java index d8782c4f1b0d2..180795f1ae0e3 100644 --- a/devtools/cli/src/test/java/io/quarkus/cli/CliProjectMavenTest.java +++ b/devtools/cli/src/test/java/io/quarkus/cli/CliProjectMavenTest.java @@ -145,8 +145,10 @@ public void testExtensionList() throws Exception { CliDriver.invokeExtensionAddRedundantQute(project); CliDriver.invokeExtensionListInstallable(project); CliDriver.invokeExtensionAddMultiple(project, pom); + CliDriver.invokeExtensionAddMultipleCommas(project, pom); CliDriver.invokeExtensionRemoveQute(project, pom); CliDriver.invokeExtensionRemoveMultiple(project, pom); + CliDriver.invokeExtensionRemoveMultipleCommas(project, pom); CliDriver.invokeExtensionListInstallableSearch(project); CliDriver.invokeExtensionListFormatting(project); @@ -349,10 +351,10 @@ public void testCreateArgPassthrough() throws Exception { } @Test - public void testCreateArgJava11() throws Exception { + public void testCreateArgJava17() throws Exception { CliDriver.Result result = CliDriver.execute(workspaceRoot, "create", "app", "-e", "-B", "--verbose", - "--java", "11"); + "--java", "17"); // We don't need to retest this, just need to make sure all the arguments were passed through Assertions.assertEquals(CommandLine.ExitCode.OK, result.exitCode, "Expected OK return code." + result); @@ -360,15 +362,15 @@ public void testCreateArgJava11() throws Exception { Path pom = project.resolve("pom.xml"); String pomContent = CliDriver.readFileAsString(pom); - Assertions.assertTrue(pomContent.contains("maven.compiler.release>11<"), - "Java 11 should be used when specified. Found:\n" + pomContent); + Assertions.assertTrue(pomContent.contains("maven.compiler.release>17<"), + "Java 17 should be used when specified. Found:\n" + pomContent); } @Test - public void testCreateArgJava17() throws Exception { + public void testCreateArgJava21() throws Exception { CliDriver.Result result = CliDriver.execute(workspaceRoot, "create", "app", "-e", "-B", "--verbose", - "--java", "17"); + "--java", "21"); // We don't need to retest this, just need to make sure all the arguments were passed through Assertions.assertEquals(CommandLine.ExitCode.OK, result.exitCode, "Expected OK return code." + result); @@ -376,8 +378,8 @@ public void testCreateArgJava17() throws Exception { Path pom = project.resolve("pom.xml"); String pomContent = CliDriver.readFileAsString(pom); - Assertions.assertTrue(pomContent.contains("maven.compiler.release>17<"), - "Java 17 should be used when specified. Found:\n" + pomContent); + Assertions.assertTrue(pomContent.contains("maven.compiler.release>21<"), + "Java 21 should be used when specified. Found:\n" + pomContent); } @Test diff --git a/devtools/gradle/gradle-application-plugin/src/test/java/io/quarkus/gradle/QuarkusPluginTest.java b/devtools/gradle/gradle-application-plugin/src/test/java/io/quarkus/gradle/QuarkusPluginTest.java index 580777c029f6d..862ad0a62abb6 100644 --- a/devtools/gradle/gradle-application-plugin/src/test/java/io/quarkus/gradle/QuarkusPluginTest.java +++ b/devtools/gradle/gradle-application-plugin/src/test/java/io/quarkus/gradle/QuarkusPluginTest.java @@ -105,7 +105,7 @@ public void shouldReturnMultipleOutputSourceDirectories() { @Test public void shouldNotFailOnProjectDependenciesWithoutMain(@TempDir Path testProjectDir) throws IOException { - var kotlinVersion = System.getProperty("kotlin_version", "1.9.10"); + var kotlinVersion = System.getProperty("kotlin_version", "1.9.21"); var settingFile = testProjectDir.resolve("settings.gradle.kts"); var mppProjectDir = testProjectDir.resolve("mpp"); var quarkusProjectDir = testProjectDir.resolve("quarkus"); diff --git a/devtools/gradle/gradle/libs.versions.toml b/devtools/gradle/gradle/libs.versions.toml index 9902875d78e44..5bb10bc0b84fc 100644 --- a/devtools/gradle/gradle/libs.versions.toml +++ b/devtools/gradle/gradle/libs.versions.toml @@ -2,7 +2,7 @@ plugin-publish = "1.2.1" # updating Kotlin here makes QuarkusPluginTest > shouldNotFailOnProjectDependenciesWithoutMain(Path) fail -kotlin = "1.9.20" +kotlin = "1.9.21" smallrye-config = "3.4.4" junit5 = "5.10.1" diff --git a/devtools/gradle/gradle/wrapper/gradle-wrapper.properties b/devtools/gradle/gradle/wrapper/gradle-wrapper.properties index 82b3bd91387cb..80f3d5675f491 100644 --- a/devtools/gradle/gradle/wrapper/gradle-wrapper.properties +++ b/devtools/gradle/gradle/wrapper/gradle-wrapper.properties @@ -1,8 +1,8 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists # https://gradle.org/release-checksums/ -distributionSha256Sum=f2b9ed0faf8472cbe469255ae6c86eddb77076c75191741b4a462f33128dd419 -distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-all.zip +distributionSha256Sum=c16d517b50dd28b3f5838f0e844b7520b8f1eb610f2f29de7e4e04a1b7c9c79b +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-all.zip networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/devtools/gradle/settings.gradle.kts b/devtools/gradle/settings.gradle.kts index c276df18c0336..35d698336db3e 100644 --- a/devtools/gradle/settings.gradle.kts +++ b/devtools/gradle/settings.gradle.kts @@ -1,5 +1,5 @@ plugins { - id("com.gradle.enterprise") version "3.15.1" + id("com.gradle.enterprise") version "3.16" } gradleEnterprise { diff --git a/devtools/maven/pom.xml b/devtools/maven/pom.xml index ef37e2bdde26d..e29c8f896b6fa 100644 --- a/devtools/maven/pom.xml +++ b/devtools/maven/pom.xml @@ -15,6 +15,10 @@ Quarkus - Maven Plugin + + 11 + 11 + ${project.version} diff --git a/devtools/maven/src/main/java/io/quarkus/maven/CreateExtensionMojo.java b/devtools/maven/src/main/java/io/quarkus/maven/CreateExtensionMojo.java index 8487aa12aa3ad..99b628b4f6eed 100644 --- a/devtools/maven/src/main/java/io/quarkus/maven/CreateExtensionMojo.java +++ b/devtools/maven/src/main/java/io/quarkus/maven/CreateExtensionMojo.java @@ -179,6 +179,12 @@ public class CreateExtensionMojo extends AbstractMojo { @Parameter(property = "quarkusBomVersion") String quarkusBomVersion; + /** + * Version of Java used to build the project. + */ + @Parameter(property = "javaVersion") + private String javaVersion; + /** * Indicates whether to generate an extension codestart */ @@ -281,7 +287,8 @@ public void execute() throws MojoExecutionException { .quarkusVersion(quarkusVersion) .quarkusBomGroupId(quarkusBomGroupId) .quarkusBomArtifactId(quarkusBomArtifactId) - .quarkusBomGroupId(quarkusBomVersion) + .quarkusBomVersion(quarkusBomVersion) + .javaVersion(javaVersion) .withCodestart(withCodestart) .withoutUnitTest(withoutTests || withoutUnitTest) .withoutDevModeTest(withoutTests || withoutDevModeTest) diff --git a/devtools/maven/src/main/java/io/quarkus/maven/CreateProjectMojo.java b/devtools/maven/src/main/java/io/quarkus/maven/CreateProjectMojo.java index 254dded675ed4..3025394fba8f4 100644 --- a/devtools/maven/src/main/java/io/quarkus/maven/CreateProjectMojo.java +++ b/devtools/maven/src/main/java/io/quarkus/maven/CreateProjectMojo.java @@ -110,6 +110,9 @@ public class CreateProjectMojo extends AbstractMojo { @Parameter(property = "platformVersion", required = false) private String bomVersion; + /** + * Version of Java used to build the project. + */ @Parameter(property = "javaVersion") private String javaVersion; diff --git a/devtools/project-core-extension-codestarts/src/main/resources/codestarts/quarkus/extension-codestarts/grpc-codestart/base/src/main/resources/META-INF/resources/index.entry.qute.html b/devtools/project-core-extension-codestarts/src/main/resources/codestarts/quarkus/extension-codestarts/grpc-codestart/base/src/main/resources/META-INF/resources/index.entry.qute.html index 4f3acc377dc7f..07f8b05dacbc2 100644 --- a/devtools/project-core-extension-codestarts/src/main/resources/codestarts/quarkus/extension-codestarts/grpc-codestart/base/src/main/resources/META-INF/resources/index.entry.qute.html +++ b/devtools/project-core-extension-codestarts/src/main/resources/codestarts/quarkus/extension-codestarts/grpc-codestart/base/src/main/resources/META-INF/resources/index.entry.qute.html @@ -1,3 +1,3 @@ {#include index-entry} -{#body}
› It can be tested in the Dev UI (available in dev mode only). +{#body}
› It can be tested in the Dev UI (available in dev mode only). {/include} \ No newline at end of file diff --git a/docs/pom.xml b/docs/pom.xml index 49d905f237413..961a31af7f723 100644 --- a/docs/pom.xml +++ b/docs/pom.xml @@ -39,8 +39,8 @@ docker.io/jdkato/vale:v2.15.5 - 6.2 - 6.2 + 6.4 + 7.0 Quarkus - Documentation @@ -398,19 +398,6 @@
- - io.quarkus - quarkus-container-image-s2i-deployment - ${project.version} - pom - test - - - * - * - - - io.quarkus quarkus-core-deployment @@ -855,7 +842,7 @@ io.quarkus - quarkus-hibernate-search-orm-coordination-outbox-polling-deployment + quarkus-hibernate-search-orm-elasticsearch-deployment ${project.version} pom test @@ -868,7 +855,7 @@ io.quarkus - quarkus-hibernate-search-orm-elasticsearch-deployment + quarkus-hibernate-search-orm-outbox-polling-deployment ${project.version} pom test diff --git a/docs/src/main/asciidoc/_includes/prerequisites.adoc b/docs/src/main/asciidoc/_includes/prerequisites.adoc index d739de2f92348..2a2d0636e2b03 100644 --- a/docs/src/main/asciidoc/_includes/prerequisites.adoc +++ b/docs/src/main/asciidoc/_includes/prerequisites.adoc @@ -8,7 +8,7 @@ ifndef::prerequisites-time[] endif::[] * An IDE ifdef::prerequisites-ide[{prerequisites-ide}] -* JDK 11+ installed with `JAVA_HOME` configured appropriately +* JDK 17+ installed with `JAVA_HOME` configured appropriately ifndef::prerequisites-no-maven[] * Apache Maven {maven-version} endif::[] diff --git a/docs/src/main/asciidoc/amqp.adoc b/docs/src/main/asciidoc/amqp.adoc index 7aa090e3aa773..d70dd0e501512 100644 --- a/docs/src/main/asciidoc/amqp.adoc +++ b/docs/src/main/asciidoc/amqp.adoc @@ -257,7 +257,7 @@ Open the `src/main/resources/application.properties` file and add: mp.messaging.incoming.requests.address=quote-requests ---- -The configuration keys are structured as follows: +The configuration properties are structured as follows: `mp.messaging.[outgoing|incoming].{channel-name}.property=value` diff --git a/docs/src/main/asciidoc/ansible.adoc b/docs/src/main/asciidoc/ansible.adoc index a2708dec90c8d..05b5db94e7d8a 100644 --- a/docs/src/main/asciidoc/ansible.adoc +++ b/docs/src/main/asciidoc/ansible.adoc @@ -167,7 +167,7 @@ By default, the Ansible collection for Quarkus will install and use the OpenJDK [source,bash] ---- $ ansible-playbook -i inventory ... - -e quarkus_java_package_version: java-11-openjdk + -e quarkus_java_package_version: java-17-openjdk ---- ==== diff --git a/docs/src/main/asciidoc/building-my-first-extension.adoc b/docs/src/main/asciidoc/building-my-first-extension.adoc index e3a4f2d30aeca..a6423f43aa8dd 100644 --- a/docs/src/main/asciidoc/building-my-first-extension.adoc +++ b/docs/src/main/asciidoc/building-my-first-extension.adoc @@ -167,7 +167,7 @@ Your extension is a multi-module project. So let's start by checking out the par 3.11.0 ${surefire-plugin.version} - 11 + 17 UTF-8 UTF-8 {quarkus-version} diff --git a/docs/src/main/asciidoc/cdi-reference.adoc b/docs/src/main/asciidoc/cdi-reference.adoc index 8da9c60f50819..a41d9456f14a9 100644 --- a/docs/src/main/asciidoc/cdi-reference.adoc +++ b/docs/src/main/asciidoc/cdi-reference.adoc @@ -601,6 +601,8 @@ public class TracerConfiguration { NOTE: The runtime profile has absolutely no effect on the bean resolution using `@IfBuildProfile` and `@UnlessBuildProfile`. +TIP: It is also possible to use `@IfBuildProfile` and `@UnlessBuildProfile` on stereotypes. + [[enable_build_properties]] === Enabling Beans for Quarkus Build Properties @@ -654,6 +656,8 @@ public class TracerConfiguration { NOTE: Properties set at runtime have absolutely no effect on the bean resolution using `@IfBuildProperty`. +TIP: It is also possible to use `@IfBuildProperty` and `@UnlessBuildProperty` on stereotypes. + === Declaring Selected Alternatives In CDI, an alternative bean may be selected either globally for an application by means of `@Priority`, or for a bean archive using a `beans.xml` descriptor. diff --git a/docs/src/main/asciidoc/cli-tooling.adoc b/docs/src/main/asciidoc/cli-tooling.adoc index 5c712dde717aa..d363697be730a 100644 --- a/docs/src/main/asciidoc/cli-tooling.adoc +++ b/docs/src/main/asciidoc/cli-tooling.adoc @@ -147,7 +147,7 @@ You can use Homebrew to install (and update) the Quarkus CLI. Make sure you have a JDK installed before installing the Quarkus CLI. We haven't added an explicit dependency as we wanted to make sure you could use your preferred JDK version. -You can install a JDK with `brew install openjdk` for the latest Java version, `brew install openjdk@17` for Java 17, or `brew install openjdk@11` for Java 11. +You can install a JDK with `brew install openjdk` for the latest Java version, `brew install openjdk@17` for Java 17, or `brew install openjdk@21` for Java 21. ==== To install the Quarkus CLI using Homebrew, run the following command: @@ -190,7 +190,7 @@ You can use Chocolatey to install (and update) the Quarkus CLI. ==== Make sure you have a JDK installed before installing the Quarkus CLI. -You can install a JDK with `choco install ojdkbuild17` for Java 17 or `choco install ojdkbuild11` for Java 11. +You can install a JDK with `choco install temurin17` for Java 17 or `choco install temurin21` for Java 21. ==== To install the Quarkus CLI using Chocolatey, run the following command: @@ -226,7 +226,7 @@ You can use Scoop to install (and update) the Quarkus CLI. [NOTE] ==== Make sure you have a JDK installed before installing the Quarkus CLI. -You can install a JDK with `scoop install openjdk17` for Java 17 or `scoop install openjdk11` for Java 11. +You can install a JDK with `scoop install openjdk17` for Java 17 or `scoop install openjdk21` for Java 21. ==== To install the Quarkus CLI using Scoop, run the following command: [source,shell] diff --git a/docs/src/main/asciidoc/config-yaml.adoc b/docs/src/main/asciidoc/config-yaml.adoc index 9a84dcfb9f515..8a916f25fdc9f 100644 --- a/docs/src/main/asciidoc/config-yaml.adoc +++ b/docs/src/main/asciidoc/config-yaml.adoc @@ -192,9 +192,9 @@ The file must be present in the root of the working directory relative to the {p The values from this file override any values from the regular `application.yaml` file if it exists. -== Configuration key conflicts +== Configuration property conflicts -The MicroProfile Config specification defines configuration keys as an arbitrary `.`-delimited string. +The MicroProfile Config specification defines configuration properties as an arbitrary `.`-delimited string. However, structured formats such as YAML only support a subset of the possible configuration namespace. For example, consider the two configuration properties `quarkus.http.cors` and `quarkus.http.cors.methods`. One property is the prefix of another, so it might not be immediately evident how to specify both keys in your YAML configuration. @@ -210,4 +210,46 @@ quarkus: methods: GET,PUT,POST ---- -YAML `null` keys are not included in the assembly of the configuration property name, allowing them to be used at any level for disambiguating configuration keys. +YAML `null` keys are not included in the assembly of the configuration property name, allowing them to be used at any level for disambiguating configuration properties. + +Although Quarkus primarily uses `.properties` file extension for configuration, the snakeyaml library, which is used for parsing YAML in Quarkus, can also parse JSON structures. This means you can use YAML files with JSON content inside. + +YAML and JSON structures can be read in an application.yaml file. + +Certainly, here's a step-by-step guide on how to use complex configuration structures with Quarkus: + +* Define Your Configuration Interface. + +[source,java] +---- +@ConfigMapping(prefix = "server") +public interface ServiceConfig { + + List environments(); + + interface Environment { + String name(); + String services(); + } +} +---- + +* Create the appropriate JSON structure and store it in a YAML file. + +[source,yaml] +---- +{ + "server": { + "environments": [ + { + "name": "dev", + "services": "bookstore" + }, + { + "name": "batch", + "services": "warehouse" + } + ] + } +} +---- diff --git a/docs/src/main/asciidoc/context-propagation.adoc b/docs/src/main/asciidoc/context-propagation.adoc index 89955b1eddc97..13879ef0df4c5 100644 --- a/docs/src/main/asciidoc/context-propagation.adoc +++ b/docs/src/main/asciidoc/context-propagation.adoc @@ -174,7 +174,7 @@ The following configuration properties allow you to specify the default sets of [cols="1,1,1"] |=== -|Configuration Key|Description|Default Value +|Configuration property|Description|Default Value |`mp.context.ThreadContext.propagated` |The comma-separated set of propagated contexts diff --git a/docs/src/main/asciidoc/datasource.adoc b/docs/src/main/asciidoc/datasource.adoc index bdc6a62dd1826..81fbb90de0eba 100644 --- a/docs/src/main/asciidoc/datasource.adoc +++ b/docs/src/main/asciidoc/datasource.adoc @@ -351,7 +351,7 @@ The Hibernate ORM extension supports defining xref:hibernate-orm.adoc#multiple-p For each persistence unit, point to the datasource of your choice. ==== -Defining multiple datasources works like defining a single datasource, with one important change - you have to specify a name (configuration key) for each datasource. +Defining multiple datasources works like defining a single datasource, with one important change - you have to specify a name (configuration property) for each datasource. The following example provides three different datasources: @@ -379,7 +379,7 @@ quarkus.datasource.inventory.jdbc.url=jdbc:h2:mem:inventory quarkus.datasource.inventory.jdbc.max-size=12 ---- -Notice there is an extra section in the configuration key. +Notice there is an extra section in the configuration property. The syntax is as follows: `quarkus.datasource.[optional name.][datasource property]`. NOTE: Even when only one database extension is installed, named databases need to specify at least one build-time property so that Quarkus can detect them. diff --git a/docs/src/main/asciidoc/deploying-to-google-cloud.adoc b/docs/src/main/asciidoc/deploying-to-google-cloud.adoc index 534901043c870..b63d9d2c45fad 100644 --- a/docs/src/main/asciidoc/deploying-to-google-cloud.adoc +++ b/docs/src/main/asciidoc/deploying-to-google-cloud.adoc @@ -290,6 +290,15 @@ quarkus.vertx.prefer-native-transport=true WARNING: This only works when your application is running inside a Google Cloud managed runtime like App Engine. +=== Using Cloud SQL with native executables + +When generating native executables, you must also mark `jnr.ffi.provider.jffi.NativeFinalizer$SingletonHolder` as runtime initialized. + +[source,properties] +---- +quarkus.native.additional-build-args=--initialize-at-run-time=jnr.ffi.provider.jffi.NativeFinalizer$SingletonHolder +---- + == Going further You can find a set of extensions to access various Google Cloud Services in the Quarkiverse (a GitHub organization for Quarkus extensions maintained by the community), diff --git a/docs/src/main/asciidoc/deploying-to-heroku.adoc b/docs/src/main/asciidoc/deploying-to-heroku.adoc index 73637d12be8b8..97b717a81b25d 100644 --- a/docs/src/main/asciidoc/deploying-to-heroku.adoc +++ b/docs/src/main/asciidoc/deploying-to-heroku.adoc @@ -92,11 +92,11 @@ Two additional files are needed in your application's root directory: * `system.properties` to configure the Java version * `Procfile` to configure how Heroku starts your application -Quarkus needs JDK 11, so we specify that first: +Quarkus needs JDK 17, so we specify that first: [source,bash] ---- -echo "java.runtime.version=11" >> system.properties +echo "java.runtime.version=17" >> system.properties git add system.properties git commit -am "Configure the Java version for Heroku." ---- diff --git a/docs/src/main/asciidoc/deploying-to-kubernetes.adoc b/docs/src/main/asciidoc/deploying-to-kubernetes.adoc index 2b85072b421e2..b634f56f08b47 100644 --- a/docs/src/main/asciidoc/deploying-to-kubernetes.adoc +++ b/docs/src/main/asciidoc/deploying-to-kubernetes.adoc @@ -506,7 +506,7 @@ This would generate the following in the `env` section of your container: - env: - name: FOO valueFrom: - configMapRefKey: + configMapKeyRef: key: keyName name: my-configmap optional: false diff --git a/docs/src/main/asciidoc/deploying-to-openshift.adoc b/docs/src/main/asciidoc/deploying-to-openshift.adoc index 268d699a4b74a..93496d901bb5e 100644 --- a/docs/src/main/asciidoc/deploying-to-openshift.adoc +++ b/docs/src/main/asciidoc/deploying-to-openshift.adoc @@ -367,7 +367,7 @@ This would generate the following in the `env` section of your container: - env: - name: FOO valueFrom: - configMapRefKey: + configMapKeyRef: key: keyName name: my-configmap optional: false diff --git a/docs/src/main/asciidoc/dev-mode-differences.adoc b/docs/src/main/asciidoc/dev-mode-differences.adoc index b49ee27d3ca6b..30db2b5d0323c 100644 --- a/docs/src/main/asciidoc/dev-mode-differences.adoc +++ b/docs/src/main/asciidoc/dev-mode-differences.adoc @@ -50,7 +50,7 @@ Examples of such operations are: ==== A new Dev UI has been implemented in Quarkus 3.x. Not all the features are available yet. -You can still access the previous version of the Dev UI using: http://localhost:8080/q/dev-v1/. +You can still access the previous version of the Dev UI using: http://localhost:8080/q/dev-ui/. ==== === Error pages diff --git a/docs/src/main/asciidoc/funqy-gcp-functions.adoc b/docs/src/main/asciidoc/funqy-gcp-functions.adoc index 165a2a3900a15..02109969b2499 100644 --- a/docs/src/main/asciidoc/funqy-gcp-functions.adoc +++ b/docs/src/main/asciidoc/funqy-gcp-functions.adoc @@ -147,7 +147,7 @@ You will have a single JAR inside the `target/deployment` repository that contai Then you will be able to use `gcloud` to deploy your function to Google Cloud. The `gcloud` command will be different depending on which event triggers your function. -NOTE: We will use the Java 17 runtime but you can switch to the Java 11 runtime by using `--runtime=java11` instead of `--runtime=java17` on the deploy commands. +NOTE: We will use the Java 17 runtime but you can switch to the Java 21 runtime by using `--runtime=java21` instead of `--runtime=java17` on the deploy commands. [WARNING] ==== diff --git a/docs/src/main/asciidoc/gcp-functions-http.adoc b/docs/src/main/asciidoc/gcp-functions-http.adoc index 5a947083368d2..bb96971f19759 100644 --- a/docs/src/main/asciidoc/gcp-functions-http.adoc +++ b/docs/src/main/asciidoc/gcp-functions-http.adoc @@ -162,7 +162,7 @@ The result of the previous command is a single JAR file inside the `target/deplo Then you will be able to use `gcloud` to deploy your function to Google Cloud. -NOTE: We will use the Java 17 runtime but you can switch to the Java 11 runtime by using `--runtime=java11` instead of `--runtime=java17` on the deploy commands. +NOTE: We will use the Java 17 runtime but you can switch to the Java 21 runtime by using `--runtime=java21` instead of `--runtime=java17` on the deploy commands. [source,bash] ---- diff --git a/docs/src/main/asciidoc/gcp-functions.adoc b/docs/src/main/asciidoc/gcp-functions.adoc index 151d99319e314..319c73b226e9d 100644 --- a/docs/src/main/asciidoc/gcp-functions.adoc +++ b/docs/src/main/asciidoc/gcp-functions.adoc @@ -242,7 +242,7 @@ The result of the previous command is a single JAR file inside the `target/deplo Then you will be able to use `gcloud` to deploy your function to Google Cloud. The `gcloud` command will be different depending on which event triggers your function. -NOTE: We will use the Java 17 runtime but you can switch to the Java 11 runtime by using `--runtime=java11` instead of `--runtime=java17` on the deploy commands. +NOTE: We will use the Java 17 runtime but you can switch to the Java 21 runtime by using `--runtime=java21` instead of `--runtime=java17` on the deploy commands. [WARNING] ==== diff --git a/docs/src/main/asciidoc/grpc-service-implementation.adoc b/docs/src/main/asciidoc/grpc-service-implementation.adoc index 2d870aed2c917..8878d03c29b28 100644 --- a/docs/src/main/asciidoc/grpc-service-implementation.adoc +++ b/docs/src/main/asciidoc/grpc-service-implementation.adoc @@ -359,7 +359,7 @@ public class HelloServiceTest implements Greeter { == Trying out your services manually In the dev mode, you can try out your gRPC services in the Quarkus Dev UI. -Just go to http://localhost:8080/q/dev-v1 and click on _Services_ under the gRPC tile. +Just go to http://localhost:8080/q/dev-ui and click on _Services_ under the gRPC tile. Please note that your application needs to expose the "normal" HTTP port for the Dev UI to be accessible. If your application does not expose any HTTP endpoints, you can create a dedicated profile with a dependency on `quarkus-vertx-http`: [source,xml] diff --git a/docs/src/main/asciidoc/hibernate-orm.adoc b/docs/src/main/asciidoc/hibernate-orm.adoc index c76ac5894639b..3439cbae1e469 100644 --- a/docs/src/main/asciidoc/hibernate-orm.adoc +++ b/docs/src/main/asciidoc/hibernate-orm.adoc @@ -1242,3 +1242,82 @@ to tell Quarkus it should be used in the default persistence unit. + For <>, use `@PersistenceUnitExtension("nameOfYourPU")` <2> Implement `org.hibernate.engine.jdbc.spi.StatementInspector`. + +[[json_xml_serialization_deserialization]] +== Customizing JSON/XML serialization/deserialization + +By default, Quarkus will try to automatically configure format mappers depending on available extensions. +Globally configured `ObjectMapper` (or `Jsonb`) will be used for serialization/deserialization operations when Jackson (or JSON-B) is available. +Jackson will take precedence if both Jackson and JSON-B are available at the same time. + +JSON and XML serialization/deserialization in Hibernate ORM can be customized by implementing a `org.hibernate.type.format.FormatMapper` +and annotating the implementation with the appropriate qualifiers: + +[source,java] +---- +@JsonFormat // <1> +@PersistenceUnitExtension // <2> +public class MyJsonFormatMapper implements FormatMapper { // <3> + @Override + public String inspect(String sql) { + // ... + return sql; + } + @Override + public T fromString(CharSequence charSequence, JavaType javaType, WrapperOptions wrapperOptions) { + // ... + } + + @Override + public String toString(T value, JavaType javaType, WrapperOptions wrapperOptions) { + // ... + } +} +---- +<1> Annotate the format mapper implementation with the `@JsonFormat` qualifier +to tell Quarkus that this mapper is specific to JSON serialization/deserialization. ++ +<2> Annotate the format mapper implementation with the `@PersistenceUnitExtension` qualifier +to tell Quarkus it should be used in the default persistence unit. ++ +For <>, use `@PersistenceUnitExtension("nameOfYourPU")` +<3> Implement `org.hibernate.type.format.FormatMapper`. + +In case of a custom XML format mapper, a different CDI qualifier must be applied: + +[source,java] +---- +@XmlFormat // <1> +@PersistenceUnitExtension // <2> +public class MyJsonFormatMapper implements FormatMapper { // <3> + @Override + public String inspect(String sql) { + // ... + return sql; + } + @Override + public T fromString(CharSequence charSequence, JavaType javaType, WrapperOptions wrapperOptions) { + // ... + } + + @Override + public String toString(T value, JavaType javaType, WrapperOptions wrapperOptions) { + // ... + } +} +---- +<1> Annotate the format mapper implementation with the `@XmlFormat` qualifier +to tell Quarkus that this mapper is specific to XML serialization/deserialization. ++ +<2> Annotate the format mapper implementation with the `@PersistenceUnitExtension` qualifier +to tell Quarkus it should be used in the default persistence unit. ++ +For <>, use `@PersistenceUnitExtension("nameOfYourPU")` +<3> Implement `org.hibernate.type.format.FormatMapper`. + +[NOTE] +==== +Format mappers *must* have both `@PersistenceUnitExtension` and either `@JsonFormat` or `@XmlFormat` CDI qualifiers applied. + +Having multiple JSON (or XML) format mappers registered for the same persistence unit will result in an exception, because of the ambiguity. +==== diff --git a/docs/src/main/asciidoc/hibernate-search-orm-elasticsearch.adoc b/docs/src/main/asciidoc/hibernate-search-orm-elasticsearch.adoc index 37798c850598c..0113fdfbf6488 100644 --- a/docs/src/main/asciidoc/hibernate-search-orm-elasticsearch.adoc +++ b/docs/src/main/asciidoc/hibernate-search-orm-elasticsearch.adoc @@ -944,7 +944,7 @@ and relies on a background processor to consume these events and perform indexin To enable the `outbox-polling` coordination strategy, an additional extension is required: -:add-extension-extensions: hibernate-search-orm-coordination-outbox-polling +:add-extension-extensions: hibernate-search-orm-outbox-polling include::{includes}/devtools/extension-add.adoc[] Once the extension is there, you will need to explicitly select the `outbox-polling` strategy @@ -965,16 +965,16 @@ link:{hibernate-search-docs-url}#coordination-outbox-polling-schema[manually alt The database schema Hibernate Search will expect for outbox-polling coordination may be customized through the following configuration properties: -* link:#quarkus-hibernate-search-orm-coordination-outboxpolling_quarkus.hibernate-search-orm.coordination.entity.mapping.agent.catalog[`quarkus.hibernate-search-orm.coordination.entity.mapping.agent.catalog`] -* link:#quarkus-hibernate-search-orm-coordination-outboxpolling_quarkus.hibernate-search-orm.coordination.entity.mapping.agent.schema[`quarkus.hibernate-search-orm.coordination.entity.mapping.agent.schema`] -* link:#quarkus-hibernate-search-orm-coordination-outboxpolling_quarkus.hibernate-search-orm.coordination.entity.mapping.agent.table[`quarkus.hibernate-search-orm.coordination.entity.mapping.agent.table`] -* link:#quarkus-hibernate-search-orm-coordination-outboxpolling_quarkus.hibernate-search-orm.coordination.entity.mapping.agent.uuid-gen-strategy[`quarkus.hibernate-search-orm.coordination.entity.mapping.agent.uuid-gen-strategy`] -* link:#quarkus-hibernate-search-orm-coordination-outboxpolling_quarkus.hibernate-search-orm.coordination.entity.mapping.agent.uuid-type[`quarkus.hibernate-search-orm.coordination.entity.mapping.agent.uuid-type`] -* link:#quarkus-hibernate-search-orm-coordination-outboxpolling_quarkus.hibernate-search-orm.coordination.entity.mapping.outbox-event.catalog[`quarkus.hibernate-search-orm.coordination.entity.mapping.outbox-event.catalog`] -* link:#quarkus-hibernate-search-orm-coordination-outboxpolling_quarkus.hibernate-search-orm.coordination.entity.mapping.outbox-event.schema[`quarkus.hibernate-search-orm.coordination.entity.mapping.outbox-event.schema`] -* link:#quarkus-hibernate-search-orm-coordination-outboxpolling_quarkus.hibernate-search-orm.coordination.entity.mapping.outbox-event.table[`quarkus.hibernate-search-orm.coordination.entity.mapping.outbox-event.table`] -* link:#quarkus-hibernate-search-orm-coordination-outboxpolling_quarkus.hibernate-search-orm.coordination.entity.mapping.outbox-event.uuid-gen-strategy[`quarkus.hibernate-search-orm.coordination.entity.mapping.outbox-event.uuid-gen-strategy`] -* link:#quarkus-hibernate-search-orm-coordination-outboxpolling_quarkus.hibernate-search-orm.coordination.entity.mapping.outbox-event.uuid-type[`quarkus.hibernate-search-orm.coordination.entity.mapping.outbox-event.uuid-type`] +* link:#quarkus-hibernate-search-orm-outboxpolling_quarkus.hibernate-search-orm.coordination.entity-mapping.agent.catalog[`quarkus.hibernate-search-orm.coordination.entity-mapping.agent.catalog`] +* link:#quarkus-hibernate-search-orm-outboxpolling_quarkus.hibernate-search-orm.coordination.entity-mapping.agent.schema[`quarkus.hibernate-search-orm.coordination.entity-mapping.agent.schema`] +* link:#quarkus-hibernate-search-orm-outboxpolling_quarkus.hibernate-search-orm.coordination.entity-mapping.agent.table[`quarkus.hibernate-search-orm.coordination.entity-mapping.agent.table`] +* link:#quarkus-hibernate-search-orm-outboxpolling_quarkus.hibernate-search-orm.coordination.entity-mapping.agent.uuid-gen-strategy[`quarkus.hibernate-search-orm.coordination.entity-mapping.agent.uuid-gen-strategy`] +* link:#quarkus-hibernate-search-orm-outboxpolling_quarkus.hibernate-search-orm.coordination.entity-mapping.agent.uuid-type[`quarkus.hibernate-search-orm.coordination.entity-mapping.agent.uuid-type`] +* link:#quarkus-hibernate-search-orm-outboxpolling_quarkus.hibernate-search-orm.coordination.entity-mapping.outbox-event.catalog[`quarkus.hibernate-search-orm.coordination.entity-mapping.outbox-event.catalog`] +* link:#quarkus-hibernate-search-orm-outboxpolling_quarkus.hibernate-search-orm.coordination.entity-mapping.outbox-event.schema[`quarkus.hibernate-search-orm.coordination.entity-mapping.outbox-event.schema`] +* link:#quarkus-hibernate-search-orm-outboxpolling_quarkus.hibernate-search-orm.coordination.entity-mapping.outbox-event.table[`quarkus.hibernate-search-orm.coordination.entity-mapping.outbox-event.table`] +* link:#quarkus-hibernate-search-orm-outboxpolling_quarkus.hibernate-search-orm.coordination.entity-mapping.outbox-event.uuid-gen-strategy[`quarkus.hibernate-search-orm.coordination.entity-mapping.outbox-event.uuid-gen-strategy`] +* link:#quarkus-hibernate-search-orm-outboxpolling_quarkus.hibernate-search-orm.coordination.entity-mapping.outbox-event.uuid-type[`quarkus.hibernate-search-orm.coordination.entity-mapping.outbox-event.uuid-type`] ==== @@ -1071,4 +1071,4 @@ for more information. NOTE: These configuration properties require an additional extension. See <>. -include::{generated-dir}/config/quarkus-hibernate-search-orm-coordination-outboxpolling.adoc[leveloffset=+1, opts=optional] +include::{generated-dir}/config/quarkus-hibernate-search-orm-outboxpolling.adoc[leveloffset=+1, opts=optional] diff --git a/docs/src/main/asciidoc/http-reference.adoc b/docs/src/main/asciidoc/http-reference.adoc index 100241d911879..e8333a697dff3 100644 --- a/docs/src/main/asciidoc/http-reference.adoc +++ b/docs/src/main/asciidoc/http-reference.adoc @@ -151,9 +151,9 @@ Refer to the xref:./management-interface-reference.adoc[management interface ref ==== [[ssl]] -== Supporting secure connections with SSL +== Supporting secure connections with TLS/SSL -In order to have Quarkus support secure connections, you must either provide a certificate and associated key file, or supply a keystore. +To have Quarkus support secure connections, you must either provide a certificate and associated key file, or supply a keystore. In both cases, a password must be provided. See the designated paragraph for a detailed description of how to provide it. @@ -212,7 +212,7 @@ However, instead of providing the password as plain-text in the configuration fi as the environment variable `QUARKUS_HTTP_SSL_CERTIFICATE_KEY_STORE_PASSWORD`. This will also work in tandem with link:https://kubernetes.io/docs/concepts/configuration/secret/#using-secrets-as-environment-variables[Kubernetes secrets]. -_Note: in order to remain compatible with earlier versions of Quarkus (before 0.16) the default password is set to "password". It is therefore not a mandatory parameter!_ +_Note: To remain compatible with earlier versions of Quarkus (before 0.16) the default password is set to "password". It is therefore not a mandatory parameter!_ === Configure the HTTPS port @@ -298,7 +298,7 @@ include::{generated-dir}/config/quarkus-vertx-http-config-group-filter-config.ad == Support 100-Continue in vert.x -In order to support `100-continue`, the `quarkus.http.handle-100-continue-automatically` option needs to be enabled explicitly +To support `100-continue`, the `quarkus.http.handle-100-continue-automatically` option needs to be enabled explicitly For additional information check https://datatracker.ietf.org/doc/html/rfc7231#section-5.1.1[100-continue] and the related https://vertx.io/docs/apidocs/io/vertx/core/http/HttpServerOptions.html#DEFAULT_HANDLE_100_CONTINE_AUTOMATICALLY[Vert.x documentation]. @@ -431,6 +431,14 @@ include::{generated-dir}/config/quarkus-vertx-http-config-group-access-log-confi Set `quarkus.http.record-request-start-time=true` to enable recording request start times when using any of the attributes related to logging request processing times. ==== +[NOTE] +==== +Assuming security has been set up for the application (see our xref:security-overview.adoc[guide] for more details), +logging attribute `Remote user that was authenticated` is set to the value of the `io.quarkus.security.identity.SecurityIdentity` principal. +If your application use custom xref:security-customization.adoc#jaxrs-security-context[Jakarta REST SecurityContext], the context principal is used instead. +Please refer to the xref:logging.adoc[Logging guide] for options how to add contextual log information yourself. +==== + [TIP] ==== Use `quarkus.http.access-log.exclude-pattern=/some/path/.*` to exclude all entries concerning the path `/some/path/...` (_including subsequent paths_) from the log. diff --git a/docs/src/main/asciidoc/jreleaser.adoc b/docs/src/main/asciidoc/jreleaser.adoc index afe40f060fac7..bd60c76532354 100644 --- a/docs/src/main/asciidoc/jreleaser.adoc +++ b/docs/src/main/asciidoc/jreleaser.adoc @@ -621,8 +621,8 @@ As a reference, these are the full contents of the `pom.xml`: ${project.build.directory}/distributions 3.11.0 true - 11 - 11 + 17 + 17 UTF-8 UTF-8 quarkus-bom diff --git a/docs/src/main/asciidoc/kafka-reactive-getting-started.adoc b/docs/src/main/asciidoc/kafka-reactive-getting-started.adoc index cb4c541a3c9c1..b6813bfba3ae0 100644 --- a/docs/src/main/asciidoc/kafka-reactive-getting-started.adoc +++ b/docs/src/main/asciidoc/kafka-reactive-getting-started.adoc @@ -255,7 +255,7 @@ mp.messaging.incoming.requests.auto.offset.reset=earliest ---- Note that in this case we have one incoming and one outgoing connector configuration, each one distinctly named. -The configuration keys are structured as follows: +The configuration properties are structured as follows: `mp.messaging.[outgoing|incoming].{channel-name}.property=value` diff --git a/docs/src/main/asciidoc/kafka.adoc b/docs/src/main/asciidoc/kafka.adoc index 819d70d996dbe..b559acfa4d85d 100644 --- a/docs/src/main/asciidoc/kafka.adoc +++ b/docs/src/main/asciidoc/kafka.adoc @@ -1952,10 +1952,8 @@ mp.messaging.outgoing.fruit-out.compression.type=snappy ---- In JVM mode, it will work out of the box. -However, to compile your application to a native executable, you need to: - -1. Uses GraalVM 21.+ -2. Add `quarkus.kafka.snappy.enabled=true` to your `application.properties` +However, to compile your application to a native executable, you need to +add `quarkus.kafka.snappy.enabled=true` to your `application.properties`. In native mode, Snappy is disabled by default as the use of Snappy requires embedding a native library and unpacking it when the application starts. diff --git a/docs/src/main/asciidoc/logging.adoc b/docs/src/main/asciidoc/logging.adoc index a9329d517f4b1..81afed44a01c7 100644 --- a/docs/src/main/asciidoc/logging.adoc +++ b/docs/src/main/asciidoc/logging.adoc @@ -841,4 +841,5 @@ NOTE: If applicable, MDC data is stored in a _duplicated context_, which is an i [[loggingConfigurationReference]] == Logging configuration reference +include::{generated-dir}/config/quarkus-log-logging-log-build-time-config.adoc[opts=optional, leveloffset=+1] include::{generated-dir}/config/quarkus-log-logging-log-config.adoc[opts=optional, leveloffset=+1] diff --git a/docs/src/main/asciidoc/mailer-reference.adoc b/docs/src/main/asciidoc/mailer-reference.adoc index 399c63c259b9d..82b0254667905 100644 --- a/docs/src/main/asciidoc/mailer-reference.adoc +++ b/docs/src/main/asciidoc/mailer-reference.adoc @@ -322,7 +322,7 @@ Three API flavors are exposed: Check the xref:vertx.adoc[Using Vert.x guide] for further details about these different APIs and how to select the most suitable for you. -The retrieved `MailClient` is configured using the configuration key presented above. +The retrieved `MailClient` is configured using the configuration property presented above. You can also create your own instance, and pass your own configuration. diff --git a/docs/src/main/asciidoc/native-reference.adoc b/docs/src/main/asciidoc/native-reference.adoc index 5aaf0176fef95..fe4cc0980cbb1 100644 --- a/docs/src/main/asciidoc/native-reference.adoc +++ b/docs/src/main/asciidoc/native-reference.adoc @@ -539,7 +539,7 @@ You can find instructions on how to quickly set up this application in this guid This debugging guide has the following requirements: -* JDK 11 installed with `JAVA_HOME` configured appropriately +* JDK 17 installed with `JAVA_HOME` configured appropriately * Apache Maven {maven-version} * A working container runtime (Docker, podman) @@ -590,13 +590,6 @@ quarkus.container-image.build=true quarkus.container-image.group=test ---- -[IMPORTANT] -==== -Starting with 22.3, Mandrel does not provide a `-java11` version anymore. -Note, however, that this doesn't mean that you may no longer produce native executables with Mandrel for Java 11 projects. -You can still compile your Java 11 projects using OpenJDK 11 and produce native executables from the resulting Java 11 bytecode using the `-java17` Mandrel builder images. -==== - === First Debugging Steps As a first step, change to the project directory and build the native executable for the application: @@ -846,8 +839,8 @@ we need the following cypher script which will import the data within the CSV fi [source,cypher] ---- -CREATE CONSTRAINT unique_vm_id ON (v:VM) ASSERT v.vmId IS UNIQUE; -CREATE CONSTRAINT unique_method_id ON (m:Method) ASSERT m.methodId IS UNIQUE; +CREATE CONSTRAINT unique_vm_id FOR (v:VM) REQUIRE v.vmId IS UNIQUE; +CREATE CONSTRAINT unique_method_id FOR (m:Method) REQUIRE m.methodId IS UNIQUE; LOAD CSV WITH HEADERS FROM 'file:///reports/call_tree_vm.csv' AS row MERGE (v:VM {vmId: row.Id, name: row.Name}) diff --git a/docs/src/main/asciidoc/pulsar-getting-started.adoc b/docs/src/main/asciidoc/pulsar-getting-started.adoc index 2a0dfde3bad50..3d08b39a7d713 100644 --- a/docs/src/main/asciidoc/pulsar-getting-started.adoc +++ b/docs/src/main/asciidoc/pulsar-getting-started.adoc @@ -255,7 +255,7 @@ mp.messaging.incoming.requests.subscriptionInitialPosition=Earliest ---- Note that in this case we have one incoming and one outgoing connector configuration, each one distinctly named. -The configuration keys are structured as follows: +The configuration properties are structured as follows: `mp.messaging.[outgoing|incoming].{channel-name}.property=value` diff --git a/docs/src/main/asciidoc/qute-reference.adoc b/docs/src/main/asciidoc/qute-reference.adoc index d25cfa49e0de5..e978cc97db37a 100644 --- a/docs/src/main/asciidoc/qute-reference.adoc +++ b/docs/src/main/asciidoc/qute-reference.adoc @@ -2425,7 +2425,11 @@ TIP: Quarkus detects possible namespace collisions and fails the build if a spec === Global Variables The `io.quarkus.qute.TemplateGlobal` annotation can be used to denote static fields and methods that supply _global variables_ which are accessible in any template. -Internally, each global variable is added to the data map of any `TemplateInstance` via the `TemplateInstance#data(String, Object)` method. + +Global variables are: + +* added to the data map of any `TemplateInstance` during initialization, +* accessible with the `global:` namespace. .Global Variables Definition [source,java] @@ -2454,11 +2458,11 @@ public class Globals { [source,html] ---- User: {currentUser} <1> -Age: {age} <2> +Age: {global:age} <2> Colors: {#each myColors}{it}{#if it_hasNext}, {/if}{/each} <3> ---- <1> `currentUser` resolves to `Globals#user()`. -<2> `age` resolves to `Globals#age`. +<2> The `global:` namespace is used; `age` resolves to `Globals#age`. <3> `myColors` resolves to `Globals#myColors()`. NOTE: Note that global variables implicitly add <> to all templates and so any expression that references a global variable is validated during build. @@ -2473,7 +2477,7 @@ Colors: RED, BLUE ==== Resolving Conflicts -Global variables may conflict with regular data objects. +If not accessed via the `global:` namespace the global variables may conflict with regular data objects. <> override the global variables automatically. For example, the following definition overrides the global variable supplied by the `Globals#user()` method: diff --git a/docs/src/main/asciidoc/rabbitmq.adoc b/docs/src/main/asciidoc/rabbitmq.adoc index 255969990afcf..3dc3caa368282 100644 --- a/docs/src/main/asciidoc/rabbitmq.adoc +++ b/docs/src/main/asciidoc/rabbitmq.adoc @@ -295,7 +295,7 @@ mp.messaging.outgoing.quotes.exchange.name=quotes ---- Note that in this case we have one incoming and one outgoing connector configuration, each one distinctly named. -The configuration keys are structured as follows: +The configuration properties are structured as follows: `mp.messaging.[outgoing|incoming].{channel-name}.property=value` diff --git a/docs/src/main/asciidoc/reactive-event-bus.adoc b/docs/src/main/asciidoc/reactive-event-bus.adoc index b1c03416d0770..d0c2f3bc2ab52 100644 --- a/docs/src/main/asciidoc/reactive-event-bus.adoc +++ b/docs/src/main/asciidoc/reactive-event-bus.adoc @@ -6,6 +6,7 @@ https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc = Using the event bus include::_attributes.adoc[] :categories: messaging +:keywords: vertx vert.x :summary: This guide explains how different beans can interact using the event bus. :topics: messaging,event-bus,vert.x :extensions: io.quarkus:quarkus-vertx diff --git a/docs/src/main/asciidoc/rest-client-reactive.adoc b/docs/src/main/asciidoc/rest-client-reactive.adoc index b3c063bccdda2..1fea2b5a03736 100644 --- a/docs/src/main/asciidoc/rest-client-reactive.adoc +++ b/docs/src/main/asciidoc/rest-client-reactive.adoc @@ -974,7 +974,7 @@ public interface SseClient { Multi> get(); - class HeartbeatFilter implements Predicate> { + class HeartbeatFilter implements Predicate> { @Override public boolean test(SseEvent event) { diff --git a/docs/src/main/asciidoc/scheduler-reference.adoc b/docs/src/main/asciidoc/scheduler-reference.adoc index b999361cdb397..9b3164a3bddf9 100644 --- a/docs/src/main/asciidoc/scheduler-reference.adoc +++ b/docs/src/main/asciidoc/scheduler-reference.adoc @@ -144,6 +144,8 @@ So for example, `15m` can be used instead of `PT15M` and is parsed as "15 minute void every15Mins() { } ---- +WARNING: A value less than one second may not be supported by the underlying scheduler implementation. In that case a warning message is logged during build and application start. + The `every` attribute supports <> including default values and nested Property Expressions. (Note that `"{property.path}"` style expressions are still supported but don't offer the full functionality of Property Expressions.) @@ -164,7 +166,7 @@ So for example a Property Expression with the default value `"off"` can be used void myMethod() { } ---- - +[#identity] === Identity By default, a unique identifier is generated for each scheduled method. @@ -417,7 +419,7 @@ If the xref:smallrye-metrics.adoc[SmallRye Metrics extension] is present, then a == OpenTelemetry Tracing -If `quarkus.scheduler.tracing.enabled` is set to `true` and the xref:opentelemetry.adoc[OpenTelemetry extension] is present then the `@io.opentelemetry.instrumentation.annotations.WithSpan` annotation is added automatically to every `@Scheduled` method. As a result, each execution of this method has a new `io.opentelemetry.api.trace.Span` associated. +If `quarkus.scheduler.tracing.enabled` is set to `true` and the xref:opentelemetry.adoc[OpenTelemetry extension] is present then every job execution, either defined with the `@Scheduled` annotation or scheduled programmatically, automatically creates a span named after the job's <>. [[virtual_threads]] == Run @Scheduled methods on virtual threads diff --git a/docs/src/main/asciidoc/security-authentication-mechanisms.adoc b/docs/src/main/asciidoc/security-authentication-mechanisms.adoc index 108657ce55dc0..97cd7d8e899ff 100644 --- a/docs/src/main/asciidoc/security-authentication-mechanisms.adoc +++ b/docs/src/main/asciidoc/security-authentication-mechanisms.adoc @@ -71,7 +71,7 @@ For more information, see the following documentation: [[form-auth]] === Form-based authentication -Quarkus provides form-based authentication that works similarly to traditional Servlet form-based auth. +Quarkus provides form-based authentication that works similarly to traditional Servlet form-based authentication. Unlike traditional form authentication, the authenticated user is not stored in an HTTP session because Quarkus does not support clustered HTTP sessions. Instead, the authentication information is stored in an encrypted cookie, which can be read by all cluster members who share the same encryption key. @@ -92,12 +92,12 @@ quarkus.http.auth.form.landing-page= quarkus.http.auth.form.login-page= quarkus.http.auth.form.error-page= -# HttpOnly must be false if you want to logout on the client, it can be true if logging out on from the server +# HttpOnly must be false if you want to log out on the client; it can be true if logging out from the server quarkus.http.auth.form.http-only-cookie=false ---- -Now that you have disabled redirects for the SPA, you must login and logout programmatically from your client. -Below are example JavaScript methods for logging into the `j_security_check` endpoint and logging out of the application by destroying the cookie. +Now that you have disabled redirects for the SPA, you must log in and log out programmatically from your client. +Below are examples of JavaScript methods for logging into the `j_security_check` endpoint and logging out of the application by destroying the cookie. [source,javascript] ---- @@ -130,21 +130,21 @@ const login = () => { }; ---- -To logout of the SPA from the client the cookie must be set to `quarkus.http.auth.form.http-only-cookie=false` so you can destroy +To log out of the SPA from the client, the cookie must be set to `quarkus.http.auth.form.http-only-cookie=false` so you can destroy the cookie and possibly redirect back to your main page. [source,javascript] ---- const logout= () => { - // delete the credential cookie essentially killing the session + // delete the credential cookie, essentially killing the session const removeCookie = `quarkus-credential=; Max-Age=0;path=/`; document.cookie = removeCookie; - - // perform post logout actions here such as redirecting back to your login page + + // perform post-logout actions here, such as redirecting back to your login page }; ---- -To logout of the SPA from the server the cookie can be set to `quarkus.http.auth.form.http-only-cookie=true` and use this example +To log out of the SPA from the server, the cookie can be set to `quarkus.http.auth.form.http-only-cookie=true` and use this example code to destroy the cookie. [source,java] @@ -173,6 +173,7 @@ public Response logout() { The following properties can be used to configure form-based authentication: include::{generated-dir}/config/quarkus-vertx-http-config-group-form-auth-config.adoc[opts=optional, leveloffset=+1] +include::{generated-dir}/config/quarkus-vertx-http-config-group-auth-runtime-config.adoc[opts=optional, leveloffset=+1] [[mutual-tls]] === Mutual TLS authentication @@ -180,7 +181,7 @@ include::{generated-dir}/config/quarkus-vertx-http-config-group-form-auth-config Quarkus provides mutual TLS (mTLS) authentication so that you can authenticate users based on their X.509 certificates. To use this authentication method, you must first enable SSL/TLS for your application. -For more information, see the xref:http-reference.adoc#ssl[Supporting secure connections with SSL] section of the Quarkus "HTTP reference" guide. +For more information, see the xref:http-reference.adoc#ssl[Supporting secure connections with SSL/TLS] section of the Quarkus "HTTP reference" guide. After your application accepts secure connections, the next step is to configure the `quarkus.http.ssl.certificate.trust-store-file` property with the name of that file that holds all the certificates your application trusts. The specified file also includes information about how your application asks for certificates when a client, such as a browser or other service, tries to access one of its protected resources. @@ -193,6 +194,7 @@ quarkus.http.ssl.certificate.trust-store-password=the_trust_store_secret quarkus.http.ssl.client-auth=required <3> quarkus.http.auth.permission.default.paths=/* <4> quarkus.http.auth.permission.default.policy=authenticated +quarkus.http.insecure-requests=disabled <5> ---- <1> The keystore where the server's private key is located. <2> The truststore from which the trusted certificates are loaded. @@ -200,6 +202,7 @@ quarkus.http.auth.permission.default.policy=authenticated To relax this requirement so that the server accepts requests without a certificate, set the value to `REQUEST`. This option is useful when you are also supporting authentication methods other than mTLS. <4> Defines a policy where only authenticated users should have access to resources from your application. +<5> Optionally,explicitly disable the plain HTTP protocol, and consequently require all requests to be made over HTTPS. If `quarkus.http.ssl.client-auth` is set to `required`, the `quarkus.http.insecure-requests` property is automatically set to `disabled`. When the incoming request matches a valid certificate in the truststore, your application can obtain the subject by injecting a `SecurityIdentity` as follows: @@ -250,7 +253,7 @@ Quarkus Security also supports the following authentication mechanisms through e https://webauthn.guide/[WebAuthn] is an authentication mechanism that replaces passwords. When you write a service for registering new users, or logging them in, instead of asking for a password, you can use WebAuthn, which replaces the password. -For more information, see the xref:security-webauthn.adoc[Secure a Quarkus application by using the WebAuthn authentication mechanism] guide. +For more information, see the xref:security-webauthn.adoc[Secure a Quarkus application by using the WebAuthn authentication mechanism] guide. [[openid-connect-authentication]] === OpenID Connect authentication @@ -290,7 +293,7 @@ For more information about OIDC authentication and authorization methods that yo [NOTE] ==== To enable the Quarkus OIDC extension at runtime, set `quarkus.oidc.tenant-enabled=false` at build time. -Then re-enable it at runtime by using a system property. +Then, re-enable it at runtime by using a system property. For more information about managing the individual tenant configurations in multitenant OIDC deployments, see the xref:security-openid-connect-multitenancy.adoc#disable-tenant[Disabling tenant configurations] section in the "Using OpenID Connect (OIDC) multi-tenancy" guide. ==== @@ -329,7 +332,7 @@ It represents them as `org.eclipse.microprofile.jwt.JsonWebToken`. `quarkus-smallrye-jwt` is an alternative to the `quarkus-oidc` Bearer token authentication mechanism and verifies only `JWT` tokens by using either Privacy Enhanced Mail (PEM) keys or the refreshable `JWK` key set. `quarkus-smallrye-jwt` also provides the JWT generation API, which you can use to easily create `signed`, `inner-signed`, and `encrypted` `JWT` tokens. -For more information, see the xref:security-jwt.adoc[Using JWT RBAC] guide. +For more information, see the xref:security-jwt.adoc[Using JWT RBAC] guide. [[oauth2-authentication]] === OAuth2 authentication @@ -343,7 +346,7 @@ For more information, see the Quarkus xref:security-oauth2.adoc[Using OAuth2] gu Use the following information to select the appropriate token authentication mechanism to secure your Quarkus applications. -.List of authentication mechanism use-cases +.List of authentication mechanism use cases * `quarkus-oidc` requires an OpenID Connect provider such as Keycloak, which can verify the bearer tokens or authenticate the end users with the Authorization Code flow. In both cases, `quarkus-oidc` requires a connection to the specified OpenID Connect provider. @@ -355,7 +358,7 @@ In both cases, `quarkus-oidc` requires a connection to the specified OpenID Conn * If your bearer tokens are in a JSON web token (JWT) format, you can use any extensions in the preceding list. Both `quarkus-oidc` and `quarkus-smallrye-jwt` support refreshing the `JsonWebKey` (JWK) set when the OpenID Connect provider rotates the keys. -Therefore, if remote token introspection must be avoided or is unsupported by the providers, use `quarkus-oidc` or `quarkus-smallrye-jwt` for verifying JWT tokens. +Therefore, if remote token introspection must be avoided or is unsupported by the providers, use `quarkus-oidc` or `quarkus-smallrye-jwt` to verify JWT tokens. * To introspect the JWT tokens remotely, you can use either `quarkus-oidc` or `quarkus-elytron-security-oauth2` because they support verifying the opaque or binary tokens by using remote introspection. `quarkus-smallrye-jwt` does not support the remote introspection of both opaque or JWT tokens but instead relies on the locally available keys that are usually retrieved from the OpenID Connect provider. @@ -392,7 +395,7 @@ Nonetheless, the providers effectively delegate most of the token-associated sta s|Bearer JWT verification ^|Local verification or introspection ^|Local verification ^|Introspection s|Bearer opaque token verification ^|Introspection ^|No ^|Introspection -s|Refreshing `JsonWebKey` set for verifying JWT tokens ^|Yes ^|Yes ^|No +s|Refreshing `JsonWebKey` set to verify JWT tokens ^|Yes ^|Yes ^|No s|Represent token as `Principal` ^|Yes ^|Yes ^|Yes s|Inject JWT as MP JWT ^|Yes ^|Yes ^|No 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 698225dd0f3a3..0dc755ba4788d 100644 --- a/docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc +++ b/docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc @@ -87,9 +87,9 @@ import io.vertx.ext.web.RoutingContext; @ApplicationScoped public class CustomNamedHttpSecPolicy implements HttpSecurityPolicy { @Override - public Uni checkPermission(RoutingContext request, Uni identity, + public Uni checkPermission(RoutingContext event, Uni identity, AuthorizationRequestContext requestContext) { - if (customRequestAuthorization(request)) { + if (customRequestAuthorization(event)) { return Uni.createFrom().item(CheckResult.PERMIT); } return Uni.createFrom().item(CheckResult.DENY); @@ -99,6 +99,11 @@ public class CustomNamedHttpSecPolicy implements HttpSecurityPolicy { public String name() { return "custom"; <1> } + + private static boolean customRequestAuthorization(RoutingContext event) { + // here comes your own security check + return !event.request().path().endsWith("denied"); + } } ---- <1> Named HTTP Security policy will only be applied to requests matched by the `application.properties` path matching rules. @@ -335,6 +340,19 @@ This configuration only impacts resources served from the fixed or static URL, ` For more information, see link:https://quarkus.io/blog/path-resolution-in-quarkus/[Path Resolution in Quarkus]. +[[map-security-identity-roles]] +=== Map `SecurityIdentity` roles + +Winning role-based policy can map the `SecurityIdentity` roles to the deployment specific roles. +These roles can later be used for endpoint authorization with the `@RolesAllowed` annotation. + +[source,properties] +---- +quarkus.http.auth.policy.admin-policy1.roles.admin=Admin1 <1> +quarkus.http.auth.permission.roles1.paths=/* +quarkus.http.auth.permission.roles1.policy=admin-policy1 +---- +<1> Map the `admin` role to `Admin1` role. The `SecurityIdentity` will have both `admin` and `Admin1` roles. [[standard-security-annotations]] == Authorization using annotations @@ -363,8 +381,6 @@ The following <> demonstrates an endpoint that uses both Jakart ---- import java.security.Principal; -import jakarta.annotation.security.DenyAll; -import jakarta.annotation.security.PermitAll; import jakarta.annotation.security.RolesAllowed; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; @@ -604,7 +620,7 @@ quarkus.http.auth.permission.roles1.paths=/crud/modify/*,/crud/id/* quarkus.http.auth.permission.roles1.policy=role-policy1 quarkus.http.auth.policy.role-policy2.permissions.user=list -quarkus.http.auth.policy.role-policy2.permission-class=org.acme.crud.CRUDResource.CustomPermission <4> +quarkus.http.auth.policy.role-policy2.permission-class=org.acme.crud.CRUDResource$CustomPermission <4> quarkus.http.auth.permission.roles2.paths=/crud/list quarkus.http.auth.permission.roles2.policy=role-policy2 ---- @@ -613,7 +629,7 @@ Similarly, for the `@PermissionsAllowed` annotation, `io.quarkus.security.String <2> Permissions `create`, `update`, and `read` are mapped to the role `admin`. <3> The role policy `role-policy1` allows only authenticated requests to access `/crud/modify` and `/crud/id` sub-paths. For more information about the path-matching algorithm, see <> later in this guide. -<4> You can also specify a custom implementation of the `java.security.Permission` class. +<4> You can specify a custom implementation of the `java.security.Permission` class. Your custom class must define exactly one constructor that accepts the permission name and optionally some actions, for example, `String` array. In this scenario, the permission `list` is added to the `SecurityIdentity` instance as `new CustomPermission("list")`. @@ -625,6 +641,8 @@ Later, Quarkus instantiates your custom permission with actual arguments, with w [source,java] ---- +package org.acme.library; + import java.security.Permission; import java.util.Arrays; import java.util.Set; @@ -652,7 +670,7 @@ public class LibraryPermission extends Permission { return false; } - ... + // here comes your own implementation of the `java.security.Permission` class methods public static abstract class Library { @@ -671,7 +689,7 @@ public class LibraryPermission extends Permission { } public static class TvLibrary extends MediaLibrary { - ... + // TvLibrary specific implementation of the 'isParentLibraryOf' method } } ---- @@ -686,8 +704,11 @@ The following example shows how the `LibraryPermission` class can be used: [source,java] ---- +package org.acme.library; + import io.quarkus.security.PermissionsAllowed; import jakarta.enterprise.context.ApplicationScoped; +import org.acme.library.LibraryPermission.Library; @ApplicationScoped public class LibraryService { @@ -716,6 +737,15 @@ The permission constructor and the annotated method must have the parameter `lib [source,java] ---- +package org.acme.library; + +import io.quarkus.security.PermissionsAllowed; +import jakarta.inject.Inject; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import org.acme.library.LibraryPermission.Library; + @Path("/library") public class LibraryResource { @@ -746,9 +776,6 @@ 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 { @@ -769,10 +796,12 @@ quarkus.http.auth.policy.role-policy3.permission-class=org.acme.library.MediaLib quarkus.http.auth.permission.roles3.paths=/library/* quarkus.http.auth.permission.roles3.policy=role-policy3 ---- - <1> Grants the permission `media-library`, which permits `read`, `write`, and `list` actions. Because `MediaLibrary` is the `TvLibrary` class parent, a user with the `admin` role is also permitted to modify `TvLibrary`. +TIP: The `/library/*` path can be tested from a Keycloak provider Dev UI page, because the user `alice` which is created +automatically by the xref:security-openid-connect-dev-services.adoc[Dev Services for Keycloak] has an `admin` role. + The examples provided so far use role-to-permission mapping. You can also add permissions to the `SecurityIdentity` instance programmatically. In the following example, xref:security-customization.adoc#security-identity-customization[`SecurityIdentity` is customized] to add the same permission that was previously granted with the HTTP role-based policy. diff --git a/docs/src/main/asciidoc/security-csrf-prevention.adoc b/docs/src/main/asciidoc/security-csrf-prevention.adoc index 880487683477f..b56e7d49ad941 100644 --- a/docs/src/main/asciidoc/security-csrf-prevention.adoc +++ b/docs/src/main/asciidoc/security-csrf-prevention.adoc @@ -11,7 +11,7 @@ include::_attributes.adoc[] https://owasp.org/www-community/attacks/csrf[Cross-Site Request Forgery (CSRF)] is an attack that forces an end user to execute unwanted actions on a web application in which they are currently authenticated. -Quarkus Security provides a CSRF prevention feature which implements https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#double-submit-cookie[Double Submit Cookie] and [CSRF Request Header] techniques. +Quarkus Security provides a CSRF prevention feature which implements https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#double-submit-cookie[Double Submit Cookie] and <> techniques. `Double Submit Cookie` technique requires that the CSRF token sent as `HTTPOnly`, optionally signed, cookie to the client, and directly embedded in a hidden form input of server-side rendered HTML forms, or submitted as a request header value. @@ -139,6 +139,7 @@ You can get `HMAC` signatures created for the generated CSRF tokens and have the quarkus.csrf-reactive.token-signature-key=AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow ---- +[[csrf-request-header]] == CSRF Request Header If HTML `form` tags are not used and you need to pass CSRF token as a header, then inject the header name and token, for example, into HTMX: diff --git a/docs/src/main/asciidoc/security-getting-started-tutorial.adoc b/docs/src/main/asciidoc/security-getting-started-tutorial.adoc index da06f8669bc1a..30d84156ad5fa 100644 --- a/docs/src/main/asciidoc/security-getting-started-tutorial.adoc +++ b/docs/src/main/asciidoc/security-getting-started-tutorial.adoc @@ -462,9 +462,9 @@ As you can see in this code sample, you do not need to start the test container [NOTE] ==== -When you start your application in dev mode, `Dev Services for PostgreSQL` launches a `PostgreSQL` `devmode` container so that you can start developing your application. +When you start your application in dev mode, Dev Services for PostgreSQL launches a PostgreSQL dev mode container so that you can start developing your application. While developing your application, you can add tests one by one and run them using the xref:continuous-testing.adoc[Continuous Testing] feature. -`Dev Services for PostgreSQL` supports testing while you develop by providing a separate `PostgreSQL` test container that does not conflict with the `devmode` container. +Dev Services for PostgreSQL supports testing while you develop by providing a separate PostgreSQL test container that does not conflict with the dev mode container. ==== === Use Curl or a browser to test your application diff --git a/docs/src/main/asciidoc/security-keycloak-authorization.adoc b/docs/src/main/asciidoc/security-keycloak-authorization.adoc index 6f58b3c0681c1..8d6c6a063ac92 100644 --- a/docs/src/main/asciidoc/security-keycloak-authorization.adoc +++ b/docs/src/main/asciidoc/security-keycloak-authorization.adoc @@ -235,7 +235,7 @@ include::{includes}/devtools/dev.adoc[] xref:security-openid-connect-dev-services.adoc[Dev Services for Keycloak] will launch a Keycloak container and import a `quarkus-realm.json`. -Open a xref:dev-ui.adoc[Dev UI] available at http://localhost:8080/q/dev-v1[/q/dev-v1] and click on a `Provider: Keycloak` link in an `OpenID Connect` `Dev UI` card. +Open a xref:dev-ui.adoc[Dev UI] available at http://localhost:8080/q/dev-ui[/q/dev-ui] and click on a `Provider: Keycloak` link in an `OpenID Connect` `Dev UI` card. You will be asked to log in into a `Single Page Application` provided by `OpenID Connect Dev UI`: diff --git a/docs/src/main/asciidoc/security-oidc-auth0-tutorial.adoc b/docs/src/main/asciidoc/security-oidc-auth0-tutorial.adoc index f652fa7ca262a..e0796c3b007c0 100644 --- a/docs/src/main/asciidoc/security-oidc-auth0-tutorial.adoc +++ b/docs/src/main/asciidoc/security-oidc-auth0-tutorial.adoc @@ -914,7 +914,7 @@ public class GreetingResourceTest { } ---- -If you recall, when the application was started in devmode, the following could be seen in the CLI window: +If you recall, when the application was started in dev mode, the following could be seen in the CLI window: image::auth0-devmode-started.png[Auth0 DevMode started] @@ -1075,7 +1075,7 @@ Open a browser, access http://localhost:8080/hello and get the name displayed in == Troubleshooting -The steps described in this tutorial should work exactly as the tutorial describes. You might have to clear the browser cookies when accessing the updated Quarkus endpoint if you have already completed the authentication. You might need to restart the Quarkus application manually in devmode but it is not expected. If you need help completing this tutorial, you can get in touch with the Quarkus team. +The steps described in this tutorial should work exactly as the tutorial describes. You might have to clear the browser cookies when accessing the updated Quarkus endpoint if you have already completed the authentication. You might need to restart the Quarkus application manually in dev mode but it is not expected. If you need help completing this tutorial, you can get in touch with the Quarkus team. == Summary diff --git a/docs/src/main/asciidoc/security-oidc-bearer-token-authentication-tutorial.adoc b/docs/src/main/asciidoc/security-oidc-bearer-token-authentication-tutorial.adoc index 79b64542b456d..6b26fa8d86dbf 100644 --- a/docs/src/main/asciidoc/security-oidc-bearer-token-authentication-tutorial.adoc +++ b/docs/src/main/asciidoc/security-oidc-bearer-token-authentication-tutorial.adoc @@ -52,12 +52,7 @@ You can clone the Git repository by running the command `git clone {quickstarts- The solution is located in the `security-openid-connect-quickstart` link:{quickstarts-tree-url}/security-openid-connect-quickstart[directory]. -== Procedure - -:sectnums: -:sectnumlevels: 3 - -=== Create the Maven project +== Create the Maven project You can either create a new Maven project with the `oidc` extension or you can add the extension to an existing Maven project. Complete one of the following commands: @@ -84,7 +79,6 @@ The following configuration gets added to your build file: * Using Maven (pom.xml): + ==== --- [source,xml,role="primary asciidoc-tabs-target-sync-cli asciidoc-tabs-target-sync-maven"] ---- @@ -92,7 +86,6 @@ The following configuration gets added to your build file: quarkus-oidc ---- --- ==== + * Using Gradle (build.gradle): @@ -106,7 +99,7 @@ implementation("io.quarkus:quarkus-oidc") -- ==== -=== Write the application +== Write the application . Implement the `/api/users/me` endpoint as shown in the following example, which is a regular Jakarta REST resource: + @@ -185,7 +178,7 @@ The main difference in this example is that the `@RolesAllowed` annotation is us Injection of the `SecurityIdentity` is supported in both `@RequestScoped` and `@ApplicationScoped` contexts. -=== Configure the application +== Configure the application * Configure the Quarkus OpenID Connect (OIDC) extension by setting the following configuration properties in the `src/main/resources/application.properties` file. + @@ -215,7 +208,7 @@ For more information, see the <> section. For more information, see the Quarkus xref:security-oidc-configuration-properties-reference.adoc[OpenID Connect (OIDC) configuration properties] guide. -=== Start and configure the Keycloak server +== Start and configure the Keycloak server . Put the link:{quickstarts-tree-url}/security-openid-connect-quickstart/config/quarkus-realm.json[realm configuration file] on the classpath (`target/classes` directory) so that it gets imported automatically when running in dev mode. You do not need to do this if you have already built a link:{quickstarts-tree-url}/security-openid-connect-quickstart[complete solution], in which case, this realm file is added to the classpath during the build. @@ -234,7 +227,7 @@ For more information, see the <> section. docker run --name keycloak -e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=admin -p 8180:8080 quay.io/keycloak/keycloak:{keycloak.version} start-dev ---- ==== -* Where the `keycloak.version` is set to version `17.0.0` or later. +* Where the `keycloak.version` is set to version `23.0.0` or later. . You can access your Keycloak Server at http://localhost:8180[localhost:8180]. . To access the Keycloak Administration Console, log in as the `admin` user by using the following login credentials: @@ -243,27 +236,27 @@ docker run --name keycloak -e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=ad . Import the link:{quickstarts-tree-url}/security-openid-connect-quickstart/config/quarkus-realm.json[realm configuration file] from the upstream community repository to create a new realm. -For more information, see the Keycloak documentation about link:https://www.keycloak.org/docs/latest/server_admin/index.html#_create-realm[creating a new realm]. +For more information, see the Keycloak documentation about link:https://www.keycloak.org/docs/latest/server_admin/index.html#configuring-realms[create and configure a new realm]. [NOTE] ==== If you want to use the Keycloak Admin Client to configure your server from your application, you need to include either the `quarkus-keycloak-admin-client` or the `quarkus-keycloak-admin-client-reactive` (if the application uses `quarkus-rest-client-reactive`) extension. -For more information, see the link:{url-quarkusio-guides}security-keycloak-admin-client[Quarkus Keycloak Admin Client] guide. +For more information, see the xref:security-keycloak-admin-client.adoc[Quarkus Keycloak Admin Client] guide. ==== [[keycloak-dev-mode]] -=== Run the application in dev mode +== Run the application in dev mode . To run the application in dev mode, run the following commands: + ==== include::{includes}/devtools/dev.adoc[] ==== -* link:{quarkusio-guides}/security-openid-connect-dev-services[Dev Services for Keycloak] will start a Keycloak container and import a `quarkus-realm.json`. -. Open a link:{url-quarkusio-guides}dev-ui[Dev UI], which you can find at http://localhost:8080/q/dev-v1[/q/dev-v1], then click a `Provider: Keycloak` link in an `OpenID Connect` `Dev UI` card. +* xref:security-openid-connect-dev-services.adoc[Dev Services for Keycloak] will start a Keycloak container and import a `quarkus-realm.json`. +. Open a xref:dev-ui.adoc[Dev UI], which you can find at http://localhost:8080/q/dev-ui[/q/dev-ui], then click a `Provider: Keycloak` link in an `OpenID Connect` `Dev UI` card. . When prompted to log in to a `Single Page Application` provided by `OpenID Connect Dev UI`, do the following steps: * Log in as `alice` (password: `alice`), who has a `user` role. @@ -273,7 +266,7 @@ include::{includes}/devtools/dev.adoc[] ** Accessing `/api/admin` returns `200`. ** Accessing `/api/users/me` returns `200`. -=== Run the Application in JVM mode +== Run the Application in JVM mode When you are done with dev mode, you can run the application as a standard Java application. @@ -291,7 +284,7 @@ java -jar target/quarkus-app/quarkus-run.jar ---- ==== -=== Run the application in native mode +== Run the application in native mode You can compile this same demo as-is into native mode without needing any modifications. This implies that you no longer need to install a JVM on your production environment. @@ -314,7 +307,7 @@ include::{includes}/devtools/build-native.adoc[] ---- ==== -=== Test the application +== Test the application For information about testing your application in dev mode, see the preceding <> section. diff --git a/docs/src/main/asciidoc/security-oidc-bearer-token-authentication.adoc b/docs/src/main/asciidoc/security-oidc-bearer-token-authentication.adoc index b6d397bbb3d72..16261ebf9deb5 100644 --- a/docs/src/main/asciidoc/security-oidc-bearer-token-authentication.adoc +++ b/docs/src/main/asciidoc/security-oidc-bearer-token-authentication.adoc @@ -114,6 +114,7 @@ If the token is opaque (binary) then a `scope` property from the remote token in If UserInfo is the source of the roles then set `quarkus.oidc.authentication.user-info-required=true` and `quarkus.oidc.roles.source=userinfo`, and if needed, `quarkus.oidc.roles.role-claim-path`. Additionally, a custom `SecurityIdentityAugmentor` can also be used to add the roles as documented in xref:security-customization.adoc#security-identity-customization[Security Identity Customization]. +You can also map `SecurityIdentity` roles created from token claims to deployment specific roles with the xref:security-authorize-web-endpoints-reference.adoc#map-security-identity-roles[HTTP Security policy]. [[token-scopes-and-security-identity-permissions]] === Token scopes And SecurityIdentity permissions @@ -1109,6 +1110,12 @@ xref:security-openid-connect-multitenancy.adoc#tenant-config-resolver[Dynamic te Authentication that requires dynamic tenant will fail. ==== +[[oidc-request-filters]] +== OIDC request filters + +You can filter OIDC requests made by Quarkus to the OIDC provider by registering one or more `OidcRequestFiler` implementations, which can update or add new request headers, as well as log requests. +For more information, see xref:security-code-flow-authentication#oidc-request-filters[OIDC request filters]. + == References * xref:security-oidc-configuration-properties-reference.adoc[OIDC configuration properties] diff --git a/docs/src/main/asciidoc/security-oidc-code-flow-authentication-tutorial.adoc b/docs/src/main/asciidoc/security-oidc-code-flow-authentication-tutorial.adoc index 750c18e1eb83a..157370f05a139 100644 --- a/docs/src/main/asciidoc/security-oidc-code-flow-authentication-tutorial.adoc +++ b/docs/src/main/asciidoc/security-oidc-code-flow-authentication-tutorial.adoc @@ -42,9 +42,10 @@ Clone the Git repository: `git clone {quickstarts-clone-url}`, or download an {q The solution is located in the `security-openid-connect-web-authentication-quickstart` link:{quickstarts-tree-url}/security-openid-connect-web-authentication-quickstart[directory]. -== Procedure +:sectnums: +:sectnumlevels: 3 -=== Create the Maven project +== Create the Maven project First, we need a new project. Create a new project with the following command: @@ -75,7 +76,7 @@ This will add the following to your build file: implementation("io.quarkus:quarkus-oidc") ---- -=== Write the application +== Write the application Let's write a simple Jakarta REST resource which has all the tokens returned in the authorization code grant response injected: @@ -88,6 +89,7 @@ import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; +import org.eclipse.microprofile.jwt.Claims; import org.eclipse.microprofile.jwt.JsonWebToken; import io.quarkus.oidc.IdToken; @@ -129,7 +131,7 @@ public class TokenResource { .append("
    "); - Object userName = this.idToken.getClaim("preferred_username"); + Object userName = this.idToken.getClaim(Claims.preferred_username); if (userName != null) { response.append("
  • username: ").append(userName.toString()).append("
  • "); @@ -155,7 +157,7 @@ Note that you do not have to inject the tokens - it is only required if the endp // SJ: TO DO - update link to point to new reference guide. For more information, see <> section. -=== Configure the application +== Configure the application The OIDC extension allows you to define the configuration using the `application.properties` file which should be located at the `src/main/resources` directory. @@ -179,7 +181,7 @@ Finally, the `quarkus.http.auth.permission.authenticated` permission is set to t In this case, all paths are being protected by a policy that ensures that only `authenticated` users are allowed to access. For more information, see xref:security-authorize-web-endpoints-reference.adoc[Security Authorization Guide]. -=== Start and configure the Keycloak server +== Start and configure the Keycloak server To start a Keycloak server, use Docker and run the following command: @@ -188,7 +190,7 @@ To start a Keycloak server, use Docker and run the following command: docker run --name keycloak -e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=admin -p 8180:8080 quay.io/keycloak/keycloak:{keycloak.version} start-dev ---- -where `keycloak.version` should be set to `17.0.0` or higher. +where `keycloak.version` should be set to `23.0.0` or higher. You should be able to access your Keycloak Server at http://localhost:8180[localhost:8180]. @@ -196,9 +198,9 @@ To access the Keycloak Administration Console, log in as the `admin` user. Username should be `admin` and password `admin`. Import the link:{quickstarts-tree-url}/security-openid-connect-web-authentication-quickstart/config/quarkus-realm.json[realm configuration file] to create a new realm. -For more information, see the Keycloak documentation about how to https://www.keycloak.org/docs/latest/server_admin/index.html#_create-realm[create a new realm]. +For more information, see the Keycloak documentation about how to https://www.keycloak.org/docs/latest/server_admin/index.html#configuring-realms[create and configure a new realm]. -=== Run the application in dev and JVM modes +== Run the application in dev and JVM modes To run the application in a dev mode, use: @@ -217,7 +219,7 @@ Then, run it: java -jar target/quarkus-app/quarkus-run.jar ---- -=== Run the application in Native mode +== Run the application in Native mode This same demo can be compiled into native code. No modifications are required. @@ -237,7 +239,7 @@ After getting a cup of coffee, you can run this binary directly: ./target/security-openid-connect-web-authentication-quickstart-runner ---- -=== Test the application +== Test the application To test the application, open your browser and access the following URL: @@ -251,9 +253,13 @@ To authenticate to the application, type the following credentials when at the K * Username: *alice* * Password: *alice* -After clicking the `Login` button, you are redirected back to the application. +After clicking the `Login` button, you are redirected back to the application and a session cookie will be created. -For more information about writing the integration tests that depend on `Dev Services for Keycloak`, see the <> section. +The session for this demo is short-lived and you will be asked to re-authenticate on every page refresh. Please follow the Keycloak https://www.keycloak.org/docs/latest/server_admin/#_timeouts[session timeout] documentation to learn how to increase the session timeouts. For example, you can access Keycloak Admin console directly from Dev UI by selecting a `Keycloak Admin` link if you use xref:security-oidc-code-flow-authentication.adoc#integration-testing-keycloak-devservices[Dev Services for Keycloak] in dev mode: + +image::dev-ui-oidc-keycloak-card.png[alt=Dev UI OpenID Connect Card,role="center"] + +For more information about writing the integration tests that depend on `Dev Services for Keycloak`, see the xref:security-oidc-code-flow-authentication.adoc#integration-testing-keycloak-devservices[Dev Services for Keycloak] section. == Summary 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 7d65dd79d7d2e..d0e37b9d320d4 100644 --- a/docs/src/main/asciidoc/security-oidc-code-flow-authentication.adoc +++ b/docs/src/main/asciidoc/security-oidc-code-flow-authentication.adoc @@ -279,11 +279,70 @@ quarkus.oidc.introspection-credentials.name=introspection-user-name quarkus.oidc.introspection-credentials.secret=introspection-user-secret ---- -[[oidc-client-filters]] -==== OIDC request customization +[[oidc-request-filters]] +==== OIDC request filters -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 more information, see xref:security-openid-connect-client-reference#oidc-client-filters[Client request customization]. +You can filter OIDC requests made by Quarkus to the OIDC provider by registering one or more `OidcRequestFiler` implementations, which can update or add new request headers, as well as log requests. + +For example: + +[source,java] +---- +package io.quarkus.it.keycloak; + +import jakarta.enterprise.context.ApplicationScoped; + +import io.quarkus.arc.Unremovable; +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 OidcTokenRequestCustomizer implements OidcRequestFilter { + @Override + public void filter(HttpRequest request, Buffer buffer, OidcRequestContextProperties contextProps) { + OidcConfigurationMetadata metadata = contextProps.get(OidcConfigurationMetadata.class.getName()); <1> + // Metadata URI is absolute, request URI value is relative + if (metadata.getTokenUri().endsWith(request.uri())) { <2> + request.putHeader("TokenGrantDigest", calculateDigest(buffer.toString())); + } + } + private String calculateDigest(String bodyString) { + // Apply the required digest algorithm to the body string + } +} +---- +<1> Get `OidcConfigurationMetadata` which contains all supported OIDC endpoint addresses. +<2> Use `OidcConfigurationMetadata` to filter requests to the OIDC token endpoint only. + +Alternatively, you can use `OidcRequestFilter.Endpoint` enum to make sure this filter is applied to the token endpoint requests only: + +[source,java] +---- +import jakarta.enterprise.context.ApplicationScoped; + +import io.quarkus.arc.Unremovable; +import io.quarkus.oidc.common.OidcEndpoint; +import io.quarkus.oidc.common.OidcEndpoint.Type; +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 +@OidcEndpoint(value = Type.DISCOVERY) <1> +public class OidcDiscoveryRequestCustomizer implements OidcRequestFilter { + + @Override + public void filter(HttpRequest request, Buffer buffer, OidcRequestContextProperties contextProps) { + request.putHeader("Discovery", "OK"); + } +} +---- +<1> Restrict this filter to requests targeting the OIDC discovery endpoint only. ==== Redirecting to and from the OIDC provider @@ -438,6 +497,7 @@ If the access token contains the roles and this access token is not meant to be If UserInfo is the source of the roles then set `quarkus.oidc.roles.source=userinfo`, and if needed, `quarkus.oidc.roles.role-claim-path`. Additionally, a custom `SecurityIdentityAugmentor` can also be used to add the roles. For more information, see xref:security-customization.adoc#security-identity-customization[SecurityIdentity customization]. +You can also map `SecurityIdentity` roles created from token claims to deployment specific roles with the xref:security-authorize-web-endpoints-reference.adoc#map-security-identity-roles[HTTP Security policy]. === Ensuring validity of tokens and authentication data 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 692575db26ef7..dd6c988ccc53a 100644 --- a/docs/src/main/asciidoc/security-openid-connect-client-reference.adoc +++ b/docs/src/main/asciidoc/security-openid-connect-client-reference.adoc @@ -3,20 +3,22 @@ This guide is maintained in the main Quarkus repository and pull requests should be submitted there: https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc //// -= OpenID Connect (OIDC) and OAuth2 Client and Filters Reference Guide += OpenID Connect (OIDC) and OAuth2 client and filters include::_attributes.adoc[] +:diataxis-type: reference :categories: security :topics: security,oidc,client :extensions: io.quarkus:quarkus-oidc-client -This reference guide explains how to use: +You can use Quarkus extensions to acquire and refresh access tokens from OIDC and OAuth 2.0 compliant servers and propagate access tokens. - - `quarkus-oidc-client`, `quarkus-oidc-client-reactive-filter` and `quarkus-oidc-client-filter` extensions to acquire and refresh access tokens from OpenID Connect and OAuth 2.0 compliant Authorization Servers such as link:https://www.keycloak.org[Keycloak] - - `quarkus-oidc-token-propagation-reactive` and `quarkus-oidc-token-propagation` extensions to propagate the current `Bearer` or `Authorization Code Flow` access tokens +Here, you can learn how to use `quarkus-oidc-client`, `quarkus-oidc-client-reactive-filter` and `quarkus-oidc-client-filter` extensions to acquire and refresh access tokens from OpenID Connect and OAuth 2.0 compliant servers such as link:https://www.keycloak.org[Keycloak]. + +You can also learn how to use `quarkus-oidc-token-propagation-reactive` and `quarkus-oidc-token-propagation` extensions to propagate the current `Bearer` or `Authorization Code Flow` access tokens The access tokens managed by these extensions can be used as HTTP Authorization Bearer tokens to access the remote services. -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 @@ -30,11 +32,11 @@ Add the following dependency: ---- -`quarkus-oidc-client` extension provides a reactive `io.quarkus.oidc.client.OidcClient` which can be used to acquire and refresh tokens using SmallRye Mutiny `Uni` and `Vert.x WebClient`. +The `quarkus-oidc-client` extension provides a reactive `io.quarkus.oidc.client.OidcClient`, which can be used to acquire and refresh tokens using SmallRye Mutiny `Uni` and `Vert.x WebClient`. -`OidcClient` is initialized at the build time with the IDP token endpoint URL which can be auto-discovered or manually configured and uses this endpoint to acquire access tokens using the token grants such as `client_credentials` or `password` and refresh the tokens using a `refresh_token` grant. +`OidcClient` is initialized at build time with the IDP token endpoint URL, which can be auto-discovered or manually configured. `OidcClient` uses this endpoint to acquire access tokens by using token grants such as `client_credentials` or `password` and refresh the tokens by using a `refresh_token` grant. -=== Token Endpoint Configuration +=== Token endpoint configuration By default, the token endpoint address is discovered by adding a `/.well-known/openid-configuration` path to the configured `quarkus.oidc-client.auth-server-url`. @@ -47,7 +49,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 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: +Alternatively, if the discovery endpoint is unavailable 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] ---- @@ -66,11 +68,11 @@ quarkus.oidc-client.token-path=http://localhost:8180/auth/realms/quarkus/protoco Setting `quarkus.oidc-client.auth-server-url` and `quarkus.oidc-client.discovery-enabled` is not required in this case. -=== Supported Token Grants +=== Supported token grants -The main token grants which `OidcClient` can use to acquire the tokens are the `client_credentials` (default) and `password` grants. +The main token grants that `OidcClient` can use to acquire the tokens are the `client_credentials` (default) and `password` grants. -==== Client Credentials Grant +==== Client credentials grant Here is how `OidcClient` can be configured to use the `client_credentials` grant: @@ -81,7 +83,7 @@ quarkus.oidc-client.client-id=quarkus-app quarkus.oidc-client.credentials.secret=secret ---- -The `client_credentials` grant allows to set extra parameters to the token request via `quarkus.oidc-client.grant-options.client.=`. Here is how to set the intended token recipient via the `audience` parameter: +The `client_credentials` grant allows setting extra parameters for the token request by using `quarkus.oidc-client.grant-options.client.=`. Here is how to set the intended token recipient by using the `audience` parameter: [source,properties] ---- @@ -93,7 +95,7 @@ quarkus.oidc-client.grant.type=client quarkus.oidc-client.grant-options.client.audience=https://example.com/api ---- -==== Password Grant +==== Password grant Here is how `OidcClient` can be configured to use the `password` grant: @@ -107,13 +109,14 @@ quarkus.oidc-client.grant-options.password.username=alice quarkus.oidc-client.grant-options.password.password=alice ---- -It can be further customized using a `quarkus.oidc-client.grant-options.password` configuration prefix, similarly to how the client credentials grant can be customized. +It can be further customized by using a `quarkus.oidc-client.grant-options.password` configuration prefix, similar to how the client credentials grant can be customized. -==== Other Grants +==== Other grants -`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`. +`OidcClient` can also help acquire the tokens by using grants that require some extra input parameters that cannot be captured in the configuration. These grants are `refresh_token` (with the external refresh token), `authorization_code`, and two grants which can be used to exchange the current access token, namely, `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: +If you need to acquire an access token and have posted an existing refresh token to the current Quarkus endpoint, you must use the `refresh_token` grant. This grant employs an out-of-band refresh token to obtain a new token set. +In this case, configure `OidcClient` as follows: [source,properties] ---- @@ -123,11 +126,11 @@ quarkus.oidc-client.credentials.secret=secret quarkus.oidc-client.grant.type=refresh ---- -and then you can use `OidcClient.refreshTokens` method with a provided refresh token to get the access token. +Then you can use the `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 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: +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: [source,properties] ---- @@ -137,7 +140,7 @@ quarkus.oidc-client.credentials.secret=secret quarkus.oidc-client.grant.type=code ---- -and then you can use `OidcClient.accessTokens` method accepting a Map of extra properties and pass the current `code` and `redirect_uri` parameters to exchange the authorization code for the tokens. +Then, you can use the `OidcClient.accessTokens` method to accept a Map of extra properties and pass the current `code` and `redirect_uri` parameters to exchange the authorization code for the tokens. `OidcClient` also supports the `urn:openid:params:grant-type:ciba` grant: @@ -149,11 +152,11 @@ quarkus.oidc-client.credentials.secret=secret quarkus.oidc-client.grant.type=ciba ---- -and then you can use `OidcClient.accessTokens` method accepting a Map of extra properties and pass `auth_req_id` parameter to exchange the authorization code for the tokens. +Then, you can use the `OidcClient.accessTokens` method to accept a Map of extra properties and pass the `auth_req_id` parameter to exchange the token authorization code. ==== Grant scopes -You might 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 be 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 @@ -196,9 +199,9 @@ public class OidcClientResource { } ---- -=== Inject Tokens +=== Inject tokens -You can inject `Tokens` which uses `OidcClient` internally. `Tokens` can be used to acquire the access tokens and refresh them if necessary: +You can inject `Tokens` that use `OidcClient` internally. `Tokens` can be used to acquire the access tokens and refresh them if necessary: [source,java] ---- @@ -236,7 +239,7 @@ quarkus.oidc-client.jwt-secret.client-id=quarkus-app quarkus.oidc-client.jwt-secret.credentials.jwt.secret=AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow ---- -Note in this case the default client is disabled with a `client-enabled=false` property. The `jwt-secret` client can be accessed like this: +In this case, the default client is disabled with a `client-enabled=false` property. The `jwt-secret` client can be accessed like this: [source,java] ---- @@ -256,14 +259,14 @@ public class OidcClientResource { @GET public String getResponse() { OidcClient client = clients.getClient("jwt-secret"); - // use this client to get the token + //Use this client to get the token } } ---- [NOTE] ==== -If you also use xref:security-openid-connect-multitenancy.adoc[OIDC multitenancy] and each OIDC tenant has its own associated `OidcClient` then you can use a Vert.x `RoutingContext` `tenantId` attribute, for example: +If you also use xref:security-openid-connect-multitenancy.adoc[OIDC multitenancy], and each OIDC tenant has its own associated `OidcClient`, you can use a Vert.x `RoutingContext` `tenantId` attribute. For example: [source,java] ---- @@ -288,13 +291,13 @@ public class OidcClientResource { String tenantId = context.get("tenantId"); // named OIDC tenant and client configurations use the same key: OidcClient client = clients.getClient(tenantId); - // use this client to get the token + //Use this client to get the token } } ---- ==== -If you need you can also create new `OidcClient` programmatically like this: +If you need, you can also create a new `OidcClient` programmatically like this: [source,java] ---- @@ -322,15 +325,15 @@ public class OidcClientResource { cfg.setClientId("quarkus"); cfg.getCredentials().setSecret("secret"); Uni client = clients.newClient(cfg); - // use this client to get the token + //Use this client to get the token } } ---- [[named-oidc-clients]] -=== Inject named OidcClient and Tokens +=== Inject named OidcClient and tokens -In case of multiple configured ``OidcClient``s you can specify the `OidcClient` injection target by the extra qualifier `@NamedOidcClient` instead of working with `OidcClients`: +In case of multiple configured `OidcClient` objects, you can specify the `OidcClient` injection target by the extra qualifier `@NamedOidcClient` instead of working with `OidcClients`: [source,java] ---- @@ -349,7 +352,7 @@ public class OidcClientResource { @GET public String getResponse() { - // use client to get the token + //Use the client to get the token } } ---- @@ -391,7 +394,7 @@ Note it will also bring `io.quarkus:quarkus-oidc-client`. `quarkus-oidc-client-reactive-filter` extension provides `io.quarkus.oidc.client.filter.OidcClientRequestReactiveFilter`. -It works similarly to the way `OidcClientRequestFilter` does (see <>) - it uses `OidcClient` to acquire the access token, refresh it if needed, and set it as an HTTP `Authorization` `Bearer` scheme value. The difference is that it works with xref:rest-client-reactive.adoc[Reactive RestClient] and implements a non-blocking client filter which does not block the current IO thread when acquiring or refreshing the tokens. +It works similarly to the way `OidcClientRequestFilter` does (see <>) - it uses `OidcClient` to acquire the access token, refresh it if needed, and set it as an HTTP `Authorization` `Bearer` scheme value. The difference is that it works with xref:rest-client-reactive.adoc[Reactive RestClient] and implements a non-blocking client filter that does not block the current IO thread when acquiring or refreshing the tokens. `OidcClientRequestReactiveFilter` delays an initial token acquisition until it is executed to avoid blocking an IO thread. @@ -434,7 +437,7 @@ public interface ProtectedResourceService { ---- `OidcClientRequestReactiveFilter` uses a default `OidcClient` by default. A named `OidcClient` can be selected with a `quarkus.oidc-client-reactive-filter.client-name` configuration property. -You can also select `OidcClient` by setting `value` attribute of the `@OidcClientFilter` annotation. The client name set through annotation has higher priority than the `quarkus.oidc-client-reactive-filter.client-name` configuration property. +You can also select `OidcClient` by setting the `value` attribute of the `@OidcClientFilter` annotation. The client name set through annotation has higher priority than the `quarkus.oidc-client-reactive-filter.client-name` configuration property. For example, given <> `jwt-secret` named OIDC client declaration, you can refer to this client like this: [source,java] @@ -471,7 +474,7 @@ Note it will also bring `io.quarkus:quarkus-oidc-client`. `quarkus-oidc-client-filter` extension provides `io.quarkus.oidc.client.filter.OidcClientRequestFilter` Jakarta REST ClientRequestFilter which uses `OidcClient` to acquire the access token, refresh it if needed, and set it as an HTTP `Authorization` `Bearer` scheme value. -By default, this filter will get `OidcClient` to acquire the first pair of access and refresh tokens at its initialization time. If the access tokens are short-lived and refresh tokens are not available then the token acquisition should be delayed with `quarkus.oidc-client.early-tokens-acquisition=false`. +By default, this filter will get `OidcClient` to acquire the first pair of access and refresh tokens at its initialization time. If the access tokens are short-lived and refresh tokens are unavailable, then the token acquisition should be delayed with `quarkus.oidc-client.early-tokens-acquisition=false`. You can selectively register `OidcClientRequestFilter` by using either `io.quarkus.oidc.client.filter.OidcClientFilter` or `org.eclipse.microprofile.rest.client.annotation.RegisterProvider` annotations: @@ -508,10 +511,10 @@ public interface ProtectedResourceService { } ---- -Alternatively, `OidcClientRequestFilter` can be registered automatically with all MP Rest or Jakarta REST clients if `quarkus.oidc-client-filter.register-filter=true` property is set. +Alternatively, `OidcClientRequestFilter` can be registered automatically with all MP Rest or Jakarta REST clients if the `quarkus.oidc-client-filter.register-filter=true` property is set. `OidcClientRequestFilter` uses a default `OidcClient` by default. A named `OidcClient` can be selected with a `quarkus.oidc-client-filter.client-name` configuration property. -You can also select `OidcClient` by setting `value` attribute of the `@OidcClientFilter` annotation. The client name set through annotation has higher priority than the `quarkus.oidc-client-filter.client-name` configuration property. +You can also select `OidcClient` by setting the `value` attribute of the `@OidcClientFilter` annotation. The client name set through annotation has higher priority than the `quarkus.oidc-client-filter.client-name` configuration property. For example, given <> `jwt-secret` named OIDC client declaration, you can refer to this client like this: [source,java] @@ -529,9 +532,9 @@ public interface ProtectedResourceService { } ---- -=== Use Custom RestClient ClientFilter +=== Use a custom RestClient ClientFilter -If you prefer you can use your own custom filter and inject `Tokens`: +If you prefer, you can use your own custom filter and inject `Tokens`: [source,java] ---- @@ -556,26 +559,26 @@ The `Tokens` producer will acquire and refresh the tokens, and the custom filter You can also inject named `Tokens`, see <> [[refresh-access-tokens]] -=== Refreshing Access Tokens +=== Refreshing access tokens `OidcClientRequestReactiveFilter`, `OidcClientRequestFilter` and `Tokens` producers will refresh the current expired access token if the refresh token is available. -Additionally, `quarkus.oidc-client.refresh-token-time-skew` property can be used for a preemptive access token refreshment to avoid sending nearly expired access tokens that might cause HTTP 401 errors. For example if this property is set to `3S` and the access token will expire in less than 3 seconds then this token will be auto-refreshed. +Additionally, the `quarkus.oidc-client.refresh-token-time-skew` property can be used for a preemptive access token refreshment to avoid sending nearly expired access tokens that might cause HTTP 401 errors. For example, if this property is set to `3S` and the access token will expire in less than 3 seconds, then this token will be auto-refreshed. -If the access token needs to be refreshed but no refresh token is available then an attempt will be made to acquire a new token using the configured grant such as `client_credentials`. +If the access token needs to be refreshed, but no refresh token is available, then an attempt is made to acquire a new token by using a configured grant, such as `client_credentials`. -Note that some OpenID Connect Providers will not return a refresh token in a `client_credentials` grant response. For example, starting from Keycloak 12 a refresh token will not be returned by default for `client_credentials`. The providers might also restrict the number of times a refresh token can be used. +Some OpenID Connect Providers will not return a refresh token in a `client_credentials` grant response. For example, starting from Keycloak 12, a refresh token will not be returned by default for `client_credentials`. The providers might also restrict the number of times a refresh token can be used. [[revoke-access-tokens]] -=== Revoking Access Tokens +=== Revoking access tokens -If your OpenId Connect provider such as Keycloak supports a token revocation endpoint then `OidcClient#revokeAccessToken` can be used to revoke the current access token. The revocation endpoint URL will be discovered alongside the token request URI or can be configured with `quarkus.oidc-client.revoke-path`. +If your OpenId Connect provider, such as Keycloak, supports a token revocation endpoint, then `OidcClient#revokeAccessToken` can be used to revoke the current access token. The revocation endpoint URL will be discovered alongside the token request URI or can be configured with `quarkus.oidc-client.revoke-path`. -You might want to have the access token revoked if using this token with a REST client fails with HTTP `401` or the access token has already been used for a long time and you'd like to refresh it. +You might want to have the access token revoked if using this token with a REST client fails with an HTTP `401` status code or if the access token has already been used for a long time and you would like to refresh it. -This can be achieved by requesting a token refresh using a refresh token. However, if the refresh token is not available then you can refresh it by revoking it first and then request a new access token. +This can be achieved by requesting a token refresh by using a refresh token. However, if the refresh token is unavailable, you can refresh it by revoking it first and then requesting a new access token. [[oidc-client-authentication]] -=== OidcClient Authentication +=== OidcClient authentication `OidcClient` has to authenticate to the OpenID Connect Provider for the `client_credentials` and other grant requests to succeed. All the https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication[OIDC Client Authentication] options are supported, for example: @@ -598,14 +601,14 @@ quarkus.oidc-client.client-id=quarkus-app quarkus.oidc-client.credentials.client-secret.value=mysecret ---- -or with the secret retrieved from a xref:credentials-provider.adoc[CredentialsProvider]: +Or with the secret retrieved from a xref:credentials-provider.adoc[CredentialsProvider]: [source,properties] ---- quarkus.oidc-client.auth-server-url=http://localhost:8180/auth/realms/quarkus/ quarkus.oidc-client.client-id=quarkus-app -# This is a key which will be used to retrieve a secret from the map of credentials returned from CredentialsProvider +# This key is used to retrieve a secret from the map of credentials returned from CredentialsProvider quarkus.oidc-client.credentials.client-secret.provider.key=mysecret-key # Set it only if more than one CredentialsProvider can be registered quarkus.oidc-client.credentials.client-secret.provider.name=oidc-credentials-provider @@ -630,14 +633,14 @@ quarkus.oidc-client.client-id=quarkus-app quarkus.oidc-client.credentials.jwt.secret=AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow ---- -or with the secret retrieved from a xref:credentials-provider.adoc[CredentialsProvider], signature algorithm is `HS256`: +Or with the secret retrieved from a xref:credentials-provider.adoc[CredentialsProvider], signature algorithm is `HS256`: [source,properties] ---- quarkus.oidc-client.auth-server-url=http://localhost:8180/auth/realms/quarkus/ quarkus.oidc-client.client-id=quarkus-app -# This is a key which will be used to retrieve a secret from the map of credentials returned from CredentialsProvider +# This is a key that will be used to retrieve a secret from the map of credentials returned from CredentialsProvider quarkus.oidc-client.credentials.jwt.secret-provider.key=mysecret-key # Set it only if more than one CredentialsProvider can be registered quarkus.oidc-client.credentials.jwt.secret-provider.name=oidc-credentials-provider @@ -668,9 +671,9 @@ quarkus.oidc-client.credentials.jwt.key-id=mykeyAlias Using `client_secret_jwt` or `private_key_jwt` authentication methods ensures that no client secret goes over the wire. -==== Additional JWT Authentication options +==== Additional JWT authentication options -If either `client_secret_jwt` or `private_key_jwt` authentication methods are used then the JWT signature algorithm, key identifier, audience, subject and issuer can be customized, for example: +If either `client_secret_jwt` or `private_key_jwt` authentication methods are used, then the JWT signature algorithm, key identifier, audience, subject, and issuer can be customized, for example: [source,properties] ---- @@ -681,28 +684,28 @@ quarkus.oidc-client.client-id=quarkus-app quarkus.oidc-client.credentials.jwt.key-file=privateKey.pem # This is a token key identifier 'kid' header - set it if your OpenID Connect provider requires it. -# Note if the key is represented in a JSON Web Key (JWK) format with a `kid` property then -# using 'quarkus.oidc-client.credentials.jwt.token-key-id' is not necessary. +# Note that if the key is represented in a JSON Web Key (JWK) format with a `kid` property, then +# using 'quarkus.oidc-client.credentials.jwt.token-key-id' is unnecessary. quarkus.oidc-client.credentials.jwt.token-key-id=mykey -# Use RS512 signature algorithm instead of the default RS256 +# Use the RS512 signature algorithm instead of the default RS256 quarkus.oidc-client.credentials.jwt.signature-algorithm=RS512 -# The token endpoint URL is the default audience value, use the base address URL instead: +# The token endpoint URL is the default audience value; use the base address URL instead: quarkus.oidc-client.credentials.jwt.audience=${quarkus.oidc-client.auth-server-url} -# custom subject instead of the client id : +# custom subject instead of the client ID: quarkus.oidc-client.credentials.jwt.subject=custom-subject -# custom issuer instead of the client id : +# custom issuer instead of the client ID: quarkus.oidc-client.credentials.jwt.issuer=custom-issuer ---- ==== Apple POST JWT -Apple OpenID Connect Provider uses a `client_secret_post` method where a secret is a JWT produced with a `private_key_jwt` authentication method but with Apple account specific issuer and subject properties. +Apple OpenID Connect Provider uses a `client_secret_post` method where a secret is a JWT produced with a `private_key_jwt` authentication method but with Apple account-specific issuer and subject properties. -`quarkus-oidc-client` supports a non-standard `client_secret_post_jwt` authentication method which can be configured as follows: +`quarkus-oidc-client` supports a non-standard `client_secret_post_jwt` authentication method, which can be configured as follows: [source,properties] ---- @@ -837,7 +840,7 @@ Set `application.properties`, for example: [source, properties] ---- -# Use 'keycloak.url' property set by the test KeycloakRealmResourceManager +# Use the 'keycloak.url' property set by the test KeycloakRealmResourceManager quarkus.oidc-client.auth-server-url=${keycloak.url} quarkus.oidc-client.discovery-enabled=false quarkus.oidc-client.token-path=/tokens @@ -848,11 +851,11 @@ quarkus.oidc-client.grant-options.password.username=alice quarkus.oidc-client.grant-options.password.password=alice ---- -and finally write the test code. Given the Wiremock-based resource above, the first test invocation should return `access_token_1` access token which will expire in 4 seconds. Use `awaitility` to wait for about 5 seconds, and now the next test invocation should return `access_token_2` access token which confirms the expired `access_token_1` access token has been refreshed. +And finally, write the test code. Given the Wiremock-based resource above, the first test invocation should return the `access_token_1` access token, which will expire in 4 seconds. Use `awaitility` to wait for about 5 seconds, and now the next test invocation should return the `access_token_2` access token, which confirms the expired `access_token_1` access token has been refreshed. ==== Keycloak -If you work with Keycloak then you can use the same approach as described in the xref:security-oidc-bearer-token-authentication.adoc#integration-testing-keycloak[OpenID Connect Bearer Token Integration testing] Keycloak section. +If you work with Keycloak, you can use the same approach described in the xref:security-oidc-bearer-token-authentication.adoc#integration-testing-keycloak[OpenID Connect Bearer Token Integration testing] Keycloak section. === How to check the errors in the logs @@ -872,10 +875,10 @@ quarkus.log.category."io.quarkus.oidc.client.runtime.OidcClientRecorder".level=T quarkus.log.category."io.quarkus.oidc.client.runtime.OidcClientRecorder".min-level=TRACE ---- -[[oidc-client-filters]] -== OIDC request customization +[[oidc-request-filters]] +== OIDC request filters -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: +You can filter 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] ---- @@ -912,7 +915,7 @@ public class OidcRequestCustomizer implements OidcRequestFilter { [[token-propagation-reactive]] == Token Propagation Reactive -The `quarkus-oidc-token-propagation-reactive` extension provides RestEasy Reactive Client `io.quarkus.oidc.token.propagation.reactive.AccessTokenRequestReactiveFilter` that simplifies the propagation of authentication information by propagating the xref:security-oidc-bearer-token-authentication.adoc[Bearer token] present in the current active request or the token acquired from the xref:security-oidc-code-flow-authentication.adoc[Authorization code flow mechanism], as the HTTP `Authorization` header's `Bearer` scheme value. +The `quarkus-oidc-token-propagation-reactive` extension provides a RestEasy Reactive Client, `io.quarkus.oidc.token.propagation.reactive.AccessTokenRequestReactiveFilter`, that simplifies the propagation of authentication information. This client propagates the xref:security-oidc-bearer-token-authentication.adoc[bearer token] present in the currently active request or the token acquired from the xref:security-oidc-code-flow-authentication.adoc[authorization code flow mechanism] as the HTTP `Authorization` header's `Bearer` scheme value. You can selectively register `AccessTokenRequestReactiveFilter` by using either `io.quarkus.oidc.token.propagation.AccessToken` or `org.eclipse.microprofile.rest.client.annotation.RegisterProvider` annotation, for example: @@ -952,7 +955,7 @@ public interface ProtectedResourceService { Additionally, `AccessTokenRequestReactiveFilter` can support a complex application that needs to exchange the tokens before propagating them. -If you work with link:https://www.keycloak.org/docs/latest/securing_apps/#_token-exchange[Keycloak] or other OpenID Connect Providers which support a link:https://tools.ietf.org/html/rfc8693[Token Exchange] token grant then you can configure `AccessTokenRequestReactiveFilter` to exchange the token like this: +If you work with link:https://www.keycloak.org/docs/latest/securing_apps/#_token-exchange[Keycloak] or another OIDC provider that supports a link:https://tools.ietf.org/html/rfc8693[Token Exchange] token grant, then you can configure `AccessTokenRequestReactiveFilter` to exchange the token like this: [source,properties] ---- @@ -989,19 +992,19 @@ quarkus.oidc-token-propagation-reactive.exchange-token=true The `quarkus-oidc-token-propagation` extension provides two Jakarta REST `jakarta.ws.rs.client.ClientRequestFilter` class implementations that simplify the propagation of authentication information. `io.quarkus.oidc.token.propagation.AccessTokenRequestFilter` propagates the xref:security-oidc-bearer-token-authentication.adoc[Bearer token] present in the current active request or the token acquired from the xref:security-oidc-code-flow-authentication.adoc[Authorization code flow mechanism], as the HTTP `Authorization` header's `Bearer` scheme value. -The `io.quarkus.oidc.token.propagation.JsonWebTokenRequestFilter` provides the same functionality, but in addition provides support for JWT tokens. +The `io.quarkus.oidc.token.propagation.JsonWebTokenRequestFilter` provides the same functionality but, in addition, provides support for JWT tokens. -When you need to propagate the current Authorization Code Flow access token then the immediate token propagation will work well - as the code flow access tokens (as opposed to ID tokens) are meant to be propagated for the current Quarkus endpoint to access the remote services on behalf of the currently authenticated user. +When you need to propagate the current Authorization Code Flow access token, then the immediate token propagation will work well - as the code flow access tokens (as opposed to ID tokens) are meant to be propagated for the current Quarkus endpoint to access the remote services on behalf of the currently authenticated user. -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. +However, the direct end-to-end Bearer token propagation should be avoided. For example, `Client -> Service A -> Service B` where `Service B` receives a token sent by `Client` to `Service A`. In such cases, `Service B` cannot 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 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`. +Additionally, a complex application might need to exchange or update the tokens before propagating them. For example, the access context might be different when `Service A` is accessing `Service B`. In this case, `Service A` might be granted a narrow or completely different set of scopes to access `Service B`. The following sections show how `AccessTokenRequestFilter` and `JsonWebTokenRequestFilter` can help. === RestClient AccessTokenRequestFilter -`AccessTokenRequestFilter` treats all tokens as Strings and as such it can work with both JWT and opaque tokens. +`AccessTokenRequestFilter` treats all tokens as Strings, and as such, it can work with both JWT and opaque tokens. You can selectively register `AccessTokenRequestFilter` by using either `io.quarkus.oidc.token.propagation.AccessToken` or `org.eclipse.microprofile.rest.client.annotation.RegisterProvider`, for example: @@ -1037,9 +1040,9 @@ public interface ProtectedResourceService { } ---- -Alternatively, `AccessTokenRequestFilter` can be registered automatically with all MP Rest or Jakarta REST clients if `quarkus.oidc-token-propagation.register-filter` property is set to `true` and `quarkus.oidc-token-propagation.json-web-token` property is set to `false` (which is a default value). +Alternatively, `AccessTokenRequestFilter` can be registered automatically with all MP Rest or Jakarta REST clients if the `quarkus.oidc-token-propagation.register-filter` property is set to `true` and `quarkus.oidc-token-propagation.json-web-token` property is set to `false` (which is a default value). -==== Exchange Token Before Propagation +==== Exchange token before propagation If the current access token needs to be exchanged before propagation and you work with link:https://www.keycloak.org/docs/latest/securing_apps/#_token-exchange[Keycloak] or other OpenID Connect Provider which supports a link:https://tools.ietf.org/html/rfc8693[Token Exchange] token grant then you can configure `AccessTokenRequestFilter` like this: @@ -1075,9 +1078,9 @@ Note `AccessTokenRequestFilter` will use `OidcClient` to exchange the current to === RestClient JsonWebTokenRequestFilter -Using `JsonWebTokenRequestFilter` is recommended if you work with Bearer JWT tokens where these tokens can have their claims such as `issuer` and `audience` modified and the updated tokens secured (for example, re-signed) again. It expects an injected `org.eclipse.microprofile.jwt.JsonWebToken` and therefore will not work with the opaque tokens. Also, if your OpenID Connect Provider supports a Token Exchange protocol then it is recommended to use `AccessTokenRequestFilter` instead - as both JWT and opaque bearer tokens can be securely exchanged with `AccessTokenRequestFilter`. +Using `JsonWebTokenRequestFilter` is recommended if you work with Bearer JWT tokens where these tokens can have their claims, such as `issuer` and `audience` modified and the updated tokens secured (for example, re-signed) again. It expects an injected `org.eclipse.microprofile.jwt.JsonWebToken` and, therefore, will not work with the opaque tokens. Also, if your OpenID Connect Provider supports a Token Exchange protocol, then it is recommended to use `AccessTokenRequestFilter` instead - as both JWT and opaque bearer tokens can be securely exchanged with `AccessTokenRequestFilter`. -`JsonWebTokenRequestFilter` makes it easy for `Service A` implementations to update the injected `org.eclipse.microprofile.jwt.JsonWebToken` with the new `issuer` and `audience` claim values and secure the updated token again with a new signature. The only difficult step is to ensure `Service A` has a signing key - it should be provisioned from a secure file system or from the remote secure storage such as Vault. +`JsonWebTokenRequestFilter` makes it easy for `Service A` implementations to update the injected `org.eclipse.microprofile.jwt.JsonWebToken` with the new `issuer` and `audience` claim values and secure the updated token again with a new signature. The only difficult step is ensuring that `Service A` has a signing key; it should be provisioned from a secure file system or remote secure storage such as Vault. You can selectively register `JsonWebTokenRequestFilter` by using either `io.quarkus.oidc.token.propagation.JsonWebToken` or `org.eclipse.microprofile.rest.client.annotation.RegisterProvider`, for example: @@ -1115,9 +1118,9 @@ public interface ProtectedResourceService { Alternatively, `JsonWebTokenRequestFilter` can be registered automatically with all MicroProfile REST or Jakarta REST clients if both `quarkus.oidc-token-propagation.register-filter` and `quarkus.oidc-token-propagation.json-web-token` properties are set to `true`. -==== Update Token Before Propagation +==== Update token before propagation -If the injected token needs to have its `iss` (issuer) and/or `aud` (audience) claims updated and secured again with a new signature then you can configure `JsonWebTokenRequestFilter` like this: +If the injected token needs to have its `iss` (issuer) or `aud` (audience) claims updated and secured again with a new signature, then you can configure `JsonWebTokenRequestFilter` like this: [source,properties] ---- @@ -1131,7 +1134,7 @@ smallrye.jwt.new-token.audience=http://downstream-resource smallrye.jwt.new-token.override-matching-claims=true ---- -As already noted above, use `AccessTokenRequestFilter` if you work with Keycloak or OpenID Connect Provider which supports a Token Exchange protocol. +As noted above, use `AccessTokenRequestFilter` if you work with Keycloak or OpenID Connect Provider, which supports a Token Exchange protocol. [[integration-testing-token-propagation]] === Testing @@ -1154,21 +1157,17 @@ 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. +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 of the tokens before the propagation. However, these features might be added in the future. [[oidc-client-graphql-client]] == GraphQL client integration -The `quarkus-oidc-client-graphql` extension provides a way to integrate an -OIDC client into xref:smallrye-graphql-client.adoc[GraphQL clients]. This -works similarly as with REST clients. When this extension is present, any -configured (that means NOT created programmatically via the builder, but via -configuration properties) GraphQL client will attempt to use the OIDC client -to obtain an access token and set it as an `Authorization` header value. -OIDC client will also refresh expired access tokens. +The `quarkus-oidc-client-graphql` extension provides a way to integrate an OIDC client into xref:smallrye-graphql-client.adoc[GraphQL clients] paralleling the approach used with REST clients. +When this extension is active, any GraphQL client configured through properties (rather than programmatically by the builder) will use the OIDC client to acquire an access token, which it will then set as the `Authorization` header value. +The OIDC client will also refresh expired access tokens. -To configure which OIDC client should be used by GraphQL client, select one of the configured OIDC clients with the `quarkus.oidc-client-graphql.client-name` property, for example: +To configure which OIDC client should be used by the GraphQL client, select one of the configured OIDC clients with the `quarkus.oidc-client-graphql.client-name` property, for example: ---- quarkus.oidc-client-graphql.client-name=oidc-client-for-graphql @@ -1195,7 +1194,7 @@ per-client basis by annotating the `GraphQLClientApi` interface with @GraphQLClientApi(configKey = "order-client") @OidcClientFilter("oidc-client-for-graphql") public interface OrdersGraphQLClient { - // queries, mutations and subscriptions go here... + // Queries, mutations, and subscriptions go here. } ---- @@ -1219,7 +1218,7 @@ VertxDynamicGraphQLClient client = builder.build(); == References -* xref:security-openid-connect-client.adoc[OpenID Connect Client and Token Propagation Quickstart] +* xref:security-openid-connect-client.adoc[OpenID Connect client and token propagation quickstart]. * xref:security-oidc-bearer-token-authentication.adoc[OIDC Bearer token authentication] * xref:security-oidc-code-flow-authentication.adoc[OIDC code flow mechanism for protecting web applications] * xref:security-overview.adoc[Quarkus Security overview] diff --git a/docs/src/main/asciidoc/security-openid-connect-client.adoc b/docs/src/main/asciidoc/security-openid-connect-client.adoc index 62a63de2f71a4..bcce8989a88da 100644 --- a/docs/src/main/asciidoc/security-openid-connect-client.adoc +++ b/docs/src/main/asciidoc/security-openid-connect-client.adoc @@ -363,7 +363,7 @@ include::{includes}/devtools/dev.adoc[] xref:security-openid-connect-dev-services.adoc[Dev Services for Keycloak] will launch a Keycloak container and import a `quarkus-realm.json`. -Open a xref:dev-ui.adoc[Dev UI] available at http://localhost:8080/q/dev-v1[/q/dev-v1] and click on a `Provider: Keycloak` link in an `OpenID Connect` `Dev UI` card. +Open a xref:dev-ui.adoc[Dev UI] available at http://localhost:8080/q/dev-ui[/q/dev-ui] and click on a `Provider: Keycloak` link in an `OpenID Connect` `Dev UI` card. You will be asked to log in into a `Single Page Application` provided by `OpenID Connect Dev UI`: 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 3e4f7fc20672e..ef10bb79181c1 100644 --- a/docs/src/main/asciidoc/security-openid-connect-dev-services.adoc +++ b/docs/src/main/asciidoc/security-openid-connect-dev-services.adoc @@ -204,7 +204,7 @@ To make Dev UI more useful for supporting the development of OIDC `web-app` appl It will ensure that all Dev UI options described in <> will be available when your `web-app` application is run in dev mode. The limitation of this approach is that both access and ID tokens returned with the code flow and acquired with Dev UI will be sent to the endpoint as HTTP `Bearer` tokens - which will not work well if your endpoint requires the injection of `IdToken`. However, it will work as expected if your `web-app` application only uses the access token, for example, as a source of roles or to get `UserInfo`, even if it is assumed to be a `service` application in dev mode. -Even a better option is to use a `hybrid` application type in devmode: +Even a better option is to use a `hybrid` application type in dev mode: [source,properties] ---- @@ -225,7 +225,7 @@ For more information, see xref:security-oidc-bearer-token-authentication.adoc#in [[keycloak-initialization]] === Keycloak Initialization -The `quay.io/keycloak/keycloak:22.0.5` image which contains a Keycloak distribution powered by Quarkus is used to start a container by default. +The `quay.io/keycloak/keycloak:23.0.1` image which contains a Keycloak distribution powered by Quarkus is used to start a container by default. `quarkus.keycloak.devservices.image-name` can be used to change the Keycloak image name. For example, set it to `quay.io/keycloak/keycloak:19.0.3-legacy` to use a Keycloak distribution powered by WildFly. Note that only a Quarkus based Keycloak distribution is available starting from Keycloak `20.0.0`. @@ -419,7 +419,7 @@ Please follow the xref:dev-ui.adoc[Dev UI] tutorial as well as check the `extens == Non Application Root Path Considerations -This document refers to the `http://localhost:8080/q/dev-v1` Dev UI URL in several places where `q` is a default non application root path. If you customize `quarkus.http.root-path` and/or `quarkus.http.non-application-root-path` properties then replace `q` accordingly, please see https://quarkus.io/blog/path-resolution-in-quarkus/[Path Resolution in Quarkus] for more information. +This document refers to the `http://localhost:8080/q/dev-ui` Dev UI URL in several places where `q` is a default non application root path. If you customize `quarkus.http.root-path` and/or `quarkus.http.non-application-root-path` properties then replace `q` accordingly, please see https://quarkus.io/blog/path-resolution-in-quarkus/[Path Resolution in Quarkus] for more information. == References diff --git a/docs/src/main/asciidoc/security-openid-connect-multitenancy.adoc b/docs/src/main/asciidoc/security-openid-connect-multitenancy.adoc index 9622829047e1a..d45903a867056 100644 --- a/docs/src/main/asciidoc/security-openid-connect-multitenancy.adoc +++ b/docs/src/main/asciidoc/security-openid-connect-multitenancy.adoc @@ -270,7 +270,7 @@ public class CustomTenantResolver implements TenantResolver { You can define multiple tenants in your configuration file, just make sure they have a unique alias so that you can map them properly when resolving a tenant from your `TenantResolver` implementation. -However, using a static tenant resolution (configuring tenants in `application.properties` and resolving them with `TenantResolver`) prevents testing the endpoint with `Dev Services for Keycloak` since `Dev Services for Keycloak` has no knowledge of how the requests will be mapped to individual tenants and can not dynamically provide tenant-specific `quarkus.oidc..auth-server-url` values and therefore using `%prod` prefixes with the tenant-specific URLs in `application.properties` will not work in tests or devmode. +However, using a static tenant resolution (configuring tenants in `application.properties` and resolving them with `TenantResolver`) prevents testing the endpoint with `Dev Services for Keycloak` since `Dev Services for Keycloak` has no knowledge of how the requests will be mapped to individual tenants and can not dynamically provide tenant-specific `quarkus.oidc..auth-server-url` values and therefore using `%prod` prefixes with the tenant-specific URLs in `application.properties` will not work in test or dev mode. [NOTE] ==== @@ -586,48 +586,12 @@ user `alice` exists in both tenants, for the application they are distinct users When you set multiple tenant configurations in the `application.properties` file, you only need to specify how the tenant identifier gets resolved. To configure the resolution of the tenant identifier, use one of the following options: -* <> * <> +* <> * <> -[[default-tenant-resolver]] -=== Default resolution - -The default resolution for a tenant identifier is convention based, whereby the authentication request must include the tenant identifier in the last segment of the request path. - -The following `application.properties` example shows how you can configure two tenants named `google` and `github`: - -[source,properties] ----- -# Tenant 'google' configuration -quarkus.oidc.google.provider=google -quarkus.oidc.google.client-id=${google-client-id} -quarkus.oidc.google.credentials.secret=${google-client-secret} -quarkus.oidc.google.authentication.redirect-path=/signed-in - -# Tenant 'github' configuration -quarkus.oidc.github.provider=google -quarkus.oidc.github.client-id=${github-client-id} -quarkus.oidc.github.credentials.secret=${github-client-secret} -quarkus.oidc.github.authentication.redirect-path=/signed-in ----- - -In this example, both tenants configure OIDC `web-app` applications to use an authorization code flow to authenticate users and also require session cookies to get generated after the authentication has taken place. -After either Google or GitHub authenticates the current user, the user gets returned to the `/signed-in` area for authenticated users, for example, a secured resource path on the JAX-RS endpoint. - -Finally, to complete the default tenant resolution, set the following configuration property: - -[source,properties] ----- -quarkus.http.auth.permission.login.paths=/google,/github -quarkus.http.auth.permission.login.policy=authenticated ----- - -If the endpoint is running on `http://localhost:8080`, you can also provide UI options for users to log in to either `http://localhost:8080/google` or `http://localhost:8080/github`, without having to add specific`/google` or `/github` JAX-RS resource paths. -Tenant identifiers are also recorded in the session cookie names after the authentication is completed. -Therefore, authenticated users can access the secured application area without requiring either the `google` or `github` path values to be included in the secured URL. - -Default resolution can also work for Bearer token authentication but it might be less practical in this case because a tenant identifier will always need to be set as the last path segment value. +These tenant resolution options will be tried in turn, in the order they are listed, until the tenant id gets resolved. +If the tenant id remains unresolved (`null`) in the end then the default (unnamed) tenant configuration will be selected. [[tenant-resolver]] === Resolve with `TenantResolver` @@ -672,6 +636,45 @@ public class CustomTenantResolver implements TenantResolver { In this example, the value of the last request path segment is a tenant ID, but if required, you can implement a more complex tenant identifier resolution logic. +[[default-tenant-resolver]] +=== Default resolution + +The default resolution for a tenant identifier is convention based, whereby the authentication request must include the tenant identifier in the last segment of the request path. + +The following `application.properties` example shows how you can configure two tenants named `google` and `github`: + +[source,properties] +---- +# Tenant 'google' configuration +quarkus.oidc.google.provider=google +quarkus.oidc.google.client-id=${google-client-id} +quarkus.oidc.google.credentials.secret=${google-client-secret} +quarkus.oidc.google.authentication.redirect-path=/signed-in + +# Tenant 'github' configuration +quarkus.oidc.github.provider=google +quarkus.oidc.github.client-id=${github-client-id} +quarkus.oidc.github.credentials.secret=${github-client-secret} +quarkus.oidc.github.authentication.redirect-path=/signed-in +---- + +In this example, both tenants configure OIDC `web-app` applications to use an authorization code flow to authenticate users and also require session cookies to get generated after the authentication has taken place. +After either Google or GitHub authenticates the current user, the user gets returned to the `/signed-in` area for authenticated users, for example, a secured resource path on the JAX-RS endpoint. + +Finally, to complete the default tenant resolution, set the following configuration property: + +[source,properties] +---- +quarkus.http.auth.permission.login.paths=/google,/github +quarkus.http.auth.permission.login.policy=authenticated +---- + +If the endpoint is running on `http://localhost:8080`, you can also provide UI options for users to log in to either `http://localhost:8080/google` or `http://localhost:8080/github`, without having to add specific`/google` or `/github` JAX-RS resource paths. +Tenant identifiers are also recorded in the session cookie names after the authentication is completed. +Therefore, authenticated users can access the secured application area without requiring either the `google` or `github` path values to be included in the secured URL. + +Default resolution can also work for Bearer token authentication but it might be less practical in this case because a tenant identifier will always need to be set as the last path segment value. + [[annotations-tenant-resolver]] === Resolve with annotations @@ -775,6 +778,8 @@ public class CustomTenantConfigResolver implements TenantConfigResolver { The `OidcTenantConfig` returned from this method is the same used to parse the `oidc` namespace configuration from the `application.properties`. You can populate it using any of the settings supported by the `quarkus-oidc` extension. +If the dynamic tenant resolver returns `null` then a <> will be attempted next. + === Tenant resolution for OIDC `web-app` applications The simplest option for resolving OIDC `web-app` application configuration is to follow the steps described in the <> section. @@ -817,10 +822,10 @@ public class CustomTenantResolver implements TenantResolver { List tenantIdQuery = context.queryParam("tenantId"); if (!tenantIdQuery.isEmpty()) { String tenantId = tenantIdQuery.get(0); - context.addCookie(Cookie.cookie("tenant", tenantId)); + context.response().addCookie(Cookie.cookie("tenant", tenantId)); return tenantId; - } else if (context.cookieMap().containsKey("tenant")) { - return context.getCookie("tenant").getValue(); + } else if (!context.request().cookies("tenant").isEmpty()) { + return context.request().getCookie("tenant").getValue(); } return null; diff --git a/docs/src/main/asciidoc/security-openid-connect-providers.adoc b/docs/src/main/asciidoc/security-openid-connect-providers.adoc index 2c0d7860d3c40..79a39a1e0aa7a 100644 --- a/docs/src/main/asciidoc/security-openid-connect-providers.adoc +++ b/docs/src/main/asciidoc/security-openid-connect-providers.adoc @@ -662,7 +662,7 @@ The pattern of authenticating with a given provider, where the endpoint uses eit == HTTPS Redirect URL -Some providers will only accept HTTPS-based redirect URLs. Tools such as https://ngrok.com/[ngrok] https://linuxhint.com/set-up-use-ngrok/[can be set up] to help testing such providers with Quarkus endpoints running on localhost in devmode. +Some providers will only accept HTTPS-based redirect URLs. Tools such as https://ngrok.com/[ngrok] https://linuxhint.com/set-up-use-ngrok/[can be set up] to help testing such providers with Quarkus endpoints running on localhost in dev mode. == Rate Limiting diff --git a/docs/src/main/asciidoc/security-overview.adoc b/docs/src/main/asciidoc/security-overview.adoc index bb6031e5a8872..066c08635b3ed 100644 --- a/docs/src/main/asciidoc/security-overview.adoc +++ b/docs/src/main/asciidoc/security-overview.adoc @@ -73,10 +73,9 @@ For more information, see the xref:http-reference.adoc#same-site-cookie[SameSite [[secrets-engines]] === Secrets engines -Secrets engines are components that store, generate, or encrypt data. +You can use secrets engines with Quarkus to store, generate, or encrypt data. -Quarkus provides comprehensive HashiCorp Vault support. -For more information, see the link:{vault-guide}[Quarkus and HashiCorp Vault] documentation. +Quarkus provides additional extensions in Quarkiverse for securely storing credentials, for example, link:{vault-guide}[Quarkus and HashiCorp Vault]. == Secrets in environment properties diff --git a/docs/src/main/asciidoc/spring-security.adoc b/docs/src/main/asciidoc/spring-security.adoc index 8867bbd1b9db5..1b256ee98d88d 100644 --- a/docs/src/main/asciidoc/spring-security.adoc +++ b/docs/src/main/asciidoc/spring-security.adoc @@ -33,7 +33,9 @@ The solution is located in the `spring-security-quickstart` link:{quickstarts-tr First, we need a new project. Create a new project with the following command: :create-app-artifact-id: spring-security-quickstart +:create-app-group-id: org.acme.spring.security :create-app-extensions: spring-web,spring-security,quarkus-elytron-security-properties-file,resteasy-reactive-jackson +:create-app-code: include::{includes}/devtools/create-app.adoc[] This command generates a project which imports the `spring-web`, `spring-security` and `security-properties-file` extensions. @@ -81,7 +83,7 @@ For more information about `security-properties-file`, you can check out the gui == GreetingController The Quarkus Maven plugin automatically generated a controller with the Spring Web annotations to define our REST endpoint (instead of the Jakarta REST ones used by default). -First create a `src/main/java/org/acme/spring/web/GreetingController.java`, a controller with the Spring Web annotations to define our REST endpoint, as follows: +First create a `src/main/java/org/acme/spring/security/GreetingController.java`, a controller with the Spring Web annotations to define our REST endpoint, as follows: [source,java] ---- @@ -97,7 +99,7 @@ public class GreetingController { @GetMapping public String hello() { - return "hello"; + return "Hello Spring"; } } ---- @@ -117,15 +119,14 @@ import static io.restassured.RestAssured.given; import static org.hamcrest.CoreMatchers.is; @QuarkusTest -public class GreetingControllerTest { - +class GreetingControllerTest { @Test - public void testHelloEndpoint() { + void testHelloEndpoint() { given() .when().get("/greeting") .then() .statusCode(200) - .body(is("hello")); + .body(is("Hello Spring")); } } @@ -141,6 +142,7 @@ Open your browser to http://localhost:8080/greeting. The result should be: `{"message": "hello"}`. +[#secure] == Modify the controller to secure the `hello` method In order to restrict access to the `hello` method to users with certain roles, the `@Secured` annotation will be utilized. @@ -220,6 +222,16 @@ public class GreetingControllerTest { == Test the changes +=== Automatically + +Press `r`, while in dev mode, or run the application with: + +include::{includes}/devtools/test.adoc[] + +All tests should succeed. + +=== Manually + Access allowed:: Open your browser again to http://localhost:8080/greeting and introduce `scott` and `jb0ss` in the dialog displayed. @@ -239,15 +251,14 @@ You don't have authorization to view this page. HTTP ERROR 403 ---- -== Run the application as a native executable - -You can generate the native executable with: - -include::{includes}/devtools/build-native.adoc[] +[TIP] +==== +Some browsers save credentials for basic authentication. If the dialog is not displayed, try to clear saved logins or use the Private mode +==== -== Supported Spring Security functionalities +== Supported Spring Security annotations -Quarkus currently only supports a subset of the functionalities that Spring Security provides with more features being planned. More specifically, Quarkus supports the security related features of role-based authorization semantics +Quarkus currently only supports a subset of the functionality that Spring Security provides with more features being planned. More specifically, Quarkus supports the security related features of role-based authorization semantics (think of `@Secured` instead of `@RolesAllowed`). === Annotations @@ -256,13 +267,15 @@ The table below summarizes the supported annotations: .Supported Spring Security annotations |=== -|Name|Comments +|Name|Comments|Spring documentation |@Secured -| +| See <> +| link:https://docs.spring.io/spring-security/reference/servlet/authorization/method-security.html#use-secured[Authorizing Method Invocation with @Secured] |@PreAuthorize |See next section for more details +|link:https://docs.spring.io/spring-security/reference/servlet/authorization/method-security.html#use-preauthorize[Authorizing Method Invocation with @PreAuthorize] |=== @@ -320,6 +333,7 @@ public class Person { this.name = name; } + // this syntax requires getters for field access public String getName() { return name; } @@ -373,7 +387,6 @@ An example of the `PersonChecker` could be: @Component public class PersonChecker { - @Override public boolean check(Person person, String username) { return person.getName().equals(username); } @@ -407,8 +420,11 @@ Some examples of allowed expressions are: } ---- +[IMPORTANT] +==== +Currently, expressions do not support parentheses for logical operators and are evaluated from left to right +==== -Also to be noted that currently parentheses are not supported and expressions are evaluated from left to right when needed. == Important Technical Note @@ -428,6 +444,10 @@ The following table shows how Spring Security annotations can be converted to Ja |@RolesAllowed("admin") | +|@PreAuthorize +|No direct replacement +|Quarkus handles complex authorisation differently, see link:https://quarkus.io/guides/security-authorize-web-endpoints-reference[this guide] for details + |=== == More Spring guides diff --git a/docs/src/main/asciidoc/telemetry-micrometer.adoc b/docs/src/main/asciidoc/telemetry-micrometer.adoc index 2c753d8013993..146df14de7d24 100644 --- a/docs/src/main/asciidoc/telemetry-micrometer.adoc +++ b/docs/src/main/asciidoc/telemetry-micrometer.adoc @@ -603,8 +603,59 @@ The `@Timed` annotation will wrap the execution of a method and will emit the fo in addition to any tags defined on the annotation itself: class, method, and exception (either "none" or the simple class name of a detected exception). -Using annotations is limited, as you can't dynamically assign meaningful tag values. -Also note that many methods, e.g. REST endpoint methods or Vert.x Routes, are counted and timed by the micrometer extension out of the box. +Parameters to `@Counted` and `@Timed` can be annotated with `@MeterTag` to dynamically assign meaningful tag values. + +`MeterTag.resolver` can be used to extract a tag from a method parameter, by creating a bean +implementing `io.micrometer.common.annotation.ValueResolver` and referring to this class: `@MeterTag(resolver=CustomResolver.class) + +`MeterTag.expression` is also supported, but you have to implement the evaluation of the expression +by creating a bean implementing `io.micrometer.common.annotation.ValueExpressionResolver` that can evaluate expressions. + +[source,java] +---- +enum Currency { USD, EUR } + +@Singleton +class EnumOrdinalResolver implements ValueResolver { + @Override + public String resolve(Object parameter) { + if(parameter instanceof Enum) { + return String.valueOf(((Enum) parameter).ordinal()); + } + return null; + } +} + +@Singleton +public class MyExpressionResolver implements ValueExpressionResolver { + @Override + public String resolve(String expression, Object parameter) { + return someParser.parse(expression).evaluate(parameter); + } +} + +// tags = type=with_enum, currency=${currency.toString()} +@Timed(value="time_something", extraTags = {"type", "with_enum"}) +public Something calculateSomething(@MeterTag Currency currency) { ... } + +// tags = type=with_enum, the_currency=${currency.toString()} +@Timed(value="time_something", extraTags = {"type", "with_enum"}) +public Something calculateSomething(@MeterTag(key="the_currency") Currency currency) { ... } + +// tags = type=with_enum, currency=${currency.ordinal()} +@Timed(value="time_something", extraTags = {"type", "with_enum"}) +public Something calculateSomething(@MeterTag(resolver=EnumOrdinalResolver.class) Currency currency) { ... } + +// tags = type=with_enum, currency=${currency.ordinal()} +@Timed(value="time_something", extraTags = {"type", "with_enum"}) +public Something calculateSomething(@MeterTag(expression="currency.ordinal()") Currency currency) { ... } +---- + +IMPORTANT: Provided tag values MUST BE of LOW-CARDINALITY. +High-cardinality values can lead to performance and storage issues in your metrics backend (a "cardinality explosion"). +Tag values should not use end-user data, since those could be high-cardinality. + +Many methods, like REST endpoint methods or Vert.x Routes, are counted and timed by the micrometer extension out of the box. == Support for the MicroProfile Metrics API diff --git a/docs/src/main/asciidoc/vertx-reference.adoc b/docs/src/main/asciidoc/vertx-reference.adoc index e08da1e8ce0ea..5802a913f61dd 100644 --- a/docs/src/main/asciidoc/vertx-reference.adoc +++ b/docs/src/main/asciidoc/vertx-reference.adoc @@ -6,6 +6,7 @@ https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc = Vert.x Reference Guide include::_attributes.adoc[] :categories: miscellaneous +:keywords: vertx event verticle :summary: This reference guide provides advanced details about the usage and the configuration of the Vert.x instance used by Quarkus. https://vertx.io[Vert.x] is a toolkit for building reactive applications. diff --git a/docs/src/main/asciidoc/vertx.adoc b/docs/src/main/asciidoc/vertx.adoc index 09fb4a2a4b706..c5bbb73681ebd 100644 --- a/docs/src/main/asciidoc/vertx.adoc +++ b/docs/src/main/asciidoc/vertx.adoc @@ -6,6 +6,7 @@ https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc = Using Eclipse Vert.x API from a Quarkus Application include::_attributes.adoc[] :categories: miscellaneous +:keywords: vertx event verticle :summary: This guide explains how to use Vert.x in Quarkus to build reactive applications. https://vertx.io[Vert.x] is a toolkit for building reactive applications. diff --git a/docs/sync-web-site.sh b/docs/sync-web-site.sh index 1041831b12477..171aaed2e66c6 100755 --- a/docs/sync-web-site.sh +++ b/docs/sync-web-site.sh @@ -32,11 +32,16 @@ fi if [ -z $TARGET_DIR ]; then TARGET_DIR=target/web-site + rm -rf ${TARGET_DIR} GIT_OPTIONS="" if [[ "$QUARKUS_WEB_SITE_PUSH" != "true" ]]; then GIT_OPTIONS="--depth=1" fi - git clone -b develop --single-branch $GIT_OPTIONS git@github.com:quarkusio/quarkusio.github.io.git ${TARGET_DIR} + if [ -n "${RELEASE_GITHUB_TOKEN}" ]; then + git clone -b develop --single-branch $GIT_OPTIONS https://github.com/quarkusio/quarkusio.github.io.git ${TARGET_DIR} + else + git clone -b develop --single-branch $GIT_OPTIONS git@github.com:quarkusio/quarkusio.github.io.git ${TARGET_DIR} + fi fi if [ $BRANCH == "main" ] && [ "$QUARKUS_RELEASE" == "true" ]; then diff --git a/extensions/amazon-lambda-http/maven-archetype/src/main/resources/archetype-resources/pom.xml b/extensions/amazon-lambda-http/maven-archetype/src/main/resources/archetype-resources/pom.xml index 60ac4b04c37a8..1cefbfdac67cf 100644 --- a/extensions/amazon-lambda-http/maven-archetype/src/main/resources/archetype-resources/pom.xml +++ b/extensions/amazon-lambda-http/maven-archetype/src/main/resources/archetype-resources/pom.xml @@ -10,8 +10,8 @@ 3.1.0 3.11.0 true - 11 - 11 + 17 + 17 UTF-8 UTF-8 999-SNAPSHOT diff --git a/extensions/amazon-lambda-rest/maven-archetype/src/main/resources/archetype-resources/pom.xml b/extensions/amazon-lambda-rest/maven-archetype/src/main/resources/archetype-resources/pom.xml index 80fb04957d4b0..dbf8f6d16d12e 100644 --- a/extensions/amazon-lambda-rest/maven-archetype/src/main/resources/archetype-resources/pom.xml +++ b/extensions/amazon-lambda-rest/maven-archetype/src/main/resources/archetype-resources/pom.xml @@ -10,8 +10,8 @@ 3.1.0 3.11.0 true - 11 - 11 + 17 + 17 UTF-8 UTF-8 999-SNAPSHOT diff --git a/extensions/amazon-lambda/maven-archetype/src/main/resources/archetype-resources/pom.xml b/extensions/amazon-lambda/maven-archetype/src/main/resources/archetype-resources/pom.xml index 7a0b9e4544cc1..4cc069988466d 100644 --- a/extensions/amazon-lambda/maven-archetype/src/main/resources/archetype-resources/pom.xml +++ b/extensions/amazon-lambda/maven-archetype/src/main/resources/archetype-resources/pom.xml @@ -9,8 +9,8 @@ 3.11.0 true - 11 - 11 + 17 + 17 UTF-8 UTF-8 999-SNAPSHOT diff --git a/extensions/arc/deployment/pom.xml b/extensions/arc/deployment/pom.xml index 750b73d939433..16ef31d3f3bb0 100644 --- a/extensions/arc/deployment/pom.xml +++ b/extensions/arc/deployment/pom.xml @@ -44,6 +44,10 @@ assertj-core test + + io.quarkus + quarkus-arc-test-supplement + jakarta.ejb diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/BuildTimeConditionBuildItem.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/BuildTimeConditionBuildItem.java index 1554e7b54eb8c..19c8c11543e9e 100644 --- a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/BuildTimeConditionBuildItem.java +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/BuildTimeConditionBuildItem.java @@ -17,7 +17,7 @@ public BuildTimeConditionBuildItem(AnnotationTarget target, boolean enabled) { this.target = target; break; default: - throw new IllegalArgumentException("'target' can only be a class, a field or a method"); + throw new IllegalArgumentException("'target' can only be a class, a field or a method: " + target); } this.enabled = enabled; } diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/BuildTimeEnabledProcessor.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/BuildTimeEnabledProcessor.java index d5ae68152c298..0aabb8a88f308 100644 --- a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/BuildTimeEnabledProcessor.java +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/BuildTimeEnabledProcessor.java @@ -5,16 +5,17 @@ import static java.util.function.Predicate.not; import static java.util.stream.Collectors.groupingBy; +import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collections; +import java.util.Deque; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; -import java.util.function.BiConsumer; -import java.util.function.BiFunction; +import java.util.function.Function; import java.util.stream.Collectors; import org.eclipse.microprofile.config.Config; @@ -23,14 +24,13 @@ import org.jboss.jandex.AnnotationTarget; import org.jboss.jandex.AnnotationTarget.Kind; import org.jboss.jandex.AnnotationValue; +import org.jboss.jandex.ClassInfo; import org.jboss.jandex.DotName; -import org.jboss.jandex.FieldInfo; +import org.jboss.jandex.EquivalenceKey; import org.jboss.jandex.IndexView; -import org.jboss.jandex.MethodInfo; import org.jboss.logging.Logger; import io.quarkus.arc.processor.AnnotationsTransformer; -import io.quarkus.arc.processor.AnnotationsTransformer.TransformationContext; import io.quarkus.arc.processor.DotNames; import io.quarkus.arc.processor.Transformation; import io.quarkus.arc.profile.IfBuildProfile; @@ -59,95 +59,212 @@ public class BuildTimeEnabledProcessor { IF_BUILD_PROPERTY, IF_BUILD_PROPERTY_CONTAINER, UNLESS_BUILD_PROPERTY, UNLESS_BUILD_PROPERTY_CONTAINER); @BuildStep - void ifBuildProfile(CombinedIndexBuildItem index, BuildProducer producer) { - List annotationInstances = getAnnotations(index.getIndex(), IF_BUILD_PROFILE); - for (AnnotationInstance instance : annotationInstances) { - boolean enabled = BuildProfile.from(instance).enabled(); - if (enabled) { - LOGGER.debug("Enabling " + instance.target() + " since the profile value matches the active profile."); - } else { - LOGGER.debug("Disabling " + instance.target() + " since the profile value does not match the active profile."); + BuildTimeEnabledStereotypesBuildItem findEnablementStereotypes(CombinedIndexBuildItem combinedIndex) { + IndexView index = combinedIndex.getIndex(); + + // find all stereotypes + Set stereotypeNames = new HashSet<>(); + for (AnnotationInstance annotation : index.getAnnotations(DotNames.STEREOTYPE)) { + if (annotation.target() != null + && annotation.target().kind() == Kind.CLASS + && annotation.target().asClass().isAnnotation()) { + stereotypeNames.add(annotation.target().asClass().name()); } - producer.produce(new BuildTimeConditionBuildItem(instance.target(), enabled)); } - } + // ideally, we would also consider all `StereotypeRegistrarBuildItem`s here, + // but there is a build step cycle involving Spring DI and RESTEasy Reactive + // that I'm not capable of breaking + + // for each stereotype, find all enablement annotations, present either directly or transitively + List buildTimeEnabledStereotypes = new ArrayList<>(); + for (DotName stereotypeToScan : stereotypeNames) { + Map> result = new HashMap<>(); + + Set alreadySeen = new HashSet<>(); // to guard against hypothetical stereotype cycle + Deque worklist = new ArrayDeque<>(); + worklist.add(stereotypeToScan); + while (!worklist.isEmpty()) { + DotName stereotype = worklist.poll(); + if (alreadySeen.contains(stereotype)) { + continue; + } + alreadySeen.add(stereotype); - @BuildStep - void unlessBuildProfile(CombinedIndexBuildItem index, BuildProducer producer) { - List annotationInstances = getAnnotations(index.getIndex(), UNLESS_BUILD_PROFILE); - for (AnnotationInstance instance : annotationInstances) { - boolean enabled = BuildProfile.from(instance).disabled(); - if (enabled) { - LOGGER.debug("Enabling " + instance.target() + " since the profile value matches the active profile."); - } else { - LOGGER.debug("Disabling " + instance.target() + " since the profile value does not match the active profile."); + ClassInfo stereotypeClass = index.getClassByName(stereotype); + if (stereotypeClass == null) { + continue; + } + + for (DotName enablementAnnotation : List.of(IF_BUILD_PROFILE, UNLESS_BUILD_PROFILE, IF_BUILD_PROPERTY, + UNLESS_BUILD_PROPERTY)) { + AnnotationInstance ann = stereotypeClass.declaredAnnotation(enablementAnnotation); + if (ann != null) { + result.computeIfAbsent(enablementAnnotation, ignored -> new ArrayList<>()).add(ann); + } + } + for (Map.Entry entry : Map.of(IF_BUILD_PROPERTY_CONTAINER, IF_BUILD_PROPERTY, + UNLESS_BUILD_PROPERTY_CONTAINER, UNLESS_BUILD_PROPERTY).entrySet()) { + DotName enablementContainerAnnotation = entry.getKey(); + DotName enablementAnnotation = entry.getValue(); + + AnnotationInstance containerAnn = stereotypeClass.declaredAnnotation(enablementContainerAnnotation); + if (containerAnn != null) { + for (AnnotationInstance ann : containerAnn.value().asNestedArray()) { + result.computeIfAbsent(enablementAnnotation, ignored -> new ArrayList<>()).add(ann); + } + } + } + + for (AnnotationInstance metaAnn : stereotypeClass.declaredAnnotations()) { + if (stereotypeNames.contains(metaAnn.name())) { + worklist.add(metaAnn.name()); + } + } + } + + if (!result.isEmpty()) { + ClassInfo stereotypeClass = index.getClassByName(stereotypeToScan); + boolean inheritable = stereotypeClass != null && stereotypeClass.hasDeclaredAnnotation(DotNames.INHERITED); + buildTimeEnabledStereotypes.add(new BuildTimeEnabledStereotypesBuildItem.BuildTimeEnabledStereotype( + stereotypeToScan, inheritable, result)); } - producer.produce(new BuildTimeConditionBuildItem(instance.target(), enabled)); } + + return new BuildTimeEnabledStereotypesBuildItem(buildTimeEnabledStereotypes); } @BuildStep - void ifBuildProperty(CombinedIndexBuildItem index, BuildProducer conditions) { - buildProperty(IF_BUILD_PROPERTY, IF_BUILD_PROPERTY_CONTAINER, new BiFunction() { - @Override - public Boolean apply(String stringValue, String expectedStringValue) { - return stringValue.equals(expectedStringValue); - } - }, index.getIndex(), new BiConsumer() { - @Override - public void accept(AnnotationTarget target, Boolean enabled) { - conditions.produce(new BuildTimeConditionBuildItem(target, enabled)); - } - }); + void ifBuildProfile(CombinedIndexBuildItem index, BuildTimeEnabledStereotypesBuildItem stereotypes, + BuildProducer producer) { + enablementAnnotations(IF_BUILD_PROFILE, null, index.getIndex(), stereotypes, producer, + new Function() { + @Override + public Boolean apply(AnnotationInstance annotation) { + return BuildProfile.from(annotation).enabled(); + } + }); } @BuildStep - void unlessBuildProperty(CombinedIndexBuildItem index, BuildProducer conditions) { - buildProperty(UNLESS_BUILD_PROPERTY, UNLESS_BUILD_PROPERTY_CONTAINER, new BiFunction() { - @Override - public Boolean apply(String stringValue, String expectedStringValue) { - return !stringValue.equals(expectedStringValue); - } - }, index.getIndex(), new BiConsumer() { - @Override - public void accept(AnnotationTarget target, Boolean enabled) { - conditions.produce(new BuildTimeConditionBuildItem(target, enabled)); - } - }); + void unlessBuildProfile(CombinedIndexBuildItem index, BuildTimeEnabledStereotypesBuildItem stereotypes, + BuildProducer producer) { + enablementAnnotations(UNLESS_BUILD_PROFILE, null, index.getIndex(), stereotypes, producer, + new Function() { + @Override + public Boolean apply(AnnotationInstance annotation) { + return BuildProfile.from(annotation).disabled(); + } + }); } - void buildProperty(DotName annotationName, DotName containingAnnotationName, BiFunction testFun, - IndexView index, BiConsumer producer) { + @BuildStep + void ifBuildProperty(CombinedIndexBuildItem index, BuildTimeEnabledStereotypesBuildItem stereotypes, + BuildProducer conditions) { Config config = ConfigProviderResolver.instance().getConfig(); + enablementAnnotations(IF_BUILD_PROPERTY, IF_BUILD_PROPERTY_CONTAINER, index.getIndex(), stereotypes, conditions, + new Function() { + @Override + public Boolean apply(AnnotationInstance annotation) { + return BuildProperty.from(annotation).enabled(config); + } + }); + } + + @BuildStep + void unlessBuildProperty(CombinedIndexBuildItem index, BuildTimeEnabledStereotypesBuildItem stereotypes, + BuildProducer conditions) { + Config config = ConfigProviderResolver.instance().getConfig(); + enablementAnnotations(UNLESS_BUILD_PROPERTY, UNLESS_BUILD_PROPERTY_CONTAINER, index.getIndex(), stereotypes, conditions, + new Function() { + @Override + public Boolean apply(AnnotationInstance annotation) { + return BuildProperty.from(annotation).disabled(config); + } + }); + } + + private void enablementAnnotations(DotName annotationName, DotName containingAnnotationName, IndexView index, + BuildTimeEnabledStereotypesBuildItem stereotypes, BuildProducer producer, + Function test) { + + // instances of enablement annotation directly on affected declarations List annotationInstances = getAnnotations(index, annotationName, containingAnnotationName); - for (AnnotationInstance instance : annotationInstances) { - String propertyName = instance.value("name").asString(); - String expectedStringValue = instance.value("stringValue").asString(); - AnnotationValue enableIfMissingValue = instance.value("enableIfMissing"); - boolean enableIfMissing = enableIfMissingValue != null && enableIfMissingValue.asBoolean(); + for (AnnotationInstance annotation : annotationInstances) { + AnnotationTarget target = annotation.target(); + boolean enabled = test.apply(annotation); + if (enabled) { + LOGGER.debugf("Enabling %s due to %s", target, annotation); + } else { + LOGGER.debugf("Disabling %s due to %s", target, annotation); + } + producer.produce(new BuildTimeConditionBuildItem(target, enabled)); + } - Optional optionalValue = config.getOptionalValue(propertyName, String.class); - boolean enabled; - if (optionalValue.isPresent()) { - if (testFun.apply(optionalValue.get(), expectedStringValue)) { - LOGGER.debugf("Enabling %s since the property value matches the expected one.", instance.target()); - enabled = true; - } else { - LOGGER.debugf("Disabling %s since the property value matches the specified value one.", instance.target()); - enabled = false; + // instances of stereotypes (with enablement annotation) directly on affected declarations + Set processedClasses = new HashSet<>(); + List classesWithPossiblyInheritedStereotype = new ArrayList<>(); + for (BuildTimeEnabledStereotypesBuildItem.BuildTimeEnabledStereotype stereotype : stereotypes.all()) { + for (AnnotationInstance stereotypeUsage : getAnnotations(index, stereotype.name)) { + AnnotationTarget target = stereotypeUsage.target(); + for (AnnotationInstance annotation : stereotype.getEnablementAnnotations(annotationName)) { + boolean enabled = test.apply(annotation); + if (enabled) { + LOGGER.debugf("Enabling %s due to %s on stereotype %s", target, annotation, stereotype.name); + } else { + LOGGER.debugf("Disabling %s due to %s on stereotype %s", target, annotation, stereotype.name); + } + producer.produce(new BuildTimeConditionBuildItem(target, enabled)); } - } else { - if (enableIfMissing) { - LOGGER.debugf("Enabling %s since the property has not been set and 'enableIfMissing' is set to 'true'.", - instance.target()); - enabled = true; - } else { - LOGGER.debugf("Disabling %s since the property has not been set and 'enableIfMissing' is set to 'false'.", - instance.target()); - enabled = false; + + // annotations are inherited only on classes (and only from superclasses) + if (target.kind() == Kind.CLASS) { + ClassInfo clazz = target.asClass(); + processedClasses.add(clazz.name()); + if (stereotype.inheritable && !clazz.isInterface()) { + classesWithPossiblyInheritedStereotype.addAll(index.getAllKnownSubclasses(clazz.name())); + } } } - producer.accept(instance.target(), enabled); + } + + // instances of stereotypes (with enablement annotation) inherited from a superclass + for (ClassInfo clazz : classesWithPossiblyInheritedStereotype) { + if (processedClasses.contains(clazz.name())) { + continue; + } + processedClasses.add(clazz.name()); + + ClassInfo superclass = index.getClassByName(clazz.superName()); + Set seenStereotypes = new HashSet<>(); // avoid "inheriting" the same annotation multiple times + while (superclass != null && !DotNames.OBJECT.equals(superclass.name())) { + for (AnnotationInstance ann : superclass.declaredAnnotations()) { + if (!stereotypes.isStereotype(ann.name()) || seenStereotypes.contains(ann.name())) { + continue; + } + + BuildTimeEnabledStereotypesBuildItem.BuildTimeEnabledStereotype stereotype = stereotypes + .getStereotype(ann.name()); + if (stereotype == null) { + continue; + } + + for (AnnotationInstance annotation : stereotype.getEnablementAnnotations(annotationName)) { + boolean enabled = test.apply(annotation); + if (enabled) { + LOGGER.debugf("Enabling %s due to %s on stereotype %s inherited from %s", + clazz, annotation, stereotype.name, superclass.name()); + } else { + LOGGER.debugf("Disabling %s due to %s on stereotype %s inherited from %s", + clazz, annotation, stereotype.name, superclass.name()); + } + producer.produce(new BuildTimeConditionBuildItem(clazz, enabled)); + } + + seenStereotypes.add(ann.name()); + } + + superclass = index.getClassByName(superclass.superName()); + } } } @@ -163,49 +280,29 @@ void conditionTransformer(List buildTimeConditions, * Done this way in order to support having different annotation specify different conditions * under which the bean is enabled and then combining all of them using a logical 'AND' */ - final Map classTargets = new HashMap<>(); //don't use ClassInfo because it doesn't implement equals and hashCode - final Map fieldTargets = new HashMap<>(); // don't use FieldInfo because it doesn't implement equals and hashCode - final Map methodTargets = new HashMap<>(); + final Map enabled = new HashMap<>(); for (BuildTimeConditionBuildItem buildTimeCondition : buildTimeConditions) { AnnotationTarget target = buildTimeCondition.getTarget(); - AnnotationTarget.Kind kind = target.kind(); - if (kind == AnnotationTarget.Kind.CLASS) { - DotName classDotName = target.asClass().name(); - Boolean allPreviousConditionsTrue = classTargets.getOrDefault(classDotName, true); - classTargets.put(classDotName, allPreviousConditionsTrue && buildTimeCondition.isEnabled()); - } else if (kind == AnnotationTarget.Kind.METHOD) { - MethodInfo method = target.asMethod(); - Boolean allPreviousConditionsTrue = methodTargets.getOrDefault(method, true); - methodTargets.put(method, allPreviousConditionsTrue && buildTimeCondition.isEnabled()); - } else if (kind == AnnotationTarget.Kind.FIELD) { - String uniqueFieldName = toUniqueString(target.asField()); - Boolean allPreviousConditionsTrue = fieldTargets.getOrDefault(uniqueFieldName, true); - fieldTargets.put(uniqueFieldName, allPreviousConditionsTrue && buildTimeCondition.isEnabled()); - } + EquivalenceKey key = EquivalenceKey.of(target); + Boolean allPreviousConditionsTrue = enabled.getOrDefault(key, true); + enabled.put(key, allPreviousConditionsTrue && buildTimeCondition.isEnabled()); } // the transformer just tries to match targets and then enables or disables the bean accordingly annotationsTransformer.produce(new AnnotationsTransformerBuildItem(new AnnotationsTransformer() { - @Override public void transform(TransformationContext ctx) { AnnotationTarget target = ctx.getTarget(); - if (ctx.isClass()) { - DotName classDotName = target.asClass().name(); - if (classTargets.containsKey(classDotName)) { - transformBean(target, ctx, classTargets.get(classDotName)); - } - } else if (ctx.isMethod()) { - MethodInfo method = target.asMethod(); - if (methodTargets.containsKey(method)) { - transformBean(target, ctx, methodTargets.get(method)); - } - } else if (ctx.isField()) { - FieldInfo field = target.asField(); - String uniqueFieldName = toUniqueString(field); - if (fieldTargets.containsKey(uniqueFieldName)) { - transformBean(target, ctx, fieldTargets.get(uniqueFieldName)); + if (!enabled.getOrDefault(EquivalenceKey.of(target), Boolean.TRUE)) { + Transformation transform = ctx.transform(); + if (target.kind() == Kind.CLASS) { + // Veto the class + transform.add(DotNames.VETOED); + } else { + // Veto the producer + transform.add(DotNames.VETOED_PRODUCER); } + transform.done(); } } })); @@ -230,42 +327,35 @@ BuildExclusionsBuildItem buildExclusions(List build map.getOrDefault(AnnotationTarget.Kind.FIELD, Collections.emptySet())); } - private String toUniqueString(FieldInfo field) { - return field.declaringClass().name().toString() + "." + field.name(); - } - - private void transformBean(AnnotationTarget target, TransformationContext ctx, boolean enabled) { - if (!enabled) { - Transformation transform = ctx.transform(); - if (target.kind() == Kind.CLASS) { - // Veto the class - transform.add(DotNames.VETOED); - } else { - // Veto the producer - transform.add(DotNames.VETOED_PRODUCER); + private static List getAnnotations(IndexView index, DotName annotationName) { + List result = new ArrayList<>(); + for (AnnotationInstance annotation : index.getAnnotations(annotationName)) { + AnnotationTarget target = annotation.target(); + if (target != null && (target.kind() != Kind.CLASS || !target.asClass().isAnnotation())) { + result.add(annotation); } - transform.done(); } + return result; } - private static List getAnnotations(IndexView index, DotName annotationName) { - return new ArrayList<>(index.getAnnotations(annotationName)); - } - - private static List getAnnotations( - IndexView index, - DotName annotationName, + private static List getAnnotations(IndexView index, DotName annotationName, DotName containingAnnotationName) { // Single annotation List annotationInstances = getAnnotations(index, annotationName); + if (containingAnnotationName == null) { + return annotationInstances; + } // Collect containing annotation instances // Note that we can't just use the IndexView.getAnnotationsWithRepeatable() method because the containing annotation is not part of the index for (AnnotationInstance containingInstance : index.getAnnotations(containingAnnotationName)) { - for (AnnotationInstance nestedInstance : containingInstance.value().asNestedArray()) { - // We need to set the target of the containing instance - annotationInstances.add( - AnnotationInstance.create(nestedInstance.name(), containingInstance.target(), nestedInstance.values())); + AnnotationTarget target = containingInstance.target(); + if (target != null && (target.kind() != Kind.CLASS || !target.asClass().isAnnotation())) { + for (AnnotationInstance nestedInstance : containingInstance.value().asNestedArray()) { + // We need to set the target of the containing instance + annotationInstances.add( + AnnotationInstance.create(nestedInstance.name(), target, nestedInstance.values())); + } } } @@ -333,4 +423,44 @@ private static BuildProfile from(AnnotationInstance instance) { return new BuildProfile(allOf, anyOf); } } + + static class BuildProperty { + private final String propertyName; + private final String expectedStringValue; + private final boolean enableIfMissing; + + private BuildProperty(String propertyName, String expectedStringValue, boolean enableIfMissing) { + this.propertyName = propertyName; + this.expectedStringValue = expectedStringValue; + this.enableIfMissing = enableIfMissing; + } + + boolean enabled(Config config) { + Optional optionalValue = config.getOptionalValue(propertyName, String.class); + if (optionalValue.isPresent()) { + return expectedStringValue.equalsIgnoreCase(optionalValue.get()); + } else { + return enableIfMissing; + } + } + + boolean disabled(Config config) { + // cannot just negate `enabled()`, that would change the meaning of `enableIfMissing` + Optional optionalValue = config.getOptionalValue(propertyName, String.class); + if (optionalValue.isPresent()) { + return !expectedStringValue.equalsIgnoreCase(optionalValue.get()); + } else { + return enableIfMissing; + } + } + + static BuildProperty from(AnnotationInstance instance) { + String propertyName = instance.value("name").asString(); + String expectedStringValue = instance.value("stringValue").asString(); + AnnotationValue enableIfMissingValue = instance.value("enableIfMissing"); + boolean enableIfMissing = enableIfMissingValue != null && enableIfMissingValue.asBoolean(); + + return new BuildProperty(propertyName, expectedStringValue, enableIfMissing); + } + } } diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/BuildTimeEnabledStereotypesBuildItem.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/BuildTimeEnabledStereotypesBuildItem.java new file mode 100644 index 0000000000000..fc6b165faac28 --- /dev/null +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/BuildTimeEnabledStereotypesBuildItem.java @@ -0,0 +1,53 @@ +package io.quarkus.arc.deployment; + +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.DotName; + +import io.quarkus.builder.item.SimpleBuildItem; + +final class BuildTimeEnabledStereotypesBuildItem extends SimpleBuildItem { + private final Map map; + + BuildTimeEnabledStereotypesBuildItem(List buildTimeEnabledStereotypes) { + Map map = new HashMap<>(); + for (BuildTimeEnabledStereotype buildTimeEnabledStereotype : buildTimeEnabledStereotypes) { + map.put(buildTimeEnabledStereotype.name, buildTimeEnabledStereotype); + } + this.map = map; + } + + boolean isStereotype(DotName name) { + return map.containsKey(name); + } + + BuildTimeEnabledStereotype getStereotype(DotName stereotypeName) { + return map.get(stereotypeName); + } + + Collection all() { + return map.values(); + } + + static final class BuildTimeEnabledStereotype { + final DotName name; + final boolean inheritable; // meta-annotated `@Inherited` + + // enablement annotations present directly _or transitively_ on this stereotype + final Map> annotations; + + BuildTimeEnabledStereotype(DotName name, boolean inheritable, Map> annotations) { + this.name = name; + this.inheritable = inheritable; + this.annotations = annotations; + } + + List getEnablementAnnotations(DotName enablementAnnotationName) { + return annotations.getOrDefault(enablementAnnotationName, List.of()); + } + } +} diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/WrongAnnotationUsageProcessor.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/WrongAnnotationUsageProcessor.java index 5bbd2bdfcc330..3477a61bf3870 100644 --- a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/WrongAnnotationUsageProcessor.java +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/WrongAnnotationUsageProcessor.java @@ -44,6 +44,7 @@ void detect(ArcConfig config, ApplicationIndexBuildItem applicationIndex, Custom unsupported.add(new UnsupportedAnnotation("com.google.inject.Singleton", correctSingleton)); unsupported.add(new UnsupportedAnnotation("jakarta.ejb.Singleton", correctSingleton)); unsupported.add(new UnsupportedAnnotation("groovy.lang.Singleton", correctSingleton)); + unsupported.add(new UnsupportedAnnotation("javax.inject.Singleton", correctSingleton)); String correctInject = "@jakarta.inject.Inject"; unsupported.add(new UnsupportedAnnotation("javax.inject.Inject", correctInject)); diff --git a/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/cdi/bcextensions/SynthBeanForExternalClass.java b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/cdi/bcextensions/SynthBeanForExternalClass.java new file mode 100644 index 0000000000000..e973a867e756e --- /dev/null +++ b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/cdi/bcextensions/SynthBeanForExternalClass.java @@ -0,0 +1,96 @@ +package io.quarkus.arc.test.cdi.bcextensions; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.context.Dependent; +import jakarta.enterprise.inject.Instance; +import jakarta.enterprise.inject.build.compatible.spi.BuildCompatibleExtension; +import jakarta.enterprise.inject.build.compatible.spi.Parameters; +import jakarta.enterprise.inject.build.compatible.spi.Synthesis; +import jakarta.enterprise.inject.build.compatible.spi.SyntheticBeanCreator; +import jakarta.enterprise.inject.build.compatible.spi.SyntheticBeanDisposer; +import jakarta.enterprise.inject.build.compatible.spi.SyntheticComponents; +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.arc.test.supplement.SomeClassInExternalLibrary; +import io.quarkus.builder.Version; +import io.quarkus.maven.dependency.Dependency; +import io.quarkus.test.QuarkusUnitTest; + +public class SynthBeanForExternalClass { + // the test includes an _application_ that declares a build compatible extension + // (in the Runtime CL), which creates a synthetic bean for a class that is _outside_ + // of the application (in the Base Runtime CL) + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(MyBean.class, MyExtension.class, MySyntheticBeanCreator.class) + .addAsServiceProvider(BuildCompatibleExtension.class, MyExtension.class)) + // we need a non-application archive, so cannot use `withAdditionalDependency()` + .setForcedDependencies(List.of(Dependency.of("io.quarkus", "quarkus-arc-test-supplement", Version.getVersion()))); + + @Inject + MyBean bean; + + @Test + public void test() { + assertFalse(MySyntheticBeanCreator.created); + assertFalse(MySyntheticBeanDisposer.disposed); + + assertEquals("OK", bean.doSomething()); + assertTrue(MySyntheticBeanCreator.created); + assertTrue(MySyntheticBeanDisposer.disposed); + } + + @ApplicationScoped + public static class MyBean { + @Inject + Instance someClass; + + public String doSomething() { + SomeClassInExternalLibrary instance = someClass.get(); + instance.toString(); // force instantiating the bean + someClass.destroy(instance); // force destroying the instance + return "OK"; + } + } + + public static class MyExtension implements BuildCompatibleExtension { + @Synthesis + public void synthesis(SyntheticComponents syn) { + syn.addBean(SomeClassInExternalLibrary.class) + .type(SomeClassInExternalLibrary.class) + .scope(Dependent.class) + .createWith(MySyntheticBeanCreator.class) + .disposeWith(MySyntheticBeanDisposer.class); + } + } + + public static class MySyntheticBeanCreator implements SyntheticBeanCreator { + public static boolean created; + + public SomeClassInExternalLibrary create(Instance lookup, Parameters params) { + SomeClassInExternalLibrary result = new SomeClassInExternalLibrary(); + created = true; + return result; + } + } + + public static class MySyntheticBeanDisposer implements SyntheticBeanDisposer { + public static boolean disposed; + + @Override + public void dispose(SomeClassInExternalLibrary instance, Instance lookup, Parameters params) { + disposed = true; + } + } +} diff --git a/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/cdi/bcextensions/SynthObserverAsIfInExternalClass.java b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/cdi/bcextensions/SynthObserverAsIfInExternalClass.java new file mode 100644 index 0000000000000..b9f0bb7af9193 --- /dev/null +++ b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/cdi/bcextensions/SynthObserverAsIfInExternalClass.java @@ -0,0 +1,79 @@ +package io.quarkus.arc.test.cdi.bcextensions; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.event.Event; +import jakarta.enterprise.inject.build.compatible.spi.BuildCompatibleExtension; +import jakarta.enterprise.inject.build.compatible.spi.Parameters; +import jakarta.enterprise.inject.build.compatible.spi.Synthesis; +import jakarta.enterprise.inject.build.compatible.spi.SyntheticComponents; +import jakarta.enterprise.inject.build.compatible.spi.SyntheticObserver; +import jakarta.enterprise.inject.spi.EventContext; +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.arc.test.supplement.SomeClassInExternalLibrary; +import io.quarkus.builder.Version; +import io.quarkus.maven.dependency.Dependency; +import io.quarkus.test.QuarkusUnitTest; + +public class SynthObserverAsIfInExternalClass { + // the test includes an _application_ that declares a build compatible extension + // (in the Runtime CL), which creates a synthetic observer which is "as if" declared + // in a class that is _outside_ of the application (in the Base Runtime CL) + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(MyBean.class, MyExtension.class, MySyntheticObserver.class) + .addAsServiceProvider(BuildCompatibleExtension.class, MyExtension.class)) + // we need a non-application archive, so cannot use `withAdditionalDependency()` + .setForcedDependencies(List.of(Dependency.of("io.quarkus", "quarkus-arc-test-supplement", Version.getVersion()))); + + @Inject + MyBean bean; + + @Test + public void test() { + assertFalse(MySyntheticObserver.notified); + + assertEquals("OK", bean.doSomething()); + assertTrue(MySyntheticObserver.notified); + } + + @ApplicationScoped + public static class MyBean { + @Inject + Event event; + + public String doSomething() { + event.fire(""); // force notifying the observer + return "OK"; + } + } + + public static class MyExtension implements BuildCompatibleExtension { + @Synthesis + public void synthesis(SyntheticComponents syn) { + syn.addObserver(String.class) + .declaringClass(SomeClassInExternalLibrary.class) + .observeWith(MySyntheticObserver.class); + } + } + + public static class MySyntheticObserver implements SyntheticObserver { + public static boolean notified; + + @Override + public void observe(EventContext event, Parameters params) { + notified = true; + } + } +} diff --git a/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/config/staticinit/StaticInitConfigInjectionMissingValueFailureTest.java b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/config/staticinit/StaticInitConfigInjectionMissingValueFailureTest.java new file mode 100644 index 0000000000000..76ee3650b6ee7 --- /dev/null +++ b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/config/staticinit/StaticInitConfigInjectionMissingValueFailureTest.java @@ -0,0 +1,58 @@ +package io.quarkus.arc.test.config.staticinit; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.fail; + +import java.util.Optional; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.context.Initialized; +import jakarta.enterprise.event.Observes; +import jakarta.inject.Singleton; + +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class StaticInitConfigInjectionMissingValueFailureTest { + + static final String PROPERTY_NAME = "static.init.missing.apfelstrudel"; + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot(root -> root + .addClasses(StaticInitBean.class)) + .assertException(t -> { + assertThat(t).isInstanceOf(IllegalStateException.class) + .hasMessageContainingAll( + "A runtime config property value differs from the value that was injected during the static intialization phase", + "the runtime value of '" + PROPERTY_NAME + + "' is [gizmo] but the value [null] was injected into io.quarkus.arc.test.config.staticinit.StaticInitConfigInjectionMissingValueFailureTest$StaticInitBean#value"); + }); + + @Test + public void test() { + fail(); + } + + @Singleton + public static class StaticInitBean { + + @ConfigProperty(name = PROPERTY_NAME) + Optional value; + + // bean is instantiated during STATIC_INIT + void onInit(@Observes @Initialized(ApplicationScoped.class) Object event) { + System.setProperty(PROPERTY_NAME, "gizmo"); + } + + } + + @AfterAll + static void afterAll() { + System.clearProperty(PROPERTY_NAME); + } +} diff --git a/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/profile/IfBuildProfileStereotypeTest.java b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/profile/IfBuildProfileStereotypeTest.java new file mode 100644 index 0000000000000..2a6e79e72a7a1 --- /dev/null +++ b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/profile/IfBuildProfileStereotypeTest.java @@ -0,0 +1,195 @@ +package io.quarkus.arc.test.profile; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.Set; +import java.util.stream.Collectors; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Any; +import jakarta.enterprise.inject.Instance; +import jakarta.enterprise.inject.Produces; +import jakarta.enterprise.inject.Stereotype; +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.arc.profile.IfBuildProfile; +import io.quarkus.test.QuarkusUnitTest; + +public class IfBuildProfileStereotypeTest { + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(DevOnly.class, InheritableDevOnly.class, TransitiveDevOnly.class, + InheritableTransitiveDevOnly.class, MyService.class, DevOnlyMyService.class, + InheritableDevOnlyMyService.class, TransitiveDevOnlyMyService.class, + InheritableTransitiveDevOnlyMyService.class, MyServiceSimple.class, + MyServiceDevOnlyDirect.class, MyServiceDevOnlyTransitive.class, + MyServiceDevOnlyOnSuperclassNotInheritable.class, + MyServiceDevOnlyOnSuperclassInheritable.class, + MyServiceDevOnlyTransitiveOnSuperclassNotInheritable.class, + MyServiceDevOnlyTransitiveOnSuperclassInheritable.class, Producers.class)); + + @Inject + @Any + Instance services; + + @Test + public void test() { + Set hello = services.stream().map(MyService::hello).collect(Collectors.toSet()); + Set expected = Set.of( + MyServiceSimple.class.getSimpleName(), + MyServiceDevOnlyOnSuperclassNotInheritable.class.getSimpleName(), + MyServiceDevOnlyTransitiveOnSuperclassNotInheritable.class.getSimpleName(), + Producers.SIMPLE); + assertEquals(expected, hello); + } + + @IfBuildProfile("dev") + @Stereotype + @Target({ ElementType.TYPE, ElementType.METHOD, ElementType.FIELD }) + @Retention(RetentionPolicy.RUNTIME) + public @interface DevOnly { + } + + @IfBuildProfile("dev") + @Stereotype + @Inherited + @Target({ ElementType.TYPE, ElementType.METHOD, ElementType.FIELD }) + @Retention(RetentionPolicy.RUNTIME) + public @interface InheritableDevOnly { + } + + @DevOnly + @Stereotype + @Target({ ElementType.TYPE, ElementType.METHOD, ElementType.FIELD }) + @Retention(RetentionPolicy.RUNTIME) + public @interface TransitiveDevOnly { + } + + @DevOnly + @Stereotype + @Inherited + @Target({ ElementType.TYPE, ElementType.METHOD, ElementType.FIELD }) + @Retention(RetentionPolicy.RUNTIME) + public @interface InheritableTransitiveDevOnly { + } + + interface MyService { + String hello(); + } + + @DevOnly + static abstract class DevOnlyMyService implements MyService { + } + + @InheritableDevOnly + static abstract class InheritableDevOnlyMyService implements MyService { + } + + @TransitiveDevOnly + static abstract class TransitiveDevOnlyMyService implements MyService { + } + + @InheritableTransitiveDevOnly + static abstract class InheritableTransitiveDevOnlyMyService implements MyService { + } + + @ApplicationScoped + static class MyServiceSimple implements MyService { + @Override + public String hello() { + return MyServiceSimple.class.getSimpleName(); + } + } + + @ApplicationScoped + @DevOnly + static class MyServiceDevOnlyDirect implements MyService { + @Override + public String hello() { + return MyServiceDevOnlyDirect.class.getSimpleName(); + } + } + + @ApplicationScoped + @TransitiveDevOnly + static class MyServiceDevOnlyTransitive implements MyService { + @Override + public String hello() { + return MyServiceDevOnlyTransitive.class.getSimpleName(); + } + } + + @ApplicationScoped + static class MyServiceDevOnlyOnSuperclassNotInheritable extends DevOnlyMyService { + @Override + public String hello() { + return MyServiceDevOnlyOnSuperclassNotInheritable.class.getSimpleName(); + } + } + + @ApplicationScoped + static class MyServiceDevOnlyOnSuperclassInheritable extends InheritableDevOnlyMyService { + @Override + public String hello() { + return MyServiceDevOnlyOnSuperclassInheritable.class.getSimpleName(); + } + } + + @ApplicationScoped + static class MyServiceDevOnlyTransitiveOnSuperclassNotInheritable extends TransitiveDevOnlyMyService { + @Override + public String hello() { + return MyServiceDevOnlyTransitiveOnSuperclassNotInheritable.class.getSimpleName(); + } + } + + @ApplicationScoped + static class MyServiceDevOnlyTransitiveOnSuperclassInheritable extends InheritableTransitiveDevOnlyMyService { + @Override + public String hello() { + return MyServiceDevOnlyTransitiveOnSuperclassInheritable.class.getSimpleName(); + } + } + + @ApplicationScoped + static class Producers { + static final String SIMPLE = "Producers.simple"; + static final String DEV_ONLY_DIRECT = "Producers.devOnlyDirect"; + static final String DEV_ONLY_TRANSITIVE = "Producers.devOnlyTransitive"; + + @Produces + MyService simple = new MyService() { + @Override + public String hello() { + return SIMPLE; + } + }; + + @Produces + @DevOnly + MyService devOnlyDirect = new MyService() { + @Override + public String hello() { + return DEV_ONLY_DIRECT; + } + }; + + @Produces + @TransitiveDevOnly + MyService devOnlyTransitive = new MyService() { + @Override + public String hello() { + return DEV_ONLY_TRANSITIVE; + } + }; + } +} diff --git a/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/profile/UnlessBuildProfileStereotypeTest.java b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/profile/UnlessBuildProfileStereotypeTest.java new file mode 100644 index 0000000000000..ff9d8c982bc39 --- /dev/null +++ b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/profile/UnlessBuildProfileStereotypeTest.java @@ -0,0 +1,204 @@ +package io.quarkus.arc.test.profile; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.Set; +import java.util.stream.Collectors; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Any; +import jakarta.enterprise.inject.Instance; +import jakarta.enterprise.inject.Produces; +import jakarta.enterprise.inject.Stereotype; +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.arc.profile.UnlessBuildProfile; +import io.quarkus.test.QuarkusUnitTest; + +public class UnlessBuildProfileStereotypeTest { + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(TestNever.class, InheritableTestNever.class, TransitiveTestNever.class, + InheritableTransitiveTestNever.class, MyService.class, TestNeverMyService.class, + InheritableTestNeverMyService.class, TransitiveTestNeverMyService.class, + InheritableTransitiveTestNeverMyService.class, MyServiceSimple.class, + MyServiceTestNeverDirect.class, MyServiceTestNeverTransitive.class, + MyServiceTestNeverOnSuperclassNotInheritable.class, + MyServiceTestNeverOnSuperclassInheritable.class, + MyServiceTestNeverTransitiveOnSuperclassNotInheritable.class, + MyServiceTestNeverTransitiveOnSuperclassInheritable.class, Producers.class)); + + @Inject + @Any + Instance services; + + @Test + public void test() { + Set hello = services.stream().map(MyService::hello).collect(Collectors.toSet()); + Set expected = Set.of( + MyServiceSimple.class.getSimpleName(), + MyServiceTestNeverOnSuperclassNotInheritable.class.getSimpleName(), + MyServiceTestNeverTransitiveOnSuperclassNotInheritable.class.getSimpleName(), + Producers.SIMPLE); + assertEquals(expected, hello); + } + + @UnlessBuildProfile("test") + @Stereotype + @Target({ ElementType.TYPE, ElementType.METHOD, ElementType.FIELD }) + @Retention(RetentionPolicy.RUNTIME) + public @interface TestNever { + } + + @UnlessBuildProfile("test") + @Stereotype + @Inherited + @Target({ ElementType.TYPE, ElementType.METHOD, ElementType.FIELD }) + @Retention(RetentionPolicy.RUNTIME) + public @interface InheritableTestNever { + } + + @TestNever + @Stereotype + @Target({ ElementType.TYPE, ElementType.METHOD, ElementType.FIELD }) + @Retention(RetentionPolicy.RUNTIME) + public @interface TransitiveTestNever { + } + + @TestNever + @Stereotype + @Inherited + @Target({ ElementType.TYPE, ElementType.METHOD, ElementType.FIELD }) + @Retention(RetentionPolicy.RUNTIME) + public @interface InheritableTransitiveTestNever { + } + + interface MyService { + String hello(); + } + + @TestNever + static abstract class TestNeverMyService implements MyService { + } + + @InheritableTestNever + static abstract class InheritableTestNeverMyService implements MyService { + } + + @TransitiveTestNever + static abstract class TransitiveTestNeverMyService implements MyService { + } + + @InheritableTransitiveTestNever + static abstract class InheritableTransitiveTestNeverMyService implements MyService { + } + + @ApplicationScoped + static class MyServiceSimple implements MyService { + @Override + public String hello() { + return MyServiceSimple.class.getSimpleName(); + } + } + + @ApplicationScoped + @TestNever + static class MyServiceTestNeverDirect implements MyService { + @Override + public String hello() { + return MyServiceTestNeverDirect.class.getSimpleName(); + } + } + + @ApplicationScoped + @TransitiveTestNever + static class MyServiceTestNeverTransitive implements MyService { + @Override + public String hello() { + return MyServiceTestNeverTransitive.class.getSimpleName(); + } + } + + @ApplicationScoped + static class MyServiceTestNeverOnSuperclassNotInheritable extends TestNeverMyService { + @Override + public String hello() { + return MyServiceTestNeverOnSuperclassNotInheritable.class.getSimpleName(); + } + } + + @ApplicationScoped + static class MyServiceTestNeverOnSuperclassInheritable extends InheritableTestNeverMyService { + @Override + public String hello() { + return MyServiceTestNeverOnSuperclassInheritable.class.getSimpleName(); + } + } + + @ApplicationScoped + static class MyServiceTestNeverTransitiveOnSuperclassNotInheritable extends TransitiveTestNeverMyService { + @Override + public String hello() { + return MyServiceTestNeverTransitiveOnSuperclassNotInheritable.class.getSimpleName(); + } + } + + @ApplicationScoped + static class MyServiceTestNeverTransitiveOnSuperclassInheritable extends InheritableTransitiveTestNeverMyService { + @Override + public String hello() { + return MyServiceTestNeverTransitiveOnSuperclassInheritable.class.getSimpleName(); + } + } + + @ApplicationScoped + static class Producers { + static final String SIMPLE = "Producers.simple"; + static final String TEST_NEVER_DIRECT = "Producers.testNeverDirect"; + static final String TEST_NEVER_TRANSITIVE = "Producers.testNeverTransitive"; + + @Produces + @ApplicationScoped + MyService simple() { + return new MyService() { + @Override + public String hello() { + return SIMPLE; + } + }; + } + + @Produces + @ApplicationScoped + @TestNever + MyService testNeverDirect() { + return new MyService() { + @Override + public String hello() { + return TEST_NEVER_DIRECT; + } + }; + } + + @Produces + @ApplicationScoped + @TransitiveTestNever + MyService testNeverTransitive() { + return new MyService() { + @Override + public String hello() { + return TEST_NEVER_TRANSITIVE; + } + }; + } + } +} diff --git a/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/properties/IfBuildPropertyRepeatableStereotypeTest.java b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/properties/IfBuildPropertyRepeatableStereotypeTest.java new file mode 100644 index 0000000000000..243d65fa32dad --- /dev/null +++ b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/properties/IfBuildPropertyRepeatableStereotypeTest.java @@ -0,0 +1,202 @@ +package io.quarkus.arc.test.properties; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.Set; +import java.util.stream.Collectors; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Any; +import jakarta.enterprise.inject.Instance; +import jakarta.enterprise.inject.Produces; +import jakarta.enterprise.inject.Stereotype; +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.arc.properties.IfBuildProperty; +import io.quarkus.test.QuarkusUnitTest; + +public class IfBuildPropertyRepeatableStereotypeTest { + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(NotMatchingProperty.class, InheritableNotMatchingProperty.class, + TransitiveNotMatchingProperty.class, + InheritableTransitiveNotMatchingProperty.class, MyService.class, NotMatchingPropertyMyService.class, + InheritableNotMatchingPropertyMyService.class, TransitiveNotMatchingPropertyMyService.class, + InheritableTransitiveNotMatchingPropertyMyService.class, MyServiceSimple.class, + MyServiceNotMatchingPropertyDirect.class, MyServiceNotMatchingPropertyTransitive.class, + MyServiceNotMatchingPropertyOnSuperclassNotInheritable.class, + MyServiceNotMatchingPropertyOnSuperclassInheritable.class, + MyServiceNotMatchingPropertyTransitiveOnSuperclassNotInheritable.class, + MyServiceNotMatchingPropertyTransitiveOnSuperclassInheritable.class, Producers.class)) + .overrideConfigKey("foo.bar", "quux") + .overrideConfigKey("some.prop", "val"); + + @Inject + @Any + Instance services; + + @Test + public void test() { + Set hello = services.stream().map(MyService::hello).collect(Collectors.toSet()); + Set expected = Set.of( + MyServiceSimple.class.getSimpleName(), + MyServiceNotMatchingPropertyOnSuperclassNotInheritable.class.getSimpleName(), + MyServiceNotMatchingPropertyTransitiveOnSuperclassNotInheritable.class.getSimpleName(), + Producers.SIMPLE); + assertEquals(expected, hello); + } + + @IfBuildProperty(name = "foo.bar", stringValue = "baz") + @IfBuildProperty(name = "some.prop", stringValue = "val") + @Stereotype + @Target({ ElementType.TYPE, ElementType.METHOD, ElementType.FIELD }) + @Retention(RetentionPolicy.RUNTIME) + public @interface NotMatchingProperty { + } + + @IfBuildProperty(name = "foo.bar", stringValue = "quux") + @IfBuildProperty(name = "some.prop", stringValue = "none") + @Stereotype + @Inherited + @Target({ ElementType.TYPE, ElementType.METHOD, ElementType.FIELD }) + @Retention(RetentionPolicy.RUNTIME) + public @interface InheritableNotMatchingProperty { + } + + @NotMatchingProperty + @Stereotype + @Target({ ElementType.TYPE, ElementType.METHOD, ElementType.FIELD }) + @Retention(RetentionPolicy.RUNTIME) + public @interface TransitiveNotMatchingProperty { + } + + @NotMatchingProperty + @Stereotype + @Inherited + @Target({ ElementType.TYPE, ElementType.METHOD, ElementType.FIELD }) + @Retention(RetentionPolicy.RUNTIME) + public @interface InheritableTransitiveNotMatchingProperty { + } + + interface MyService { + String hello(); + } + + @NotMatchingProperty + static abstract class NotMatchingPropertyMyService implements MyService { + } + + @InheritableNotMatchingProperty + static abstract class InheritableNotMatchingPropertyMyService implements MyService { + } + + @TransitiveNotMatchingProperty + static abstract class TransitiveNotMatchingPropertyMyService implements MyService { + } + + @InheritableTransitiveNotMatchingProperty + static abstract class InheritableTransitiveNotMatchingPropertyMyService implements MyService { + } + + @ApplicationScoped + static class MyServiceSimple implements MyService { + @Override + public String hello() { + return MyServiceSimple.class.getSimpleName(); + } + } + + @ApplicationScoped + @NotMatchingProperty + static class MyServiceNotMatchingPropertyDirect implements MyService { + @Override + public String hello() { + return MyServiceNotMatchingPropertyDirect.class.getSimpleName(); + } + } + + @ApplicationScoped + @TransitiveNotMatchingProperty + static class MyServiceNotMatchingPropertyTransitive implements MyService { + @Override + public String hello() { + return MyServiceNotMatchingPropertyTransitive.class.getSimpleName(); + } + } + + @ApplicationScoped + static class MyServiceNotMatchingPropertyOnSuperclassNotInheritable extends NotMatchingPropertyMyService { + @Override + public String hello() { + return MyServiceNotMatchingPropertyOnSuperclassNotInheritable.class.getSimpleName(); + } + } + + @ApplicationScoped + static class MyServiceNotMatchingPropertyOnSuperclassInheritable extends InheritableNotMatchingPropertyMyService { + @Override + public String hello() { + return MyServiceNotMatchingPropertyOnSuperclassInheritable.class.getSimpleName(); + } + } + + @ApplicationScoped + static class MyServiceNotMatchingPropertyTransitiveOnSuperclassNotInheritable + extends TransitiveNotMatchingPropertyMyService { + @Override + public String hello() { + return MyServiceNotMatchingPropertyTransitiveOnSuperclassNotInheritable.class.getSimpleName(); + } + } + + @ApplicationScoped + static class MyServiceNotMatchingPropertyTransitiveOnSuperclassInheritable + extends InheritableTransitiveNotMatchingPropertyMyService { + @Override + public String hello() { + return MyServiceNotMatchingPropertyTransitiveOnSuperclassInheritable.class.getSimpleName(); + } + } + + @ApplicationScoped + static class Producers { + static final String SIMPLE = "Producers.simple"; + static final String NOT_MATCHING_PROPERTY_DIRECT = "Producers.notMatchingPropertyDirect"; + static final String NOT_MATCHING_PROPERTY_TRANSITIVE = "Producers.notMatchingPropertyTransitive"; + + @Produces + MyService simple = new MyService() { + @Override + public String hello() { + return SIMPLE; + } + }; + + @Produces + @NotMatchingProperty + MyService notMatchingPropertyDirect = new MyService() { + @Override + public String hello() { + return NOT_MATCHING_PROPERTY_DIRECT; + } + }; + + @Produces + @TransitiveNotMatchingProperty + MyService notMatchingPropertyTransitive = new MyService() { + @Override + public String hello() { + return NOT_MATCHING_PROPERTY_TRANSITIVE; + } + }; + } +} diff --git a/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/properties/IfBuildPropertyStereotypeTest.java b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/properties/IfBuildPropertyStereotypeTest.java new file mode 100644 index 0000000000000..56bbf64416233 --- /dev/null +++ b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/properties/IfBuildPropertyStereotypeTest.java @@ -0,0 +1,199 @@ +package io.quarkus.arc.test.properties; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.Set; +import java.util.stream.Collectors; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Any; +import jakarta.enterprise.inject.Instance; +import jakarta.enterprise.inject.Produces; +import jakarta.enterprise.inject.Stereotype; +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.arc.properties.IfBuildProperty; +import io.quarkus.test.QuarkusUnitTest; + +public class IfBuildPropertyStereotypeTest { + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(NotMatchingProperty.class, InheritableNotMatchingProperty.class, + TransitiveNotMatchingProperty.class, + InheritableTransitiveNotMatchingProperty.class, MyService.class, NotMatchingPropertyMyService.class, + InheritableNotMatchingPropertyMyService.class, TransitiveNotMatchingPropertyMyService.class, + InheritableTransitiveNotMatchingPropertyMyService.class, MyServiceSimple.class, + MyServiceNotMatchingPropertyDirect.class, MyServiceNotMatchingPropertyTransitive.class, + MyServiceNotMatchingPropertyOnSuperclassNotInheritable.class, + MyServiceNotMatchingPropertyOnSuperclassInheritable.class, + MyServiceNotMatchingPropertyTransitiveOnSuperclassNotInheritable.class, + MyServiceNotMatchingPropertyTransitiveOnSuperclassInheritable.class, Producers.class)) + .overrideConfigKey("foo.bar", "quux"); + + @Inject + @Any + Instance services; + + @Test + public void test() { + Set hello = services.stream().map(MyService::hello).collect(Collectors.toSet()); + Set expected = Set.of( + MyServiceSimple.class.getSimpleName(), + MyServiceNotMatchingPropertyOnSuperclassNotInheritable.class.getSimpleName(), + MyServiceNotMatchingPropertyTransitiveOnSuperclassNotInheritable.class.getSimpleName(), + Producers.SIMPLE); + assertEquals(expected, hello); + } + + @IfBuildProperty(name = "foo.bar", stringValue = "baz") + @Stereotype + @Target({ ElementType.TYPE, ElementType.METHOD, ElementType.FIELD }) + @Retention(RetentionPolicy.RUNTIME) + public @interface NotMatchingProperty { + } + + @IfBuildProperty(name = "foo.bar", stringValue = "baz") + @Stereotype + @Inherited + @Target({ ElementType.TYPE, ElementType.METHOD, ElementType.FIELD }) + @Retention(RetentionPolicy.RUNTIME) + public @interface InheritableNotMatchingProperty { + } + + @NotMatchingProperty + @Stereotype + @Target({ ElementType.TYPE, ElementType.METHOD, ElementType.FIELD }) + @Retention(RetentionPolicy.RUNTIME) + public @interface TransitiveNotMatchingProperty { + } + + @NotMatchingProperty + @Stereotype + @Inherited + @Target({ ElementType.TYPE, ElementType.METHOD, ElementType.FIELD }) + @Retention(RetentionPolicy.RUNTIME) + public @interface InheritableTransitiveNotMatchingProperty { + } + + interface MyService { + String hello(); + } + + @NotMatchingProperty + static abstract class NotMatchingPropertyMyService implements MyService { + } + + @InheritableNotMatchingProperty + static abstract class InheritableNotMatchingPropertyMyService implements MyService { + } + + @TransitiveNotMatchingProperty + static abstract class TransitiveNotMatchingPropertyMyService implements MyService { + } + + @InheritableTransitiveNotMatchingProperty + static abstract class InheritableTransitiveNotMatchingPropertyMyService implements MyService { + } + + @ApplicationScoped + static class MyServiceSimple implements MyService { + @Override + public String hello() { + return MyServiceSimple.class.getSimpleName(); + } + } + + @ApplicationScoped + @NotMatchingProperty + static class MyServiceNotMatchingPropertyDirect implements MyService { + @Override + public String hello() { + return MyServiceNotMatchingPropertyDirect.class.getSimpleName(); + } + } + + @ApplicationScoped + @TransitiveNotMatchingProperty + static class MyServiceNotMatchingPropertyTransitive implements MyService { + @Override + public String hello() { + return MyServiceNotMatchingPropertyTransitive.class.getSimpleName(); + } + } + + @ApplicationScoped + static class MyServiceNotMatchingPropertyOnSuperclassNotInheritable extends NotMatchingPropertyMyService { + @Override + public String hello() { + return MyServiceNotMatchingPropertyOnSuperclassNotInheritable.class.getSimpleName(); + } + } + + @ApplicationScoped + static class MyServiceNotMatchingPropertyOnSuperclassInheritable extends InheritableNotMatchingPropertyMyService { + @Override + public String hello() { + return MyServiceNotMatchingPropertyOnSuperclassInheritable.class.getSimpleName(); + } + } + + @ApplicationScoped + static class MyServiceNotMatchingPropertyTransitiveOnSuperclassNotInheritable + extends TransitiveNotMatchingPropertyMyService { + @Override + public String hello() { + return MyServiceNotMatchingPropertyTransitiveOnSuperclassNotInheritable.class.getSimpleName(); + } + } + + @ApplicationScoped + static class MyServiceNotMatchingPropertyTransitiveOnSuperclassInheritable + extends InheritableTransitiveNotMatchingPropertyMyService { + @Override + public String hello() { + return MyServiceNotMatchingPropertyTransitiveOnSuperclassInheritable.class.getSimpleName(); + } + } + + @ApplicationScoped + static class Producers { + static final String SIMPLE = "Producers.simple"; + static final String NOT_MATCHING_PROPERTY_DIRECT = "Producers.notMatchingPropertyDirect"; + static final String NOT_MATCHING_PROPERTY_TRANSITIVE = "Producers.notMatchingPropertyTransitive"; + + @Produces + MyService simple = new MyService() { + @Override + public String hello() { + return SIMPLE; + } + }; + + @Produces + @NotMatchingProperty + MyService notMatchingPropertyDirect = new MyService() { + @Override + public String hello() { + return NOT_MATCHING_PROPERTY_DIRECT; + } + }; + + @Produces + @TransitiveNotMatchingProperty + MyService notMatchingPropertyTransitive = new MyService() { + @Override + public String hello() { + return NOT_MATCHING_PROPERTY_TRANSITIVE; + } + }; + } +} diff --git a/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/properties/UnlessBuildPropertyRepeatableStereotypeTest.java b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/properties/UnlessBuildPropertyRepeatableStereotypeTest.java new file mode 100644 index 0000000000000..30889db56e8cf --- /dev/null +++ b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/properties/UnlessBuildPropertyRepeatableStereotypeTest.java @@ -0,0 +1,209 @@ +package io.quarkus.arc.test.properties; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.Set; +import java.util.stream.Collectors; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Any; +import jakarta.enterprise.inject.Instance; +import jakarta.enterprise.inject.Produces; +import jakarta.enterprise.inject.Stereotype; +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.arc.properties.UnlessBuildProperty; +import io.quarkus.test.QuarkusUnitTest; + +public class UnlessBuildPropertyRepeatableStereotypeTest { + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(MatchingProperty.class, InheritableMatchingProperty.class, TransitiveMatchingProperty.class, + InheritableTransitiveMatchingProperty.class, MyService.class, MatchingPropertyMyService.class, + InheritableMatchingPropertyMyService.class, TransitiveMatchingPropertyMyService.class, + InheritableTransitiveMatchingPropertyMyService.class, MyServiceSimple.class, + MyServiceMatchingPropertyDirect.class, MyServiceMatchingPropertyTransitive.class, + MyServiceMatchingPropertyOnSuperclassNotInheritable.class, + MyServiceMatchingPropertyOnSuperclassInheritable.class, + MyServiceMatchingPropertyTransitiveOnSuperclassNotInheritable.class, + MyServiceMatchingPropertyTransitiveOnSuperclassInheritable.class, Producers.class)) + .overrideConfigKey("foo.bar", "baz") + .overrideConfigKey("some.prop", "val"); + + @Inject + @Any + Instance services; + + @Test + public void test() { + Set hello = services.stream().map(MyService::hello).collect(Collectors.toSet()); + Set expected = Set.of( + MyServiceSimple.class.getSimpleName(), + MyServiceMatchingPropertyOnSuperclassNotInheritable.class.getSimpleName(), + MyServiceMatchingPropertyTransitiveOnSuperclassNotInheritable.class.getSimpleName(), + Producers.SIMPLE); + assertEquals(expected, hello); + } + + @UnlessBuildProperty(name = "foo.bar", stringValue = "baz") + @UnlessBuildProperty(name = "some.prop", stringValue = "none") + @Stereotype + @Target({ ElementType.TYPE, ElementType.METHOD, ElementType.FIELD }) + @Retention(RetentionPolicy.RUNTIME) + public @interface MatchingProperty { + } + + @UnlessBuildProperty(name = "foo.bar", stringValue = "quux") + @UnlessBuildProperty(name = "some.prop", stringValue = "val") + @Stereotype + @Inherited + @Target({ ElementType.TYPE, ElementType.METHOD, ElementType.FIELD }) + @Retention(RetentionPolicy.RUNTIME) + public @interface InheritableMatchingProperty { + } + + @MatchingProperty + @Stereotype + @Target({ ElementType.TYPE, ElementType.METHOD, ElementType.FIELD }) + @Retention(RetentionPolicy.RUNTIME) + public @interface TransitiveMatchingProperty { + } + + @MatchingProperty + @Stereotype + @Inherited + @Target({ ElementType.TYPE, ElementType.METHOD, ElementType.FIELD }) + @Retention(RetentionPolicy.RUNTIME) + public @interface InheritableTransitiveMatchingProperty { + } + + interface MyService { + String hello(); + } + + @MatchingProperty + static abstract class MatchingPropertyMyService implements MyService { + } + + @InheritableMatchingProperty + static abstract class InheritableMatchingPropertyMyService implements MyService { + } + + @TransitiveMatchingProperty + static abstract class TransitiveMatchingPropertyMyService implements MyService { + } + + @InheritableTransitiveMatchingProperty + static abstract class InheritableTransitiveMatchingPropertyMyService implements MyService { + } + + @ApplicationScoped + static class MyServiceSimple implements MyService { + @Override + public String hello() { + return MyServiceSimple.class.getSimpleName(); + } + } + + @ApplicationScoped + @MatchingProperty + static class MyServiceMatchingPropertyDirect implements MyService { + @Override + public String hello() { + return MyServiceMatchingPropertyDirect.class.getSimpleName(); + } + } + + @ApplicationScoped + @TransitiveMatchingProperty + static class MyServiceMatchingPropertyTransitive implements MyService { + @Override + public String hello() { + return MyServiceMatchingPropertyTransitive.class.getSimpleName(); + } + } + + @ApplicationScoped + static class MyServiceMatchingPropertyOnSuperclassNotInheritable extends MatchingPropertyMyService { + @Override + public String hello() { + return MyServiceMatchingPropertyOnSuperclassNotInheritable.class.getSimpleName(); + } + } + + @ApplicationScoped + static class MyServiceMatchingPropertyOnSuperclassInheritable extends InheritableMatchingPropertyMyService { + @Override + public String hello() { + return MyServiceMatchingPropertyOnSuperclassInheritable.class.getSimpleName(); + } + } + + @ApplicationScoped + static class MyServiceMatchingPropertyTransitiveOnSuperclassNotInheritable extends TransitiveMatchingPropertyMyService { + @Override + public String hello() { + return MyServiceMatchingPropertyTransitiveOnSuperclassNotInheritable.class.getSimpleName(); + } + } + + @ApplicationScoped + static class MyServiceMatchingPropertyTransitiveOnSuperclassInheritable + extends InheritableTransitiveMatchingPropertyMyService { + @Override + public String hello() { + return MyServiceMatchingPropertyTransitiveOnSuperclassInheritable.class.getSimpleName(); + } + } + + @ApplicationScoped + static class Producers { + static final String SIMPLE = "Producers.simple"; + static final String MATCHING_PROPERTY_DIRECT = "Producers.matchingPropertyDirect"; + static final String MATCHING_PROPERTY_TRANSITIVE = "Producers.matchingPropertyTransitive"; + + @Produces + @ApplicationScoped + MyService simple() { + return new MyService() { + @Override + public String hello() { + return SIMPLE; + } + }; + } + + @Produces + @ApplicationScoped + @MatchingProperty + MyService matchingPropertyDirect() { + return new MyService() { + @Override + public String hello() { + return MATCHING_PROPERTY_DIRECT; + } + }; + } + + @Produces + @ApplicationScoped + @TransitiveMatchingProperty + MyService matchingPropertyTransitive() { + return new MyService() { + @Override + public String hello() { + return MATCHING_PROPERTY_TRANSITIVE; + } + }; + } + } +} diff --git a/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/properties/UnlessBuildPropertyStereotypeTest.java b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/properties/UnlessBuildPropertyStereotypeTest.java new file mode 100644 index 0000000000000..3e527fc6a7fa6 --- /dev/null +++ b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/properties/UnlessBuildPropertyStereotypeTest.java @@ -0,0 +1,206 @@ +package io.quarkus.arc.test.properties; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.Set; +import java.util.stream.Collectors; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Any; +import jakarta.enterprise.inject.Instance; +import jakarta.enterprise.inject.Produces; +import jakarta.enterprise.inject.Stereotype; +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.arc.properties.UnlessBuildProperty; +import io.quarkus.test.QuarkusUnitTest; + +public class UnlessBuildPropertyStereotypeTest { + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(MatchingProperty.class, InheritableMatchingProperty.class, TransitiveMatchingProperty.class, + InheritableTransitiveMatchingProperty.class, MyService.class, MatchingPropertyMyService.class, + InheritableMatchingPropertyMyService.class, TransitiveMatchingPropertyMyService.class, + InheritableTransitiveMatchingPropertyMyService.class, MyServiceSimple.class, + MyServiceMatchingPropertyDirect.class, MyServiceMatchingPropertyTransitive.class, + MyServiceMatchingPropertyOnSuperclassNotInheritable.class, + MyServiceMatchingPropertyOnSuperclassInheritable.class, + MyServiceMatchingPropertyTransitiveOnSuperclassNotInheritable.class, + MyServiceMatchingPropertyTransitiveOnSuperclassInheritable.class, Producers.class)) + .overrideConfigKey("foo.bar", "baz"); + + @Inject + @Any + Instance services; + + @Test + public void test() { + Set hello = services.stream().map(MyService::hello).collect(Collectors.toSet()); + Set expected = Set.of( + MyServiceSimple.class.getSimpleName(), + MyServiceMatchingPropertyOnSuperclassNotInheritable.class.getSimpleName(), + MyServiceMatchingPropertyTransitiveOnSuperclassNotInheritable.class.getSimpleName(), + Producers.SIMPLE); + assertEquals(expected, hello); + } + + @UnlessBuildProperty(name = "foo.bar", stringValue = "baz") + @Stereotype + @Target({ ElementType.TYPE, ElementType.METHOD, ElementType.FIELD }) + @Retention(RetentionPolicy.RUNTIME) + public @interface MatchingProperty { + } + + @UnlessBuildProperty(name = "foo.bar", stringValue = "baz") + @Stereotype + @Inherited + @Target({ ElementType.TYPE, ElementType.METHOD, ElementType.FIELD }) + @Retention(RetentionPolicy.RUNTIME) + public @interface InheritableMatchingProperty { + } + + @MatchingProperty + @Stereotype + @Target({ ElementType.TYPE, ElementType.METHOD, ElementType.FIELD }) + @Retention(RetentionPolicy.RUNTIME) + public @interface TransitiveMatchingProperty { + } + + @MatchingProperty + @Stereotype + @Inherited + @Target({ ElementType.TYPE, ElementType.METHOD, ElementType.FIELD }) + @Retention(RetentionPolicy.RUNTIME) + public @interface InheritableTransitiveMatchingProperty { + } + + interface MyService { + String hello(); + } + + @MatchingProperty + static abstract class MatchingPropertyMyService implements MyService { + } + + @InheritableMatchingProperty + static abstract class InheritableMatchingPropertyMyService implements MyService { + } + + @TransitiveMatchingProperty + static abstract class TransitiveMatchingPropertyMyService implements MyService { + } + + @InheritableTransitiveMatchingProperty + static abstract class InheritableTransitiveMatchingPropertyMyService implements MyService { + } + + @ApplicationScoped + static class MyServiceSimple implements MyService { + @Override + public String hello() { + return MyServiceSimple.class.getSimpleName(); + } + } + + @ApplicationScoped + @MatchingProperty + static class MyServiceMatchingPropertyDirect implements MyService { + @Override + public String hello() { + return MyServiceMatchingPropertyDirect.class.getSimpleName(); + } + } + + @ApplicationScoped + @TransitiveMatchingProperty + static class MyServiceMatchingPropertyTransitive implements MyService { + @Override + public String hello() { + return MyServiceMatchingPropertyTransitive.class.getSimpleName(); + } + } + + @ApplicationScoped + static class MyServiceMatchingPropertyOnSuperclassNotInheritable extends MatchingPropertyMyService { + @Override + public String hello() { + return MyServiceMatchingPropertyOnSuperclassNotInheritable.class.getSimpleName(); + } + } + + @ApplicationScoped + static class MyServiceMatchingPropertyOnSuperclassInheritable extends InheritableMatchingPropertyMyService { + @Override + public String hello() { + return MyServiceMatchingPropertyOnSuperclassInheritable.class.getSimpleName(); + } + } + + @ApplicationScoped + static class MyServiceMatchingPropertyTransitiveOnSuperclassNotInheritable extends TransitiveMatchingPropertyMyService { + @Override + public String hello() { + return MyServiceMatchingPropertyTransitiveOnSuperclassNotInheritable.class.getSimpleName(); + } + } + + @ApplicationScoped + static class MyServiceMatchingPropertyTransitiveOnSuperclassInheritable + extends InheritableTransitiveMatchingPropertyMyService { + @Override + public String hello() { + return MyServiceMatchingPropertyTransitiveOnSuperclassInheritable.class.getSimpleName(); + } + } + + @ApplicationScoped + static class Producers { + static final String SIMPLE = "Producers.simple"; + static final String MATCHING_PROPERTY_DIRECT = "Producers.matchingPropertyDirect"; + static final String MATCHING_PROPERTY_TRANSITIVE = "Producers.matchingPropertyTransitive"; + + @Produces + @ApplicationScoped + MyService simple() { + return new MyService() { + @Override + public String hello() { + return SIMPLE; + } + }; + } + + @Produces + @ApplicationScoped + @MatchingProperty + MyService matchingPropertyDirect() { + return new MyService() { + @Override + public String hello() { + return MATCHING_PROPERTY_DIRECT; + } + }; + } + + @Produces + @ApplicationScoped + @TransitiveMatchingProperty + MyService matchingPropertyTransitive() { + return new MyService() { + @Override + public String hello() { + return MATCHING_PROPERTY_TRANSITIVE; + } + }; + } + } +} diff --git a/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/wrongannotations/WrongSingletonTest.java b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/wrongannotations/WrongSingletonTest.java index fa23c3038be6b..04bb22f7ad028 100644 --- a/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/wrongannotations/WrongSingletonTest.java +++ b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/wrongannotations/WrongSingletonTest.java @@ -21,11 +21,12 @@ public class WrongSingletonTest { @RegisterExtension static final QuarkusUnitTest config = new QuarkusUnitTest() - .withApplicationRoot((jar) -> jar - .addClasses(EjbSingleton.class)) + .withApplicationRoot(root -> root + .addClasses(EjbSingleton.class, GuiceProducers.class, JavaxSingleton.class)) .assertException(t -> { Throwable rootCause = ExceptionUtil.getRootCause(t); assertTrue(rootCause.getMessage().contains("jakarta.ejb.Singleton"), t.toString()); + assertTrue(rootCause.getMessage().contains("javax.inject.Singleton"), t.toString()); assertTrue(rootCause.getMessage().contains("com.google.inject.Singleton"), t.toString()); }); @@ -55,4 +56,9 @@ List produceEjbSingleton() { } + @javax.inject.Singleton + static class JavaxSingleton { + + } + } diff --git a/extensions/arc/pom.xml b/extensions/arc/pom.xml index 455b95ec65d3a..386df972b8bcb 100644 --- a/extensions/arc/pom.xml +++ b/extensions/arc/pom.xml @@ -16,6 +16,7 @@ deployment runtime + test-supplement diff --git a/extensions/arc/runtime/src/main/java/io/quarkus/arc/properties/IfBuildProperty.java b/extensions/arc/runtime/src/main/java/io/quarkus/arc/properties/IfBuildProperty.java index 850c6e85490e7..9dac62c08ab40 100644 --- a/extensions/arc/runtime/src/main/java/io/quarkus/arc/properties/IfBuildProperty.java +++ b/extensions/arc/runtime/src/main/java/io/quarkus/arc/properties/IfBuildProperty.java @@ -11,7 +11,7 @@ * if the Quarkus build time property matches the provided value. *

    * By default, the bean is not enabled when the build time property is not defined at all, but this behavior is configurable - * via the {#code enableIfMissing} property. + * via the {@code enableIfMissing} property. *

    * This annotation is repeatable. A bean will only be enabled if all the conditions defined by the {@link IfBuildProperty} * annotations are satisfied. diff --git a/extensions/arc/runtime/src/main/java/io/quarkus/arc/properties/UnlessBuildProperty.java b/extensions/arc/runtime/src/main/java/io/quarkus/arc/properties/UnlessBuildProperty.java index 2999c5c1d83ee..6854eeb0ce720 100644 --- a/extensions/arc/runtime/src/main/java/io/quarkus/arc/properties/UnlessBuildProperty.java +++ b/extensions/arc/runtime/src/main/java/io/quarkus/arc/properties/UnlessBuildProperty.java @@ -11,7 +11,7 @@ * if the Quarkus build time property does not match the provided value. *

    * By default, the bean is not enabled when the build time property is not defined at all, but this behavior is configurable - * via the {#code enableIfMissing} property. + * via the {@code enableIfMissing} property. *

    * This annotation is repeatable. A bean will only be enabled if all the conditions defined by the * {@link UnlessBuildProperty} annotations are satisfied. diff --git a/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/ConfigStaticInitCheckInterceptor.java b/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/ConfigStaticInitCheckInterceptor.java index e02621937e7b8..dc06f683503a5 100644 --- a/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/ConfigStaticInitCheckInterceptor.java +++ b/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/ConfigStaticInitCheckInterceptor.java @@ -69,8 +69,7 @@ static void recordConfigValue(InjectionPoint injectionPoint, ConfigStaticInitVal value = getDefaultValue(injectionPoint, configProperty); } if (value == null) { - LOG.debugf("No config value found for %s", propertyName); - return; + LOG.debugf("No config value found for %s - recording value", propertyName); } if (configValues == null) { configValues = Arc.container().instance(ConfigStaticInitValues.class).get(); diff --git a/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/ConfigStaticInitValues.java b/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/ConfigStaticInitValues.java index f464416e7603f..821f458c0a265 100644 --- a/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/ConfigStaticInitValues.java +++ b/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/ConfigStaticInitValues.java @@ -3,6 +3,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Objects; import jakarta.annotation.Priority; import jakarta.enterprise.event.Observes; @@ -43,7 +44,9 @@ void onStart(@Observes @Priority(Integer.MIN_VALUE) StartupEvent event) { List mismatches = new ArrayList<>(); for (InjectedValue injectedValue : injectedValues) { ConfigValue currentValue = config.getConfigValue(injectedValue.name); - if (currentValue.getValue() != null && !injectedValue.value.equals(currentValue.getValue())) { + if (currentValue.getValue() != null + && !Objects.equals(currentValue.getValue(), injectedValue.value)) { + // Config property is set at runtime and the value differs from the value injected during STATIC_INIT bootstrap phase mismatches.add( " - the runtime value of '" + injectedValue.name + "' is [" + currentValue.getValue() + "] but the value [" + injectedValue.value diff --git a/extensions/container-image/container-image-s2i/pom.xml b/extensions/arc/test-supplement/pom.xml similarity index 57% rename from extensions/container-image/container-image-s2i/pom.xml rename to extensions/arc/test-supplement/pom.xml index c7464d927fdbc..38b5974d6a65a 100644 --- a/extensions/container-image/container-image-s2i/pom.xml +++ b/extensions/arc/test-supplement/pom.xml @@ -3,18 +3,14 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> - quarkus-container-image-parent + quarkus-arc-parent io.quarkus 999-SNAPSHOT 4.0.0 - quarkus-container-image-s2i-parent - Quarkus - Container Image - S2I - Parent - pom - - deployment - runtime - + quarkus-arc-test-supplement + Quarkus - ArC - Test Supplement + Supplement archive for ArC tests diff --git a/extensions/arc/test-supplement/src/main/java/io/quarkus/arc/test/supplement/SomeClassInExternalLibrary.java b/extensions/arc/test-supplement/src/main/java/io/quarkus/arc/test/supplement/SomeClassInExternalLibrary.java new file mode 100644 index 0000000000000..cb045ffd57fd1 --- /dev/null +++ b/extensions/arc/test-supplement/src/main/java/io/quarkus/arc/test/supplement/SomeClassInExternalLibrary.java @@ -0,0 +1,4 @@ +package io.quarkus.arc.test.supplement; + +public class SomeClassInExternalLibrary { +} diff --git a/extensions/awt/deployment/src/main/java/io/quarkus/awt/deployment/AwtProcessor.java b/extensions/awt/deployment/src/main/java/io/quarkus/awt/deployment/AwtProcessor.java index a3b4d06c21221..081b8e5c674e0 100644 --- a/extensions/awt/deployment/src/main/java/io/quarkus/awt/deployment/AwtProcessor.java +++ b/extensions/awt/deployment/src/main/java/io/quarkus/awt/deployment/AwtProcessor.java @@ -1,12 +1,15 @@ package io.quarkus.awt.deployment; import static io.quarkus.deployment.builditem.nativeimage.UnsupportedOSBuildItem.Os.WINDOWS; +import static io.quarkus.deployment.pkg.steps.GraalVM.Version.CURRENT; import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.stream.Stream; +import org.jboss.logging.Logger; + import io.quarkus.awt.runtime.graal.DarwinAwtFeature; import io.quarkus.deployment.Feature; import io.quarkus.deployment.annotations.BuildProducer; @@ -26,9 +29,12 @@ import io.quarkus.deployment.pkg.builditem.ProcessInheritIODisabledBuildItem; import io.quarkus.deployment.pkg.steps.GraalVM; import io.quarkus.deployment.pkg.steps.NativeOrNativeSourcesBuild; +import io.quarkus.deployment.pkg.steps.NoopNativeImageBuildRunner; class AwtProcessor { + private static final Logger log = Logger.getLogger(AwtProcessor.class); + @BuildStep FeatureBuildItem feature() { return new FeatureBuildItem(Feature.AWT); @@ -74,6 +80,7 @@ ReflectiveClassBuildItem setupReflectionClasses() { @BuildStep(onlyIf = NativeOrNativeSourcesBuild.class) ReflectiveClassBuildItem setupReflectionClassesWithMethods() { + //@formatter:off return ReflectiveClassBuildItem.builder( "javax.imageio.plugins.tiff.BaselineTIFFTagSet", "javax.imageio.plugins.tiff.ExifGPSTagSet", @@ -91,7 +98,9 @@ ReflectiveClassBuildItem setupReflectionClassesWithMethods() { "sun.java2d.loops.SetDrawRectANY", "sun.java2d.loops.SetFillPathANY", "sun.java2d.loops.SetFillRectANY", - "sun.java2d.loops.SetFillSpansANY").methods().build(); + "sun.java2d.loops.SetFillSpansANY" + ).methods().build(); + //@formatter:on } @BuildStep(onlyIf = NativeOrNativeSourcesBuild.class) @@ -103,7 +112,14 @@ void setupAWTInit(BuildProducer jc, Optional processInheritIODisabledBuildItem) { nativeImageRunnerBuildItem.getBuildRunner() .setup(processInheritIODisabled.isPresent() || processInheritIODisabledBuildItem.isPresent()); - final GraalVM.Version v = nativeImageRunnerBuildItem.getBuildRunner().getGraalVMVersion(); + final GraalVM.Version v; + if (nativeImageRunnerBuildItem.getBuildRunner() instanceof NoopNativeImageBuildRunner) { + v = CURRENT; + log.warnf("native-image is not installed. " + + "Using the default %s version as a reference to build native-sources step.", v.getVersionAsString()); + } else { + v = nativeImageRunnerBuildItem.getBuildRunner().getGraalVMVersion(); + } // Dynamically loading shared objects instead // of baking in static libs: https://github.com/oracle/graal/issues/4921 if (v.compareTo(GraalVM.Version.VERSION_23_0_0) >= 0) { @@ -130,7 +146,12 @@ JniRuntimeAccessBuildItem setupJava2DClasses(NativeImageRunnerBuildItem nativeIm Optional processInheritIODisabledBuildItem) { nativeImageRunnerBuildItem.getBuildRunner() .setup(processInheritIODisabled.isPresent() || processInheritIODisabledBuildItem.isPresent()); - final GraalVM.Version v = nativeImageRunnerBuildItem.getBuildRunner().getGraalVMVersion(); + final GraalVM.Version v; + if (nativeImageRunnerBuildItem.getBuildRunner() instanceof NoopNativeImageBuildRunner) { + v = CURRENT; + } else { + v = nativeImageRunnerBuildItem.getBuildRunner().getGraalVMVersion(); + } final List classes = new ArrayList<>(); classes.add("com.sun.imageio.plugins.jpeg.JPEGImageReader"); classes.add("com.sun.imageio.plugins.jpeg.JPEGImageWriter"); @@ -283,6 +304,7 @@ void runtimeInitializedClasses(BuildProducer * Note that this initialization is not enough if user wants to deserialize actual images * (e.g. from XML). AWT Extension must be loaded for decoding JDK supported image formats. */ + //@formatter:off Stream.of( "com.sun.imageio", "java.awt", @@ -292,5 +314,6 @@ void runtimeInitializedClasses(BuildProducer "sun.java2d") .map(RuntimeInitializedPackageBuildItem::new) .forEach(runtimeInitilizedPackages::produce); + //@formatter:on } } diff --git a/extensions/cache/deployment/src/main/resources/dev-ui/qwc-cache-caches.js b/extensions/cache/deployment/src/main/resources/dev-ui/qwc-cache-caches.js index 5fa92053ea64b..e0374f7855967 100644 --- a/extensions/cache/deployment/src/main/resources/dev-ui/qwc-cache-caches.js +++ b/extensions/cache/deployment/src/main/resources/dev-ui/qwc-cache-caches.js @@ -10,6 +10,7 @@ import '@vaadin/checkbox'; import '@vaadin/grid'; import { columnBodyRenderer } from '@vaadin/grid/lit.js'; import '@vaadin/grid/vaadin-grid-sort-column.js'; +import './qwc-cache-keys.js'; export class QwcCacheCaches extends LitElement { @@ -28,7 +29,13 @@ export class QwcCacheCaches extends LitElement { // Component properties static properties = { - "_caches": {state: true} + "_caches": {state: true}, + _selectedCache: {state: true} + } + + constructor() { + super(); + this._selectedCache = null; } // Components callbacks @@ -52,7 +59,11 @@ export class QwcCacheCaches extends LitElement { */ render() { if (this._caches) { - return this._renderCacheTable(); + if(this._selectedCache){ + return this._renderCacheKeys(); + }else{ + return this._renderCacheTable(); + } } else { return html`Loading caches...`; } @@ -81,11 +92,21 @@ export class QwcCacheCaches extends LitElement { `; } + + _renderCacheKeys(){ + return html``; + } _actionRenderer(cache) { return html` this._clear(cache.name)} class="button"> Clear + +  |  + this._showCacheKeys(cache)} class="button"> + Keys `; } @@ -115,6 +136,14 @@ export class QwcCacheCaches extends LitElement { this.requestUpdate(); } } + + _showCacheKeys(cache){ + this._selectedCache = cache; + } + + _showCacheTable(){ + this._selectedCache = null; + } } customElements.define('qwc-cache-caches', QwcCacheCaches); diff --git a/extensions/cache/deployment/src/main/resources/dev-ui/qwc-cache-keys.js b/extensions/cache/deployment/src/main/resources/dev-ui/qwc-cache-keys.js new file mode 100644 index 0000000000000..6017c452d8ead --- /dev/null +++ b/extensions/cache/deployment/src/main/resources/dev-ui/qwc-cache-keys.js @@ -0,0 +1,81 @@ +import { LitElement, html, css} from 'lit'; +import { JsonRpc } from 'jsonrpc'; +import '@vaadin/icon'; +import '@vaadin/button'; +import '@vaadin/grid'; +import { columnBodyRenderer } from '@vaadin/grid/lit.js'; +import '@vaadin/grid/vaadin-grid-sort-column.js'; + +/** + * This component shows the keys of a specific cache. + */ +export class QwcCacheKeys extends LitElement { + + static styles = css` + .keys { + padding-left: 20px; + justify-content: space-between; + padding-right: 20px; + } + + .keys h4 { + color: var(--lumo-contrast-60pct); + margin-bottom: 0px; + } + `; + + jsonRpc = new JsonRpc("io.quarkus.quarkus-cache"); + + static properties = { + cacheName: {type: String}, + _keys: {state: true}, + _numberOfKeys: {state: true} + }; + + constructor() { + super(); + this.cacheName = null; + this._numberOfKeys = 0; + } + + connectedCallback() { + super.connectedCallback(); + this.jsonRpc.getKeys({name: this.cacheName}).then(jsonRpcResponse => { + this._keys = jsonRpcResponse.result; + this._numberOfKeys = jsonRpcResponse.result.length; + }); + } + + render() { + return html` +

    + + + Back + +

    Found ${this._numberOfKeys} keys in ${this.cacheName}

    + + + + +
    `; + } + + _keyRenderer(cacheKey) { + return html`${cacheKey}`; + } + + _backAction(){ + const back = new CustomEvent("cache-keys-back", { + detail: {}, + bubbles: true, + cancelable: true, + composed: false, + }); + this.dispatchEvent(back); + } + +} +customElements.define('qwc-cache-keys', QwcCacheKeys); \ No newline at end of file diff --git a/extensions/cache/runtime/src/main/java/io/quarkus/cache/runtime/devui/CacheJsonRPCService.java b/extensions/cache/runtime/src/main/java/io/quarkus/cache/runtime/devui/CacheJsonRPCService.java index 59293a3f5e322..d4962c5ec1496 100644 --- a/extensions/cache/runtime/src/main/java/io/quarkus/cache/runtime/devui/CacheJsonRPCService.java +++ b/extensions/cache/runtime/src/main/java/io/quarkus/cache/runtime/devui/CacheJsonRPCService.java @@ -71,4 +71,18 @@ public JsonObject refresh(String name) { } } + public JsonArray getKeys(String name) { + Optional cache = manager.getCache(name); + if (cache.isPresent()) { + CaffeineCache caffeineCache = (CaffeineCache) cache.get(); + JsonArray keys = new JsonArray(); + for (Object key : caffeineCache.keySet()) { + keys.add(key.toString()); + } + return keys; + } else { + return JsonArray.of(); + } + } + } diff --git a/extensions/container-image/container-image-docker/deployment/src/main/java/io/quarkus/container/image/docker/deployment/DockerProcessor.java b/extensions/container-image/container-image-docker/deployment/src/main/java/io/quarkus/container/image/docker/deployment/DockerProcessor.java index 5cf80244940bb..959b6b2618ee9 100644 --- a/extensions/container-image/container-image-docker/deployment/src/main/java/io/quarkus/container/image/docker/deployment/DockerProcessor.java +++ b/extensions/container-image/container-image-docker/deployment/src/main/java/io/quarkus/container/image/docker/deployment/DockerProcessor.java @@ -4,7 +4,7 @@ import static io.quarkus.container.image.deployment.util.EnablementUtil.buildContainerImageNeeded; import static io.quarkus.container.image.deployment.util.EnablementUtil.pushContainerImageNeeded; import static io.quarkus.container.util.PathsUtil.findMainSourcesRoot; -import static io.quarkus.runtime.util.ContainerRuntimeUtil.detectContainerRuntime; +import static io.quarkus.deployment.util.ContainerRuntimeUtil.detectContainerRuntime; import java.nio.file.Files; import java.nio.file.Path; diff --git a/extensions/container-image/container-image-docker/deployment/src/test/java/io/quarkus/container/image/docker/deployment/RedHatOpenJDKRuntimeBaseProviderTest.java b/extensions/container-image/container-image-docker/deployment/src/test/java/io/quarkus/container/image/docker/deployment/RedHatOpenJDKRuntimeBaseProviderTest.java index 6114615670116..bbb55c3e1ec94 100644 --- a/extensions/container-image/container-image-docker/deployment/src/test/java/io/quarkus/container/image/docker/deployment/RedHatOpenJDKRuntimeBaseProviderTest.java +++ b/extensions/container-image/container-image-docker/deployment/src/test/java/io/quarkus/container/image/docker/deployment/RedHatOpenJDKRuntimeBaseProviderTest.java @@ -11,16 +11,6 @@ class RedHatOpenJDKRuntimeBaseProviderTest { private final DockerFileBaseInformationProvider sut = new RedHatOpenJDKRuntimeBaseProvider(); - @Test - void testImageWithJava11() { - Path path = getPath("openjdk-11-runtime"); - var result = sut.determine(path); - assertThat(result).hasValueSatisfying(v -> { - assertThat(v.getBaseImage()).isEqualTo("registry.access.redhat.com/ubi8/openjdk-11-runtime:1.18"); - assertThat(v.getJavaVersion()).isEqualTo(11); - }); - } - @Test void testImageWithJava17() { Path path = getPath("openjdk-17-runtime"); @@ -43,7 +33,7 @@ void testImageWithJava21() { @Test void testUnhandled() { - Path path = getPath("ubi-java11"); + Path path = getPath("ubi-java17"); var result = sut.determine(path); assertThat(result).isEmpty(); } diff --git a/extensions/container-image/container-image-docker/deployment/src/test/java/io/quarkus/container/image/docker/deployment/UbiMinimalBaseProviderTest.java b/extensions/container-image/container-image-docker/deployment/src/test/java/io/quarkus/container/image/docker/deployment/UbiMinimalBaseProviderTest.java index 29c266279ac38..a1b4b9d6747a4 100644 --- a/extensions/container-image/container-image-docker/deployment/src/test/java/io/quarkus/container/image/docker/deployment/UbiMinimalBaseProviderTest.java +++ b/extensions/container-image/container-image-docker/deployment/src/test/java/io/quarkus/container/image/docker/deployment/UbiMinimalBaseProviderTest.java @@ -11,16 +11,6 @@ class UbiMinimalBaseProviderTest { private final DockerFileBaseInformationProvider sut = new UbiMinimalBaseProvider(); - @Test - void testImageWithJava11() { - Path path = getPath("ubi-java11"); - var result = sut.determine(path); - assertThat(result).hasValueSatisfying(v -> { - assertThat(v.getBaseImage()).isEqualTo("registry.access.redhat.com/ubi8/ubi-minimal:8.9"); - assertThat(v.getJavaVersion()).isEqualTo(11); - }); - } - @Test void testImageWithJava17() { Path path = getPath("ubi-java17"); @@ -43,7 +33,7 @@ void testImageWithJava21() { @Test void testUnhandled() { - Path path = getPath("openjdk-11-runtime"); + Path path = getPath("openjdk-17-runtime"); var result = sut.determine(path); assertThat(result).isEmpty(); } diff --git a/extensions/container-image/container-image-docker/deployment/src/test/resources/openjdk-11-runtime b/extensions/container-image/container-image-docker/deployment/src/test/resources/openjdk-11-runtime deleted file mode 100644 index eb3b9e643de4c..0000000000000 --- a/extensions/container-image/container-image-docker/deployment/src/test/resources/openjdk-11-runtime +++ /dev/null @@ -1,17 +0,0 @@ -FROM registry.access.redhat.com/ubi8/openjdk-11-runtime:1.18 - -ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en' - -# Append additional options to the java process, you can add -XshowSettings:vm to also display the heap size. -ENV JAVA_OPTS_APPEND="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager" - -# We make four distinct layers so if there are application changes the library layers can be re-used -COPY --chown=185 target/quarkus-app/lib/ /deployments/lib/ -COPY --chown=185 target/quarkus-app/*.jar /deployments/ -COPY --chown=185 target/quarkus-app/app/ /deployments/app/ -COPY --chown=185 target/quarkus-app/quarkus/ /deployments/quarkus/ - -EXPOSE 8080 -USER 185 - -ENTRYPOINT [ "java", "-jar", "/deployments/quarkus-run.jar" ] diff --git a/extensions/container-image/container-image-docker/deployment/src/test/resources/openjdk-17-runtime b/extensions/container-image/container-image-docker/deployment/src/test/resources/openjdk-17-runtime index 14f5d447dbd36..9bc56f98c9d33 100644 --- a/extensions/container-image/container-image-docker/deployment/src/test/resources/openjdk-17-runtime +++ b/extensions/container-image/container-image-docker/deployment/src/test/resources/openjdk-17-runtime @@ -1,4 +1,3 @@ -# Use Java 17 base image FROM registry.access.redhat.com/ubi8/openjdk-17-runtime:1.18 ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en' diff --git a/extensions/container-image/container-image-docker/deployment/src/test/resources/ubi-java11 b/extensions/container-image/container-image-docker/deployment/src/test/resources/ubi-java11 deleted file mode 100644 index 64397357e0628..0000000000000 --- a/extensions/container-image/container-image-docker/deployment/src/test/resources/ubi-java11 +++ /dev/null @@ -1,31 +0,0 @@ -FROM registry.access.redhat.com/ubi8/ubi-minimal:8.9 - -ARG JAVA_PACKAGE=java-11-openjdk-headless -ARG RUN_JAVA_VERSION=1.3.8 -ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en' -# Install java and the run-java script -# Also set up permissions for user `1001` -RUN microdnf install curl ca-certificates ${JAVA_PACKAGE} \ - && microdnf update \ - && microdnf clean all \ - && mkdir /deployments \ - && chown 1001 /deployments \ - && chmod "g+rwX" /deployments \ - && chown 1001:root /deployments \ - && curl https://repo1.maven.org/maven2/io/fabric8/run-java-sh/${RUN_JAVA_VERSION}/run-java-sh-${RUN_JAVA_VERSION}-sh.sh -o /deployments/run-java.sh \ - && chown 1001 /deployments/run-java.sh \ - && chmod 540 /deployments/run-java.sh \ - && echo "securerandom.source=file:/dev/urandom" >> /etc/alternatives/jre/lib/security/java.security - -# Configure the JAVA_OPTIONS, you can add -XshowSettings:vm to also display the heap size. -ENV JAVA_OPTIONS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager" -# We make four distinct layers so if there are application changes the library layers can be re-used -COPY --chown=1001 target/quarkus-app/lib/ /deployments/lib/ -COPY --chown=1001 target/quarkus-app/*.jar /deployments/ -COPY --chown=1001 target/quarkus-app/app/ /deployments/app/ -COPY --chown=1001 target/quarkus-app/quarkus/ /deployments/quarkus/ - -EXPOSE 8080 -USER 1001 - -ENTRYPOINT [ "/deployments/run-java.sh" ] diff --git a/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/ContainerImageJibConfig.java b/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/ContainerImageJibConfig.java index b79d0303741fe..8ff58ffc172a7 100644 --- a/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/ContainerImageJibConfig.java +++ b/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/ContainerImageJibConfig.java @@ -18,10 +18,7 @@ public class ContainerImageJibConfig { * * When the application is built against Java 21 or higher, {@code registry.access.redhat.com/ubi8/openjdk-21-runtime:1.18} * is used as the default. - * When the application is built against Java 17 or higher (but less than 21), - * {@code registry.access.redhat.com/ubi8/openjdk-17-runtime:1.18} - * is used as the default. - * Otherwise {@code registry.access.redhat.com/ubi8/openjdk-11-runtime:1.18} is used as the default. + * Otherwise {@code registry.access.redhat.com/ubi8/openjdk-17-runtime:1.18} is used as the default. */ @ConfigItem public Optional baseJvmImage; diff --git a/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/JibProcessor.java b/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/JibProcessor.java index c3cb77ddc4a16..aadd239c1c265 100644 --- a/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/JibProcessor.java +++ b/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/JibProcessor.java @@ -80,9 +80,9 @@ import io.quarkus.deployment.pkg.builditem.UpxCompressedBuildItem; import io.quarkus.deployment.pkg.steps.JarResultBuildStep; import io.quarkus.deployment.pkg.steps.NativeBuild; +import io.quarkus.deployment.util.ContainerRuntimeUtil; import io.quarkus.fs.util.ZipUtils; import io.quarkus.maven.dependency.ResolvedDependency; -import io.quarkus.runtime.util.ContainerRuntimeUtil; public class JibProcessor { @@ -92,9 +92,16 @@ public class JibProcessor { private static final IsClassPredicate IS_CLASS_PREDICATE = new IsClassPredicate(); private static final String BINARY_NAME_IN_CONTAINER = "application"; - private static final String JAVA_21_BASE_IMAGE = "registry.access.redhat.com/ubi8/openjdk-21-runtime:1.18"; - private static final String JAVA_17_BASE_IMAGE = "registry.access.redhat.com/ubi8/openjdk-17-runtime:1.18"; - private static final String JAVA_11_BASE_IMAGE = "registry.access.redhat.com/ubi8/openjdk-11-runtime:1.18"; + private static final String UBI8_PREFIX = "registry.access.redhat.com/ubi8"; + private static final String OPENJDK_PREFIX = "openjdk"; + private static final String RUNTIME_SUFFIX = "runtime"; + + private static final String JAVA_21_BASE_IMAGE = String.format("%s/%s-21-%s:1.18", UBI8_PREFIX, OPENJDK_PREFIX, + RUNTIME_SUFFIX); + private static final String JAVA_17_BASE_IMAGE = String.format("%s/%s-17-%s:1.18", UBI8_PREFIX, OPENJDK_PREFIX, + RUNTIME_SUFFIX); + + private static final String RUN_JAVA_PATH = "/opt/jboss/container/java/run/run-java.sh"; private static final String DEFAULT_BASE_IMAGE_USER = "185"; @@ -139,10 +146,7 @@ private String determineBaseJvmImage(ContainerImageJibConfig jibConfig, Compiled if (javaVersion.isJava21OrHigher() == CompiledJavaVersionBuildItem.JavaVersion.Status.TRUE) { return JAVA_21_BASE_IMAGE; } - if (javaVersion.isJava17OrHigher() == CompiledJavaVersionBuildItem.JavaVersion.Status.TRUE) { - return JAVA_17_BASE_IMAGE; - } - return JAVA_11_BASE_IMAGE; + return JAVA_17_BASE_IMAGE; } @BuildStep(onlyIf = { IsNormal.class, JibBuild.class }, onlyIfNot = NativeBuild.class) @@ -429,17 +433,25 @@ private JibContainerBuilder createContainerBuilderFromFastJar(String baseJvmImag Path appLibDir = componentsPath.resolve(JarResultBuildStep.LIB).resolve(JarResultBuildStep.MAIN); AbsoluteUnixPath workDirInContainer = AbsoluteUnixPath.get(jibConfig.workingDirectory); + Map envVars = createEnvironmentVariables(jibConfig); List entrypoint; if (jibConfig.jvmEntrypoint.isPresent()) { - entrypoint = jibConfig.jvmEntrypoint.get(); + entrypoint = Collections.unmodifiableList(jibConfig.jvmEntrypoint.get()); + } else if (containsRunJava(baseJvmImage) && appCDSResult.isEmpty()) { + // we want to use run-java.sh by default. However, if AppCDS are being used, run-java.sh cannot be used because it would lead to using different JVM args + // which would mean AppCDS would not be taken into account at all + entrypoint = List.of(RUN_JAVA_PATH); + envVars.put("JAVA_APP_JAR", workDirInContainer + "/" + JarResultBuildStep.QUARKUS_RUN_JAR); + envVars.put("JAVA_OPTS_APPEND", String.join(" ", determineEffectiveJvmArguments(jibConfig, appCDSResult))); } else { List effectiveJvmArguments = determineEffectiveJvmArguments(jibConfig, appCDSResult); - entrypoint = new ArrayList<>(3 + effectiveJvmArguments.size()); - entrypoint.add("java"); - entrypoint.addAll(effectiveJvmArguments); - entrypoint.add("-jar"); - entrypoint.add(JarResultBuildStep.QUARKUS_RUN_JAR); + List argsList = new ArrayList<>(3 + effectiveJvmArguments.size()); + argsList.add("java"); + argsList.addAll(effectiveJvmArguments); + argsList.add("-jar"); + argsList.add(JarResultBuildStep.QUARKUS_RUN_JAR); + entrypoint = Collections.unmodifiableList(argsList); } List fastChangingLibs = new ArrayList<>(); @@ -612,7 +624,7 @@ private JibContainerBuilder createContainerBuilderFromFastJar(String baseJvmImag jibContainerBuilder .setWorkingDirectory(workDirInContainer) .setEntrypoint(entrypoint) - .setEnvironment(getEnvironmentVariables(jibConfig)) + .setEnvironment(envVars) .setLabels(allLabels(jibConfig, containerImageConfig, containerImageLabels)); mayInheritEntrypoint(jibContainerBuilder, entrypoint, jibConfig.jvmArguments); @@ -634,6 +646,12 @@ private JibContainerBuilder createContainerBuilderFromFastJar(String baseJvmImag } } + // TODO: this needs to be a lot more sophisticated + private boolean containsRunJava(String baseJvmImage) { + return baseJvmImage.startsWith(UBI8_PREFIX) && baseJvmImage.contains(OPENJDK_PREFIX) + && baseJvmImage.contains(RUNTIME_SUFFIX); + } + public JibContainerBuilder addLayer(JibContainerBuilder jibContainerBuilder, List files, AbsoluteUnixPath pathInContainer, String name, boolean isMutableJar, Instant now) @@ -722,7 +740,7 @@ private JibContainerBuilder createContainerBuilderFromLegacyJar(String baseJvmIm } JibContainerBuilder jibContainerBuilder = javaContainerBuilder.toContainerBuilder() - .setEnvironment(getEnvironmentVariables(jibConfig)) + .setEnvironment(createEnvironmentVariables(jibConfig)) .setLabels(allLabels(jibConfig, containerImageConfig, containerImageLabels)); if (jibConfig.useCurrentTimestamp) { @@ -766,7 +784,7 @@ private JibContainerBuilder createContainerBuilderFromNative(ContainerImageJibCo .build()) .setWorkingDirectory(workDirInContainer) .setEntrypoint(entrypoint) - .setEnvironment(getEnvironmentVariables(jibConfig)) + .setEnvironment(createEnvironmentVariables(jibConfig)) .setLabels(allLabels(jibConfig, containerImageConfig, containerImageLabels)); mayInheritEntrypoint(jibContainerBuilder, entrypoint, jibConfig.nativeArguments.orElse(null)); @@ -784,7 +802,7 @@ private JibContainerBuilder createContainerBuilderFromNative(ContainerImageJibCo } } - private Map getEnvironmentVariables(ContainerImageJibConfig jibConfig) { + private Map createEnvironmentVariables(ContainerImageJibConfig jibConfig) { Map original = jibConfig.environmentVariables; if (original.isEmpty()) { return original; diff --git a/extensions/container-image/container-image-openshift/deployment/src/main/java/io/quarkus/container/image/openshift/deployment/ContainerImageOpenshiftConfig.java b/extensions/container-image/container-image-openshift/deployment/src/main/java/io/quarkus/container/image/openshift/deployment/ContainerImageOpenshiftConfig.java index f049d4a6692fe..c8ad868835b6d 100644 --- a/extensions/container-image/container-image-openshift/deployment/src/main/java/io/quarkus/container/image/openshift/deployment/ContainerImageOpenshiftConfig.java +++ b/extensions/container-image/container-image-openshift/deployment/src/main/java/io/quarkus/container/image/openshift/deployment/ContainerImageOpenshiftConfig.java @@ -15,7 +15,6 @@ @ConfigRoot(name = "openshift", phase = ConfigPhase.BUILD_TIME) public class ContainerImageOpenshiftConfig { - public static final String DEFAULT_BASE_JVM_JDK11_IMAGE = "registry.access.redhat.com/ubi8/openjdk-11:1.18"; public static final String DEFAULT_BASE_JVM_JDK17_IMAGE = "registry.access.redhat.com/ubi8/openjdk-17:1.18"; public static final String DEFAULT_BASE_JVM_JDK21_IMAGE = "registry.access.redhat.com/ubi8/openjdk-21:1.18"; @@ -33,11 +32,8 @@ public class ContainerImageOpenshiftConfig { public static String getDefaultJvmImage(CompiledJavaVersionBuildItem.JavaVersion version) { if (version.isJava21OrHigher() == CompiledJavaVersionBuildItem.JavaVersion.Status.TRUE) { return DEFAULT_BASE_JVM_JDK21_IMAGE; - } else if (version.isJava17OrHigher() == CompiledJavaVersionBuildItem.JavaVersion.Status.TRUE) { - return DEFAULT_BASE_JVM_JDK17_IMAGE; - } else { - return DEFAULT_BASE_JVM_JDK11_IMAGE; } + return DEFAULT_BASE_JVM_JDK17_IMAGE; } /** @@ -53,9 +49,7 @@ public static String getDefaultJvmImage(CompiledJavaVersionBuildItem.JavaVersion * instead. * When the application is built against Java 21 or higher, {@code registry.access.redhat.com/ubi8/openjdk-21:1.18} * is used as the default. - * When the application is built against Java [17, 20], {@code registry.access.redhat.com/ubi8/openjdk-17:1.18} - * is used as the default. - * Otherwise {@code registry.access.redhat.com/ubi8/openjdk-11:1.18} is used as the default. + * Otherwise {@code registry.access.redhat.com/ubi8/openjdk-17:1.18} is used as the default. */ @ConfigItem public Optional baseJvmImage; diff --git a/extensions/container-image/container-image-openshift/deployment/src/main/java/io/quarkus/container/image/openshift/deployment/OpenshiftBaseJavaImage.java b/extensions/container-image/container-image-openshift/deployment/src/main/java/io/quarkus/container/image/openshift/deployment/OpenshiftBaseJavaImage.java deleted file mode 100644 index 4cee5990eb667..0000000000000 --- a/extensions/container-image/container-image-openshift/deployment/src/main/java/io/quarkus/container/image/openshift/deployment/OpenshiftBaseJavaImage.java +++ /dev/null @@ -1,86 +0,0 @@ - -package io.quarkus.container.image.openshift.deployment; - -import java.util.Optional; - -import io.quarkus.container.image.deployment.util.ImageUtil; - -public enum OpenshiftBaseJavaImage { - - //We only compare `repositories` so registries and tags are stripped - FABRIC8("fabric8/s2i-java:latest", "/deployments", "JAVA_MAIN_CLASS", "JAVA_APP_JAR", "JAVA_LIB_DIR", "JAVA_CLASSPATH", - "JAVA_OPTIONS"), - OPENJDK_8_RHEL7("redhat-openjdk-18/openjdk18-openshift:latest", "/deployments", "JAVA_MAIN_CLASS", "JAVA_APP_JAR", - "JAVA_LIB_DIR", "JAVA_CLASSPATH", "JAVA_OPTIONS"), - OPENJDK_8_RHEL8("openjdk/openjdk-8-rhel8:latest", "/deployments", "JAVA_MAIN_CLASS", "JAVA_APP_JAR", "JAVA_LIB_DIR", - "JAVA_CLASSPATH", "JAVA_OPTIONS"), - OPENJDK_11_RHEL7("openjdk/openjdk-11-rhel7:latest", "/deployments", "JAVA_MAIN_CLASS", "JAVA_APP_JAR", "JAVA_LIB_DIR", - "JAVA_CLASSPATH", "JAVA_OPTIONS"), - OPENJDK_11_RHEL8("openjdk/openjdk-11-rhel8:latest", "/deployments", "JAVA_MAIN_CLASS", "JAVA_APP_JAR", "JAVA_LIB_DIR", - "JAVA_CLASSPATH", "JAVA_OPTIONS"), - OPENJ9_8_RHEL7("openj9/openj9-8-rhel7:latest", "/deployments", "JAVA_MAIN_CLASS", "JAVA_APP_JAR", "JAVA_LIB_DIR", - "JAVA_CLASSPATH", "JAVA_OPTIONS"), - OPENJ9_8_RHEL8("openj9/openj9-8-rhel8:latest", "/deployments", "JAVA_MAIN_CLASS", "JAVA_APP_JAR", "JAVA_LIB_DIR", - "JAVA_CLASSPATH", "JAVA_OPTIONS"), - OPENJ9_11_RHEL7("openj9/openj9-11-rhel7:latest", "/deployments", "JAVA_MAIN_CLASS", "JAVA_APP_JAR", "JAVA_LIB_DIR", - "JAVA_CLASSPATH", "JAVA_OPTIONS"), - OPENJ9_11_RHEL8("openj9/openj9-11-rhel8:latest", "/deployments", "JAVA_MAIN_CLASS", "JAVA_APP_JAR", "JAVA_LIB_DIR", - "JAVA_CLASSPATH", "JAVA_OPTIONS"); - - private final String image; - private final String jarDirectory; - private final String javaMainClassEnvVar; - private final String jarEnvVar; - private final String jarLibEnvVar; - private final String classpathEnvVar; - private final String jvmOptionsEnvVar; - - public static Optional findMatching(String image) { - for (OpenshiftBaseJavaImage candidate : OpenshiftBaseJavaImage.values()) { - if (ImageUtil.getRepository(candidate.getImage()).equals(ImageUtil.getRepository(image))) { - return Optional.of(candidate); - } - } - return Optional.empty(); - } - - private OpenshiftBaseJavaImage(String image, String jarDirectory, String javaMainClassEnvVar, String jarEnvVar, - String jarLibEnvVar, String classpathEnvVar, String jvmOptionsEnvVar) { - this.image = image; - this.jarDirectory = jarDirectory; - this.javaMainClassEnvVar = javaMainClassEnvVar; - this.jarEnvVar = jarEnvVar; - this.jarLibEnvVar = jarLibEnvVar; - this.classpathEnvVar = classpathEnvVar; - this.jvmOptionsEnvVar = jvmOptionsEnvVar; - } - - public String getImage() { - return image; - } - - public String getJarDirectory() { - return this.jarDirectory; - } - - public String getJavaMainClassEnvVar() { - return javaMainClassEnvVar; - } - - public String getJvmOptionsEnvVar() { - return jvmOptionsEnvVar; - } - - public String getClasspathEnvVar() { - return classpathEnvVar; - } - - public String getJarLibEnvVar() { - return jarLibEnvVar; - } - - public String getJarEnvVar() { - return jarEnvVar; - } - -} diff --git a/extensions/container-image/container-image-openshift/deployment/src/main/java/io/quarkus/container/image/openshift/deployment/OpenshiftBaseNativeImage.java b/extensions/container-image/container-image-openshift/deployment/src/main/java/io/quarkus/container/image/openshift/deployment/OpenshiftBaseNativeImage.java deleted file mode 100644 index b1f143efac5c6..0000000000000 --- a/extensions/container-image/container-image-openshift/deployment/src/main/java/io/quarkus/container/image/openshift/deployment/OpenshiftBaseNativeImage.java +++ /dev/null @@ -1,57 +0,0 @@ - -package io.quarkus.container.image.openshift.deployment; - -import java.util.Optional; - -import io.quarkus.container.image.deployment.util.ImageUtil; - -public enum OpenshiftBaseNativeImage { - - //We only compare `repositories` so registries and tags are stripped - QUARKUS("quarkus/ubi-quarkus-native-binary-s2i:2.0", "/home/quarkus/", "application", "QUARKUS_HOME", "QUARKUS_OPTS"); - - private final String image; - private final String nativeBinaryDirectory; - private final String fixedNativeBinaryName; - private final String homeDirEnvVar; - private final String optsEnvVar; - - public static Optional findMatching(String image) { - for (OpenshiftBaseNativeImage candidate : OpenshiftBaseNativeImage.values()) { - if (ImageUtil.getRepository(candidate.getImage()).equals(ImageUtil.getRepository(image))) { - return Optional.of(candidate); - } - } - return Optional.empty(); - } - - private OpenshiftBaseNativeImage(String image, String nativeBinaryDirectory, String fixedNativeBinaryName, - String homeDirEnvVar, String optsEnvVar) { - this.image = image; - this.nativeBinaryDirectory = nativeBinaryDirectory; - this.fixedNativeBinaryName = fixedNativeBinaryName; - this.homeDirEnvVar = homeDirEnvVar; - this.optsEnvVar = optsEnvVar; - } - - public String getImage() { - return image; - } - - public String getNativeBinaryDirectory() { - return nativeBinaryDirectory; - } - - public String getFixedNativeBinaryName() { - return this.fixedNativeBinaryName; - } - - public String getHomeDirEnvVar() { - return homeDirEnvVar; - } - - public String getOptsEnvVar() { - return optsEnvVar; - } - -} diff --git a/extensions/container-image/container-image-openshift/deployment/src/main/java/io/quarkus/container/image/openshift/deployment/OpenshiftProcessor.java b/extensions/container-image/container-image-openshift/deployment/src/main/java/io/quarkus/container/image/openshift/deployment/OpenshiftProcessor.java index 7354e21cab4e2..f445a45abb515 100644 --- a/extensions/container-image/container-image-openshift/deployment/src/main/java/io/quarkus/container/image/openshift/deployment/OpenshiftProcessor.java +++ b/extensions/container-image/container-image-openshift/deployment/src/main/java/io/quarkus/container/image/openshift/deployment/OpenshiftProcessor.java @@ -146,24 +146,14 @@ public void openshiftRequirementsJvm(ContainerImageOpenshiftConfig openshiftConf boolean hasCustomJvmArguments = config.jvmArguments.isPresent(); builderImageProducer.produce(new BaseImageInfoBuildItem(baseJvmImage)); - Optional baseImage = OpenshiftBaseJavaImage.findMatching(baseJvmImage); if (config.buildStrategy == BuildStrategy.BINARY) { // Jar directory priorities: // 1. explicitly specified by the user. - // 2. detected via OpenshiftBaseJavaImage // 3. fallback value - String jarDirectory = config.jarDirectory - .orElse(baseImage.map(i -> i.getJarDirectory()).orElse(config.FALLBACK_JAR_DIRECTORY)); + String jarDirectory = config.jarDirectory.orElse(config.FALLBACK_JAR_DIRECTORY); String pathToJar = concatUnixPaths(jarDirectory, jarFileName); - // If the image is known, we can define env vars for classpath, jar, lib etc. - baseImage.ifPresent(b -> { - envProducer.produce(KubernetesEnvBuildItem.createSimpleVar(b.getJarEnvVar(), pathToJar, null)); - envProducer.produce(KubernetesEnvBuildItem.createSimpleVar(b.getJvmOptionsEnvVar(), - String.join(" ", config.getEffectiveJvmArguments()), null)); - }); - //In all other cases its the responsibility of the image to set those up correctly. if (hasCustomJarPath || hasCustomJvmArguments) { List cmd = new ArrayList<>(); @@ -172,8 +162,6 @@ public void openshiftRequirementsJvm(ContainerImageOpenshiftConfig openshiftConf cmd.addAll(Arrays.asList("-jar", pathToJar)); envProducer.produce(KubernetesEnvBuildItem.createSimpleVar(JAVA_APP_JAR, pathToJar, null)); commandProducer.produce(KubernetesCommandBuildItem.command(cmd)); - } else if (baseImage.isEmpty()) { - envProducer.produce(KubernetesEnvBuildItem.createSimpleVar(JAVA_APP_JAR, pathToJar, null)); } } } @@ -209,26 +197,12 @@ public void openshiftRequirementsNative(ContainerImageOpenshiftConfig openshiftC if (config.buildStrategy == BuildStrategy.BINARY) { builderImageProducer.produce(new BaseImageInfoBuildItem(config.baseNativeImage)); - Optional baseImage = OpenshiftBaseNativeImage.findMatching(config.baseNativeImage); // Native binary directory priorities: // 1. explicitly specified by the user. - // 2. detected via OpenshiftBaseNativeImage - // 3. fallback value + // 2. fallback vale - String nativeBinaryDirectory = config.nativeBinaryDirectory - .orElse(baseImage.map(i -> i.getNativeBinaryDirectory()).orElse(config.FALLBACK_NATIVE_BINARY_DIRECTORY)); + String nativeBinaryDirectory = config.nativeBinaryDirectory.orElse(config.FALLBACK_NATIVE_BINARY_DIRECTORY); String pathToNativeBinary = concatUnixPaths(nativeBinaryDirectory, nativeBinaryFileName); - - baseImage.ifPresent(b -> { - envProducer.produce( - KubernetesEnvBuildItem.createSimpleVar(b.getHomeDirEnvVar(), nativeBinaryDirectory, OPENSHIFT)); - config.nativeArguments.ifPresent(nativeArguments -> { - envProducer.produce(KubernetesEnvBuildItem.createSimpleVar(b.getOptsEnvVar(), - String.join(" ", nativeArguments), OPENSHIFT)); - }); - - }); - if (hasCustomNativePath || hasCustomNativeArguments) { commandProducer .produce(KubernetesCommandBuildItem.commandWithArgs(pathToNativeBinary, config.nativeArguments.get())); diff --git a/extensions/container-image/container-image-openshift/deployment/src/main/java/io/quarkus/container/image/openshift/deployment/S2iBaseJavaImage.java b/extensions/container-image/container-image-openshift/deployment/src/main/java/io/quarkus/container/image/openshift/deployment/S2iBaseJavaImage.java index 138292aaf28a8..5512d7725407c 100644 --- a/extensions/container-image/container-image-openshift/deployment/src/main/java/io/quarkus/container/image/openshift/deployment/S2iBaseJavaImage.java +++ b/extensions/container-image/container-image-openshift/deployment/src/main/java/io/quarkus/container/image/openshift/deployment/S2iBaseJavaImage.java @@ -8,23 +8,7 @@ public enum S2iBaseJavaImage { //We only compare `repositories` so registries and tags are stripped - FABRIC8("fabric8/s2i-java:latest", "JAVA_MAIN_CLASS", "JAVA_APP_JAR", "JAVA_LIB_DIR", "JAVA_CLASSPATH", "JAVA_OPTIONS"), - OPENJDK_8_RHEL7("redhat-openjdk-18/openjdk18-openshift:latest", "JAVA_MAIN_CLASS", "JAVA_APP_JAR", "JAVA_LIB_DIR", - "JAVA_CLASSPATH", "JAVA_OPTIONS"), - OPENJDK_8_RHEL8("openjdk/openjdk-8-rhel8:latest", "JAVA_MAIN_CLASS", "JAVA_APP_JAR", "JAVA_LIB_DIR", "JAVA_CLASSPATH", - "JAVA_OPTIONS"), - OPENJDK_11_RHEL7("openjdk/openjdk-11-rhel7:latest", "JAVA_MAIN_CLASS", "JAVA_APP_JAR", "JAVA_LIB_DIR", "JAVA_CLASSPATH", - "JAVA_OPTIONS"), - OPENJDK_11_RHEL8("openjdk/openjdk-11-rhel8:latest", "JAVA_MAIN_CLASS", "JAVA_APP_JAR", "JAVA_LIB_DIR", "JAVA_CLASSPATH", - "JAVA_OPTIONS"), - OPENJ9_8_RHEL7("openj9/openj9-8-rhel7:latest", "JAVA_MAIN_CLASS", "JAVA_APP_JAR", "JAVA_LIB_DIR", "JAVA_CLASSPATH", - "JAVA_OPTIONS"), - OPENJ9_8_RHEL8("openj9/openj9-8-rhel8:latest", "JAVA_MAIN_CLASS", "JAVA_APP_JAR", "JAVA_LIB_DIR", "JAVA_CLASSPATH", - "JAVA_OPTIONS"), - OPENJ9_11_RHEL7("openj9/openj9-11-rhel7:latest", "JAVA_MAIN_CLASS", "JAVA_APP_JAR", "JAVA_LIB_DIR", "JAVA_CLASSPATH", - "JAVA_OPTIONS"), - OPENJ9_11_RHEL8("openj9/openj9-11-rhel8:latest", "JAVA_MAIN_CLASS", "JAVA_APP_JAR", "JAVA_LIB_DIR", "JAVA_CLASSPATH", - "JAVA_OPTIONS"); + FABRIC8("fabric8/s2i-java:latest", "JAVA_MAIN_CLASS", "JAVA_APP_JAR", "JAVA_LIB_DIR", "JAVA_CLASSPATH", "JAVA_OPTIONS"); private final String image; private final String javaMainClassEnvVar; diff --git a/extensions/container-image/container-image-openshift/deployment/src/main/java/io/quarkus/container/image/openshift/deployment/S2iConfig.java b/extensions/container-image/container-image-openshift/deployment/src/main/java/io/quarkus/container/image/openshift/deployment/S2iConfig.java index 0c88e16cba29e..defde810dc8a7 100644 --- a/extensions/container-image/container-image-openshift/deployment/src/main/java/io/quarkus/container/image/openshift/deployment/S2iConfig.java +++ b/extensions/container-image/container-image-openshift/deployment/src/main/java/io/quarkus/container/image/openshift/deployment/S2iConfig.java @@ -12,8 +12,8 @@ @ConfigRoot(phase = ConfigPhase.BUILD_TIME) public class S2iConfig { - public static final String DEFAULT_BASE_JVM_JDK11_IMAGE = "registry.access.redhat.com/ubi8/openjdk-11:1.17"; - public static final String DEFAULT_BASE_JVM_JDK17_IMAGE = "registry.access.redhat.com/ubi8/openjdk-17:1.17"; + public static final String DEFAULT_BASE_JVM_JDK17_IMAGE = "registry.access.redhat.com/ubi8/openjdk-17:1.18"; + public static final String DEFAULT_BASE_JVM_JDK21_IMAGE = "registry.access.redhat.com/ubi8/openjdk-21:1.18"; public static final String DEFAULT_BASE_NATIVE_IMAGE = "quay.io/quarkus/ubi-quarkus-native-binary-s2i:2.0"; public static final String DEFAULT_NATIVE_TARGET_FILENAME = "application"; @@ -24,11 +24,11 @@ public class S2iConfig { public static final String FALLBACK_NATIVE_BINARY_DIRECTORY = "/home/quarkus/"; public static String getDefaultJvmImage(CompiledJavaVersionBuildItem.JavaVersion version) { - switch (version.isJava17OrHigher()) { + switch (version.isJava21OrHigher()) { case TRUE: - return DEFAULT_BASE_JVM_JDK17_IMAGE; + return DEFAULT_BASE_JVM_JDK21_IMAGE; default: - return DEFAULT_BASE_JVM_JDK11_IMAGE; + return DEFAULT_BASE_JVM_JDK17_IMAGE; } } @@ -41,9 +41,9 @@ public static String getDefaultJvmImage(CompiledJavaVersionBuildItem.JavaVersion /** * The base image to be used when a container image is being produced for the jar build. * - * When the application is built against Java 17 or higher, {@code registry.access.redhat.com/ubi8/openjdk-17:1.18} + * When the application is built against Java 21 or higher, {@code registry.access.redhat.com/ubi8/openjdk-21:1.18} * is used as the default. - * Otherwise {@code registry.access.redhat.com/ubi8/openjdk-11:1.18} is used as the default. + * Otherwise {@code registry.access.redhat.com/ubi8/openjdk-17:1.18} is used as the default. */ @ConfigItem public Optional baseJvmImage; diff --git a/extensions/container-image/container-image-s2i/deployment/pom.xml b/extensions/container-image/container-image-s2i/deployment/pom.xml deleted file mode 100644 index 70621164b7f18..0000000000000 --- a/extensions/container-image/container-image-s2i/deployment/pom.xml +++ /dev/null @@ -1,85 +0,0 @@ - - - 4.0.0 - - - io.quarkus - quarkus-container-image-s2i-parent - 999-SNAPSHOT - - - quarkus-container-image-s2i-deployment - Quarkus - Container Image - S2I - Deployment - - - - io.quarkus - quarkus-kubernetes-client-internal-deployment - - - io.quarkus - quarkus-kubernetes-client-spi - - - io.quarkus - quarkus-kubernetes-spi - - - io.quarkus - quarkus-container-image-s2i - - - io.quarkus - quarkus-container-image-deployment - - - org.jboss.logging - commons-logging-jboss-logging - - - io.dekorate - openshift-annotations - noapt - - - io.sundr - * - - - com.sun - tools - - - io.fabric8 - kubernetes-client - - - - - - io.quarkus - quarkus-junit5-internal - test - - - - - - - maven-compiler-plugin - - - - io.quarkus - quarkus-extension-processor - ${project.version} - - - - - - - - diff --git a/extensions/container-image/container-image-s2i/deployment/src/main/java/io/quarkus/container/image/s2i/RemoveEnvVarDecorator.java b/extensions/container-image/container-image-s2i/deployment/src/main/java/io/quarkus/container/image/s2i/RemoveEnvVarDecorator.java deleted file mode 100644 index cd16d3c53f451..0000000000000 --- a/extensions/container-image/container-image-s2i/deployment/src/main/java/io/quarkus/container/image/s2i/RemoveEnvVarDecorator.java +++ /dev/null @@ -1,58 +0,0 @@ -package io.quarkus.container.image.s2i; - -import io.dekorate.kubernetes.decorator.AddEnvVarDecorator; -import io.dekorate.kubernetes.decorator.ApplicationContainerDecorator; -import io.dekorate.kubernetes.decorator.Decorator; -import io.dekorate.kubernetes.decorator.ResourceProvidingDecorator; -import io.fabric8.kubernetes.api.model.ContainerFluent; - -public class RemoveEnvVarDecorator extends ApplicationContainerDecorator> { - - private final String envVarName; - - public RemoveEnvVarDecorator(String envVarName) { - this(ANY, envVarName); - } - - public RemoveEnvVarDecorator(String name, String envVarName) { - super(name); - this.envVarName = envVarName; - } - - public void andThenVisit(ContainerFluent container) { - container.removeMatchingFromEnv(e -> e.getName().equals(envVarName)); - } - - public String getEnvVarKey() { - return this.envVarName; - } - - public Class[] after() { - return new Class[] { ResourceProvidingDecorator.class, AddEnvVarDecorator.class }; - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + ((envVarName == null) ? 0 : envVarName.hashCode()); - return result; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (getClass() != obj.getClass()) - return false; - RemoveEnvVarDecorator other = (RemoveEnvVarDecorator) obj; - if (envVarName == null) { - if (other.envVarName != null) - return false; - } else if (!envVarName.equals(other.envVarName)) - return false; - return true; - } -} diff --git a/extensions/container-image/container-image-s2i/deployment/src/main/java/io/quarkus/container/image/s2i/deployment/ApplyDockerfileToBuildConfigDecorator.java b/extensions/container-image/container-image-s2i/deployment/src/main/java/io/quarkus/container/image/s2i/deployment/ApplyDockerfileToBuildConfigDecorator.java deleted file mode 100644 index aadde3e3166f2..0000000000000 --- a/extensions/container-image/container-image-s2i/deployment/src/main/java/io/quarkus/container/image/s2i/deployment/ApplyDockerfileToBuildConfigDecorator.java +++ /dev/null @@ -1,37 +0,0 @@ - -package io.quarkus.container.image.s2i.deployment; - -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.nio.file.Path; - -import io.dekorate.kubernetes.decorator.NamedResourceDecorator; -import io.fabric8.kubernetes.api.model.ObjectMeta; -import io.fabric8.openshift.api.model.BuildConfigSpecFluent; -import io.quarkus.deployment.util.FileUtil; - -public class ApplyDockerfileToBuildConfigDecorator extends NamedResourceDecorator> { - - private final Path pathToDockerfile; - - public ApplyDockerfileToBuildConfigDecorator(String name, Path pathToDockerfile) { - super(name); - this.pathToDockerfile = pathToDockerfile; - } - - @Override - public void andThenVisit(final BuildConfigSpecFluent spec, ObjectMeta meta) { - try (InputStream is = new FileInputStream(pathToDockerfile.toFile())) { - spec.withNewSource() - .withDockerfile(new String(FileUtil.readFileContents(is))) - .endSource() - .withNewStrategy() - .withNewDockerStrategy() - .endDockerStrategy() - .endStrategy(); - } catch (IOException e) { - throw new RuntimeException(e); - } - } -} diff --git a/extensions/container-image/container-image-s2i/deployment/src/main/java/io/quarkus/container/image/s2i/deployment/BuildStrategy.java b/extensions/container-image/container-image-s2i/deployment/src/main/java/io/quarkus/container/image/s2i/deployment/BuildStrategy.java deleted file mode 100644 index 5d5c5ed0a368e..0000000000000 --- a/extensions/container-image/container-image-s2i/deployment/src/main/java/io/quarkus/container/image/s2i/deployment/BuildStrategy.java +++ /dev/null @@ -1,9 +0,0 @@ - -package io.quarkus.container.image.s2i.deployment; - -public enum BuildStrategy { - - BINARY, - DOCKER; - -} diff --git a/extensions/container-image/container-image-s2i/deployment/src/main/java/io/quarkus/container/image/s2i/deployment/ContainerImageS2iConfig.java b/extensions/container-image/container-image-s2i/deployment/src/main/java/io/quarkus/container/image/s2i/deployment/ContainerImageS2iConfig.java deleted file mode 100644 index 10e9f127f17b8..0000000000000 --- a/extensions/container-image/container-image-s2i/deployment/src/main/java/io/quarkus/container/image/s2i/deployment/ContainerImageS2iConfig.java +++ /dev/null @@ -1,123 +0,0 @@ -package io.quarkus.container.image.s2i.deployment; - -import java.time.Duration; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; - -import io.quarkus.deployment.pkg.builditem.CompiledJavaVersionBuildItem; -import io.quarkus.runtime.annotations.ConfigItem; -import io.quarkus.runtime.annotations.ConfigPhase; -import io.quarkus.runtime.annotations.ConfigRoot; - -@ConfigRoot(name = "s2i", phase = ConfigPhase.BUILD_TIME) -public class ContainerImageS2iConfig { - - public static final String DEFAULT_BASE_JVM_JDK11_IMAGE = "registry.access.redhat.com/ubi8/openjdk-11"; - public static final String DEFAULT_BASE_JVM_JDK17_IMAGE = "registry.access.redhat.com/ubi8/openjdk-17"; - public static final String DEFAULT_BASE_JVM_JDK21_IMAGE = "registry.access.redhat.com/ubi8/openjdk-21"; - public static final String DEFAULT_BASE_NATIVE_IMAGE = "quay.io/quarkus/ubi-quarkus-native-binary-s2i:2.0"; - public static final String DEFAULT_NATIVE_TARGET_FILENAME = "application"; - - public static String getDefaultJvmImage(CompiledJavaVersionBuildItem.JavaVersion version) { - if (version.isJava21OrHigher() == CompiledJavaVersionBuildItem.JavaVersion.Status.TRUE) { - return DEFAULT_BASE_JVM_JDK21_IMAGE; - } else if (version.isJava17OrHigher() == CompiledJavaVersionBuildItem.JavaVersion.Status.TRUE) { - return DEFAULT_BASE_JVM_JDK17_IMAGE; - } else { - return DEFAULT_BASE_JVM_JDK11_IMAGE; - } - } - - /** - * The base image to be used when a container image is being produced for the jar build - */ - @ConfigItem - public Optional baseJvmImage; - - /** - * The base image to be used when a container image is being produced for the native binary build - */ - @ConfigItem(defaultValue = DEFAULT_BASE_NATIVE_IMAGE) - public String baseNativeImage; - - /** - * The JVM arguments to pass to the JVM when starting the application - */ - @ConfigItem(defaultValue = "-Djava.util.logging.manager=org.jboss.logmanager.LogManager") - public List jvmArguments; - - /** - * Additional JVM arguments to pass to the JVM when starting the application - */ - @ConfigItem - public Optional> jvmAdditionalArguments; - - /** - * Additional arguments to pass when starting the native application - */ - @ConfigItem - public Optional> nativeArguments; - - /** - * The directory where the jar is added during the assemble phase. - * This is dependent on the S2I image and should be supplied if a non default image is used. - */ - @ConfigItem(defaultValue = "/deployments/") - public String jarDirectory; - - /** - * The resulting filename of the jar in the S2I image. - * This option may be used if the selected S2I image uses a fixed name for the jar. - */ - @ConfigItem - public Optional jarFileName; - - /** - * The directory where the native binary is added during the assemble phase. - * This is dependent on the S2I image and should be supplied if a non-default image is used. - */ - @ConfigItem(defaultValue = "/home/quarkus/") - public String nativeBinaryDirectory; - - /** - * The resulting filename of the native binary in the S2I image. - * This option may be used if the selected S2I image uses a fixed name for the native binary. - */ - @ConfigItem - public Optional nativeBinaryFileName; - - /** - * The build timeout. - */ - @ConfigItem(defaultValue = "PT5M") - Duration buildTimeout; - - /** - * Check if baseJvmImage is the default - * - * @returns true if baseJvmImage is the default - */ - public boolean hasDefaultBaseJvmImage() { - return baseJvmImage.isPresent(); - } - - /** - * Check if baseNativeImage is the default - * - * @returns true if baseNativeImage is the default - */ - public boolean hasDefaultBaseNativeImage() { - return baseNativeImage.equals(DEFAULT_BASE_NATIVE_IMAGE); - } - - /** - * @return the effective JVM arguments to use by getting the jvmArguments and the jvmAdditionalArguments properties. - */ - public List getEffectiveJvmArguments() { - List effectiveJvmArguments = new ArrayList<>(jvmArguments); - jvmAdditionalArguments.ifPresent(effectiveJvmArguments::addAll); - return effectiveJvmArguments; - } - -} diff --git a/extensions/container-image/container-image-s2i/deployment/src/main/java/io/quarkus/container/image/s2i/deployment/PackageUtil.java b/extensions/container-image/container-image-s2i/deployment/src/main/java/io/quarkus/container/image/s2i/deployment/PackageUtil.java deleted file mode 100644 index 4dda1ae61b569..0000000000000 --- a/extensions/container-image/container-image-s2i/deployment/src/main/java/io/quarkus/container/image/s2i/deployment/PackageUtil.java +++ /dev/null @@ -1,216 +0,0 @@ - -package io.quarkus.container.image.s2i.deployment; - -import java.io.BufferedOutputStream; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; -import java.nio.file.FileVisitResult; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.SimpleFileVisitor; -import java.nio.file.attribute.BasicFileAttributes; -import java.util.Arrays; -import java.util.Set; -import java.util.stream.Collectors; - -import org.apache.commons.compress.archivers.tar.TarArchiveEntry; -import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream; - -import io.dekorate.DekorateException; - -public class PackageUtil { - - private static final String DEFAULT_DOCKERFILE = "Dockerfile"; - private static final String DOCKER_IGNORE = ".dockerignore"; - - protected static final String DEFAULT_TEMP_DIR = System.getProperty("java.io.tmpdir", "/tmp"); - protected static final String DOCKER_PREFIX = "docker-"; - protected static final String BZIP2_SUFFIX = ".tar.bzip2"; - - private static final Charset UTF_8 = StandardCharsets.UTF_8; - - public static File packageFile(String path) { - return packageFile(path, null); - } - - public static File packageFile(String path, String base) { - try { - final Path root = Paths.get(path).getParent(); - File tempFile = Files.createTempFile(Paths.get(DEFAULT_TEMP_DIR), DOCKER_PREFIX, BZIP2_SUFFIX).toFile(); - try (final TarArchiveOutputStream tout = buildTarStream(tempFile)) { - Files.walkFileTree(root, new SimpleFileVisitor() { - @Override - public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) { - return FileVisitResult.CONTINUE; - } - - @Override - public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { - String absolutePath = file.toAbsolutePath().toString(); - if (!path.equals(absolutePath)) { - return FileVisitResult.CONTINUE; - } - final Path relativePath = root.relativize(file); - final boolean hasBasePath = base != null && !base.isBlank(); - final TarArchiveEntry entry = hasBasePath - ? new TarArchiveEntry(base + File.separator + file.toFile()) - : new TarArchiveEntry(file.toFile()); - - entry.setName(hasBasePath ? base + File.separator + relativePath.toString() : relativePath.toString()); - if (file.toFile().canExecute()) { - entry.setMode(entry.getMode() | 0755); - } - entry.setSize(attrs.size()); - putTarEntry(tout, entry, file); - return FileVisitResult.CONTINUE; - } - }); - tout.flush(); - } - return tempFile; - - } catch (IOException e) { - throw DekorateException.launderThrowable(e); - } - } - - public static File packageFile(Path root, Path... additional) { - return packageFile(root, null, additional); - } - - public static File packageFile(Path root, String base, Path... additional) { - try { - final Set includes = Arrays - .stream(additional) - .map(p -> p.toAbsolutePath().toString()) - .collect(Collectors.toSet()); - - File tempFile = Files.createTempFile(Paths.get(DEFAULT_TEMP_DIR), DOCKER_PREFIX, BZIP2_SUFFIX).toFile(); - try (final TarArchiveOutputStream tout = buildTarStream(tempFile)) { - Files.walkFileTree(root, new SimpleFileVisitor() { - @Override - public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) { - return FileVisitResult.CONTINUE; - } - - @Override - public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { - String absolutePath = file.toAbsolutePath().toString(); - if (!shouldInclude(absolutePath, includes)) { - return FileVisitResult.CONTINUE; - } - final Path relativePath = root.relativize(file); - final boolean hasBasePath = base != null && !base.isBlank(); - final TarArchiveEntry entry = hasBasePath - ? new TarArchiveEntry(base + File.separator + file.toFile()) - : new TarArchiveEntry(file.toFile()); - - entry.setName(hasBasePath ? base + File.separator + relativePath.toString() : relativePath.toString()); - if (file.toFile().canExecute()) { - entry.setMode(entry.getMode() | 0755); - } - entry.setSize(attrs.size()); - putTarEntry(tout, entry, file); - return FileVisitResult.CONTINUE; - } - }); - tout.flush(); - } - return tempFile; - - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - public static void putTarEntry(TarArchiveOutputStream tarArchiveOutputStream, TarArchiveEntry tarArchiveEntry, - Path inputPath) throws IOException { - tarArchiveEntry.setSize(Files.size(inputPath)); - tarArchiveOutputStream.putArchiveEntry(tarArchiveEntry); - Files.copy(inputPath, tarArchiveOutputStream); - tarArchiveOutputStream.closeArchiveEntry(); - } - - public static TarArchiveOutputStream buildTarStream(File outputPath) throws IOException { - FileOutputStream fout = new FileOutputStream(outputPath); - BufferedOutputStream bout = new BufferedOutputStream(fout); - //BZip2CompressorOutputStream bzout = new BZip2CompressorOutputStream(bout); - TarArchiveOutputStream stream = new TarArchiveOutputStream(bout); - stream.setLongFileMode(TarArchiveOutputStream.LONGFILE_POSIX); - return stream; - } - - public static void tar(Path inputPath, Path outputPath) throws IOException { - if (!Files.exists(inputPath)) { - throw new FileNotFoundException("File not found " + inputPath); - } - - try (TarArchiveOutputStream tarArchiveOutputStream = buildTarStream(outputPath.toFile())) { - if (!Files.isDirectory(inputPath)) { - TarArchiveEntry tarEntry = new TarArchiveEntry(inputPath.toFile().getName()); - if (inputPath.toFile().canExecute()) { - tarEntry.setMode(tarEntry.getMode() | 0755); - } - putTarEntry(tarArchiveOutputStream, tarEntry, inputPath); - } else { - Files.walkFileTree(inputPath, - new TarDirWalker(inputPath, tarArchiveOutputStream)); - } - tarArchiveOutputStream.flush(); - } - } - - private static boolean shouldInclude(String candidate, String path) { - return candidate.equals(path) || candidate.startsWith(path); - } - - private static boolean shouldInclude(String candidate, Set paths) { - for (String path : paths) { - if (shouldInclude(candidate, path)) { - return true; - } - } - return false; - } - - public static class TarDirWalker extends SimpleFileVisitor { - private Path basePath; - private TarArchiveOutputStream tarArchiveOutputStream; - - public TarDirWalker(Path basePath, TarArchiveOutputStream tarArchiveOutputStream) { - this.basePath = basePath; - this.tarArchiveOutputStream = tarArchiveOutputStream; - } - - @Override - public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { - if (!dir.equals(basePath)) { - tarArchiveOutputStream.putArchiveEntry(new TarArchiveEntry(basePath.relativize(dir).toFile())); - tarArchiveOutputStream.closeArchiveEntry(); - } - return FileVisitResult.CONTINUE; - } - - @Override - public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { - TarArchiveEntry tarEntry = new TarArchiveEntry(basePath.relativize(file).toFile()); - tarEntry.setSize(attrs.size()); - if (file.toFile().canExecute()) { - tarEntry.setMode(tarEntry.getMode() | 0755); - } - putTarEntry(tarArchiveOutputStream, tarEntry, file); - return FileVisitResult.CONTINUE; - } - - @Override - public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException { - tarArchiveOutputStream.close(); - throw exc; - } - } -} diff --git a/extensions/container-image/container-image-s2i/deployment/src/main/java/io/quarkus/container/image/s2i/deployment/S2iBaseJavaImage.java b/extensions/container-image/container-image-s2i/deployment/src/main/java/io/quarkus/container/image/s2i/deployment/S2iBaseJavaImage.java deleted file mode 100644 index 06dbbab34a0af..0000000000000 --- a/extensions/container-image/container-image-s2i/deployment/src/main/java/io/quarkus/container/image/s2i/deployment/S2iBaseJavaImage.java +++ /dev/null @@ -1,79 +0,0 @@ - -package io.quarkus.container.image.s2i.deployment; - -import java.util.Optional; - -import io.quarkus.container.image.deployment.util.ImageUtil; - -public enum S2iBaseJavaImage { - - //We only compare `repositories` so registries and tags are stripped - FABRIC8("fabric8/s2i-java:latest", "JAVA_MAIN_CLASS", "JAVA_APP_JAR", "JAVA_LIB_DIR", "JAVA_CLASSPATH", "JAVA_OPTIONS"), - OPENJDK_8_RHEL7("redhat-openjdk-18/openjdk18-openshift:latest", "JAVA_MAIN_CLASS", "JAVA_APP_JAR", "JAVA_LIB_DIR", - "JAVA_CLASSPATH", "JAVA_OPTIONS"), - OPENJDK_8_RHEL8("openjdk/openjdk-8-rhel8:latest", "JAVA_MAIN_CLASS", "JAVA_APP_JAR", "JAVA_LIB_DIR", "JAVA_CLASSPATH", - "JAVA_OPTIONS"), - OPENJDK_11_RHEL7("openjdk/openjdk-11-rhel7:latest", "JAVA_MAIN_CLASS", "JAVA_APP_JAR", "JAVA_LIB_DIR", "JAVA_CLASSPATH", - "JAVA_OPTIONS"), - OPENJDK_11_RHEL8("openjdk/openjdk-11-rhel8:latest", "JAVA_MAIN_CLASS", "JAVA_APP_JAR", "JAVA_LIB_DIR", "JAVA_CLASSPATH", - "JAVA_OPTIONS"), - OPENJ9_8_RHEL7("openj9/openj9-8-rhel7:latest", "JAVA_MAIN_CLASS", "JAVA_APP_JAR", "JAVA_LIB_DIR", "JAVA_CLASSPATH", - "JAVA_OPTIONS"), - OPENJ9_8_RHEL8("openj9/openj9-8-rhel8:latest", "JAVA_MAIN_CLASS", "JAVA_APP_JAR", "JAVA_LIB_DIR", "JAVA_CLASSPATH", - "JAVA_OPTIONS"), - OPENJ9_11_RHEL7("openj9/openj9-11-rhel7:latest", "JAVA_MAIN_CLASS", "JAVA_APP_JAR", "JAVA_LIB_DIR", "JAVA_CLASSPATH", - "JAVA_OPTIONS"), - OPENJ9_11_RHEL8("openj9/openj9-11-rhel8:latest", "JAVA_MAIN_CLASS", "JAVA_APP_JAR", "JAVA_LIB_DIR", "JAVA_CLASSPATH", - "JAVA_OPTIONS"); - - private final String image; - private final String javaMainClassEnvVar; - private final String jarEnvVar; - private final String jarLibEnvVar; - private final String classpathEnvVar; - private final String jvmOptionsEnvVar; - - public static Optional findMatching(String image) { - for (S2iBaseJavaImage candidate : S2iBaseJavaImage.values()) { - if (ImageUtil.getRepository(candidate.getImage()).equals(ImageUtil.getRepository(image))) { - return Optional.of(candidate); - } - } - return Optional.empty(); - } - - private S2iBaseJavaImage(String image, String javaMainClassEnvVar, String jarEnvVar, String jarLibEnvVar, - String classpathEnvVar, String jvmOptionsEnvVar) { - this.image = image; - this.javaMainClassEnvVar = javaMainClassEnvVar; - this.jarEnvVar = jarEnvVar; - this.jarLibEnvVar = jarLibEnvVar; - this.classpathEnvVar = classpathEnvVar; - this.jvmOptionsEnvVar = jvmOptionsEnvVar; - } - - public String getImage() { - return image; - } - - public String getJavaMainClassEnvVar() { - return javaMainClassEnvVar; - } - - public String getJvmOptionsEnvVar() { - return jvmOptionsEnvVar; - } - - public String getClasspathEnvVar() { - return classpathEnvVar; - } - - public String getJarLibEnvVar() { - return jarLibEnvVar; - } - - public String getJarEnvVar() { - return jarEnvVar; - } - -} diff --git a/extensions/container-image/container-image-s2i/deployment/src/main/java/io/quarkus/container/image/s2i/deployment/S2iBaseNativeImage.java b/extensions/container-image/container-image-s2i/deployment/src/main/java/io/quarkus/container/image/s2i/deployment/S2iBaseNativeImage.java deleted file mode 100644 index afa83e1963937..0000000000000 --- a/extensions/container-image/container-image-s2i/deployment/src/main/java/io/quarkus/container/image/s2i/deployment/S2iBaseNativeImage.java +++ /dev/null @@ -1,49 +0,0 @@ - -package io.quarkus.container.image.s2i.deployment; - -import java.util.Optional; - -import io.quarkus.container.image.deployment.util.ImageUtil; - -public enum S2iBaseNativeImage { - - //We only compare `repositories` so registries and tags are stripped - QUARKUS("quarkus/ubi-quarkus-native-binary-s2i:2.0", "application", "QUARKUS_HOME", "QUARKUS_OPTS"); - - private final String image; - private final String fixedNativeBinaryName; - private final String homeDirEnvVar; - private final String optsEnvVar; - - public static Optional findMatching(String image) { - for (S2iBaseNativeImage candidate : S2iBaseNativeImage.values()) { - if (ImageUtil.getRepository(candidate.getImage()).equals(ImageUtil.getRepository(image))) { - return Optional.of(candidate); - } - } - return Optional.empty(); - } - - private S2iBaseNativeImage(String image, String fixedNativeBinaryName, String homeDirEnvVar, String optsEnvVar) { - this.image = image; - this.fixedNativeBinaryName = fixedNativeBinaryName; - this.homeDirEnvVar = homeDirEnvVar; - this.optsEnvVar = optsEnvVar; - } - - public String getImage() { - return image; - } - - public String getFixedNativeBinaryName() { - return this.fixedNativeBinaryName; - } - - public String getHomeDirEnvVar() { - return homeDirEnvVar; - } - - public String getOptsEnvVar() { - return optsEnvVar; - } -} diff --git a/extensions/container-image/container-image-s2i/deployment/src/main/java/io/quarkus/container/image/s2i/deployment/S2iBuild.java b/extensions/container-image/container-image-s2i/deployment/src/main/java/io/quarkus/container/image/s2i/deployment/S2iBuild.java deleted file mode 100644 index 7ac85ef0ad4e1..0000000000000 --- a/extensions/container-image/container-image-s2i/deployment/src/main/java/io/quarkus/container/image/s2i/deployment/S2iBuild.java +++ /dev/null @@ -1,19 +0,0 @@ -package io.quarkus.container.image.s2i.deployment; - -import java.util.function.BooleanSupplier; - -import io.quarkus.container.image.deployment.ContainerImageConfig; - -public class S2iBuild implements BooleanSupplier { - - private final ContainerImageConfig containerImageConfig; - - S2iBuild(ContainerImageConfig containerImageConfig) { - this.containerImageConfig = containerImageConfig; - } - - @Override - public boolean getAsBoolean() { - return containerImageConfig.builder.map(b -> b.equals(S2iProcessor.S2I)).orElse(true); - } -} diff --git a/extensions/container-image/container-image-s2i/deployment/src/main/java/io/quarkus/container/image/s2i/deployment/S2iProcessor.java b/extensions/container-image/container-image-s2i/deployment/src/main/java/io/quarkus/container/image/s2i/deployment/S2iProcessor.java deleted file mode 100644 index 8d93544dfc221..0000000000000 --- a/extensions/container-image/container-image-s2i/deployment/src/main/java/io/quarkus/container/image/s2i/deployment/S2iProcessor.java +++ /dev/null @@ -1,453 +0,0 @@ -package io.quarkus.container.image.s2i.deployment; - -import java.io.BufferedReader; -import java.io.ByteArrayInputStream; -import java.io.Closeable; -import java.io.File; -import java.io.IOException; -import java.io.InputStreamReader; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.StandardCopyOption; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; -import java.util.function.Predicate; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import org.jboss.logging.Logger; - -import io.dekorate.utils.Clients; -import io.dekorate.utils.Serialization; -import io.fabric8.kubernetes.api.model.HasMetadata; -import io.fabric8.kubernetes.api.model.KubernetesList; -import io.fabric8.kubernetes.api.model.Secret; -import io.fabric8.kubernetes.client.Config; -import io.fabric8.kubernetes.client.KubernetesClient; -import io.fabric8.kubernetes.client.KubernetesClientException; -import io.fabric8.kubernetes.client.dsl.LogWatch; -import io.fabric8.openshift.api.model.Build; -import io.fabric8.openshift.api.model.BuildConfig; -import io.fabric8.openshift.api.model.ImageStream; -import io.fabric8.openshift.client.OpenShiftClient; -import io.quarkus.container.image.deployment.ContainerImageConfig; -import io.quarkus.container.image.deployment.util.ImageUtil; -import io.quarkus.container.spi.AvailableContainerImageExtensionBuildItem; -import io.quarkus.container.spi.BaseImageInfoBuildItem; -import io.quarkus.container.spi.ContainerImageBuildRequestBuildItem; -import io.quarkus.container.spi.ContainerImageBuilderBuildItem; -import io.quarkus.container.spi.ContainerImageInfoBuildItem; -import io.quarkus.container.spi.ContainerImagePushRequestBuildItem; -import io.quarkus.deployment.IsNormalNotRemoteDev; -import io.quarkus.deployment.annotations.BuildProducer; -import io.quarkus.deployment.annotations.BuildStep; -import io.quarkus.deployment.builditem.ArchiveRootBuildItem; -import io.quarkus.deployment.builditem.GeneratedFileSystemResourceBuildItem; -import io.quarkus.deployment.pkg.PackageConfig; -import io.quarkus.deployment.pkg.builditem.ArtifactResultBuildItem; -import io.quarkus.deployment.pkg.builditem.CompiledJavaVersionBuildItem; -import io.quarkus.deployment.pkg.builditem.CurateOutcomeBuildItem; -import io.quarkus.deployment.pkg.builditem.JarBuildItem; -import io.quarkus.deployment.pkg.builditem.NativeImageBuildItem; -import io.quarkus.deployment.pkg.builditem.OutputTargetBuildItem; -import io.quarkus.deployment.pkg.steps.NativeBuild; -import io.quarkus.kubernetes.client.deployment.KubernetesClientErrorHandler; -import io.quarkus.kubernetes.client.spi.KubernetesClientBuildItem; -import io.quarkus.kubernetes.spi.KubernetesCommandBuildItem; -import io.quarkus.kubernetes.spi.KubernetesEnvBuildItem; -import io.quarkus.maven.dependency.ResolvedDependency; - -public class S2iProcessor { - - public static final String S2I = "s2i"; - private static final String OPENSHIFT = "openshift"; - private static final String BUILD_CONFIG_NAME = "openshift.io/build-config.name"; - private static final String RUNNING = "Running"; - - private static final Logger LOG = Logger.getLogger(S2iProcessor.class); - - @BuildStep - public AvailableContainerImageExtensionBuildItem availability() { - return new AvailableContainerImageExtensionBuildItem(S2I); - } - - @BuildStep(onlyIf = { IsNormalNotRemoteDev.class, S2iBuild.class }, onlyIfNot = NativeBuild.class) - public void s2iRequirementsJvm(ContainerImageS2iConfig s2iConfig, - CurateOutcomeBuildItem curateOutcomeBuildItem, - OutputTargetBuildItem out, - PackageConfig packageConfig, - JarBuildItem jarBuildItem, - CompiledJavaVersionBuildItem compiledJavaVersion, - BuildProducer envProducer, - BuildProducer builderImageProducer, - BuildProducer commandProducer) { - - final Collection appDeps = curateOutcomeBuildItem.getApplicationModel() - .getRuntimeDependencies(); - String outputJarFileName = jarBuildItem.getPath().getFileName().toString(); - String classpath = appDeps.stream() - .map(d -> d.getGroupId() + "." - + d.getResolvedPaths().getSinglePath().getFileName()) - .map(s -> concatUnixPaths(s2iConfig.jarDirectory, "lib", s)) - .collect(Collectors.joining(":")); - - String jarFileName = s2iConfig.jarFileName.orElse(outputJarFileName); - String jarDirectory = s2iConfig.jarDirectory; - String pathToJar = concatUnixPaths(jarDirectory, jarFileName); - String baseJvmImage = s2iConfig.baseJvmImage - .orElse(ContainerImageS2iConfig.getDefaultJvmImage(compiledJavaVersion.getJavaVersion())); - - builderImageProducer.produce(new BaseImageInfoBuildItem(baseJvmImage)); - Optional baseImage = S2iBaseJavaImage.findMatching(baseJvmImage); - - baseImage.ifPresent(b -> { - envProducer.produce(KubernetesEnvBuildItem.createSimpleVar(b.getJarEnvVar(), pathToJar, OPENSHIFT)); - envProducer.produce(KubernetesEnvBuildItem.createSimpleVar(b.getJarLibEnvVar(), - concatUnixPaths(jarDirectory, "lib"), OPENSHIFT)); - envProducer.produce(KubernetesEnvBuildItem.createSimpleVar(b.getClasspathEnvVar(), classpath, OPENSHIFT)); - envProducer.produce(KubernetesEnvBuildItem.createSimpleVar(b.getJvmOptionsEnvVar(), - String.join(" ", s2iConfig.getEffectiveJvmArguments()), OPENSHIFT)); - }); - - if (!baseImage.isPresent()) { - List cmd = new ArrayList<>(); - cmd.add("java"); - cmd.addAll(s2iConfig.getEffectiveJvmArguments()); - cmd.addAll(Arrays.asList("-jar", pathToJar, "-cp", classpath)); - commandProducer.produce(KubernetesCommandBuildItem.command(cmd)); - } - } - - @BuildStep(onlyIf = { IsNormalNotRemoteDev.class, S2iBuild.class, NativeBuild.class }) - public void s2iRequirementsNative(ContainerImageS2iConfig s2iConfig, - CurateOutcomeBuildItem curateOutcomeBuildItem, - OutputTargetBuildItem out, - PackageConfig packageConfig, - NativeImageBuildItem nativeImage, - BuildProducer envProducer, - BuildProducer builderImageProducer, - BuildProducer commandProducer) { - - boolean usingDefaultBuilder = ImageUtil.getRepository(ContainerImageS2iConfig.DEFAULT_BASE_NATIVE_IMAGE) - .equals(ImageUtil.getRepository(s2iConfig.baseNativeImage)); - String outputNativeBinaryFileName = nativeImage.getPath().getFileName().toString(); - - String nativeBinaryFileName = null; - - //The default s2i builder for native builds, renames the native binary. - //To make things easier for the user, we need to handle it. - if (usingDefaultBuilder && !s2iConfig.nativeBinaryFileName.isPresent()) { - nativeBinaryFileName = ContainerImageS2iConfig.DEFAULT_NATIVE_TARGET_FILENAME; - } else { - nativeBinaryFileName = s2iConfig.nativeBinaryFileName.orElse(outputNativeBinaryFileName); - } - - String pathToNativeBinary = concatUnixPaths(s2iConfig.nativeBinaryDirectory, nativeBinaryFileName); - - builderImageProducer.produce(new BaseImageInfoBuildItem(s2iConfig.baseNativeImage)); - Optional baseImage = S2iBaseNativeImage.findMatching(s2iConfig.baseNativeImage); - List nativeArguments = s2iConfig.nativeArguments.orElse(Collections.emptyList()); - baseImage.ifPresent(b -> { - envProducer.produce( - KubernetesEnvBuildItem.createSimpleVar(b.getHomeDirEnvVar(), s2iConfig.nativeBinaryDirectory, OPENSHIFT)); - envProducer.produce( - KubernetesEnvBuildItem.createSimpleVar(b.getOptsEnvVar(), String.join(" ", nativeArguments), - OPENSHIFT)); - }); - - if (!baseImage.isPresent()) { - commandProducer.produce(KubernetesCommandBuildItem.commandWithArgs(pathToNativeBinary, nativeArguments)); - } - } - - @BuildStep(onlyIf = { IsNormalNotRemoteDev.class, S2iBuild.class }, onlyIfNot = NativeBuild.class) - public void s2iBuildFromJar(ContainerImageS2iConfig s2iConfig, ContainerImageConfig containerImageConfig, - KubernetesClientBuildItem kubernetesClientBuilder, - ContainerImageInfoBuildItem containerImage, - ArchiveRootBuildItem archiveRoot, OutputTargetBuildItem out, PackageConfig packageConfig, - List generatedResources, - Optional buildRequest, - Optional pushRequest, - BuildProducer artifactResultProducer, - BuildProducer containerImageBuilder, - // used to ensure that the jar has been built - JarBuildItem jar) { - - if (containerImageConfig.isBuildExplicitlyDisabled()) { - return; - } - - if (!containerImageConfig.isBuildExplicitlyEnabled() && !containerImageConfig.isPushExplicitlyEnabled() - && !buildRequest.isPresent() && !pushRequest.isPresent()) { - return; - } - - Optional openshiftYml = generatedResources - .stream() - .filter(r -> r.getName().endsWith(File.separator + "openshift.yml")) - .findFirst(); - - if (openshiftYml.isEmpty()) { - LOG.warn( - "No Openshift manifests were generated so no s2i process will be taking place"); - return; - } - try (KubernetesClient kubernetesClient = kubernetesClientBuilder.buildClient()) { - String namespace = Optional.ofNullable(kubernetesClient.getNamespace()).orElse("default"); - LOG.info("Performing s2i binary build with jar on server: " + kubernetesClient.getMasterUrl() - + " in namespace:" + namespace + "."); - - createContainerImage(kubernetesClient, openshiftYml.get(), s2iConfig, out.getOutputDirectory(), jar.getPath(), - out.getOutputDirectory().resolve("lib")); - artifactResultProducer.produce(new ArtifactResultBuildItem(null, "jar-container", Collections.emptyMap())); - containerImageBuilder.produce(new ContainerImageBuilderBuildItem(S2I)); - } - } - - @BuildStep(onlyIf = { IsNormalNotRemoteDev.class, S2iBuild.class, NativeBuild.class }) - public void s2iBuildFromNative(ContainerImageS2iConfig s2iConfig, ContainerImageConfig containerImageConfig, - KubernetesClientBuildItem kubernetesClientBuilder, - ContainerImageInfoBuildItem containerImage, - ArchiveRootBuildItem archiveRoot, OutputTargetBuildItem out, PackageConfig packageConfig, - List generatedResources, - Optional buildRequest, - Optional pushRequest, - BuildProducer artifactResultProducer, - BuildProducer containerImageBuilder, - NativeImageBuildItem nativeImage) { - - if (containerImageConfig.isBuildExplicitlyDisabled()) { - return; - } - - if (!containerImageConfig.isBuildExplicitlyEnabled() && !containerImageConfig.isPushExplicitlyEnabled() - && !buildRequest.isPresent() && !pushRequest.isPresent()) { - return; - } - - try (KubernetesClient kubernetesClient = kubernetesClientBuilder.buildClient()) { - String namespace = Optional.ofNullable(kubernetesClient.getNamespace()).orElse("default"); - LOG.info("Performing s2i binary build with native image on server: " + kubernetesClient.getMasterUrl() - + " in namespace:" + namespace + "."); - - Optional openshiftYml = generatedResources - .stream() - .filter(r -> r.getName().endsWith(File.separator + "openshift.yml")) - .findFirst(); - - if (openshiftYml.isEmpty()) { - LOG.warn( - "No Openshift manifests were generated so no s2i process will be taking place"); - return; - } - - createContainerImage(kubernetesClient, openshiftYml.get(), s2iConfig, out.getOutputDirectory(), - nativeImage.getPath()); - artifactResultProducer.produce(new ArtifactResultBuildItem(null, "native-container", Collections.emptyMap())); - containerImageBuilder.produce(new ContainerImageBuilderBuildItem(S2I)); - } - } - - public static void createContainerImage(KubernetesClient kubernetesClient, - GeneratedFileSystemResourceBuildItem openshiftManifests, - ContainerImageS2iConfig s2iConfig, - Path output, - Path... additional) { - - File tar; - try { - File original = PackageUtil.packageFile(output, additional); - //Let's rename the archive and give it a more descriptive name, as it may appear in the logs. - tar = Files.createTempFile("quarkus-", "-s2i").toFile(); - Files.move(original.toPath(), tar.toPath(), StandardCopyOption.REPLACE_EXISTING); - } catch (Exception e) { - throw new RuntimeException("Error creating the s2i binary build archive.", e); - } - - Config config = kubernetesClient.getConfiguration(); - //Let's disable http2 as it causes issues with duplicate build triggers. - config.setHttp2Disable(true); - try (KubernetesClient client = Clients.fromConfig(config)) { - OpenShiftClient openShiftClient = client.adapt(OpenShiftClient.class); - KubernetesList kubernetesList = Serialization - .unmarshalAsList(new ByteArrayInputStream(openshiftManifests.getData())); - - List buildResources = kubernetesList.getItems().stream() - .filter(i -> i instanceof BuildConfig || i instanceof ImageStream || i instanceof Secret) - .collect(Collectors.toList()); - - applyS2iResources(openShiftClient, buildResources); - s2iBuild(openShiftClient, buildResources, tar, s2iConfig); - } - } - - /** - * Apply the s2i resources and wait until ImageStreamTags are created. - * - * @param client the client instance - * @param buildResources resources to apply - */ - private static void applyS2iResources(OpenShiftClient client, List buildResources) { - // Apply build resource requirements - try { - for (HasMetadata i : distinct(buildResources)) { - if (i instanceof BuildConfig) { - client.resource(i).cascading(true).delete(); - try { - client.resource(i).waitUntilCondition(d -> d == null, 10, TimeUnit.SECONDS); - } catch (IllegalArgumentException e) { - // We should ignore that, as its expected to be thrown when item is actually - // deleted. - } - } else if (i instanceof ImageStream) { - ImageStream is = (ImageStream) i; - ImageStream existing = client.imageStreams().withName(i.getMetadata().getName()).get(); - if (existing != null && - existing.getSpec() != null && - existing.getSpec().getDockerImageRepository() != null && - existing.getSpec().getDockerImageRepository().equals(is.getSpec().getDockerImageRepository())) { - LOG.info("Found: " + i.getKind() + " " + i.getMetadata().getName() + " repository: " - + existing.getSpec().getDockerImageRepository()); - continue; - } - } - client.resource(i).createOrReplace(); - LOG.info("Applied: " + i.getKind() + " " + i.getMetadata().getName()); - } - S2iUtils.waitForImageStreamTags(client, buildResources, 2, TimeUnit.MINUTES); - - } catch (KubernetesClientException e) { - KubernetesClientErrorHandler.handle(e); - } - } - - private static void s2iBuild(OpenShiftClient client, List buildResources, File binaryFile, - ContainerImageS2iConfig s2iConfig) { - distinct(buildResources).stream().filter(i -> i instanceof BuildConfig).map(i -> (BuildConfig) i) - .forEach(bc -> s2iBuild(client, bc, binaryFile, s2iConfig)); - } - - /** - * Performs the binary build of the specified {@link BuildConfig} with the given - * binary input. - * - * @param client The openshift client instance - * @param buildConfig The build config - * @param binaryFile The binary file - * @param s2iConfig The s2i configuration - */ - private static void s2iBuild(OpenShiftClient client, BuildConfig buildConfig, File binaryFile, - ContainerImageS2iConfig s2iConfig) { - Build build; - try { - build = client.buildConfigs().withName(buildConfig.getMetadata().getName()) - .instantiateBinary() - .withTimeoutInMillis(s2iConfig.buildTimeout.toMillis()) - .fromFile(binaryFile); - } catch (Exception e) { - Optional running = runningBuildsOf(client, buildConfig).findFirst(); - if (running.isPresent()) { - LOG.warn("An exception: '" + e.getMessage() - + " ' occurred while instantiating the build, however the build has been started."); - build = running.get(); - } else { - throw s2iException(e); - } - } - - final String buildName = build.getMetadata().getName(); - try (LogWatch w = client.builds().withName(build.getMetadata().getName()).withPrettyOutput().watchLog(); - BufferedReader reader = new BufferedReader(new InputStreamReader(w.getOutput()))) { - waitForBuildComplete(client, s2iConfig, buildName, w); - for (String line = reader.readLine(); line != null; line = reader.readLine()) { - LOG.info(line); - } - } catch (IOException e) { - throw s2iException(e); - } - } - - private static void waitForBuildComplete(OpenShiftClient client, ContainerImageS2iConfig s2iConfig, String buildName, - Closeable watch) { - CountDownLatch latch = new CountDownLatch(1); - ExecutorService executor = Executors.newSingleThreadExecutor(); - executor.execute(() -> { - try { - client.builds().withName(buildName).waitUntilCondition(b -> !RUNNING.equalsIgnoreCase(b.getStatus().getPhase()), - s2iConfig.buildTimeout.toMillis(), TimeUnit.MILLISECONDS); - } finally { - latch.countDown(); - } - }); - try { - latch.await(); - } catch (InterruptedException e) { - LOG.debug("Error waiting for build to complete.", e); - } finally { - try { - watch.close(); - } catch (IOException e) { - LOG.debug("Error closing log reader.", e); - } - try { - executor.shutdown(); - } catch (Exception e) { - LOG.debug("Error shutting down executor", e); - } - } - - } - - public static Predicate distinctByResourceKey() { - Map seen = new ConcurrentHashMap<>(); - return t -> seen.putIfAbsent(t.getApiVersion() + "/" + t.getKind() + ":" + t.getMetadata().getName(), - Boolean.TRUE) == null; - } - - private static Collection distinct(Collection resources) { - return resources.stream().filter(distinctByResourceKey()).collect(Collectors.toList()); - } - - private static List buildsOf(OpenShiftClient client, BuildConfig config) { - return client.builds().withLabel(BUILD_CONFIG_NAME, config.getMetadata().getName()).list().getItems(); - } - - private static Stream runningBuildsOf(OpenShiftClient client, BuildConfig config) { - return buildsOf(client, config).stream().filter(b -> RUNNING.equalsIgnoreCase(b.getStatus().getPhase())); - } - - private static RuntimeException s2iException(Throwable t) { - if (t instanceof KubernetesClientException) { - KubernetesClientErrorHandler.handle((KubernetesClientException) t); - } - return new RuntimeException("Execution of s2i build failed. See s2i output for more details", t); - } - - // visible for test - static String concatUnixPaths(String... elements) { - StringBuilder result = new StringBuilder(); - for (String element : elements) { - if (element.endsWith("/")) { - element = element.substring(0, element.length() - 1); - } - if (element.isEmpty()) { - continue; - } - if (!element.startsWith("/") && result.length() > 0) { - result.append('/'); - } - result.append(element); - } - return result.toString(); - } -} diff --git a/extensions/container-image/container-image-s2i/deployment/src/main/java/io/quarkus/container/image/s2i/deployment/S2iUtils.java b/extensions/container-image/container-image-s2i/deployment/src/main/java/io/quarkus/container/image/s2i/deployment/S2iUtils.java deleted file mode 100644 index b473418487ad9..0000000000000 --- a/extensions/container-image/container-image-s2i/deployment/src/main/java/io/quarkus/container/image/s2i/deployment/S2iUtils.java +++ /dev/null @@ -1,75 +0,0 @@ -package io.quarkus.container.image.s2i.deployment; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.concurrent.TimeUnit; - -import io.dekorate.kubernetes.decorator.Decorator; -import io.fabric8.kubernetes.api.model.HasMetadata; -import io.fabric8.kubernetes.api.model.KubernetesListBuilder; -import io.fabric8.kubernetes.api.model.ObjectReference; -import io.fabric8.openshift.api.model.ImageStreamTag; -import io.fabric8.openshift.api.model.SourceBuildStrategyFluent; -import io.fabric8.openshift.client.OpenShiftClient; - -/** - * This class is copied from Dekorate, with the difference that the {@code waitForImageStreamTags} method - * take a client as the argument - * - * TODO: Update dekorate to take the client as an argument and then remove this class - */ -public class S2iUtils { - - /** - * Wait for the references ImageStreamTags to become available. - * - * @param client The openshift client used to check the status of the ImageStream - * @param items A list of items, possibly referencing image stream tags. - * @param amount The max amount of time to wait. - * @param timeUnit The time unit of the time to wait. - * @return True if the items became available false otherwise. - */ - public static boolean waitForImageStreamTags(OpenShiftClient client, Collection items, long amount, - TimeUnit timeUnit) { - if (items == null || items.isEmpty()) { - return true; - } - final List tags = new ArrayList<>(); - new KubernetesListBuilder() - .withItems(new ArrayList<>(items)) - .accept(new Decorator() { - @Override - public void visit(SourceBuildStrategyFluent strategy) { - ObjectReference from = strategy.buildFrom(); - if (from.getKind().equals("ImageStreamTag")) { - tags.add(from.getName()); - } - } - }).build(); - - boolean tagsMissing = true; - long started = System.currentTimeMillis(); - long elapsed = 0; - - while (tagsMissing && elapsed < timeUnit.toMillis(amount) && !Thread.interrupted()) { - tagsMissing = false; - for (String tag : tags) { - ImageStreamTag t = client.imageStreamTags().withName(tag).get(); - if (t == null) { - tagsMissing = true; - } - } - - if (tagsMissing) { - try { - Thread.sleep(1000); - elapsed = System.currentTimeMillis() - started; - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - } - } - return !tagsMissing; - } -} diff --git a/extensions/container-image/container-image-s2i/deployment/src/test/java/io/quarkus/container/image/s2i/deployment/S2iProcessorTest.java b/extensions/container-image/container-image-s2i/deployment/src/test/java/io/quarkus/container/image/s2i/deployment/S2iProcessorTest.java deleted file mode 100644 index 3dbb8d8337f6c..0000000000000 --- a/extensions/container-image/container-image-s2i/deployment/src/test/java/io/quarkus/container/image/s2i/deployment/S2iProcessorTest.java +++ /dev/null @@ -1,56 +0,0 @@ -package io.quarkus.container.image.s2i.deployment; - -import static io.quarkus.container.image.s2i.deployment.S2iProcessor.concatUnixPaths; -import static org.junit.jupiter.api.Assertions.assertEquals; - -import org.junit.jupiter.api.Test; - -public class S2iProcessorTest { - @Test - public void concatUnixPathsTest() { - assertEquals("foo/bar", concatUnixPaths("foo", "bar")); - assertEquals("foo/bar", concatUnixPaths("foo/", "bar")); - assertEquals("foo/bar", concatUnixPaths("foo/", "/bar")); - assertEquals("foo/bar", concatUnixPaths("foo", "/bar")); - assertEquals("foo/bar", concatUnixPaths("foo", "bar/")); - assertEquals("foo/bar", concatUnixPaths("foo/", "bar/")); - assertEquals("foo/bar", concatUnixPaths("foo/", "/bar/")); - assertEquals("foo/bar", concatUnixPaths("foo", "/bar/")); - - assertEquals("foo/bar", concatUnixPaths("foo", "/", "bar")); - assertEquals("foo/bar", concatUnixPaths("foo/", "/", "bar")); - assertEquals("foo/bar", concatUnixPaths("foo/", "/", "/bar")); - assertEquals("foo/bar", concatUnixPaths("foo", "/", "/bar")); - assertEquals("foo/bar", concatUnixPaths("foo", "/", "bar/")); - assertEquals("foo/bar", concatUnixPaths("foo/", "/", "bar/")); - assertEquals("foo/bar", concatUnixPaths("foo/", "/", "/bar/")); - assertEquals("foo/bar", concatUnixPaths("foo", "/", "/bar/")); - - assertEquals("/foo/bar", concatUnixPaths("/foo", "bar")); - assertEquals("/foo/bar", concatUnixPaths("/foo/", "bar")); - assertEquals("/foo/bar", concatUnixPaths("/foo/", "/bar")); - assertEquals("/foo/bar", concatUnixPaths("/foo", "/bar")); - assertEquals("/foo/bar", concatUnixPaths("/foo", "bar/")); - assertEquals("/foo/bar", concatUnixPaths("/foo/", "bar/")); - assertEquals("/foo/bar", concatUnixPaths("/foo/", "/bar/")); - assertEquals("/foo/bar", concatUnixPaths("/foo", "/bar/")); - - assertEquals("/foo/bar", concatUnixPaths("/foo", "/", "bar")); - assertEquals("/foo/bar", concatUnixPaths("/foo/", "/", "bar")); - assertEquals("/foo/bar", concatUnixPaths("/foo/", "/", "/bar")); - assertEquals("/foo/bar", concatUnixPaths("/foo", "/", "/bar")); - assertEquals("/foo/bar", concatUnixPaths("/foo", "/", "bar/")); - assertEquals("/foo/bar", concatUnixPaths("/foo/", "/", "bar/")); - assertEquals("/foo/bar", concatUnixPaths("/foo/", "/", "/bar/")); - assertEquals("/foo/bar", concatUnixPaths("/foo", "/", "/bar/")); - - assertEquals("/foo/bar", concatUnixPaths("/foo", "bar", "/")); - assertEquals("/foo/bar", concatUnixPaths("/foo/", "bar", "/")); - assertEquals("/foo/bar", concatUnixPaths("/foo/", "/bar", "/")); - assertEquals("/foo/bar", concatUnixPaths("/foo", "/bar", "/")); - assertEquals("/foo/bar", concatUnixPaths("/foo", "bar/", "/")); - assertEquals("/foo/bar", concatUnixPaths("/foo/", "bar/", "/")); - assertEquals("/foo/bar", concatUnixPaths("/foo/", "/bar/", "/")); - assertEquals("/foo/bar", concatUnixPaths("/foo", "/bar/", "/")); - } -} diff --git a/extensions/container-image/container-image-s2i/runtime/pom.xml b/extensions/container-image/container-image-s2i/runtime/pom.xml deleted file mode 100644 index 076d6c1ce3bd8..0000000000000 --- a/extensions/container-image/container-image-s2i/runtime/pom.xml +++ /dev/null @@ -1,57 +0,0 @@ - - - 4.0.0 - - - io.quarkus - quarkus-container-image-s2i-parent - 999-SNAPSHOT - - - quarkus-container-image-s2i - Quarkus - Container Image - S2I - Build container images of your application using OpenShift S2I - - - - - io.quarkus - quarkus-kubernetes-client-internal - - - io.quarkus - quarkus-container-image - - - - - - - io.quarkus - quarkus-extension-maven-plugin - - - - io.quarkus.container.image.s2i.deployment.S2iBuild - io.quarkus.container.image.s2i - - - - - - maven-compiler-plugin - - - - io.quarkus - quarkus-extension-processor - ${project.version} - - - - - - - diff --git a/extensions/container-image/container-image-s2i/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/container-image/container-image-s2i/runtime/src/main/resources/META-INF/quarkus-extension.yaml deleted file mode 100644 index 46f5567aa08d0..0000000000000 --- a/extensions/container-image/container-image-s2i/runtime/src/main/resources/META-INF/quarkus-extension.yaml +++ /dev/null @@ -1,11 +0,0 @@ ---- -artifact: ${project.groupId}:${project.artifactId}:${project.version} -name: "Container Image S2I" -metadata: - keywords: - - "s2i" - - "container" - - "image" - categories: - - "cloud" - status: "preview" \ No newline at end of file diff --git a/extensions/container-image/deployment/src/main/java/io/quarkus/container/image/deployment/ContainerImageCapabilitiesUtil.java b/extensions/container-image/deployment/src/main/java/io/quarkus/container/image/deployment/ContainerImageCapabilitiesUtil.java index c07c9b483f242..5a4f027a0a3a3 100644 --- a/extensions/container-image/deployment/src/main/java/io/quarkus/container/image/deployment/ContainerImageCapabilitiesUtil.java +++ b/extensions/container-image/deployment/src/main/java/io/quarkus/container/image/deployment/ContainerImageCapabilitiesUtil.java @@ -13,14 +13,12 @@ public final class ContainerImageCapabilitiesUtil { public final static Map CAPABILITY_TO_EXTENSION_NAME = Map.of( Capability.CONTAINER_IMAGE_JIB, "quarkus-container-image-jib", Capability.CONTAINER_IMAGE_DOCKER, "quarkus-container-image-docker", - Capability.CONTAINER_IMAGE_S2I, "quarkus-container-image-s2i", Capability.CONTAINER_IMAGE_OPENSHIFT, "quarkus-container-image-openshift", Capability.CONTAINER_IMAGE_BUILDPACK, "quarkus-container-image-buildpack"); private final static Map CAPABILITY_TO_BUILDER_NAME = Map.of( Capability.CONTAINER_IMAGE_JIB, "jib", Capability.CONTAINER_IMAGE_DOCKER, "docker", - Capability.CONTAINER_IMAGE_S2I, "s2i", Capability.CONTAINER_IMAGE_OPENSHIFT, "openshift", Capability.CONTAINER_IMAGE_BUILDPACK, "buildpack"); diff --git a/extensions/container-image/pom.xml b/extensions/container-image/pom.xml index 277de33c0a76f..4e84fd15310c4 100644 --- a/extensions/container-image/pom.xml +++ b/extensions/container-image/pom.xml @@ -22,7 +22,6 @@ container-image-buildpack container-image-docker container-image-jib - container-image-s2i container-image-openshift diff --git a/extensions/devservices/deployment/src/main/java/io/quarkus/devservices/deployment/DevServicesProcessor.java b/extensions/devservices/deployment/src/main/java/io/quarkus/devservices/deployment/DevServicesProcessor.java index bb299dc590516..4004531ff8bcf 100644 --- a/extensions/devservices/deployment/src/main/java/io/quarkus/devservices/deployment/DevServicesProcessor.java +++ b/extensions/devservices/deployment/src/main/java/io/quarkus/devservices/deployment/DevServicesProcessor.java @@ -42,9 +42,9 @@ import io.quarkus.deployment.console.ConsoleStateManager; import io.quarkus.deployment.dev.devservices.ContainerInfo; import io.quarkus.deployment.dev.devservices.DevServiceDescriptionBuildItem; +import io.quarkus.deployment.util.ContainerRuntimeUtil; +import io.quarkus.deployment.util.ContainerRuntimeUtil.ContainerRuntime; import io.quarkus.dev.spi.DevModeType; -import io.quarkus.runtime.util.ContainerRuntimeUtil; -import io.quarkus.runtime.util.ContainerRuntimeUtil.ContainerRuntime; public class DevServicesProcessor { diff --git a/extensions/elasticsearch-rest-client-common/deployment/src/main/java/io/quarkus/elasticsearch/restclient/common/deployment/DevServicesElasticsearchProcessor.java b/extensions/elasticsearch-rest-client-common/deployment/src/main/java/io/quarkus/elasticsearch/restclient/common/deployment/DevServicesElasticsearchProcessor.java index 065acd241b0c0..e36b279d99b4a 100644 --- a/extensions/elasticsearch-rest-client-common/deployment/src/main/java/io/quarkus/elasticsearch/restclient/common/deployment/DevServicesElasticsearchProcessor.java +++ b/extensions/elasticsearch-rest-client-common/deployment/src/main/java/io/quarkus/elasticsearch/restclient/common/deployment/DevServicesElasticsearchProcessor.java @@ -220,7 +220,7 @@ private DevServicesResultBuildItem.RunningDevService startElasticsearch( container.withLabel(DEV_SERVICE_LABEL, config.serviceName); } if (config.port.isPresent()) { - container.setPortBindings(List.of(config.port.get() + ":" + config.port.get())); + container.setPortBindings(List.of(config.port.get() + ":" + ELASTICSEARCH_PORT)); } timeout.ifPresent(container::withStartupTimeout); diff --git a/extensions/elasticsearch-rest-client/deployment/src/test/java/io/quarkus/elasticsearch/restclient/lowlevel/runtime/DevServicesElasticsearchDevModeCustomPortTestCase.java b/extensions/elasticsearch-rest-client/deployment/src/test/java/io/quarkus/elasticsearch/restclient/lowlevel/runtime/DevServicesElasticsearchDevModeCustomPortTestCase.java new file mode 100644 index 0000000000000..3ea3334ae5a1e --- /dev/null +++ b/extensions/elasticsearch-rest-client/deployment/src/test/java/io/quarkus/elasticsearch/restclient/lowlevel/runtime/DevServicesElasticsearchDevModeCustomPortTestCase.java @@ -0,0 +1,47 @@ +package io.quarkus.elasticsearch.restclient.lowlevel.runtime; + +import static org.hamcrest.Matchers.endsWith; +import static org.hamcrest.Matchers.equalTo; + +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusDevModeTest; +import io.restassured.RestAssured; + +public class DevServicesElasticsearchDevModeCustomPortTestCase { + @RegisterExtension + static QuarkusDevModeTest test = new QuarkusDevModeTest() + .withApplicationRoot((jar) -> jar + .addClass(TestResource.class) + .addAsResource(new StringAsset(""" + quarkus.elasticsearch.devservices.port=19200 + """), "application.properties")); + + @Test + public void checkConfiguredPort() { + RestAssured + .when().get("/fruits/configured-hosts") + .then().body(endsWith(":19200")); + + } + + @Test + public void testDatasource() throws Exception { + var fruit = new TestResource.Fruit(); + fruit.id = "1"; + fruit.name = "banana"; + fruit.color = "yellow"; + + RestAssured + .given().body(fruit).contentType("application/json") + .when().post("/fruits") + .then().statusCode(204); + + RestAssured.when().get("/fruits/search?term=color&match=yellow") + .then() + .statusCode(200) + .body(equalTo("[{\"id\":\"1\",\"name\":\"banana\",\"color\":\"yellow\"}]")); + } +} diff --git a/extensions/elasticsearch-rest-client/deployment/src/test/java/io/quarkus/elasticsearch/restclient/lowlevel/runtime/TestResource.java b/extensions/elasticsearch-rest-client/deployment/src/test/java/io/quarkus/elasticsearch/restclient/lowlevel/runtime/TestResource.java index 5a10c4ebb8d67..4ab00a0f385d7 100644 --- a/extensions/elasticsearch-rest-client/deployment/src/test/java/io/quarkus/elasticsearch/restclient/lowlevel/runtime/TestResource.java +++ b/extensions/elasticsearch-rest-client/deployment/src/test/java/io/quarkus/elasticsearch/restclient/lowlevel/runtime/TestResource.java @@ -11,6 +11,7 @@ import jakarta.ws.rs.QueryParam; import org.apache.http.util.EntityUtils; +import org.eclipse.microprofile.config.ConfigProvider; import org.elasticsearch.client.Request; import org.elasticsearch.client.Response; import org.elasticsearch.client.RestClient; @@ -57,6 +58,12 @@ public List search(@QueryParam("term") String term, @QueryParam("match") return results; } + @GET + @Path("/configured-hosts") + public String configuredHosts() { + return ConfigProvider.getConfig().getConfigValue("quarkus.elasticsearch.hosts").getValue(); + } + public static class Fruit { public String id; public String name; diff --git a/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayDevModeCreateFromHibernateTest.java b/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayDevModeCreateFromHibernateTest.java index 1a6e6829e0700..173d1cf53d841 100644 --- a/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayDevModeCreateFromHibernateTest.java +++ b/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayDevModeCreateFromHibernateTest.java @@ -45,7 +45,7 @@ public void testGenerateMigrationFromHibernate() throws Exception { RestAssured.get("fruit").then().statusCode(200) .body("[0].name", CoreMatchers.is("Orange")); - Map params = Map.of("ds", ""); + Map params = Map.of("ds", ""); JsonNode devuiresponse = super.executeJsonRPCMethod("create", params); Assertions.assertNotNull(devuiresponse); diff --git a/extensions/funqy/funqy-amazon-lambda/maven-archetype/src/main/resources/archetype-resources/pom.xml b/extensions/funqy/funqy-amazon-lambda/maven-archetype/src/main/resources/archetype-resources/pom.xml index ac168bc8d0116..5ba3107f6e880 100644 --- a/extensions/funqy/funqy-amazon-lambda/maven-archetype/src/main/resources/archetype-resources/pom.xml +++ b/extensions/funqy/funqy-amazon-lambda/maven-archetype/src/main/resources/archetype-resources/pom.xml @@ -9,8 +9,8 @@ 3.11.0 true - 11 - 11 + 17 + 17 UTF-8 UTF-8 999-SNAPSHOT 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 fb6c31f12404e..179d762b54e6d 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 @@ -101,12 +101,12 @@ public boolean trigger(CodeGenContext context) throws CodeGenException { } Path outDir = context.outDir(); Path workDir = context.workDir(); + Path inputDir = CodeGenProvider.resolve(context.inputDir()); Set protoDirs = new LinkedHashSet<>(); - try { List protoFiles = new ArrayList<>(); - if (Files.isDirectory(context.inputDir())) { - try (Stream protoFilesPaths = Files.walk(context.inputDir())) { + if (Files.isDirectory(inputDir)) { + try (Stream protoFilesPaths = Files.walk(inputDir)) { protoFilesPaths .filter(Files::isRegularFile) .filter(s -> s.toString().endsWith(PROTO)) @@ -114,7 +114,7 @@ public boolean trigger(CodeGenContext context) throws CodeGenException { .map(Path::toAbsolutePath) .map(Path::toString) .forEach(protoFiles::add); - protoDirs.add(context.inputDir().normalize().toAbsolutePath().toString()); + protoDirs.add(inputDir.normalize().toAbsolutePath().toString()); } } Path dirWithProtosFromDependencies = workDir.resolve("protoc-protos-from-dependencies"); @@ -174,7 +174,7 @@ public boolean trigger(CodeGenContext context) throws CodeGenException { } } catch (IOException | InterruptedException e) { throw new CodeGenException( - "Failed to generate java files from proto file in " + context.inputDir().toAbsolutePath(), e); + "Failed to generate java files from proto file in " + inputDir.toAbsolutePath(), e); } return false; diff --git a/extensions/grpc/deployment/src/main/java/io/quarkus/grpc/deployment/GrpcCommonProcessor.java b/extensions/grpc/deployment/src/main/java/io/quarkus/grpc/deployment/GrpcCommonProcessor.java new file mode 100644 index 0000000000000..76a3e825a91d1 --- /dev/null +++ b/extensions/grpc/deployment/src/main/java/io/quarkus/grpc/deployment/GrpcCommonProcessor.java @@ -0,0 +1,23 @@ +package io.quarkus.grpc.deployment; + +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.builditem.IndexDependencyBuildItem; + +/** + * A processor used for both client and server + */ +public class GrpcCommonProcessor { + + /** + * Index the gRPC stubs. + * This is used to allows application find the classes generated from the proto file included in the dependency at build + * time. + *

    + * See #37312 + */ + @BuildStep + void indexGrpcStub(BuildProducer index) { + index.produce(new IndexDependencyBuildItem("io.quarkus", "quarkus-grpc-stubs")); + } +} diff --git a/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/devui/GrpcJsonRPCService.java b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/devui/GrpcJsonRPCService.java index dc4f09e115730..e8e4defb60b12 100644 --- a/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/devui/GrpcJsonRPCService.java +++ b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/devui/GrpcJsonRPCService.java @@ -13,6 +13,8 @@ import io.quarkus.grpc.runtime.config.GrpcConfiguration; import io.quarkus.grpc.runtime.config.GrpcServerConfiguration; import io.quarkus.grpc.runtime.devmode.GrpcServices; +import io.quarkus.vertx.http.runtime.CertificateConfig; +import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig; import io.quarkus.vertx.http.runtime.HttpConfiguration; import io.smallrye.mutiny.Multi; import io.smallrye.mutiny.Uni; @@ -32,6 +34,9 @@ public class GrpcJsonRPCService { @Inject HttpConfiguration httpConfiguration; + @Inject + HttpBuildTimeConfig httpBuildTimeConfig; + @Inject GrpcConfiguration grpcConfiguration; @@ -52,10 +57,16 @@ public void init() { } else { this.host = httpConfiguration.host; this.port = httpConfiguration.port; - this.ssl = httpConfiguration.insecureRequests != HttpConfiguration.InsecureRequests.ENABLED; + this.ssl = isTLSConfigured(httpConfiguration.ssl.certificate); } } + private boolean isTLSConfigured(CertificateConfig certificate) { + return certificate.files.isPresent() + || certificate.keyFiles.isPresent() + || certificate.keyStoreFile.isPresent(); + } + public JsonArray getServices() { JsonArray services = new JsonArray(); List infos = this.grpcServices.getInfos(); diff --git a/extensions/hibernate-envers/runtime/pom.xml b/extensions/hibernate-envers/runtime/pom.xml index 95319d2fa3dab..c985316f7e31b 100644 --- a/extensions/hibernate-envers/runtime/pom.xml +++ b/extensions/hibernate-envers/runtime/pom.xml @@ -18,20 +18,6 @@ org.hibernate.orm hibernate-envers - - - - - org.glassfish.jaxb - jaxb-runtime - - - - - jakarta.activation - jakarta.activation-api - - io.quarkus diff --git a/extensions/hibernate-orm/deployment/pom.xml b/extensions/hibernate-orm/deployment/pom.xml index 84d6dd9a6d89d..e8714c990a185 100644 --- a/extensions/hibernate-orm/deployment/pom.xml +++ b/extensions/hibernate-orm/deployment/pom.xml @@ -69,6 +69,11 @@ quarkus-resteasy-deployment test + + io.quarkus + quarkus-resteasy-jackson-deployment + test + io.quarkus quarkus-hibernate-validator-deployment diff --git a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/ClassNames.java b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/ClassNames.java index 0639230faa6f6..ab0e5cd4e2ea0 100644 --- a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/ClassNames.java +++ b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/ClassNames.java @@ -57,6 +57,9 @@ private static DotName createConstant(String fqcn) { public static final DotName INTERCEPTOR = createConstant("org.hibernate.Interceptor"); public static final DotName STATEMENT_INSPECTOR = createConstant("org.hibernate.resource.jdbc.spi.StatementInspector"); + public static final DotName FORMAT_MAPPER = createConstant("org.hibernate.type.format.FormatMapper"); + public static final DotName JSON_FORMAT = createConstant("io.quarkus.hibernate.orm.JsonFormat"); + public static final DotName XML_FORMAT = createConstant("io.quarkus.hibernate.orm.XmlFormat"); public static final List GENERATORS = List.of( createConstant("org.hibernate.generator.internal.CurrentTimestampGeneration"), diff --git a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmCdiProcessor.java b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmCdiProcessor.java index 63d691520a6e8..fc207f65444c3 100644 --- a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmCdiProcessor.java +++ b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmCdiProcessor.java @@ -61,7 +61,8 @@ public class HibernateOrmCdiProcessor { ClassNames.TENANT_RESOLVER, ClassNames.TENANT_CONNECTION_RESOLVER, ClassNames.INTERCEPTOR, - ClassNames.STATEMENT_INSPECTOR); + ClassNames.STATEMENT_INSPECTOR, + ClassNames.FORMAT_MAPPER); @BuildStep AnnotationsTransformerBuildItem convertJpaResourceAnnotationsToQualifier( @@ -247,11 +248,13 @@ void generateDataSourceBeans(HibernateOrmRecorder recorder, @BuildStep void registerAnnotations(BuildProducer additionalBeans, BuildProducer beanDefiningAnnotations) { - // add the @PersistenceUnit and @PersistenceUnitExtension classes + // add the @PersistenceUnit, @PersistenceUnitExtension, @JsonFormat and @XmlFormat classes // otherwise they won't be registered as qualifiers additionalBeans.produce(AdditionalBeanBuildItem.builder() .addBeanClasses(ClassNames.QUARKUS_PERSISTENCE_UNIT.toString(), - ClassNames.PERSISTENCE_UNIT_EXTENSION.toString()) + ClassNames.PERSISTENCE_UNIT_EXTENSION.toString(), + ClassNames.JSON_FORMAT.toString(), + ClassNames.XML_FORMAT.toString()) .build()); // Register the default scope for @PersistenceUnitExtension and make such beans unremovable by default diff --git a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmProcessor.java b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmProcessor.java index 7d0101630b26a..7c90798d1ade0 100644 --- a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmProcessor.java +++ b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmProcessor.java @@ -182,9 +182,9 @@ void registerHibernateOrmMetadataForCoreDialects( producer.produce(new DatabaseKindDialectBuildItem(DatabaseKind.MSSQL, "org.hibernate.dialect.SQLServerDialect", DialectVersions.Defaults.MSSQL)); producer.produce(new DatabaseKindDialectBuildItem(DatabaseKind.MYSQL, - "org.hibernate.dialect.MySQLDialect", DialectVersions.Defaults.MYSQL)); + "org.hibernate.dialect.MySQLDialect")); producer.produce(new DatabaseKindDialectBuildItem(DatabaseKind.ORACLE, - "org.hibernate.dialect.OracleDialect", DialectVersions.Defaults.ORACLE)); + "org.hibernate.dialect.OracleDialect")); producer.produce(new DatabaseKindDialectBuildItem(DatabaseKind.POSTGRESQL, "org.hibernate.dialect.PostgreSQLDialect")); } @@ -326,7 +326,7 @@ public void configurationDescriptorBuilding( hibernateOrmConfig.database.ormCompatibilityVersion, Collections.emptyMap()), null, jpaModel.getXmlMappings(persistenceXmlDescriptorBuildItem.getDescriptor().getName()), - false, true)); + false, true, capabilities)); } if (impliedPU.shouldGenerateImpliedBlockingPersistenceUnit()) { @@ -1124,7 +1124,7 @@ private static void producePersistenceUnitDescriptorFromConfig( persistenceUnitConfig.unsupportedProperties), persistenceUnitConfig.multitenantSchemaDatasource.orElse(null), xmlMappings, - false, false)); + false, false, capabilities)); } private static void collectDialectConfig(String persistenceUnitName, diff --git a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmTypes.java b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmTypes.java index 3b8a7f8e9e0ab..e9f713c98007a 100644 --- a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmTypes.java +++ b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmTypes.java @@ -36,6 +36,7 @@ private HibernateOrmTypes() { DotName.createSimple("org.hibernate.annotations.NamedNativeQuery"), DotName.createSimple("org.hibernate.annotations.NamedQueries"), DotName.createSimple("org.hibernate.annotations.NamedQuery"), + DotName.createSimple("org.hibernate.annotations.SoftDelete"), DotName.createSimple("org.hibernate.annotations.TypeRegistration"), DotName.createSimple("org.hibernate.annotations.TypeRegistrations")); @@ -141,6 +142,7 @@ private HibernateOrmTypes() { DotName.createSimple("org.hibernate.annotations.AnyKeyJavaType"), DotName.createSimple("org.hibernate.annotations.AnyKeyJdbcType"), DotName.createSimple("org.hibernate.annotations.AnyKeyJdbcTypeCode"), + DotName.createSimple("org.hibernate.annotations.Array"), DotName.createSimple("org.hibernate.annotations.AttributeAccessor"), DotName.createSimple("org.hibernate.annotations.AttributeBinderType"), DotName.createSimple("org.hibernate.annotations.Bag"), @@ -149,6 +151,7 @@ private HibernateOrmTypes() { DotName.createSimple("org.hibernate.annotations.Cascade"), DotName.createSimple("org.hibernate.annotations.Check"), DotName.createSimple("org.hibernate.annotations.Checks"), + DotName.createSimple("org.hibernate.annotations.Collate"), DotName.createSimple("org.hibernate.annotations.CollectionId"), DotName.createSimple("org.hibernate.annotations.CollectionIdJavaType"), DotName.createSimple("org.hibernate.annotations.CollectionIdJdbcType"), @@ -190,6 +193,20 @@ private HibernateOrmTypes() { DotName.createSimple("org.hibernate.annotations.DialectOverride$OrderBy"), DotName.createSimple("org.hibernate.annotations.DialectOverride$OrderBys"), DotName.createSimple("org.hibernate.annotations.DialectOverride$OverridesAnnotation"), + DotName.createSimple("org.hibernate.annotations.DialectOverride$SQLDelete"), + DotName.createSimple("org.hibernate.annotations.DialectOverride$SQLDeleteAll"), + DotName.createSimple("org.hibernate.annotations.DialectOverride$SQLDeleteAlls"), + DotName.createSimple("org.hibernate.annotations.DialectOverride$SQLDeletes"), + DotName.createSimple("org.hibernate.annotations.DialectOverride$SQLInsert"), + DotName.createSimple("org.hibernate.annotations.DialectOverride$SQLInserts"), + DotName.createSimple("org.hibernate.annotations.DialectOverride$SQLOrder"), + DotName.createSimple("org.hibernate.annotations.DialectOverride$SQLOrders"), + DotName.createSimple("org.hibernate.annotations.DialectOverride$SQLRestriction"), + DotName.createSimple("org.hibernate.annotations.DialectOverride$SQLRestrictions"), + DotName.createSimple("org.hibernate.annotations.DialectOverride$SQLSelect"), + DotName.createSimple("org.hibernate.annotations.DialectOverride$SQLSelects"), + DotName.createSimple("org.hibernate.annotations.DialectOverride$SQLUpdate"), + DotName.createSimple("org.hibernate.annotations.DialectOverride$SQLUpdates"), DotName.createSimple("org.hibernate.annotations.DialectOverride$Version"), DotName.createSimple("org.hibernate.annotations.DialectOverride$Where"), DotName.createSimple("org.hibernate.annotations.DialectOverride$Wheres"), @@ -203,6 +220,8 @@ private HibernateOrmTypes() { DotName.createSimple("org.hibernate.annotations.Fetch"), DotName.createSimple("org.hibernate.annotations.FetchProfile"), DotName.createSimple("org.hibernate.annotations.FetchProfile$FetchOverride"), + DotName.createSimple("org.hibernate.annotations.FetchProfileOverride"), + DotName.createSimple("org.hibernate.annotations.FetchProfileOverrides"), DotName.createSimple("org.hibernate.annotations.FetchProfiles"), DotName.createSimple("org.hibernate.annotations.Filter"), DotName.createSimple("org.hibernate.annotations.FilterDef"), @@ -274,12 +293,16 @@ private HibernateOrmTypes() { DotName.createSimple("org.hibernate.annotations.SQLDeletes"), DotName.createSimple("org.hibernate.annotations.SQLInsert"), DotName.createSimple("org.hibernate.annotations.SQLInserts"), + DotName.createSimple("org.hibernate.annotations.SQLJoinTableRestriction"), + DotName.createSimple("org.hibernate.annotations.SQLOrder"), + DotName.createSimple("org.hibernate.annotations.SQLRestriction"), DotName.createSimple("org.hibernate.annotations.SQLSelect"), DotName.createSimple("org.hibernate.annotations.SQLUpdate"), DotName.createSimple("org.hibernate.annotations.SQLUpdates"), DotName.createSimple("org.hibernate.annotations.SecondaryRow"), DotName.createSimple("org.hibernate.annotations.SecondaryRows"), DotName.createSimple("org.hibernate.annotations.SelectBeforeUpdate"), + DotName.createSimple("org.hibernate.annotations.SoftDelete"), DotName.createSimple("org.hibernate.annotations.SortComparator"), DotName.createSimple("org.hibernate.annotations.SortNatural"), DotName.createSimple("org.hibernate.annotations.Source"), @@ -300,12 +323,12 @@ private HibernateOrmTypes() { DotName.createSimple("org.hibernate.annotations.UpdateTimestamp"), DotName.createSimple("org.hibernate.annotations.UuidGenerator"), DotName.createSimple("org.hibernate.annotations.ValueGenerationType"), + DotName.createSimple("org.hibernate.annotations.View"), DotName.createSimple("org.hibernate.annotations.Where"), DotName.createSimple("org.hibernate.annotations.WhereJoinTable")); public static final List ANNOTATED_WITH_INJECT_SERVICE = List.of( - DotName.createSimple("org.hibernate.engine.jdbc.connections.internal.DatasourceConnectionProviderImpl"), - DotName.createSimple("org.hibernate.engine.jdbc.cursor.internal.StandardRefCursorSupport")); + DotName.createSimple("org.hibernate.engine.jdbc.connections.internal.DatasourceConnectionProviderImpl")); public static final List JPA_LISTENER_ANNOTATIONS = List.of( DotName.createSimple("jakarta.persistence.PostLoad"), @@ -352,5 +375,6 @@ private HibernateOrmTypes() { DotName.createSimple("java.util.Locale"), DotName.createSimple("java.util.Map$Entry"), DotName.createSimple("java.util.TimeZone"), - DotName.createSimple("java.util.UUID")); + DotName.createSimple("java.util.UUID"), + DotName.createSimple("java.lang.Void")); } diff --git a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/PersistenceUnitDescriptorBuildItem.java b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/PersistenceUnitDescriptorBuildItem.java index 9400a67468d6c..5693396c0bbba 100644 --- a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/PersistenceUnitDescriptorBuildItem.java +++ b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/PersistenceUnitDescriptorBuildItem.java @@ -2,12 +2,16 @@ import java.util.Collection; import java.util.List; +import java.util.Optional; import org.hibernate.jpa.boot.internal.ParsedPersistenceXmlDescriptor; import io.quarkus.builder.item.MultiBuildItem; +import io.quarkus.deployment.Capabilities; +import io.quarkus.deployment.Capability; import io.quarkus.hibernate.orm.runtime.boot.QuarkusPersistenceUnitDefinition; import io.quarkus.hibernate.orm.runtime.boot.xml.RecordableXmlMapping; +import io.quarkus.hibernate.orm.runtime.customized.FormatMapperKind; import io.quarkus.hibernate.orm.runtime.integration.HibernateOrmIntegrationStaticDescriptor; import io.quarkus.hibernate.orm.runtime.recording.RecordedConfig; @@ -30,12 +34,14 @@ public final class PersistenceUnitDescriptorBuildItem extends MultiBuildItem { private final List xmlMappings; private final boolean isReactive; private final boolean fromPersistenceXml; + private final Optional jsonMapper; + private final Optional xmlMapper; public PersistenceUnitDescriptorBuildItem(ParsedPersistenceXmlDescriptor descriptor, String configurationName, RecordedConfig config, String multiTenancySchemaDataSource, List xmlMappings, - boolean isReactive, boolean fromPersistenceXml) { + boolean isReactive, boolean fromPersistenceXml, Capabilities capabilities) { this.descriptor = descriptor; this.configurationName = configurationName; this.config = config; @@ -43,6 +49,8 @@ public PersistenceUnitDescriptorBuildItem(ParsedPersistenceXmlDescriptor descrip this.xmlMappings = xmlMappings; this.isReactive = isReactive; this.fromPersistenceXml = fromPersistenceXml; + this.jsonMapper = json(capabilities); + this.xmlMapper = xml(capabilities); } public Collection getManagedClassNames() { @@ -80,6 +88,23 @@ public boolean isFromPersistenceXml() { public QuarkusPersistenceUnitDefinition asOutputPersistenceUnitDefinition( List integrationStaticDescriptors) { return new QuarkusPersistenceUnitDefinition(descriptor, configurationName, config, - xmlMappings, isReactive, fromPersistenceXml, integrationStaticDescriptors); + xmlMappings, isReactive, fromPersistenceXml, jsonMapper, xmlMapper, integrationStaticDescriptors); + } + + private Optional json(Capabilities capabilities) { + if (capabilities.isPresent(Capability.JACKSON)) { + return Optional.of(FormatMapperKind.JACKSON); + } + if (capabilities.isPresent(Capability.JSONB)) { + return Optional.of(FormatMapperKind.JSONB); + } + return Optional.empty(); + } + + private Optional xml(Capabilities capabilities) { + if (capabilities.isPresent(Capability.JAXB)) { + return Optional.of(FormatMapperKind.JAXB); + } + return Optional.empty(); } } diff --git a/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/HibernateSchemaRecreateDevConsoleTestCase.java b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/HibernateSchemaRecreateDevConsoleTestCase.java index c31f7507c1468..edcf67f16c675 100644 --- a/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/HibernateSchemaRecreateDevConsoleTestCase.java +++ b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/HibernateSchemaRecreateDevConsoleTestCase.java @@ -32,7 +32,7 @@ public void testCleanDatabase() throws Exception { RestAssured.when().get("/my-entity/count").then().body(is("2")); RestAssured.when().get("/my-entity/add").then().body(is("MyEntity:added")); RestAssured.when().get("/my-entity/count").then().body(is("3")); - Map params = Map.of("ds", ""); + Map params = Map.of("ds", ""); JsonNode success = super.executeJsonRPCMethod("reset", params); assertTrue(success.asBoolean()); RestAssured.when().get("/my-entity/count").then().body(is("2")); diff --git a/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/applicationfieldaccess/ImmutableEmbeddableFieldAccessTest.java b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/applicationfieldaccess/ImmutableEmbeddableFieldAccessTest.java new file mode 100644 index 0000000000000..123a97aa63ef5 --- /dev/null +++ b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/applicationfieldaccess/ImmutableEmbeddableFieldAccessTest.java @@ -0,0 +1,193 @@ +package io.quarkus.hibernate.orm.applicationfieldaccess; + +import static org.assertj.core.api.Assertions.assertThat; + +import jakarta.inject.Inject; +import jakarta.persistence.AttributeOverride; +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityManager; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; + +import org.hibernate.Hibernate; +import org.hibernate.annotations.Immutable; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.narayana.jta.QuarkusTransaction; +import io.quarkus.test.QuarkusUnitTest; + +/** + * Checks that access to record fields or record getters by the application works correctly. + */ +public class ImmutableEmbeddableFieldAccessTest { + + @RegisterExtension + static QuarkusUnitTest runner = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(MyEntity.class) + .addClasses(MyImmutableEmbeddableWithFieldAccess.class) + .addClasses(MyImmutableEmbeddableWithAccessors.class) + .addClass(AccessDelegate.class)) + .withConfigurationResource("application.properties"); + + @Inject + EntityManager em; + + @Test + public void immutableEmbeddableWithoutAdditionalGetters_field() { + doTestFieldAccess(new AccessDelegate() { + @Override + public void setValue(MyEntity entity, Long value) { + var embedded = new MyImmutableEmbeddableWithFieldAccess(); + embedded.value = value; + entity.embeddedWithoutAccessors = embedded; + } + + @Override + public Long getValue(MyEntity entity) { + return entity.embeddedWithoutAccessors == null ? null : entity.embeddedWithoutAccessors.value; + } + }); + } + + @Test + public void immutableEmbeddableWithAdditionalGetters_field() { + doTestFieldAccess(new AccessDelegate() { + @Override + public void setValue(MyEntity entity, Long value) { + var embedded = new MyImmutableEmbeddableWithAccessors(); + // Assuming this is changed only once on initialization, + // which is the only way the @Immutable annotation would make sense. + embedded.value = value; + entity.embeddedWithFieldAccess = embedded; + } + + @Override + public Long getValue(MyEntity entity) { + return entity.embeddedWithFieldAccess == null ? null : entity.embeddedWithFieldAccess.value; + } + }); + } + + @Test + public void immutableEmbeddableWithAccessors() { + doTestFieldAccess(new AccessDelegate() { + @Override + public void setValue(MyEntity entity, Long value) { + var embedded = new MyImmutableEmbeddableWithAccessors(); + // Assuming this is changed only once on initialization, + // which is the only way the @Immutable annotation would make sense. + embedded.setValue(value); + entity.embeddedWithFieldAccess = embedded; + } + + @Override + public Long getValue(MyEntity entity) { + return entity.embeddedWithFieldAccess == null ? null : entity.embeddedWithFieldAccess.getValue(); + } + }); + } + + // Ideally we'd make this a @ParameterizedTest and pass the access delegate as parameter, + // but we cannot do that due to JUnit using a different classloader than the test. + private void doTestFieldAccess(AccessDelegate delegate) { + Long id = QuarkusTransaction.disallowingExisting().call(() -> { + var entity = new MyEntity(); + em.persist(entity); + return entity.id; + }); + + QuarkusTransaction.disallowingExisting().run(() -> { + var entity = em.find(MyEntity.class, id); + assertThat(delegate.getValue(entity)) + .as("Loaded value before update") + .isNull(); + }); + + QuarkusTransaction.disallowingExisting().run(() -> { + var entity = em.getReference(MyEntity.class, id); + // Since field access is replaced with accessor calls, + // we expect this change to be detected by dirty tracking and persisted. + delegate.setValue(entity, 42L); + }); + + QuarkusTransaction.disallowingExisting().run(() -> { + var entity = em.find(MyEntity.class, id); + // We're working on an initialized entity. + assertThat(entity) + .as("find() should return uninitialized entity") + .returns(true, Hibernate::isInitialized); + // The above should have persisted a value that passes the assertion. + assertThat(delegate.getValue(entity)) + .as("Loaded value after update") + .isEqualTo(42L); + }); + + QuarkusTransaction.disallowingExisting().run(() -> { + var entity = em.getReference(MyEntity.class, id); + // We're working on an uninitialized entity. + assertThat(entity) + .as("getReference() should return uninitialized entity") + .returns(false, Hibernate::isInitialized); + // The above should have persisted a value that passes the assertion. + assertThat(delegate.getValue(entity)) + .as("Lazily loaded value after update") + .isEqualTo(42L); + // Accessing the value should trigger initialization of the entity. + assertThat(entity) + .as("Getting the value should initialize the entity") + .returns(true, Hibernate::isInitialized); + }); + } + + @Entity(name = "myentity") + public static class MyEntity { + @Id + @GeneratedValue + public long id; + @Embedded + @AttributeOverride(name = "value", column = @Column(name = "value1")) + public MyImmutableEmbeddableWithAccessors embeddedWithFieldAccess; + @Embedded + @AttributeOverride(name = "value", column = @Column(name = "value2")) + public MyImmutableEmbeddableWithFieldAccess embeddedWithoutAccessors; + } + + @Immutable + @Embeddable + public static class MyImmutableEmbeddableWithFieldAccess { + public Long value; + + public MyImmutableEmbeddableWithFieldAccess() { + } + } + + @Immutable + @Embeddable + public static class MyImmutableEmbeddableWithAccessors { + private Long value; + + // For Hibernate ORM instantiation + protected MyImmutableEmbeddableWithAccessors() { + } + + public Long getValue() { + return value; + } + + // For Hibernate ORM instantiation + protected void setValue(Long value) { + this.value = value; + } + } + + private interface AccessDelegate { + void setValue(MyEntity entity, Long value); + + Long getValue(MyEntity entity); + } +} diff --git a/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/applicationfieldaccess/RecordFieldAccessTest.java b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/applicationfieldaccess/RecordFieldAccessTest.java new file mode 100644 index 0000000000000..e7df873de95a7 --- /dev/null +++ b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/applicationfieldaccess/RecordFieldAccessTest.java @@ -0,0 +1,196 @@ +package io.quarkus.hibernate.orm.applicationfieldaccess; + +import static org.assertj.core.api.Assertions.assertThat; + +import jakarta.inject.Inject; +import jakarta.persistence.AttributeOverride; +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityManager; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; + +import org.hibernate.Hibernate; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.narayana.jta.QuarkusTransaction; +import io.quarkus.test.QuarkusUnitTest; + +/** + * Checks that access to fields or getters of embeddable records by the application works correctly. + * See https://github.com/quarkusio/quarkus/issues/36747 + */ +public class RecordFieldAccessTest { + + @RegisterExtension + static QuarkusUnitTest runner = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(MyEntity.class) + .addClasses(MyRecordEmbeddableWithoutAdditionalGetters.class) + .addClasses(MyRecordEmbeddableWithAdditionalGetters.class) + .addClass(AccessDelegate.class)) + .withConfigurationResource("application.properties"); + + @Inject + EntityManager em; + + @Test + public void recordWithoutAdditionalGetters_field() { + doTestFieldAccess(new AccessDelegate() { + @Override + public void setValue(MyEntity entity, Long value) { + entity.embeddedWithoutAdditionalGetters = new MyRecordEmbeddableWithoutAdditionalGetters(value); + } + + @Override + public Long getValue(MyEntity entity) { + return entity.embeddedWithoutAdditionalGetters == null ? null : entity.embeddedWithoutAdditionalGetters.value; + } + }); + } + + @Test + public void recordWithoutAdditionalGetters_recordGetter() { + doTestFieldAccess(new AccessDelegate() { + @Override + public void setValue(MyEntity entity, Long value) { + entity.embeddedWithoutAdditionalGetters = new MyRecordEmbeddableWithoutAdditionalGetters(value); + } + + @Override + public Long getValue(MyEntity entity) { + return entity.embeddedWithoutAdditionalGetters == null ? null : entity.embeddedWithoutAdditionalGetters.value(); + } + }); + } + + @Test + public void recordWithAdditionalGetters_field() { + doTestFieldAccess(new AccessDelegate() { + @Override + public void setValue(MyEntity entity, Long value) { + entity.embeddedWithAdditionalGetters = new MyRecordEmbeddableWithAdditionalGetters(value); + } + + @Override + public Long getValue(MyEntity entity) { + return entity.embeddedWithAdditionalGetters == null ? null : entity.embeddedWithAdditionalGetters.value; + } + }); + } + + @Test + public void recordWithAdditionalGetters_recordGetter() { + doTestFieldAccess(new AccessDelegate() { + @Override + public void setValue(MyEntity entity, Long value) { + entity.embeddedWithAdditionalGetters = new MyRecordEmbeddableWithAdditionalGetters(value); + } + + @Override + public Long getValue(MyEntity entity) { + return entity.embeddedWithAdditionalGetters == null ? null : entity.embeddedWithAdditionalGetters.value(); + } + }); + } + + @Test + public void recordWithAdditionalGetters_additionalGetter() { + doTestFieldAccess(new AccessDelegate() { + @Override + public void setValue(MyEntity entity, Long value) { + entity.embeddedWithAdditionalGetters = new MyRecordEmbeddableWithAdditionalGetters(value); + } + + @Override + public Long getValue(MyEntity entity) { + return entity.embeddedWithAdditionalGetters == null ? null : entity.embeddedWithAdditionalGetters.getValue(); + } + }); + } + + // Ideally we'd make this a @ParameterizedTest and pass the access delegate as parameter, + // but we cannot do that due to JUnit using a different classloader than the test. + private void doTestFieldAccess(AccessDelegate delegate) { + Long id = QuarkusTransaction.disallowingExisting().call(() -> { + var entity = new MyEntity(); + em.persist(entity); + return entity.id; + }); + + QuarkusTransaction.disallowingExisting().run(() -> { + var entity = em.find(MyEntity.class, id); + assertThat(delegate.getValue(entity)) + .as("Loaded value before update") + .isNull(); + }); + + QuarkusTransaction.disallowingExisting().run(() -> { + var entity = em.getReference(MyEntity.class, id); + // Since field access is replaced with accessor calls, + // we expect this change to be detected by dirty tracking and persisted. + delegate.setValue(entity, 42L); + }); + + QuarkusTransaction.disallowingExisting().run(() -> { + var entity = em.find(MyEntity.class, id); + // We're working on an initialized entity. + assertThat(entity) + .as("find() should return uninitialized entity") + .returns(true, Hibernate::isInitialized); + // The above should have persisted a value that passes the assertion. + assertThat(delegate.getValue(entity)) + .as("Loaded value after update") + .isEqualTo(42L); + }); + + QuarkusTransaction.disallowingExisting().run(() -> { + var entity = em.getReference(MyEntity.class, id); + // We're working on an uninitialized entity. + assertThat(entity) + .as("getReference() should return uninitialized entity") + .returns(false, Hibernate::isInitialized); + // The above should have persisted a value that passes the assertion. + assertThat(delegate.getValue(entity)) + .as("Lazily loaded value after update") + .isEqualTo(42L); + // Accessing the value should trigger initialization of the entity. + assertThat(entity) + .as("Getting the value should initialize the entity") + .returns(true, Hibernate::isInitialized); + }); + } + + @Entity(name = "myentity") + public static class MyEntity { + @Id + @GeneratedValue + public long id; + @Embedded + @AttributeOverride(name = "value", column = @Column(name = "value1")) + public MyRecordEmbeddableWithAdditionalGetters embeddedWithAdditionalGetters; + @Embedded + @AttributeOverride(name = "value", column = @Column(name = "value2")) + public MyRecordEmbeddableWithoutAdditionalGetters embeddedWithoutAdditionalGetters; + } + + @Embeddable + public record MyRecordEmbeddableWithoutAdditionalGetters(Long value) { + } + + @Embeddable + public record MyRecordEmbeddableWithAdditionalGetters(Long value) { + Long getValue() { + return value; + } + } + + private interface AccessDelegate { + void setValue(MyEntity entity, Long value); + + Long getValue(MyEntity entity); + } +} diff --git a/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/dev/HibernateOrmDevInfoServiceTestResource.java b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/dev/HibernateOrmDevInfoServiceTestResource.java index 52de3788b7fc8..77c79cc7fc0d5 100644 --- a/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/dev/HibernateOrmDevInfoServiceTestResource.java +++ b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/dev/HibernateOrmDevInfoServiceTestResource.java @@ -88,7 +88,7 @@ public String checkPuInfoWithFailingDDLGeneration() { + TypeWithUnsupportedSqlCode.UNSUPPORTED_SQL_CODE + "))"); // Drop script generation doesn't involve column types, so it didn't fail assertThat(pu.getDropDDL()) - .contains("drop table MyEntityTable if exists"); + .contains("drop table if exists MyEntityTable"); return "OK"; } diff --git a/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/metadatabuildercontributor/CustomMetadataBuilderContributor.java b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/metadatabuildercontributor/CustomMetadataBuilderContributor.java index 5f72d49cc8daf..7537062c57d4c 100644 --- a/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/metadatabuildercontributor/CustomMetadataBuilderContributor.java +++ b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/metadatabuildercontributor/CustomMetadataBuilderContributor.java @@ -7,6 +7,7 @@ import org.hibernate.boot.MetadataBuilder; import org.hibernate.boot.model.FunctionContributions; import org.hibernate.boot.model.FunctionContributor; +import org.hibernate.query.ReturnableType; import org.hibernate.query.sqm.function.AbstractSqmSelfRenderingFunctionDescriptor; import org.hibernate.query.sqm.produce.function.StandardArgumentsValidators; import org.hibernate.query.sqm.produce.function.StandardFunctionArgumentTypeResolvers; @@ -49,7 +50,8 @@ private HardcodedSuffixFunction(TypeConfiguration typeConfiguration, String suff } @Override - public void render(SqlAppender sqlAppender, List sqlAstArguments, SqlAstTranslator walker) { + public void render(SqlAppender sqlAppender, List sqlAstArguments, ReturnableType returnType, + SqlAstTranslator walker) { sqlAppender.appendSql('('); walker.render(sqlAstArguments.get(0), SqlAstNodeRenderingMode.DEFAULT); sqlAppender.appendSql(" || '" + suffix + "')"); diff --git a/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/narayana/quarkus/Fruit.java b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/narayana/quarkus/Fruit.java new file mode 100644 index 0000000000000..eac44d3c7e4c3 --- /dev/null +++ b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/narayana/quarkus/Fruit.java @@ -0,0 +1,49 @@ +package io.quarkus.narayana.quarkus; + +import jakarta.persistence.Cacheable; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.NamedQuery; +import jakarta.persistence.QueryHint; +import jakarta.persistence.SequenceGenerator; +import jakarta.persistence.Table; + +@Entity +@Table(name = "known_fruits") +@NamedQuery(name = "Fruits.findAll", query = "SELECT f FROM Fruit f ORDER BY f.name", hints = @QueryHint(name = "org.hibernate.cacheable", value = "true")) +@Cacheable +public class Fruit { + @Id + @SequenceGenerator(name = "fruitsSequence", sequenceName = "known_fruits_id_seq", allocationSize = 1, initialValue = 10) + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "fruitsSequence") + private Integer id; + + @Column(length = 40, unique = true) + private String name; + + public Fruit() { + } + + public Fruit(String name) { + this.name = name; + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/narayana/quarkus/FruitResource.java b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/narayana/quarkus/FruitResource.java new file mode 100644 index 0000000000000..97ee669fb678a --- /dev/null +++ b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/narayana/quarkus/FruitResource.java @@ -0,0 +1,129 @@ +package io.quarkus.narayana.quarkus; + +import java.util.List; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.event.Event; +import jakarta.inject.Inject; +import jakarta.persistence.EntityManager; +import jakarta.transaction.Transactional; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.ExceptionMapper; +import jakarta.ws.rs.ext.Provider; + +import org.jboss.logging.Logger; +import org.jboss.resteasy.annotations.jaxrs.PathParam; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; + +@Path("fruits") +@ApplicationScoped +@Produces("application/json") +@Consumes("application/json") +public class FruitResource { + + private static final Logger LOGGER = Logger.getLogger(FruitResource.class.getName()); + + @Inject + EntityManager entityManager; + + @Inject + Event eventBus; + + @GET + public List get() { + return entityManager.createNamedQuery("Fruits.findAll", Fruit.class) + .getResultList(); + } + + @GET + @Path("{id}") + public Fruit getSingle(@PathParam Integer id) { + Fruit entity = entityManager.find(Fruit.class, id); + if (entity == null) { + throw new WebApplicationException("Fruit with id of " + id + " does not exist.", 404); + } + return entity; + } + + @POST + @Transactional + public Response create(Fruit fruit) { + if (fruit.getId() != null) { + throw new WebApplicationException("Id was invalidly set on request.", 422); + } + + entityManager.persist(fruit); + eventBus.fire(fruit); + return Response.ok(fruit).status(201).build(); + } + + @PUT + @Path("{id}") + @Transactional + public Fruit update(@PathParam Integer id, Fruit fruit) { + if (fruit.getName() == null) { + throw new WebApplicationException("Fruit Name was not set on request.", 422); + } + + Fruit entity = entityManager.find(Fruit.class, id); + + if (entity == null) { + throw new WebApplicationException("Fruit with id of " + id + " does not exist.", 404); + } + + entity.setName(fruit.getName()); + return entity; + } + + @DELETE + @Path("{id}") + @Transactional + public Response delete(@PathParam Integer id) { + Fruit entity = entityManager.getReference(Fruit.class, id); + if (entity == null) { + throw new WebApplicationException("Fruit with id of " + id + " does not exist.", 404); + } + entityManager.remove(entity); + return Response.status(204).build(); + } + + @Provider + public static class ErrorMapper implements ExceptionMapper { + + @Inject + ObjectMapper objectMapper; + + @Override + public Response toResponse(Exception exception) { + LOGGER.error("Failed to handle request", exception); + + int code = 500; + if (exception instanceof WebApplicationException) { + code = ((WebApplicationException) exception).getResponse().getStatus(); + } + + ObjectNode exceptionJson = objectMapper.createObjectNode(); + exceptionJson.put("exceptionType", exception.getClass().getName()); + exceptionJson.put("code", code); + + if (exception.getMessage() != null) { + exceptionJson.put("error", exception.getMessage()); + } + + return Response.status(code) + .entity(exceptionJson) + .build(); + } + + } +} diff --git a/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/narayana/quarkus/TSRTest.java b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/narayana/quarkus/TSRTest.java new file mode 100644 index 0000000000000..eb5a2d7b078cc --- /dev/null +++ b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/narayana/quarkus/TSRTest.java @@ -0,0 +1,213 @@ +package io.quarkus.narayana.quarkus; + +import java.util.ArrayList; +import java.util.List; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.event.Event; +import jakarta.enterprise.event.Observes; +import jakarta.enterprise.event.TransactionPhase; +import jakarta.inject.Inject; +import jakarta.persistence.EntityManager; +import jakarta.transaction.HeuristicMixedException; +import jakarta.transaction.HeuristicRollbackException; +import jakarta.transaction.NotSupportedException; +import jakarta.transaction.RollbackException; +import jakarta.transaction.Synchronization; +import jakarta.transaction.SystemException; +import jakarta.transaction.TransactionManager; +import jakarta.transaction.TransactionSynchronizationRegistry; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; + +/** + * Test that interposed synchronizations are called in the correct order + * See {@code AgroalOrderedLastSynchronizationList} for the implementation + */ +public class TSRTest { + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(Fruit.class, FruitResource.class) + .addAsResource("application-tsr.properties")); + + @Inject + TransactionSynchronizationRegistry tsr; + + @Inject + TransactionManager tm; + + @Inject + Event event; + + private enum SYNCH_TYPES { + AGROAL, + HIBERNATE, + OTHER + }; + + private static final List synchronizationCallbacks = new ArrayList<>(); + + @BeforeEach + public void before() { + synchronizationCallbacks.clear(); + } + + @Test + public void test() throws SystemException, NotSupportedException, HeuristicRollbackException, HeuristicMixedException, + RollbackException { + tm.begin(); + + RestAssured.given() + .when() + .body("{\"name\" : \"Pear\"}") + .contentType("application/json") + .post("/fruits") + .then() + .statusCode(201); + + // register a synchronization that registers more synchronizations during the beforeCompletion callback + tsr.registerInterposedSynchronization(new Synchronization() { + @Override + public void beforeCompletion() { + synchronizationCallbacks.add(SYNCH_TYPES.OTHER.name()); + + // Add another synchronization belonging to the same "category". + // This registration should succeed since it belongs to the same group that's currently being processed. + // But note that adding one for a group that has already finished should fail (but we cannot test that + // here since the other groups belong to different packages, ie hibernate and agroal). + tsr.registerInterposedSynchronization(new NormalSynchronization()); + } + + @Override + public void afterCompletion(int status) { + } + }); + + // cause ARC to register a callback for transaction lifecycle events (see ObservingBean), but since ARC + // uses a session synchronization this should *not* result in an interposed synchronization being registered + event.fire("commit"); + + tm.commit(); + + /* + * Check that the two normal synchronizations added by this test were invoked. + * The actual list is maintained by {@code AgroalOrderedLastSynchronizationList} + * and it will also include interposed synchronizations added by hibernate + * and Agroal as a result of calling the above hibernate query. + * If you want to verify that the order is correct then run the test under + * the control of a debugger and look at the order of the list maintained + * by the AgroalOrderedLastSynchronizationList class. + */ + Assertions.assertEquals(2, synchronizationCallbacks.size()); + Assertions.assertEquals(SYNCH_TYPES.OTHER.name(), synchronizationCallbacks.get(0)); + Assertions.assertEquals(SYNCH_TYPES.OTHER.name(), synchronizationCallbacks.get(1)); + } + + @Test + public void testException() + throws SystemException, NotSupportedException, HeuristicRollbackException, HeuristicMixedException, + RollbackException { + final String MESSAGE = "testException from synchronization"; + final NormalSynchronization normalSynchronization = new NormalSynchronization(); + + tm.begin(); + + RestAssured.given() + .when() + .body("{\"name\" : \"Orange\"}") // use a different fruit from the other tests in this suite + .contentType("application/json") + .post("/fruits") + .then() + .statusCode(201); + + // register a synchronization that registers more synchronizations during the beforeCompletion callback + tsr.registerInterposedSynchronization(new Synchronization() { + @Override + public void beforeCompletion() { + synchronizationCallbacks.add(SYNCH_TYPES.OTHER.name()); + + // Add another synchronization belonging to the same "category". + // This registration should succeed since it belongs to the same group that's currently being processed. + // But note that adding one for a group that has already finished should fail (but we cannot test that + // here since the other groups belong to different packages, ie hibernate and agroal). + tsr.registerInterposedSynchronization(normalSynchronization); + + // throw an exception to verify that the other beforeCompletion synchronizations still execute + throw new RuntimeException(MESSAGE); + } + + @Override + public void afterCompletion(int status) { + } + }); + + try { + tm.commit(); + + Assertions.fail("Expected commit to throw an exception"); + } catch (RollbackException | HeuristicMixedException | HeuristicRollbackException | SecurityException + | IllegalStateException | SystemException e) { + Assertions.assertNotNull(e.getCause(), "expected exception cause to be present"); + Assertions.assertTrue(e.getCause().getMessage().endsWith(MESSAGE), + "expected a different exception message"); + + // the synchronization registered a synchronization (the variable normalSynchronization) + // just before it threw the exception so now check that it was still called: + Assertions.assertTrue(normalSynchronization.wasInvoked(), + "the synchronization registered before the exception should have ran"); + + /* + * Check that the two normal synchronizations added by this test were invoked. + * The actual list is maintained by {@code AgroalOrderedLastSynchronizationList} + * and it will also include interposed synchronizations added by hibernate + * and Agroal as a result of calling the above hibernate query. + * If you want to verify that the order is correct then run the test under + * the control of a debugger and look at the order of the list maintained + * by the AgroalOrderedLastSynchronizationList class. + */ + Assertions.assertEquals(2, synchronizationCallbacks.size()); + Assertions.assertEquals(SYNCH_TYPES.OTHER.name(), synchronizationCallbacks.get(0)); + Assertions.assertEquals(SYNCH_TYPES.OTHER.name(), synchronizationCallbacks.get(1)); + } + } + + @ApplicationScoped + static class ObservingBean { + @Inject + EntityManager entityManager; + + // observing beforeCompletion is what triggered the issue about the need to order Agroal synchronizations last + public void observeBeforeCompletion(@Observes(during = TransactionPhase.BEFORE_COMPLETION) String payload) { + final var list = entityManager.createNamedQuery("Fruits.findAll", Fruit.class).getResultList(); + Assertions.assertFalse(list.isEmpty()); // a Pear should have been added + } + } + + // define another synchronization to test various things such as verifying that synchronizations can be + // registered by other synchronizations and that later synchronizations still run even though earlier ones + // may have thrown exceptions + private static class NormalSynchronization implements Synchronization { + private boolean invoked; + + @Override + public void beforeCompletion() { + synchronizationCallbacks.add(SYNCH_TYPES.OTHER.name()); + invoked = true; + } + + @Override + public void afterCompletion(int status) { + } + + public boolean wasInvoked() { + return invoked; + } + } +} diff --git a/extensions/hibernate-orm/deployment/src/test/resources/application-tsr.properties b/extensions/hibernate-orm/deployment/src/test/resources/application-tsr.properties new file mode 100644 index 0000000000000..4254ad3a251f5 --- /dev/null +++ b/extensions/hibernate-orm/deployment/src/test/resources/application-tsr.properties @@ -0,0 +1,5 @@ +quarkus.datasource.db-kind=h2 +quarkus.datasource.jdbc.url=jdbc:h2:mem:test + +quarkus.hibernate-orm.database.generation=drop-and-create +quarkus.hibernate-orm.conn.packages=io.quarkus.narayana.quarkus diff --git a/extensions/hibernate-orm/runtime/pom.xml b/extensions/hibernate-orm/runtime/pom.xml index 264d3f851db53..5db120b401cb0 100644 --- a/extensions/hibernate-orm/runtime/pom.xml +++ b/extensions/hibernate-orm/runtime/pom.xml @@ -59,20 +59,6 @@ org.hibernate.orm hibernate-core - - - org.glassfish.jaxb - jaxb-runtime - - - jakarta.activation - jakarta.activation-api - - - io.smallrye - jandex - - @@ -102,12 +88,6 @@ org.glassfish.jaxb jaxb-runtime - - - jakarta.xml.bind - jakarta.xml.bind-api - - jakarta.xml.bind @@ -143,6 +123,21 @@ io.quarkus quarkus-caffeine + + io.quarkus + quarkus-jackson + true + + + io.quarkus + quarkus-jsonb + true + + + io.quarkus + quarkus-jaxb + true + diff --git a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/JsonFormat.java b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/JsonFormat.java new file mode 100644 index 0000000000000..54d036ba9f92b --- /dev/null +++ b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/JsonFormat.java @@ -0,0 +1,37 @@ +package io.quarkus.hibernate.orm; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import jakarta.enterprise.util.AnnotationLiteral; +import jakarta.inject.Qualifier; + +import org.hibernate.type.format.FormatMapper; + +/** + * CDI qualifier for beans implementing a {@link FormatMapper}. + *

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

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

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

    + * Must be used in a combination with a {@link PersistenceUnitExtension} qualifier to define the persistence + * unit the mapper should be associated with. + */ +@Target({ TYPE, FIELD, METHOD, PARAMETER }) +@Retention(RUNTIME) +@Documented +@Qualifier +public @interface XmlFormat { + class Literal extends AnnotationLiteral implements XmlFormat { + public static XmlFormat INSTANCE = new Literal(); + + private Literal() { + } + } +} diff --git a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/PersistenceUnitUtil.java b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/PersistenceUnitUtil.java index caf85a8de7249..b91894c052e68 100644 --- a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/PersistenceUnitUtil.java +++ b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/PersistenceUnitUtil.java @@ -1,5 +1,7 @@ package io.quarkus.hibernate.orm.runtime; +import java.lang.annotation.Annotation; +import java.util.Arrays; import java.util.Comparator; import java.util.Locale; @@ -22,8 +24,10 @@ public static boolean isDefaultPersistenceUnit(String name) { } public static InjectableInstance singleExtensionInstanceForPersistenceUnit(Class beanType, - String persistenceUnitName) { - InjectableInstance instance = extensionInstanceForPersistenceUnit(beanType, persistenceUnitName); + String persistenceUnitName, + Annotation... additionalQualifiers) { + InjectableInstance instance = extensionInstanceForPersistenceUnit(beanType, persistenceUnitName, + additionalQualifiers); if (instance.isAmbiguous()) { throw new IllegalStateException(String.format(Locale.ROOT, "Multiple instances of %1$s were found for persistence unit %2$s. " @@ -33,9 +37,15 @@ public static InjectableInstance singleExtensionInstanceForPersistenceUni return instance; } - public static InjectableInstance extensionInstanceForPersistenceUnit(Class beanType, String persistenceUnitName) { - return Arc.container().select(beanType, - new PersistenceUnitExtension.Literal(persistenceUnitName)); + public static InjectableInstance extensionInstanceForPersistenceUnit(Class beanType, String persistenceUnitName, + Annotation... additionalQualifiers) { + if (additionalQualifiers.length == 0) { + return Arc.container().select(beanType, new PersistenceUnitExtension.Literal(persistenceUnitName)); + } else { + Annotation[] qualifiers = Arrays.copyOf(additionalQualifiers, additionalQualifiers.length + 1); + qualifiers[additionalQualifiers.length] = new PersistenceUnitExtension.Literal(persistenceUnitName); + return Arc.container().select(beanType, qualifiers); + } } public static class PersistenceUnitNameComparator implements Comparator { diff --git a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/ProviderUtil.java b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/ProviderUtil.java index 357883ed67fe6..8d5b8eaf237ae 100644 --- a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/ProviderUtil.java +++ b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/ProviderUtil.java @@ -20,6 +20,6 @@ public LoadState isLoadedWithReference(Object proxy, String property) { @Override public LoadState isLoaded(Object o) { - return PersistenceUtilHelper.isLoaded(o); + return PersistenceUtilHelper.getLoadState(o); } } diff --git a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/StatelessSessionLazyDelegator.java b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/StatelessSessionLazyDelegator.java index d9ccac4fec5a6..fa815d3fe5d43 100644 --- a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/StatelessSessionLazyDelegator.java +++ b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/StatelessSessionLazyDelegator.java @@ -1,15 +1,20 @@ package io.quarkus.hibernate.orm.runtime; +import java.util.List; import java.util.function.Supplier; +import jakarta.persistence.EntityGraph; import jakarta.persistence.criteria.CriteriaDelete; import jakarta.persistence.criteria.CriteriaQuery; import jakarta.persistence.criteria.CriteriaUpdate; import org.hibernate.HibernateException; import org.hibernate.LockMode; +import org.hibernate.SessionFactory; import org.hibernate.StatelessSession; import org.hibernate.Transaction; +import org.hibernate.graph.GraphSemantic; +import org.hibernate.graph.RootGraph; import org.hibernate.jdbc.ReturningWork; import org.hibernate.jdbc.Work; import org.hibernate.procedure.ProcedureCall; @@ -116,6 +121,11 @@ public String getTenantIdentifier() { return delegate.get().getTenantIdentifier(); } + @Override + public Object getTenantIdentifierValue() { + return delegate.get().getTenantIdentifier(); + } + @Override public boolean isOpen() { return delegate.get().isOpen(); @@ -349,4 +359,54 @@ public NativeQuery getNamedNativeQuery(String name) { public NativeQuery getNamedNativeQuery(String name, String resultSetMapping) { return delegate.get().getNamedNativeQuery(name, resultSetMapping); } + + @Override + public RootGraph createEntityGraph(Class rootType) { + return delegate.get().createEntityGraph(rootType); + } + + @Override + public RootGraph createEntityGraph(String graphName) { + return delegate.get().createEntityGraph(graphName); + } + + @Override + public RootGraph createEntityGraph(Class rootType, String graphName) { + return delegate.get().createEntityGraph(rootType, graphName); + } + + @Override + public RootGraph getEntityGraph(String graphName) { + return delegate.get().getEntityGraph(graphName); + } + + @Override + public List> getEntityGraphs(Class entityClass) { + return delegate.get().getEntityGraphs(entityClass); + } + + @Override + public SessionFactory getFactory() { + return delegate.get().getFactory(); + } + + @Override + public void upsert(Object entity) { + delegate.get().upsert(entity); + } + + @Override + public void upsert(String entityName, Object entity) { + delegate.get().upsert(entityName, entity); + } + + @Override + public T get(EntityGraph graph, GraphSemantic graphSemantic, Object id) { + return delegate.get().get(graph, graphSemantic, id); + } + + @Override + public T get(EntityGraph graph, GraphSemantic graphSemantic, Object id, LockMode lockMode) { + return delegate.get().get(graph, graphSemantic, id, lockMode); + } } diff --git a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/boot/FastBootEntityManagerFactoryBuilder.java b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/boot/FastBootEntityManagerFactoryBuilder.java index 52db112ee6e5f..0e1d9307ad580 100644 --- a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/boot/FastBootEntityManagerFactoryBuilder.java +++ b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/boot/FastBootEntityManagerFactoryBuilder.java @@ -30,8 +30,11 @@ import org.hibernate.tool.schema.spi.CommandAcceptanceException; import org.hibernate.tool.schema.spi.DelayedDropRegistryNotAvailableImpl; import org.hibernate.tool.schema.spi.SchemaManagementToolCoordinator; +import org.hibernate.type.format.FormatMapper; import io.quarkus.arc.InjectableInstance; +import io.quarkus.hibernate.orm.JsonFormat; +import io.quarkus.hibernate.orm.XmlFormat; import io.quarkus.hibernate.orm.runtime.PersistenceUnitUtil; import io.quarkus.hibernate.orm.runtime.RuntimeSettings; import io.quarkus.hibernate.orm.runtime.migration.MultiTenancyStrategy; @@ -211,6 +214,17 @@ protected void populate(String persistenceUnitName, SessionFactoryOptionsBuilder if (!statementInspectorInstance.isUnsatisfied()) { options.applyStatementInspector(statementInspectorInstance.get()); } + + InjectableInstance jsonFormatMapper = PersistenceUnitUtil.singleExtensionInstanceForPersistenceUnit( + FormatMapper.class, persistenceUnitName, JsonFormat.Literal.INSTANCE); + if (!jsonFormatMapper.isUnsatisfied()) { + options.applyJsonFormatMapper(jsonFormatMapper.get()); + } + InjectableInstance xmlFormatMapper = PersistenceUnitUtil.singleExtensionInstanceForPersistenceUnit( + FormatMapper.class, persistenceUnitName, XmlFormat.Literal.INSTANCE); + if (!xmlFormatMapper.isUnsatisfied()) { + options.applyXmlFormatMapper(xmlFormatMapper.get()); + } } private static class ServiceRegistryCloser implements SessionFactoryObserver { diff --git a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/boot/FastBootMetadataBuilder.java b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/boot/FastBootMetadataBuilder.java index 3eabdd8e7bc79..33f24af8d789b 100644 --- a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/boot/FastBootMetadataBuilder.java +++ b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/boot/FastBootMetadataBuilder.java @@ -399,6 +399,15 @@ private MergedSettings mergeSettings(QuarkusPersistenceUnitDefinition puDefiniti } } + // If there's any mapping lib that we can work with available we'll set the default mapper: + if (puDefinition.getJsonMapperCreator().isPresent()) { + cfg.put(AvailableSettings.JSON_FORMAT_MAPPER, puDefinition.getJsonMapperCreator().get().create()); + } + // If there's any mapping lib that we can work with available we'll set the default mapper: + if (puDefinition.getXmlMapperCreator().isPresent()) { + cfg.put(AvailableSettings.XML_FORMAT_MAPPER, puDefinition.getXmlMapperCreator().get().create()); + } + return mergedSettings; } diff --git a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/boot/QuarkusPersistenceUnitDefinition.java b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/boot/QuarkusPersistenceUnitDefinition.java index a59c32123a15b..ce7bf595a82ba 100644 --- a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/boot/QuarkusPersistenceUnitDefinition.java +++ b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/boot/QuarkusPersistenceUnitDefinition.java @@ -2,10 +2,12 @@ import java.util.List; import java.util.Objects; +import java.util.Optional; import org.hibernate.jpa.boot.spi.PersistenceUnitDescriptor; import io.quarkus.hibernate.orm.runtime.boot.xml.RecordableXmlMapping; +import io.quarkus.hibernate.orm.runtime.customized.FormatMapperKind; import io.quarkus.hibernate.orm.runtime.integration.HibernateOrmIntegrationStaticDescriptor; import io.quarkus.hibernate.orm.runtime.recording.RecordedConfig; import io.quarkus.runtime.annotations.RecordableConstructor; @@ -21,12 +23,16 @@ public final class QuarkusPersistenceUnitDefinition { private final List xmlMappings; private final boolean isReactive; private final boolean fromPersistenceXml; + private final Optional jsonMapperCreator; + private final Optional xmlMapperCreator; private final List integrationStaticDescriptors; public QuarkusPersistenceUnitDefinition(PersistenceUnitDescriptor persistenceUnitDescriptor, String configurationName, RecordedConfig config, List xmlMappings, boolean isReactive, boolean fromPersistenceXml, + Optional jsonMapperCreator, + Optional xmlMapperCreator, List integrationStaticDescriptors) { Objects.requireNonNull(persistenceUnitDescriptor); Objects.requireNonNull(config); @@ -36,6 +42,8 @@ public QuarkusPersistenceUnitDefinition(PersistenceUnitDescriptor persistenceUni this.xmlMappings = xmlMappings; this.isReactive = isReactive; this.fromPersistenceXml = fromPersistenceXml; + this.jsonMapperCreator = jsonMapperCreator; + this.xmlMapperCreator = xmlMapperCreator; this.integrationStaticDescriptors = integrationStaticDescriptors; } @@ -45,6 +53,8 @@ public QuarkusPersistenceUnitDefinition(RuntimePersistenceUnitDescriptor actualH List xmlMappings, boolean reactive, boolean fromPersistenceXml, + Optional jsonMapperCreator, + Optional xmlMapperCreator, List integrationStaticDescriptors) { Objects.requireNonNull(actualHibernateDescriptor); Objects.requireNonNull(config); @@ -53,6 +63,8 @@ public QuarkusPersistenceUnitDefinition(RuntimePersistenceUnitDescriptor actualH this.xmlMappings = xmlMappings; this.isReactive = reactive; this.fromPersistenceXml = fromPersistenceXml; + this.jsonMapperCreator = jsonMapperCreator; + this.xmlMapperCreator = xmlMapperCreator; this.integrationStaticDescriptors = integrationStaticDescriptors; } @@ -81,6 +93,14 @@ public boolean isFromPersistenceXml() { return fromPersistenceXml; } + public Optional getJsonMapperCreator() { + return jsonMapperCreator; + } + + public Optional getXmlMapperCreator() { + return xmlMapperCreator; + } + public List getIntegrationStaticDescriptors() { return integrationStaticDescriptors; } diff --git a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/boot/RuntimePersistenceUnitDescriptor.java b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/boot/RuntimePersistenceUnitDescriptor.java index d8895698bfe27..a6845708ccf4e 100644 --- a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/boot/RuntimePersistenceUnitDescriptor.java +++ b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/boot/RuntimePersistenceUnitDescriptor.java @@ -11,6 +11,7 @@ import jakarta.persistence.spi.PersistenceUnitTransactionType; import org.hibernate.bytecode.enhance.spi.EnhancementContext; +import org.hibernate.bytecode.spi.ClassTransformer; import org.hibernate.jpa.boot.spi.PersistenceUnitDescriptor; import io.quarkus.runtime.annotations.RecordableConstructor; @@ -193,4 +194,10 @@ public String toString() { + ", validationMode=" + validationMode + ", sharedCachemode=" + sharedCacheMode + ", managedClassNames=" + managedClassNames + ", properties=" + properties + '}'; } + + @Override + public ClassTransformer getClassTransformer() { + // We transform classes during the build, not on bootstrap. + return null; + } } diff --git a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/config/DialectVersions.java b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/config/DialectVersions.java index 845b7f3ffa104..0e853fac4ae50 100644 --- a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/config/DialectVersions.java +++ b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/config/DialectVersions.java @@ -16,12 +16,13 @@ public final class DialectVersions { public static final class Defaults { // The following constants must be at least equal to the default dialect version in Hibernate ORM + // These constants must be removed as soon as Hibernate ORM's minimum requirements become + // greater than or equal to these versions. public static final String MARIADB = "10.6"; public static final String MSSQL = "13"; // 2016 - public static final String MYSQL = "8"; - public static final String ORACLE = "12"; // This must be aligned on the H2 version in the Quarkus BOM + // This must never be removed public static final String H2 = "2.2.224"; private Defaults() { diff --git a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/customized/FormatMapperKind.java b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/customized/FormatMapperKind.java new file mode 100644 index 0000000000000..1dec3cf5413e4 --- /dev/null +++ b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/customized/FormatMapperKind.java @@ -0,0 +1,38 @@ +package io.quarkus.hibernate.orm.runtime.customized; + +import jakarta.json.bind.Jsonb; + +import org.hibernate.type.format.FormatMapper; +import org.hibernate.type.format.jackson.JacksonJsonFormatMapper; +import org.hibernate.type.format.jakartajson.JsonBJsonFormatMapper; +import org.hibernate.type.format.jaxb.JaxbXmlFormatMapper; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import io.quarkus.arc.Arc; + +public enum FormatMapperKind { + JACKSON { + @Override + public FormatMapper create() { + // NOTE: we are not creating a Jackson based XML mapper since that one + // requires an additional lib (jackson-dataformat-xml-2.15.2) being available + // as well as an XmlMapper instance instead of an ObjectMapper... + return new JacksonJsonFormatMapper(Arc.container().instance(ObjectMapper.class).get()); + } + }, + JSONB { + @Override + public FormatMapper create() { + return new JsonBJsonFormatMapper(Arc.container().instance(Jsonb.class).get()); + } + }, + JAXB { + @Override + public FormatMapper create() { + return new JaxbXmlFormatMapper(); + } + }; + + public abstract FormatMapper create(); +} diff --git a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/customized/QuarkusJtaPlatform.java b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/customized/QuarkusJtaPlatform.java index 0a5a419a8ee34..9828bcf9c2028 100644 --- a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/customized/QuarkusJtaPlatform.java +++ b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/customized/QuarkusJtaPlatform.java @@ -1,22 +1,25 @@ package io.quarkus.hibernate.orm.runtime.customized; +import static jakarta.transaction.Status.STATUS_ACTIVE; + import jakarta.transaction.Synchronization; import jakarta.transaction.SystemException; import jakarta.transaction.Transaction; import jakarta.transaction.TransactionManager; +import jakarta.transaction.TransactionSynchronizationRegistry; import jakarta.transaction.UserTransaction; -import org.hibernate.engine.transaction.jta.platform.internal.JtaSynchronizationStrategy; import org.hibernate.engine.transaction.jta.platform.internal.TransactionManagerAccess; -import org.hibernate.engine.transaction.jta.platform.internal.TransactionManagerBasedSynchronizationStrategy; import org.hibernate.engine.transaction.jta.platform.spi.JtaPlatform; +import org.hibernate.engine.transaction.jta.platform.spi.JtaPlatformException; + +import io.quarkus.arc.Arc; public final class QuarkusJtaPlatform implements JtaPlatform, TransactionManagerAccess { public static final QuarkusJtaPlatform INSTANCE = new QuarkusJtaPlatform(); - private final JtaSynchronizationStrategy tmSynchronizationStrategy = new TransactionManagerBasedSynchronizationStrategy( - this); + private volatile TransactionSynchronizationRegistry transactionSynchronizationRegistry; private volatile TransactionManager transactionManager; private volatile UserTransaction userTransaction; @@ -24,6 +27,16 @@ private QuarkusJtaPlatform() { //nothing } + public TransactionSynchronizationRegistry retrieveTransactionSynchronizationRegistry() { + TransactionSynchronizationRegistry transactionSynchronizationRegistry = this.transactionSynchronizationRegistry; + if (transactionSynchronizationRegistry == null) { + transactionSynchronizationRegistry = Arc.container().instance(TransactionSynchronizationRegistry.class).get(); + + this.transactionSynchronizationRegistry = transactionSynchronizationRegistry; + } + return transactionSynchronizationRegistry; + } + @Override public TransactionManager retrieveTransactionManager() { TransactionManager transactionManager = this.transactionManager; @@ -42,7 +55,7 @@ public TransactionManager getTransactionManager() { @Override public UserTransaction retrieveUserTransaction() { UserTransaction userTransaction = this.userTransaction; - if (this.userTransaction == null) { + if (userTransaction == null) { userTransaction = com.arjuna.ats.jta.UserTransaction.userTransaction(); this.userTransaction = userTransaction; } @@ -56,12 +69,17 @@ public Object getTransactionIdentifier(final Transaction transaction) { @Override public void registerSynchronization(Synchronization synchronization) { - this.tmSynchronizationStrategy.registerSynchronization(synchronization); + try { + getTransactionManager().getTransaction().registerSynchronization(synchronization); + } catch (Exception e) { + throw new JtaPlatformException("Could not access JTA Transaction to register synchronization", e); + } } @Override public boolean canRegisterSynchronization() { - return this.tmSynchronizationStrategy.canRegisterSynchronization(); + // no need to check STATUS_MARKED_ROLLBACK since synchronizations can't be registered in that state + return retrieveTransactionSynchronizationRegistry().getTransactionStatus() == STATUS_ACTIVE; } @Override diff --git a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/observers/SessionFactoryObserverForNamedQueryValidation.java b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/observers/SessionFactoryObserverForNamedQueryValidation.java index 2ba26bd5f14c6..129ce3126c14d 100644 --- a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/observers/SessionFactoryObserverForNamedQueryValidation.java +++ b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/observers/SessionFactoryObserverForNamedQueryValidation.java @@ -22,7 +22,7 @@ public SessionFactoryObserverForNamedQueryValidation(MetadataImplementor metadat public void sessionFactoryCreated(SessionFactory factory) { SessionFactoryImplementor sessionFactory = (SessionFactoryImplementor) factory; final QueryEngine queryEngine = sessionFactory.getQueryEngine(); - queryEngine.prepare(sessionFactory, metadata); + queryEngine.getNamedObjectRepository().prepare(sessionFactory, metadata); if (sessionFactory.getSessionFactoryOptions().isNamedQueryStartupCheckingEnabled()) { queryEngine.validateNamedQueries(); } diff --git a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/recording/RecordableBootstrap.java b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/recording/RecordableBootstrap.java index 589018fa48c79..63bf1af0762fd 100644 --- a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/recording/RecordableBootstrap.java +++ b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/recording/RecordableBootstrap.java @@ -216,7 +216,7 @@ public StandardServiceRegistry build() { settingsCopy.putAll(settings); settingsCopy.put(org.hibernate.boot.cfgxml.spi.CfgXmlAccessService.LOADED_CONFIG_KEY, aggregatedCfgXml); - return new StandardServiceRegistryImpl(autoCloseRegistry, bootstrapServiceRegistry, initiators, + return StandardServiceRegistryImpl.create(autoCloseRegistry, bootstrapServiceRegistry, initiators, providedServices, settingsCopy); } diff --git a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/session/TransactionScopedSession.java b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/session/TransactionScopedSession.java index 93155248f8f71..9541b03d0bb8c 100644 --- a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/session/TransactionScopedSession.java +++ b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/session/TransactionScopedSession.java @@ -1142,6 +1142,13 @@ public String getTenantIdentifier() { } } + @Override + public Object getTenantIdentifierValue() { + try (SessionResult emr = acquireSession()) { + return emr.session.getTenantIdentifierValue(); + } + } + @Override public boolean isConnected() { checkBlocking(); @@ -1386,4 +1393,44 @@ public void close() { } } } + + @Override + public RootGraph createEntityGraph(Class rootType, String graphName) { + try (SessionResult emr = acquireSession()) { + return emr.session.createEntityGraph(rootType, graphName); + } + } + + @Override + public SessionFactory getFactory() { + return sessionFactory; + } + + @Override + public int getFetchBatchSize() { + try (SessionResult emr = acquireSession()) { + return emr.session.getFetchBatchSize(); + } + } + + @Override + public void setFetchBatchSize(int batchSize) { + try (SessionResult emr = acquireSession()) { + emr.session.setFetchBatchSize(batchSize); + } + } + + @Override + public boolean isSubselectFetchingEnabled() { + try (SessionResult emr = acquireSession()) { + return emr.session.isSubselectFetchingEnabled(); + } + } + + @Override + public void setSubselectFetchingEnabled(boolean enabled) { + try (SessionResult emr = acquireSession()) { + emr.session.setSubselectFetchingEnabled(enabled); + } + } } diff --git a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/session/TransactionScopedStatelessSession.java b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/session/TransactionScopedStatelessSession.java index 00ba865a6f07e..c20c509377e64 100644 --- a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/session/TransactionScopedStatelessSession.java +++ b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/session/TransactionScopedStatelessSession.java @@ -1,7 +1,10 @@ package io.quarkus.hibernate.orm.runtime.session; +import java.util.List; + import jakarta.enterprise.context.ContextNotActiveException; import jakarta.enterprise.inject.Instance; +import jakarta.persistence.EntityGraph; import jakarta.persistence.TransactionRequiredException; import jakarta.persistence.criteria.CriteriaDelete; import jakarta.persistence.criteria.CriteriaQuery; @@ -15,6 +18,8 @@ import org.hibernate.SessionFactory; import org.hibernate.StatelessSession; import org.hibernate.Transaction; +import org.hibernate.graph.GraphSemantic; +import org.hibernate.graph.RootGraph; import org.hibernate.jdbc.ReturningWork; import org.hibernate.jdbc.Work; import org.hibernate.procedure.ProcedureCall; @@ -396,6 +401,13 @@ public String getTenantIdentifier() { } } + @Override + public Object getTenantIdentifierValue() { + try (SessionResult emr = acquireSession()) { + return emr.statelessSession.getTenantIdentifierValue(); + } + } + @Override public boolean isConnected() { checkBlocking(); @@ -624,4 +636,84 @@ public void close() { } } } + + @Override + public RootGraph createEntityGraph(Class rootType) { + checkBlocking(); + try (SessionResult emr = acquireSession()) { + return emr.statelessSession.createEntityGraph(rootType); + } + } + + @Override + public RootGraph createEntityGraph(String graphName) { + checkBlocking(); + try (SessionResult emr = acquireSession()) { + return emr.statelessSession.createEntityGraph(graphName); + } + } + + @Override + public RootGraph createEntityGraph(Class rootType, String graphName) { + checkBlocking(); + try (SessionResult emr = acquireSession()) { + return emr.statelessSession.createEntityGraph(rootType, graphName); + } + } + + @Override + public RootGraph getEntityGraph(String graphName) { + checkBlocking(); + try (SessionResult emr = acquireSession()) { + return emr.statelessSession.getEntityGraph(graphName); + } + } + + @Override + public List> getEntityGraphs(Class entityClass) { + checkBlocking(); + try (SessionResult emr = acquireSession()) { + return emr.statelessSession.getEntityGraphs(entityClass); + } + } + + @Override + public SessionFactory getFactory() { + checkBlocking(); + try (SessionResult emr = acquireSession()) { + return emr.statelessSession.getFactory(); + } + } + + @Override + public void upsert(Object entity) { + checkBlocking(); + try (SessionResult emr = acquireSession()) { + emr.statelessSession.upsert(entity); + } + } + + @Override + public void upsert(String entityName, Object entity) { + checkBlocking(); + try (SessionResult emr = acquireSession()) { + emr.statelessSession.upsert(entityName, entity); + } + } + + @Override + public T get(EntityGraph graph, GraphSemantic graphSemantic, Object id) { + checkBlocking(); + try (SessionResult emr = acquireSession()) { + return emr.statelessSession.get(graph, graphSemantic, id); + } + } + + @Override + public T get(EntityGraph graph, GraphSemantic graphSemantic, Object id, LockMode lockMode) { + checkBlocking(); + try (SessionResult emr = acquireSession()) { + return emr.statelessSession.get(graph, graphSemantic, id, lockMode); + } + } } diff --git a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/tenant/HibernateCurrentTenantIdentifierResolver.java b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/tenant/HibernateCurrentTenantIdentifierResolver.java index 1989224a06406..66d38d6424791 100644 --- a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/tenant/HibernateCurrentTenantIdentifierResolver.java +++ b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/tenant/HibernateCurrentTenantIdentifierResolver.java @@ -15,7 +15,8 @@ * @author Michael Schnell * */ -public final class HibernateCurrentTenantIdentifierResolver implements CurrentTenantIdentifierResolver { +// TODO support other tenant ID types than String; see https://github.com/quarkusio/quarkus/issues/36831 +public final class HibernateCurrentTenantIdentifierResolver implements CurrentTenantIdentifierResolver { private static final Logger LOG = Logger.getLogger(HibernateCurrentTenantIdentifierResolver.class); diff --git a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/tenant/HibernateMultiTenantConnectionProvider.java b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/tenant/HibernateMultiTenantConnectionProvider.java index f29c007142fd4..8ea5f7ac188f4 100644 --- a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/tenant/HibernateMultiTenantConnectionProvider.java +++ b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/tenant/HibernateMultiTenantConnectionProvider.java @@ -23,7 +23,8 @@ * * @author Michael Schnell */ -public final class HibernateMultiTenantConnectionProvider extends AbstractMultiTenantConnectionProvider { +// TODO support other tenant ID types than String; see https://github.com/quarkusio/quarkus/issues/36831 +public final class HibernateMultiTenantConnectionProvider extends AbstractMultiTenantConnectionProvider { private static final Logger LOG = Logger.getLogger(HibernateMultiTenantConnectionProvider.class); diff --git a/extensions/hibernate-reactive/deployment/src/main/java/io/quarkus/hibernate/reactive/deployment/HibernateReactiveProcessor.java b/extensions/hibernate-reactive/deployment/src/main/java/io/quarkus/hibernate/reactive/deployment/HibernateReactiveProcessor.java index 9dff2f96a0122..d8c2130bf2445 100644 --- a/extensions/hibernate-reactive/deployment/src/main/java/io/quarkus/hibernate/reactive/deployment/HibernateReactiveProcessor.java +++ b/extensions/hibernate-reactive/deployment/src/main/java/io/quarkus/hibernate/reactive/deployment/HibernateReactiveProcessor.java @@ -40,6 +40,7 @@ import io.quarkus.datasource.deployment.spi.DefaultDataSourceDbKindBuildItem; import io.quarkus.datasource.runtime.DataSourceBuildTimeConfig; import io.quarkus.datasource.runtime.DataSourcesBuildTimeConfig; +import io.quarkus.deployment.Capabilities; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.annotations.BuildSteps; @@ -131,6 +132,7 @@ public void buildReactivePersistenceUnit( ApplicationArchivesBuildItem applicationArchivesBuildItem, LaunchModeBuildItem launchMode, JpaModelBuildItem jpaModel, + Capabilities capabilities, BuildProducer systemProperties, BuildProducer nativeImageResources, BuildProducer hotDeploymentWatchedFiles, @@ -191,7 +193,7 @@ public void buildReactivePersistenceUnit( persistenceUnitConfig.unsupportedProperties), null, jpaModel.getXmlMappings(reactivePU.getName()), - true, false)); + true, false, capabilities)); } } diff --git a/extensions/hibernate-reactive/deployment/src/test/java/io/quarkus/hibernate/reactive/mapping/id/optimizer/optimizer/IdOptimizerDefaultNoneTest.java b/extensions/hibernate-reactive/deployment/src/test/java/io/quarkus/hibernate/reactive/mapping/id/optimizer/optimizer/IdOptimizerDefaultNoneTest.java index 2ff9f955a727c..fcc9da07c9937 100644 --- a/extensions/hibernate-reactive/deployment/src/test/java/io/quarkus/hibernate/reactive/mapping/id/optimizer/optimizer/IdOptimizerDefaultNoneTest.java +++ b/extensions/hibernate-reactive/deployment/src/test/java/io/quarkus/hibernate/reactive/mapping/id/optimizer/optimizer/IdOptimizerDefaultNoneTest.java @@ -2,6 +2,7 @@ import org.hibernate.id.enhanced.NoopOptimizer; import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import io.quarkus.hibernate.reactive.SchemaUtil; @@ -22,6 +23,7 @@ public class IdOptimizerDefaultNoneTest extends AbstractIdOptimizerDefaultTest { .overrideConfigKey("quarkus.hibernate-orm.mapping.id.optimizer.default", "none"); @Override + @Test @Disabled("The 'none' optimizer will produce a different stream of IDs (1 then 51 then 101 then ...)") public void ids(UniAsserter asserter) { super.ids(asserter); diff --git a/extensions/hibernate-search-orm-coordination-outbox-polling/deployment/src/main/java/io/quarkus/hibernate/search/orm/coordination/outboxpolling/deployment/HibernateSearchOutboxPollingClasses.java b/extensions/hibernate-search-orm-coordination-outbox-polling/deployment/src/main/java/io/quarkus/hibernate/search/orm/coordination/outboxpolling/deployment/HibernateSearchOutboxPollingClasses.java deleted file mode 100644 index 4d8d68f6b0aa8..0000000000000 --- a/extensions/hibernate-search-orm-coordination-outbox-polling/deployment/src/main/java/io/quarkus/hibernate/search/orm/coordination/outboxpolling/deployment/HibernateSearchOutboxPollingClasses.java +++ /dev/null @@ -1,28 +0,0 @@ -package io.quarkus.hibernate.search.orm.coordination.outboxpolling.deployment; - -import java.util.List; - -// Workaround for https://hibernate.atlassian.net/browse/HSEARCH-4450 -public final class HibernateSearchOutboxPollingClasses { - - private HibernateSearchOutboxPollingClasses() { - } - - public static final List JPA_MODEL_CLASSES = List.of( - "org.hibernate.search.mapper.orm.coordination.outboxpolling.cluster.impl.AgentType", - "org.hibernate.search.mapper.orm.coordination.outboxpolling.event.impl.OutboxEvent", - "org.hibernate.search.mapper.orm.coordination.outboxpolling.cluster.impl.Agent", - "org.hibernate.search.mapper.orm.coordination.outboxpolling.event.impl.OutboxEvent$Status", - "org.hibernate.search.mapper.orm.coordination.outboxpolling.cluster.impl.AgentState"); - - public static final List AVRO_GENERATED_CLASSES = List.of( - "org.hibernate.search.mapper.orm.coordination.outboxpolling.avro.generated.impl.DocumentRoutesDescriptorDto$Builder", - "org.hibernate.search.mapper.orm.coordination.outboxpolling.avro.generated.impl.DirtinessDescriptorDto$Builder", - "org.hibernate.search.mapper.orm.coordination.outboxpolling.avro.generated.impl.DirtinessDescriptorDto", - "org.hibernate.search.mapper.orm.coordination.outboxpolling.avro.generated.impl.DocumentRoutesDescriptorDto", - "org.hibernate.search.mapper.orm.coordination.outboxpolling.avro.generated.impl.DocumentRouteDescriptorDto$Builder", - "org.hibernate.search.mapper.orm.coordination.outboxpolling.avro.generated.impl.PojoIndexingQueueEventPayloadDto", - "org.hibernate.search.mapper.orm.coordination.outboxpolling.avro.generated.impl.DocumentRouteDescriptorDto", - "org.hibernate.search.mapper.orm.coordination.outboxpolling.avro.generated.impl.PojoIndexingQueueEventPayloadDto$Builder"); - -} diff --git a/extensions/hibernate-search-orm-coordination-outbox-polling/deployment/src/test/java/io/quarkus/hibernate/search/orm/coordination/outboxpolling/test/HibernateSearchOutboxPollingClassesTest.java b/extensions/hibernate-search-orm-coordination-outbox-polling/deployment/src/test/java/io/quarkus/hibernate/search/orm/coordination/outboxpolling/test/HibernateSearchOutboxPollingClassesTest.java deleted file mode 100644 index db3024409f88e..0000000000000 --- a/extensions/hibernate-search-orm-coordination-outbox-polling/deployment/src/test/java/io/quarkus/hibernate/search/orm/coordination/outboxpolling/test/HibernateSearchOutboxPollingClassesTest.java +++ /dev/null @@ -1,173 +0,0 @@ -package io.quarkus.hibernate.search.orm.coordination.outboxpolling.test; - -import static org.assertj.core.api.Assertions.assertThat; - -import java.io.IOException; -import java.net.URISyntaxException; -import java.net.URL; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.HashSet; -import java.util.Set; -import java.util.stream.Collectors; - -import org.apache.avro.specific.AvroGenerated; -import org.hibernate.search.mapper.orm.coordination.outboxpolling.cfg.HibernateOrmMapperOutboxPollingSettings; -import org.hibernate.search.mapper.orm.coordination.outboxpolling.cluster.impl.Agent; -import org.hibernate.search.mapper.orm.coordination.outboxpolling.event.impl.OutboxEvent; -import org.jboss.jandex.AnnotationInstance; -import org.jboss.jandex.AnnotationTarget; -import org.jboss.jandex.ClassInfo; -import org.jboss.jandex.DotName; -import org.jboss.jandex.FieldInfo; -import org.jboss.jandex.Index; -import org.jboss.jandex.MethodInfo; -import org.jboss.jandex.Type; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; - -import io.quarkus.deployment.index.IndexingUtil; -import io.quarkus.hibernate.search.orm.coordination.outboxpolling.deployment.HibernateSearchOutboxPollingClasses; - -/** - * Tests that hardcoded lists of classes stay up-to-date. - */ -public class HibernateSearchOutboxPollingClassesTest { - - private static Index searchOutboxPollingIndex; - - @BeforeAll - public static void index() throws IOException { - searchOutboxPollingIndex = IndexingUtil.indexJar(determineJarLocation(HibernateOrmMapperOutboxPollingSettings.class, - "hibernate-search-mapper-orm-coordination-outbox-polling")); - } - - @Test - public void testNoMissingAvroGeneratedClass() { - Set annotatedClasses = new HashSet<>(); - for (AnnotationInstance annotationInstance : searchOutboxPollingIndex - .getAnnotations(DotName.createSimple(AvroGenerated.class.getName()))) { - DotName className = extractDeclaringClass(annotationInstance.target()).name(); - annotatedClasses.add(className.toString()); - } - - assertThat(annotatedClasses).isNotEmpty(); - assertThat(HibernateSearchOutboxPollingClasses.AVRO_GENERATED_CLASSES) - .containsExactlyInAnyOrderElementsOf(annotatedClasses); - } - - @Test - public void testNoMissingJpaModelClass() { - Set modelClasses = collectModelClassesRecursively(searchOutboxPollingIndex, Set.of( - DotName.createSimple(OutboxEvent.class.getName()), - DotName.createSimple(Agent.class.getName()))); - - Set modelClassNames = modelClasses.stream().map(DotName::toString).collect(Collectors.toSet()); - - // Despite being referenced from entities, these types are not included in the JPA model. - modelClassNames.removeAll(Set.of( - "org.hibernate.search.mapper.orm.coordination.outboxpolling.cluster.impl.AgentReference", - "org.hibernate.search.mapper.orm.coordination.outboxpolling.event.impl.OutboxEventReference", - "org.hibernate.search.mapper.orm.coordination.outboxpolling.cluster.impl.ShardAssignmentDescriptor")); - - assertThat(modelClassNames).isNotEmpty(); - assertThat(HibernateSearchOutboxPollingClasses.JPA_MODEL_CLASSES) - .containsExactlyInAnyOrderElementsOf(modelClassNames); - } - - private static Path determineJarLocation(Class classFromJar, String jarName) { - URL url = classFromJar.getProtectionDomain().getCodeSource().getLocation(); - if (!url.getProtocol().equals("file")) { - throw new IllegalStateException(jarName + " JAR is not a local file? " + url); - } - try { - return Paths.get(url.toURI()); - } catch (URISyntaxException e) { - throw new IllegalStateException(e); - } - } - - private static ClassInfo extractDeclaringClass(AnnotationTarget target) { - switch (target.kind()) { - case CLASS: - return target.asClass(); - case FIELD: - return target.asField().declaringClass(); - case METHOD: - return target.asMethod().declaringClass(); - case METHOD_PARAMETER: - return target.asMethodParameter().method().declaringClass(); - case TYPE: - return extractDeclaringClass(target.asType().enclosingTarget()); - default: - throw new IllegalStateException("Unsupported annotation target kind: " + target.kind()); - } - } - - private static Set collectModelClassesRecursively(Index index, Set initialClasses) { - Set classes = new HashSet<>(); - for (DotName initialClass : initialClasses) { - collectModelClassesRecursively(index, initialClass, classes); - } - return classes; - } - - private static void collectModelClassesRecursively(Index index, DotName className, Set classes) { - if (className.toString().startsWith("java.")) { - return; - } - if (!classes.add(className)) { - return; - } - ClassInfo clazz = index.getClassByName(className); - collectModelClassesRecursively(index, clazz.superName(), classes); - for (DotName interfaceName : clazz.interfaceNames()) { - collectModelClassesRecursively(index, interfaceName, classes); - } - for (FieldInfo field : clazz.fields()) { - collectModelClassesRecursively(index, field.type(), classes); - } - for (FieldInfo field : clazz.fields()) { - collectModelClassesRecursively(index, field.type(), classes); - } - for (MethodInfo methodInfo : clazz.methods()) { - if (!methodInfo.parameterTypes().isEmpty()) { - // Definitely not a getter, just skip. - continue; - } - collectModelClassesRecursively(index, methodInfo.returnType(), classes); - } - } - - private static void collectModelClassesRecursively(Index index, Type type, Set classes) { - switch (type.kind()) { - case CLASS: - collectModelClassesRecursively(index, type.name(), classes); - break; - case ARRAY: - collectModelClassesRecursively(index, type.asArrayType().constituent(), classes); - break; - case TYPE_VARIABLE: - for (Type bound : type.asTypeVariable().bounds()) { - collectModelClassesRecursively(index, bound, classes); - } - break; - case WILDCARD_TYPE: - collectModelClassesRecursively(index, type.asWildcardType().extendsBound(), classes); - collectModelClassesRecursively(index, type.asWildcardType().superBound(), classes); - break; - case PARAMETERIZED_TYPE: - collectModelClassesRecursively(index, type.name(), classes); - for (Type argument : type.asParameterizedType().arguments()) { - collectModelClassesRecursively(index, argument, classes); - } - break; - case PRIMITIVE: - case VOID: - case UNRESOLVED_TYPE_VARIABLE: - case TYPE_VARIABLE_REFERENCE: - // Ignore - break; - } - } -} diff --git a/extensions/hibernate-search-orm-coordination-outbox-polling/deployment/src/test/java/io/quarkus/hibernate/search/orm/coordination/outboxpolling/test/configuration/defaultpu/package-info.java b/extensions/hibernate-search-orm-coordination-outbox-polling/deployment/src/test/java/io/quarkus/hibernate/search/orm/coordination/outboxpolling/test/configuration/defaultpu/package-info.java deleted file mode 100644 index c71837c25f1a4..0000000000000 --- a/extensions/hibernate-search-orm-coordination-outbox-polling/deployment/src/test/java/io/quarkus/hibernate/search/orm/coordination/outboxpolling/test/configuration/defaultpu/package-info.java +++ /dev/null @@ -1,4 +0,0 @@ -@PersistenceUnit(PersistenceUnit.DEFAULT) -package io.quarkus.hibernate.search.orm.coordination.outboxpolling.test.configuration.defaultpu; - -import io.quarkus.hibernate.orm.PersistenceUnit; \ No newline at end of file diff --git a/extensions/hibernate-search-orm-coordination-outbox-polling/deployment/src/test/java/io/quarkus/hibernate/search/orm/coordination/outboxpolling/test/configuration/pu1/package-info.java b/extensions/hibernate-search-orm-coordination-outbox-polling/deployment/src/test/java/io/quarkus/hibernate/search/orm/coordination/outboxpolling/test/configuration/pu1/package-info.java deleted file mode 100644 index 838f867c889c4..0000000000000 --- a/extensions/hibernate-search-orm-coordination-outbox-polling/deployment/src/test/java/io/quarkus/hibernate/search/orm/coordination/outboxpolling/test/configuration/pu1/package-info.java +++ /dev/null @@ -1,4 +0,0 @@ -@PersistenceUnit("pu1") -package io.quarkus.hibernate.search.orm.coordination.outboxpolling.test.configuration.pu1; - -import io.quarkus.hibernate.orm.PersistenceUnit; \ No newline at end of file diff --git a/extensions/hibernate-search-orm-elasticsearch/deployment/src/test/java/io/quarkus/hibernate/search/orm/elasticsearch/test/search/shard_failure/ShardFailureIgnoreDefaultTest.java b/extensions/hibernate-search-orm-elasticsearch/deployment/src/test/java/io/quarkus/hibernate/search/orm/elasticsearch/test/search/shard_failure/ShardFailureIgnoreDefaultTest.java index ad3623a7ebae5..bdffa9df0b591 100644 --- a/extensions/hibernate-search-orm-elasticsearch/deployment/src/test/java/io/quarkus/hibernate/search/orm/elasticsearch/test/search/shard_failure/ShardFailureIgnoreDefaultTest.java +++ b/extensions/hibernate-search-orm-elasticsearch/deployment/src/test/java/io/quarkus/hibernate/search/orm/elasticsearch/test/search/shard_failure/ShardFailureIgnoreDefaultTest.java @@ -1,6 +1,6 @@ package io.quarkus.hibernate.search.orm.elasticsearch.test.search.shard_failure; -import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import java.util.List; @@ -39,12 +39,13 @@ public void testShardFailureIgnored() { session.toEntityManager().persist(new MyEntity2("42")); }); QuarkusTransaction.joiningExisting().run(() -> { - assertThat(session.search(List.of(MyEntity1.class, MyEntity2.class)) + assertThatThrownBy(() -> session.search(List.of(MyEntity1.class, MyEntity2.class)) .where(f -> f.wildcard().field("text").matching("4*")) .fetchHits(20)) // MyEntity2 fails because "text" is an integer field there - // We expect that index (shard) to be ignored - .hasSize(1); + // We expect an exception + .hasMessageContaining("Elasticsearch request failed", + "\"type\": \"query_shard_exception\""); }); } } diff --git a/extensions/hibernate-search-orm-elasticsearch/deployment/src/test/java/io/quarkus/hibernate/search/orm/elasticsearch/test/search/shard_failure/ShardFailureIgnoreFalseTest.java b/extensions/hibernate-search-orm-elasticsearch/deployment/src/test/java/io/quarkus/hibernate/search/orm/elasticsearch/test/search/shard_failure/ShardFailureIgnoreTrueTest.java similarity index 77% rename from extensions/hibernate-search-orm-elasticsearch/deployment/src/test/java/io/quarkus/hibernate/search/orm/elasticsearch/test/search/shard_failure/ShardFailureIgnoreFalseTest.java rename to extensions/hibernate-search-orm-elasticsearch/deployment/src/test/java/io/quarkus/hibernate/search/orm/elasticsearch/test/search/shard_failure/ShardFailureIgnoreTrueTest.java index dca03bec0ad4e..0f4fb1d1bbe9a 100644 --- a/extensions/hibernate-search-orm-elasticsearch/deployment/src/test/java/io/quarkus/hibernate/search/orm/elasticsearch/test/search/shard_failure/ShardFailureIgnoreFalseTest.java +++ b/extensions/hibernate-search-orm-elasticsearch/deployment/src/test/java/io/quarkus/hibernate/search/orm/elasticsearch/test/search/shard_failure/ShardFailureIgnoreTrueTest.java @@ -1,6 +1,6 @@ package io.quarkus.hibernate.search.orm.elasticsearch.test.search.shard_failure; -import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.assertThat; import java.util.List; @@ -14,7 +14,7 @@ import io.quarkus.narayana.jta.QuarkusTransaction; import io.quarkus.test.QuarkusUnitTest; -public class ShardFailureIgnoreFalseTest { +public class ShardFailureIgnoreTrueTest { @RegisterExtension static QuarkusUnitTest runner = new QuarkusUnitTest() @@ -24,8 +24,8 @@ public class ShardFailureIgnoreFalseTest { .addClass(MyEntity2.class) .addAsResource("hsearch-4915/index2.json")) .withConfigurationResource("application.properties") - // Request that shard failures cause an exception instead of being ignored - .overrideConfigKey("quarkus.hibernate-search-orm.elasticsearch.query.shard-failure.ignore", "false") + // Request that shard failures be ignored + .overrideConfigKey("quarkus.hibernate-search-orm.elasticsearch.query.shard-failure.ignore", "true") // Override the type of the keyword field to integer, to create an error in one shard only. .overrideConfigKey( "quarkus.hibernate-search-orm.elasticsearch.indexes.\"MyEntity2\".schema-management.mapping-file", @@ -41,13 +41,12 @@ public void testShardFailureIgnored() { session.toEntityManager().persist(new MyEntity2("42")); }); QuarkusTransaction.joiningExisting().run(() -> { - assertThatThrownBy(() -> session.search(List.of(MyEntity1.class, MyEntity2.class)) + assertThat(session.search(List.of(MyEntity1.class, MyEntity2.class)) .where(f -> f.wildcard().field("text").matching("4*")) .fetchHits(20)) // MyEntity2 fails because "text" is an integer field there - // We expect an exception - .hasMessageContaining("Elasticsearch request failed", - "\"type\": \"query_shard_exception\""); + // We expect that index (shard) to be ignored + .hasSize(1); }); } } diff --git a/extensions/hibernate-search-orm-elasticsearch/runtime/pom.xml b/extensions/hibernate-search-orm-elasticsearch/runtime/pom.xml index 3ac25506451b0..16d8755270292 100644 --- a/extensions/hibernate-search-orm-elasticsearch/runtime/pom.xml +++ b/extensions/hibernate-search-orm-elasticsearch/runtime/pom.xml @@ -31,13 +31,7 @@ org.hibernate.search - hibernate-search-mapper-orm-orm6 - - - jakarta.activation - jakarta.activation-api - - + hibernate-search-mapper-orm jakarta.persistence diff --git a/extensions/hibernate-search-orm-elasticsearch/runtime/src/main/java/io/quarkus/hibernate/search/orm/elasticsearch/runtime/HibernateSearchElasticsearchRuntimeConfigPersistenceUnit.java b/extensions/hibernate-search-orm-elasticsearch/runtime/src/main/java/io/quarkus/hibernate/search/orm/elasticsearch/runtime/HibernateSearchElasticsearchRuntimeConfigPersistenceUnit.java index 7eef01aa32e97..c9b5b62c1f9d1 100644 --- a/extensions/hibernate-search-orm-elasticsearch/runtime/src/main/java/io/quarkus/hibernate/search/orm/elasticsearch/runtime/HibernateSearchElasticsearchRuntimeConfigPersistenceUnit.java +++ b/extensions/hibernate-search-orm-elasticsearch/runtime/src/main/java/io/quarkus/hibernate/search/orm/elasticsearch/runtime/HibernateSearchElasticsearchRuntimeConfigPersistenceUnit.java @@ -523,10 +523,8 @@ interface ElasticsearchQueryShardFailureConfig { /** * Whether partial shard failures are ignored (`true`) * or lead to Hibernate Search throwing an exception (`false`). - *

    - * Will default to `false` in Hibernate Search 7. */ - @WithDefault("true") + @WithDefault("false") boolean ignore(); } diff --git a/extensions/hibernate-search-orm-coordination-outbox-polling/deployment/pom.xml b/extensions/hibernate-search-orm-outbox-polling/deployment/pom.xml similarity index 95% rename from extensions/hibernate-search-orm-coordination-outbox-polling/deployment/pom.xml rename to extensions/hibernate-search-orm-outbox-polling/deployment/pom.xml index a276fa8659a55..b5f2c6ebadea8 100644 --- a/extensions/hibernate-search-orm-coordination-outbox-polling/deployment/pom.xml +++ b/extensions/hibernate-search-orm-outbox-polling/deployment/pom.xml @@ -3,14 +3,14 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> - quarkus-hibernate-search-orm-coordination-outbox-polling-parent + quarkus-hibernate-search-orm-outbox-polling-parent io.quarkus 999-SNAPSHOT 4.0.0 - quarkus-hibernate-search-orm-coordination-outbox-polling-deployment - Quarkus - Hibernate Search - ORM - Coordination - Outbox Polling - Deployment + quarkus-hibernate-search-orm-outbox-polling-deployment + Quarkus - Hibernate Search - ORM - Outbox Polling - Deployment @@ -27,7 +27,7 @@ io.quarkus - quarkus-hibernate-search-orm-coordination-outbox-polling + quarkus-hibernate-search-orm-outbox-polling diff --git a/extensions/hibernate-search-orm-coordination-outbox-polling/deployment/src/main/java/io/quarkus/hibernate/search/orm/coordination/outboxpolling/deployment/HibernateSearchOutboxPollingProcessor.java b/extensions/hibernate-search-orm-outbox-polling/deployment/src/main/java/io/quarkus/hibernate/search/orm/outboxpolling/deployment/HibernateSearchOutboxPollingProcessor.java similarity index 87% rename from extensions/hibernate-search-orm-coordination-outbox-polling/deployment/src/main/java/io/quarkus/hibernate/search/orm/coordination/outboxpolling/deployment/HibernateSearchOutboxPollingProcessor.java rename to extensions/hibernate-search-orm-outbox-polling/deployment/src/main/java/io/quarkus/hibernate/search/orm/outboxpolling/deployment/HibernateSearchOutboxPollingProcessor.java index 700e9eba79097..5e0275e7f12b0 100644 --- a/extensions/hibernate-search-orm-coordination-outbox-polling/deployment/src/main/java/io/quarkus/hibernate/search/orm/coordination/outboxpolling/deployment/HibernateSearchOutboxPollingProcessor.java +++ b/extensions/hibernate-search-orm-outbox-polling/deployment/src/main/java/io/quarkus/hibernate/search/orm/outboxpolling/deployment/HibernateSearchOutboxPollingProcessor.java @@ -1,10 +1,10 @@ -package io.quarkus.hibernate.search.orm.coordination.outboxpolling.deployment; +package io.quarkus.hibernate.search.orm.outboxpolling.deployment; import java.util.List; import java.util.Optional; -import org.hibernate.search.mapper.orm.coordination.outboxpolling.cfg.HibernateOrmMapperOutboxPollingSettings; -import org.hibernate.search.mapper.orm.coordination.outboxpolling.mapping.spi.HibernateOrmMapperOutboxPollingClasses; +import org.hibernate.search.mapper.orm.outboxpolling.cfg.HibernateOrmMapperOutboxPollingSettings; +import org.hibernate.search.mapper.orm.outboxpolling.mapping.spi.HibernateOrmMapperOutboxPollingClasses; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; @@ -14,19 +14,19 @@ import io.quarkus.deployment.builditem.AdditionalIndexedClassesBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; import io.quarkus.hibernate.orm.deployment.spi.AdditionalJpaModelBuildItem; -import io.quarkus.hibernate.search.orm.coordination.outboxpolling.runtime.HibernateSearchOutboxPollingBuildTimeConfig; -import io.quarkus.hibernate.search.orm.coordination.outboxpolling.runtime.HibernateSearchOutboxPollingRecorder; -import io.quarkus.hibernate.search.orm.coordination.outboxpolling.runtime.HibernateSearchOutboxPollingRuntimeConfig; import io.quarkus.hibernate.search.orm.elasticsearch.deployment.HibernateSearchElasticsearchPersistenceUnitConfiguredBuildItem; import io.quarkus.hibernate.search.orm.elasticsearch.deployment.HibernateSearchEnabled; import io.quarkus.hibernate.search.orm.elasticsearch.deployment.HibernateSearchIntegrationRuntimeConfiguredBuildItem; import io.quarkus.hibernate.search.orm.elasticsearch.deployment.HibernateSearchIntegrationStaticConfiguredBuildItem; import io.quarkus.hibernate.search.orm.elasticsearch.runtime.HibernateSearchElasticsearchBuildTimeConfigPersistenceUnit; +import io.quarkus.hibernate.search.orm.outboxpolling.runtime.HibernateSearchOutboxPollingBuildTimeConfig; +import io.quarkus.hibernate.search.orm.outboxpolling.runtime.HibernateSearchOutboxPollingRecorder; +import io.quarkus.hibernate.search.orm.outboxpolling.runtime.HibernateSearchOutboxPollingRuntimeConfig; @BuildSteps(onlyIf = HibernateSearchEnabled.class) class HibernateSearchOutboxPollingProcessor { - private static final String HIBERNATE_SEARCH_ORM_COORDINATION_OUTBOX_POLLING = "Hibernate Search ORM - Coordination - Outbox polling"; + private static final String HIBERNATE_SEARCH_ORM_COORDINATION_OUTBOX_POLLING = "Hibernate Search ORM - Outbox polling"; @BuildStep void registerInternalModel(BuildProducer additionalIndexedClasses, diff --git a/extensions/hibernate-search-orm-coordination-outbox-polling/deployment/src/test/java/io/quarkus/hibernate/search/orm/coordination/outboxpolling/test/configuration/ConfigPropertiesTest.java b/extensions/hibernate-search-orm-outbox-polling/deployment/src/test/java/io/quarkus/hibernate/search/orm/outboxpolling/test/configuration/ConfigPropertiesTest.java similarity index 95% rename from extensions/hibernate-search-orm-coordination-outbox-polling/deployment/src/test/java/io/quarkus/hibernate/search/orm/coordination/outboxpolling/test/configuration/ConfigPropertiesTest.java rename to extensions/hibernate-search-orm-outbox-polling/deployment/src/test/java/io/quarkus/hibernate/search/orm/outboxpolling/test/configuration/ConfigPropertiesTest.java index 303c91a68fd85..5f27fefbd8d15 100644 --- a/extensions/hibernate-search-orm-coordination-outbox-polling/deployment/src/test/java/io/quarkus/hibernate/search/orm/coordination/outboxpolling/test/configuration/ConfigPropertiesTest.java +++ b/extensions/hibernate-search-orm-outbox-polling/deployment/src/test/java/io/quarkus/hibernate/search/orm/outboxpolling/test/configuration/ConfigPropertiesTest.java @@ -1,4 +1,4 @@ -package io.quarkus.hibernate.search.orm.coordination.outboxpolling.test.configuration; +package io.quarkus.hibernate.search.orm.outboxpolling.test.configuration; import static java.util.Map.entry; import static org.assertj.core.api.Assertions.assertThat; @@ -8,16 +8,16 @@ import jakarta.inject.Inject; import org.hibernate.SessionFactory; -import org.hibernate.search.mapper.orm.coordination.outboxpolling.cfg.HibernateOrmMapperOutboxPollingSettings; -import org.hibernate.search.mapper.orm.coordination.outboxpolling.cfg.UuidGenerationStrategy; +import org.hibernate.search.mapper.orm.outboxpolling.cfg.HibernateOrmMapperOutboxPollingSettings; +import org.hibernate.search.mapper.orm.outboxpolling.cfg.UuidGenerationStrategy; import org.jboss.shrinkwrap.api.ShrinkWrap; import org.jboss.shrinkwrap.api.spec.JavaArchive; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import io.quarkus.hibernate.orm.PersistenceUnit; -import io.quarkus.hibernate.search.orm.coordination.outboxpolling.test.configuration.defaultpu.IndexedEntity; -import io.quarkus.hibernate.search.orm.coordination.outboxpolling.test.configuration.pu1.IndexedEntityForPU1; +import io.quarkus.hibernate.search.orm.outboxpolling.test.configuration.defaultpu.IndexedEntity; +import io.quarkus.hibernate.search.orm.outboxpolling.test.configuration.pu1.IndexedEntityForPU1; import io.quarkus.test.QuarkusUnitTest; /** @@ -37,7 +37,7 @@ public class ConfigPropertiesTest { .overrideConfigKey("quarkus.hibernate-search-orm.coordination.entity-mapping.agent.schema", "myagentschema") .overrideConfigKey("quarkus.hibernate-search-orm.coordination.entity-mapping.agent.table", "myagenttable") .overrideConfigKey("quarkus.hibernate-search-orm.coordination.entity-mapping.agent.uuid-gen-strategy", "random") - .overrideConfigKey("quarkus.hibernate-search-orm.coordination.entity-mapping.agent.uuid-type", "uuid-char") + .overrideConfigKey("quarkus.hibernate-search-orm.coordination.entity-mapping.agent.uuid-type", "char") .overrideConfigKey("quarkus.hibernate-search-orm.coordination.entity-mapping.outbox-event.catalog", "myoutboxeventcatalog") .overrideConfigKey("quarkus.hibernate-search-orm.coordination.entity-mapping.outbox-event.schema", @@ -46,7 +46,7 @@ public class ConfigPropertiesTest { "myoutboxeventtable") .overrideConfigKey("quarkus.hibernate-search-orm.coordination.entity-mapping.outbox-event.uuid-gen-strategy", "time") - .overrideConfigKey("quarkus.hibernate-search-orm.coordination.entity-mapping.outbox-event.uuid-type", "uuid-binary") + .overrideConfigKey("quarkus.hibernate-search-orm.coordination.entity-mapping.outbox-event.uuid-type", "binary") .overrideConfigKey("quarkus.hibernate-search-orm.coordination.event-processor.enabled", "false") .overrideConfigKey("quarkus.hibernate-search-orm.coordination.event-processor.shards.total-count", "10") .overrideConfigKey("quarkus.hibernate-search-orm.coordination.event-processor.shards.assigned", "1,2") @@ -70,7 +70,7 @@ public class ConfigPropertiesTest { .overrideConfigKey("quarkus.hibernate-search-orm.\"pu1\".coordination.entity-mapping.agent.uuid-gen-strategy", "time") .overrideConfigKey("quarkus.hibernate-search-orm.\"pu1\".coordination.entity-mapping.agent.uuid-type", - "uuid-binary") + "binary") .overrideConfigKey("quarkus.hibernate-search-orm.\"pu1\".coordination.entity-mapping.outbox-event.catalog", "myoutboxeventcatalogpu1") .overrideConfigKey("quarkus.hibernate-search-orm.\"pu1\".coordination.entity-mapping.outbox-event.schema", @@ -80,7 +80,7 @@ public class ConfigPropertiesTest { .overrideConfigKey( "quarkus.hibernate-search-orm.\"pu1\".coordination.entity-mapping.outbox-event.uuid-gen-strategy", "random") .overrideConfigKey("quarkus.hibernate-search-orm.\"pu1\".coordination.entity-mapping.outbox-event.uuid-type", - "uuid-char") + "char") .overrideConfigKey("quarkus.hibernate-search-orm.\"pu1\".coordination.event-processor.enabled", "false") .overrideConfigKey("quarkus.hibernate-search-orm.\"pu1\".coordination.event-processor.shards.total-count", "110") .overrideConfigKey("quarkus.hibernate-search-orm.\"pu1\".coordination.event-processor.shards.assigned", "11,12") @@ -144,7 +144,7 @@ public void root() { entry(HibernateOrmMapperOutboxPollingSettings.COORDINATION_ENTITY_MAPPING_AGENT_TABLE, "myagenttable"), entry(HibernateOrmMapperOutboxPollingSettings.COORDINATION_ENTITY_MAPPING_AGENT_UUID_GEN_STRATEGY, UuidGenerationStrategy.RANDOM), - entry(HibernateOrmMapperOutboxPollingSettings.COORDINATION_ENTITY_MAPPING_AGENT_UUID_TYPE, "uuid-char"), + entry(HibernateOrmMapperOutboxPollingSettings.COORDINATION_ENTITY_MAPPING_AGENT_UUID_TYPE, "char"), entry(HibernateOrmMapperOutboxPollingSettings.COORDINATION_ENTITY_MAPPING_OUTBOXEVENT_CATALOG, "myoutboxeventcatalog"), entry(HibernateOrmMapperOutboxPollingSettings.COORDINATION_ENTITY_MAPPING_OUTBOXEVENT_SCHEMA, @@ -154,7 +154,7 @@ public void root() { entry(HibernateOrmMapperOutboxPollingSettings.COORDINATION_ENTITY_MAPPING_OUTBOXEVENT_UUID_GEN_STRATEGY, UuidGenerationStrategy.TIME), entry(HibernateOrmMapperOutboxPollingSettings.COORDINATION_ENTITY_MAPPING_OUTBOXEVENT_UUID_TYPE, - "uuid-binary"), + "binary"), entry(HibernateOrmMapperOutboxPollingSettings.COORDINATION_EVENT_PROCESSOR_ENABLED, false), entry(HibernateOrmMapperOutboxPollingSettings.COORDINATION_EVENT_PROCESSOR_SHARDS_TOTAL_COUNT, 10), entry(HibernateOrmMapperOutboxPollingSettings.COORDINATION_EVENT_PROCESSOR_SHARDS_ASSIGNED, @@ -183,7 +183,7 @@ public void perNamedPersistenceUnit() { entry(HibernateOrmMapperOutboxPollingSettings.COORDINATION_ENTITY_MAPPING_AGENT_UUID_GEN_STRATEGY, UuidGenerationStrategy.TIME), entry(HibernateOrmMapperOutboxPollingSettings.COORDINATION_ENTITY_MAPPING_AGENT_UUID_TYPE, - "uuid-binary"), + "binary"), entry(HibernateOrmMapperOutboxPollingSettings.COORDINATION_ENTITY_MAPPING_OUTBOXEVENT_CATALOG, "myoutboxeventcatalogpu1"), entry(HibernateOrmMapperOutboxPollingSettings.COORDINATION_ENTITY_MAPPING_OUTBOXEVENT_SCHEMA, @@ -193,7 +193,7 @@ public void perNamedPersistenceUnit() { entry(HibernateOrmMapperOutboxPollingSettings.COORDINATION_ENTITY_MAPPING_OUTBOXEVENT_UUID_GEN_STRATEGY, UuidGenerationStrategy.RANDOM), entry(HibernateOrmMapperOutboxPollingSettings.COORDINATION_ENTITY_MAPPING_OUTBOXEVENT_UUID_TYPE, - "uuid-char"), + "char"), entry(HibernateOrmMapperOutboxPollingSettings.COORDINATION_EVENT_PROCESSOR_ENABLED, false), entry(HibernateOrmMapperOutboxPollingSettings.COORDINATION_EVENT_PROCESSOR_SHARDS_TOTAL_COUNT, 110), entry(HibernateOrmMapperOutboxPollingSettings.COORDINATION_EVENT_PROCESSOR_SHARDS_ASSIGNED, diff --git a/extensions/hibernate-search-orm-coordination-outbox-polling/deployment/src/test/java/io/quarkus/hibernate/search/orm/coordination/outboxpolling/test/configuration/defaultpu/IndexedEntity.java b/extensions/hibernate-search-orm-outbox-polling/deployment/src/test/java/io/quarkus/hibernate/search/orm/outboxpolling/test/configuration/defaultpu/IndexedEntity.java similarity index 81% rename from extensions/hibernate-search-orm-coordination-outbox-polling/deployment/src/test/java/io/quarkus/hibernate/search/orm/coordination/outboxpolling/test/configuration/defaultpu/IndexedEntity.java rename to extensions/hibernate-search-orm-outbox-polling/deployment/src/test/java/io/quarkus/hibernate/search/orm/outboxpolling/test/configuration/defaultpu/IndexedEntity.java index 12c78cae95b62..9ef8130717fe9 100644 --- a/extensions/hibernate-search-orm-coordination-outbox-polling/deployment/src/test/java/io/quarkus/hibernate/search/orm/coordination/outboxpolling/test/configuration/defaultpu/IndexedEntity.java +++ b/extensions/hibernate-search-orm-outbox-polling/deployment/src/test/java/io/quarkus/hibernate/search/orm/outboxpolling/test/configuration/defaultpu/IndexedEntity.java @@ -1,4 +1,4 @@ -package io.quarkus.hibernate.search.orm.coordination.outboxpolling.test.configuration.defaultpu; +package io.quarkus.hibernate.search.orm.outboxpolling.test.configuration.defaultpu; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; diff --git a/extensions/hibernate-search-orm-outbox-polling/deployment/src/test/java/io/quarkus/hibernate/search/orm/outboxpolling/test/configuration/defaultpu/package-info.java b/extensions/hibernate-search-orm-outbox-polling/deployment/src/test/java/io/quarkus/hibernate/search/orm/outboxpolling/test/configuration/defaultpu/package-info.java new file mode 100644 index 0000000000000..b17a6d5832953 --- /dev/null +++ b/extensions/hibernate-search-orm-outbox-polling/deployment/src/test/java/io/quarkus/hibernate/search/orm/outboxpolling/test/configuration/defaultpu/package-info.java @@ -0,0 +1,4 @@ +@PersistenceUnit(PersistenceUnit.DEFAULT) +package io.quarkus.hibernate.search.orm.outboxpolling.test.configuration.defaultpu; + +import io.quarkus.hibernate.orm.PersistenceUnit; \ No newline at end of file diff --git a/extensions/hibernate-search-orm-coordination-outbox-polling/deployment/src/test/java/io/quarkus/hibernate/search/orm/coordination/outboxpolling/test/configuration/pu1/IndexedEntityForPU1.java b/extensions/hibernate-search-orm-outbox-polling/deployment/src/test/java/io/quarkus/hibernate/search/orm/outboxpolling/test/configuration/pu1/IndexedEntityForPU1.java similarity index 82% rename from extensions/hibernate-search-orm-coordination-outbox-polling/deployment/src/test/java/io/quarkus/hibernate/search/orm/coordination/outboxpolling/test/configuration/pu1/IndexedEntityForPU1.java rename to extensions/hibernate-search-orm-outbox-polling/deployment/src/test/java/io/quarkus/hibernate/search/orm/outboxpolling/test/configuration/pu1/IndexedEntityForPU1.java index 4e0c818bd3e19..e77cd15e11cfb 100644 --- a/extensions/hibernate-search-orm-coordination-outbox-polling/deployment/src/test/java/io/quarkus/hibernate/search/orm/coordination/outboxpolling/test/configuration/pu1/IndexedEntityForPU1.java +++ b/extensions/hibernate-search-orm-outbox-polling/deployment/src/test/java/io/quarkus/hibernate/search/orm/outboxpolling/test/configuration/pu1/IndexedEntityForPU1.java @@ -1,4 +1,4 @@ -package io.quarkus.hibernate.search.orm.coordination.outboxpolling.test.configuration.pu1; +package io.quarkus.hibernate.search.orm.outboxpolling.test.configuration.pu1; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; diff --git a/extensions/hibernate-search-orm-outbox-polling/deployment/src/test/java/io/quarkus/hibernate/search/orm/outboxpolling/test/configuration/pu1/package-info.java b/extensions/hibernate-search-orm-outbox-polling/deployment/src/test/java/io/quarkus/hibernate/search/orm/outboxpolling/test/configuration/pu1/package-info.java new file mode 100644 index 0000000000000..4dab021a4b64a --- /dev/null +++ b/extensions/hibernate-search-orm-outbox-polling/deployment/src/test/java/io/quarkus/hibernate/search/orm/outboxpolling/test/configuration/pu1/package-info.java @@ -0,0 +1,4 @@ +@PersistenceUnit("pu1") +package io.quarkus.hibernate.search.orm.outboxpolling.test.configuration.pu1; + +import io.quarkus.hibernate.orm.PersistenceUnit; \ No newline at end of file diff --git a/extensions/hibernate-search-orm-coordination-outbox-polling/deployment/src/test/resources/application-multiple-persistence-units.properties b/extensions/hibernate-search-orm-outbox-polling/deployment/src/test/resources/application-multiple-persistence-units.properties similarity index 100% rename from extensions/hibernate-search-orm-coordination-outbox-polling/deployment/src/test/resources/application-multiple-persistence-units.properties rename to extensions/hibernate-search-orm-outbox-polling/deployment/src/test/resources/application-multiple-persistence-units.properties diff --git a/extensions/hibernate-search-orm-coordination-outbox-polling/deployment/src/test/resources/application.properties b/extensions/hibernate-search-orm-outbox-polling/deployment/src/test/resources/application.properties similarity index 100% rename from extensions/hibernate-search-orm-coordination-outbox-polling/deployment/src/test/resources/application.properties rename to extensions/hibernate-search-orm-outbox-polling/deployment/src/test/resources/application.properties diff --git a/extensions/hibernate-search-orm-coordination-outbox-polling/pom.xml b/extensions/hibernate-search-orm-outbox-polling/pom.xml similarity index 78% rename from extensions/hibernate-search-orm-coordination-outbox-polling/pom.xml rename to extensions/hibernate-search-orm-outbox-polling/pom.xml index d13ace173b7f0..60bf47aa4fb5d 100644 --- a/extensions/hibernate-search-orm-coordination-outbox-polling/pom.xml +++ b/extensions/hibernate-search-orm-outbox-polling/pom.xml @@ -10,8 +10,8 @@ 4.0.0 - quarkus-hibernate-search-orm-coordination-outbox-polling-parent - Quarkus - Hibernate Search - ORM - Coordination - Outbox Polling + quarkus-hibernate-search-orm-outbox-polling-parent + Quarkus - Hibernate Search - ORM - Outbox Polling pom diff --git a/extensions/hibernate-search-orm-coordination-outbox-polling/runtime/pom.xml b/extensions/hibernate-search-orm-outbox-polling/runtime/pom.xml similarity index 84% rename from extensions/hibernate-search-orm-coordination-outbox-polling/runtime/pom.xml rename to extensions/hibernate-search-orm-outbox-polling/runtime/pom.xml index b88ae2c973108..24d030c4640e5 100644 --- a/extensions/hibernate-search-orm-coordination-outbox-polling/runtime/pom.xml +++ b/extensions/hibernate-search-orm-outbox-polling/runtime/pom.xml @@ -3,14 +3,14 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> - quarkus-hibernate-search-orm-coordination-outbox-polling-parent + quarkus-hibernate-search-orm-outbox-polling-parent io.quarkus 999-SNAPSHOT 4.0.0 - quarkus-hibernate-search-orm-coordination-outbox-polling - Quarkus - Hibernate Search - ORM - Coordination - Outbox Polling - Runtime + quarkus-hibernate-search-orm-outbox-polling + Quarkus - Hibernate Search - ORM - Outbox Polling - Runtime Use a transactional outbox and background polling to coordinate automatic indexing in Hibernate Search @@ -32,7 +32,7 @@ org.hibernate.search - hibernate-search-mapper-orm-coordination-outbox-polling-orm6 + hibernate-search-mapper-orm-outbox-polling diff --git a/extensions/hibernate-search-orm-coordination-outbox-polling/runtime/src/main/java/io/quarkus/hibernate/search/orm/coordination/outboxpolling/runtime/HibernateSearchOutboxPollingBuildTimeConfig.java b/extensions/hibernate-search-orm-outbox-polling/runtime/src/main/java/io/quarkus/hibernate/search/orm/outboxpolling/runtime/HibernateSearchOutboxPollingBuildTimeConfig.java similarity index 92% rename from extensions/hibernate-search-orm-coordination-outbox-polling/runtime/src/main/java/io/quarkus/hibernate/search/orm/coordination/outboxpolling/runtime/HibernateSearchOutboxPollingBuildTimeConfig.java rename to extensions/hibernate-search-orm-outbox-polling/runtime/src/main/java/io/quarkus/hibernate/search/orm/outboxpolling/runtime/HibernateSearchOutboxPollingBuildTimeConfig.java index 7b0aac61f9a3b..56e96dbbbba74 100644 --- a/extensions/hibernate-search-orm-coordination-outbox-polling/runtime/src/main/java/io/quarkus/hibernate/search/orm/coordination/outboxpolling/runtime/HibernateSearchOutboxPollingBuildTimeConfig.java +++ b/extensions/hibernate-search-orm-outbox-polling/runtime/src/main/java/io/quarkus/hibernate/search/orm/outboxpolling/runtime/HibernateSearchOutboxPollingBuildTimeConfig.java @@ -1,4 +1,4 @@ -package io.quarkus.hibernate.search.orm.coordination.outboxpolling.runtime; +package io.quarkus.hibernate.search.orm.outboxpolling.runtime; import java.util.Map; diff --git a/extensions/hibernate-search-orm-coordination-outbox-polling/runtime/src/main/java/io/quarkus/hibernate/search/orm/coordination/outboxpolling/runtime/HibernateSearchOutboxPollingBuildTimeConfigPersistenceUnit.java b/extensions/hibernate-search-orm-outbox-polling/runtime/src/main/java/io/quarkus/hibernate/search/orm/outboxpolling/runtime/HibernateSearchOutboxPollingBuildTimeConfigPersistenceUnit.java similarity index 79% rename from extensions/hibernate-search-orm-coordination-outbox-polling/runtime/src/main/java/io/quarkus/hibernate/search/orm/coordination/outboxpolling/runtime/HibernateSearchOutboxPollingBuildTimeConfigPersistenceUnit.java rename to extensions/hibernate-search-orm-outbox-polling/runtime/src/main/java/io/quarkus/hibernate/search/orm/outboxpolling/runtime/HibernateSearchOutboxPollingBuildTimeConfigPersistenceUnit.java index b3f17f275e111..58e12bec42e89 100644 --- a/extensions/hibernate-search-orm-coordination-outbox-polling/runtime/src/main/java/io/quarkus/hibernate/search/orm/coordination/outboxpolling/runtime/HibernateSearchOutboxPollingBuildTimeConfigPersistenceUnit.java +++ b/extensions/hibernate-search-orm-outbox-polling/runtime/src/main/java/io/quarkus/hibernate/search/orm/outboxpolling/runtime/HibernateSearchOutboxPollingBuildTimeConfigPersistenceUnit.java @@ -1,8 +1,8 @@ -package io.quarkus.hibernate.search.orm.coordination.outboxpolling.runtime; +package io.quarkus.hibernate.search.orm.outboxpolling.runtime; import java.util.Optional; -import org.hibernate.search.mapper.orm.coordination.outboxpolling.cfg.UuidGenerationStrategy; +import org.hibernate.search.mapper.orm.outboxpolling.cfg.UuidGenerationStrategy; import io.quarkus.runtime.annotations.ConfigDocDefault; import io.quarkus.runtime.annotations.ConfigDocSection; @@ -83,20 +83,18 @@ interface EntityMappingAgentConfig { Optional uuidGenStrategy(); /** - * The name of the Hibernate ORM basic type used for representing an UUID in the agent table. + * The name of the Hibernate ORM basic type used for representing an UUID in the outbox event table. * * Refer to - * link:{hibernate-orm-docs-url}#basic-legacy-provided[this section of the Hibernate ORM documentation] - * to see the list of available UUID representations provided by Hibernate ORM. - * - * A user defined type can also be supplied. + * link:{hibernate-orm-docs-url}#basic-uuid[this section of the Hibernate ORM documentation] + * to see the possible UUID representations. * - * Defaults to the special value `default`, which will result into one of `uuid`/`uuid-binary`/`uuid-char` + * Defaults to the special value `default`, which will result into one of `char`/`binary` * depending on the database kind. * * @asciidoclet */ - @ConfigDocDefault("uuid/uuid-binary/uuid-char depending on the database kind") + @ConfigDocDefault("char/binary depending on the database kind") Optional uuidType(); } @@ -145,17 +143,15 @@ interface EntityMappingOutboxEventConfig { * The name of the Hibernate ORM basic type used for representing an UUID in the outbox event table. * * Refer to - * link:{hibernate-orm-docs-url}#basic-legacy-provided[this section of the Hibernate ORM documentation] - * to see the list of available UUID representations provided by Hibernate ORM. - * - * A user defined type can also be supplied. + * link:{hibernate-orm-docs-url}#basic-uuid[this section of the Hibernate ORM documentation] + * to see the possible UUID representations. * - * Defaults to the special value `default`, which will result into one of `uuid`/`uuid-binary`/`uuid-char` + * Defaults to the special value `default`, which will result into one of `char`/`binary` * depending on the database kind. * * @asciidoclet */ - @ConfigDocDefault("uuid/uuid-binary/uuid-char depending on the database kind") + @ConfigDocDefault("char/binary depending on the database kind") Optional uuidType(); } diff --git a/extensions/hibernate-search-orm-coordination-outbox-polling/runtime/src/main/java/io/quarkus/hibernate/search/orm/coordination/outboxpolling/runtime/HibernateSearchOutboxPollingConfigUtil.java b/extensions/hibernate-search-orm-outbox-polling/runtime/src/main/java/io/quarkus/hibernate/search/orm/outboxpolling/runtime/HibernateSearchOutboxPollingConfigUtil.java similarity index 91% rename from extensions/hibernate-search-orm-coordination-outbox-polling/runtime/src/main/java/io/quarkus/hibernate/search/orm/coordination/outboxpolling/runtime/HibernateSearchOutboxPollingConfigUtil.java rename to extensions/hibernate-search-orm-outbox-polling/runtime/src/main/java/io/quarkus/hibernate/search/orm/outboxpolling/runtime/HibernateSearchOutboxPollingConfigUtil.java index db1557c01f90b..e890d9af6c725 100644 --- a/extensions/hibernate-search-orm-coordination-outbox-polling/runtime/src/main/java/io/quarkus/hibernate/search/orm/coordination/outboxpolling/runtime/HibernateSearchOutboxPollingConfigUtil.java +++ b/extensions/hibernate-search-orm-outbox-polling/runtime/src/main/java/io/quarkus/hibernate/search/orm/outboxpolling/runtime/HibernateSearchOutboxPollingConfigUtil.java @@ -1,11 +1,11 @@ -package io.quarkus.hibernate.search.orm.coordination.outboxpolling.runtime; +package io.quarkus.hibernate.search.orm.outboxpolling.runtime; import java.util.Optional; import java.util.OptionalInt; import java.util.function.BiConsumer; import java.util.function.Function; -import org.hibernate.search.mapper.orm.coordination.outboxpolling.cfg.HibernateOrmMapperOutboxPollingSettings; +import org.hibernate.search.mapper.orm.outboxpolling.cfg.HibernateOrmMapperOutboxPollingSettings; public final class HibernateSearchOutboxPollingConfigUtil { diff --git a/extensions/hibernate-search-orm-coordination-outbox-polling/runtime/src/main/java/io/quarkus/hibernate/search/orm/coordination/outboxpolling/runtime/HibernateSearchOutboxPollingRecorder.java b/extensions/hibernate-search-orm-outbox-polling/runtime/src/main/java/io/quarkus/hibernate/search/orm/outboxpolling/runtime/HibernateSearchOutboxPollingRecorder.java similarity index 73% rename from extensions/hibernate-search-orm-coordination-outbox-polling/runtime/src/main/java/io/quarkus/hibernate/search/orm/coordination/outboxpolling/runtime/HibernateSearchOutboxPollingRecorder.java rename to extensions/hibernate-search-orm-outbox-polling/runtime/src/main/java/io/quarkus/hibernate/search/orm/outboxpolling/runtime/HibernateSearchOutboxPollingRecorder.java index 8b9ddbbcb3609..14c478ff771e7 100644 --- a/extensions/hibernate-search-orm-coordination-outbox-polling/runtime/src/main/java/io/quarkus/hibernate/search/orm/coordination/outboxpolling/runtime/HibernateSearchOutboxPollingRecorder.java +++ b/extensions/hibernate-search-orm-outbox-polling/runtime/src/main/java/io/quarkus/hibernate/search/orm/outboxpolling/runtime/HibernateSearchOutboxPollingRecorder.java @@ -1,6 +1,6 @@ -package io.quarkus.hibernate.search.orm.coordination.outboxpolling.runtime; +package io.quarkus.hibernate.search.orm.outboxpolling.runtime; -import static io.quarkus.hibernate.search.orm.coordination.outboxpolling.runtime.HibernateSearchOutboxPollingConfigUtil.addCoordinationConfig; +import static io.quarkus.hibernate.search.orm.outboxpolling.runtime.HibernateSearchOutboxPollingConfigUtil.addCoordinationConfig; import java.util.Map.Entry; import java.util.Optional; @@ -8,11 +8,10 @@ import org.hibernate.boot.Metadata; import org.hibernate.boot.spi.BootstrapContext; -import org.hibernate.search.mapper.orm.coordination.outboxpolling.cfg.HibernateOrmMapperOutboxPollingSettings; +import org.hibernate.search.mapper.orm.outboxpolling.cfg.HibernateOrmMapperOutboxPollingSettings; import io.quarkus.hibernate.orm.runtime.integration.HibernateOrmIntegrationRuntimeInitListener; import io.quarkus.hibernate.orm.runtime.integration.HibernateOrmIntegrationStaticInitListener; -import io.quarkus.hibernate.search.orm.coordination.outboxpolling.runtime.HibernateSearchOutboxPollingRuntimeConfigPersistenceUnit.AgentsConfig; import io.quarkus.runtime.annotations.Recorder; @Recorder @@ -58,35 +57,35 @@ public void contributeBootProperties(BiConsumer propertyCollecto private void contributeCoordinationBuildTimeProperties(BiConsumer propertyCollector, HibernateSearchOutboxPollingBuildTimeConfigPersistenceUnit.CoordinationConfig config) { - addCoordinationConfig(propertyCollector, + HibernateSearchOutboxPollingConfigUtil.addCoordinationConfig(propertyCollector, HibernateOrmMapperOutboxPollingSettings.CoordinationRadicals.ENTITY_MAPPING_AGENT_CATALOG, config.entityMapping().agent().catalog()); - addCoordinationConfig(propertyCollector, + HibernateSearchOutboxPollingConfigUtil.addCoordinationConfig(propertyCollector, HibernateOrmMapperOutboxPollingSettings.CoordinationRadicals.ENTITY_MAPPING_AGENT_SCHEMA, config.entityMapping().agent().schema()); - addCoordinationConfig(propertyCollector, + HibernateSearchOutboxPollingConfigUtil.addCoordinationConfig(propertyCollector, HibernateOrmMapperOutboxPollingSettings.CoordinationRadicals.ENTITY_MAPPING_AGENT_TABLE, config.entityMapping().agent().table()); - addCoordinationConfig(propertyCollector, + HibernateSearchOutboxPollingConfigUtil.addCoordinationConfig(propertyCollector, HibernateOrmMapperOutboxPollingSettings.CoordinationRadicals.ENTITY_MAPPING_AGENT_UUID_GEN_STRATEGY, config.entityMapping().agent().uuidGenStrategy()); - addCoordinationConfig(propertyCollector, + HibernateSearchOutboxPollingConfigUtil.addCoordinationConfig(propertyCollector, HibernateOrmMapperOutboxPollingSettings.CoordinationRadicals.ENTITY_MAPPING_AGENT_UUID_TYPE, config.entityMapping().agent().uuidType()); - addCoordinationConfig(propertyCollector, + HibernateSearchOutboxPollingConfigUtil.addCoordinationConfig(propertyCollector, HibernateOrmMapperOutboxPollingSettings.CoordinationRadicals.ENTITY_MAPPING_OUTBOXEVENT_CATALOG, config.entityMapping().outboxEvent().catalog()); - addCoordinationConfig(propertyCollector, + HibernateSearchOutboxPollingConfigUtil.addCoordinationConfig(propertyCollector, HibernateOrmMapperOutboxPollingSettings.CoordinationRadicals.ENTITY_MAPPING_OUTBOXEVENT_SCHEMA, config.entityMapping().outboxEvent().schema()); - addCoordinationConfig(propertyCollector, + HibernateSearchOutboxPollingConfigUtil.addCoordinationConfig(propertyCollector, HibernateOrmMapperOutboxPollingSettings.CoordinationRadicals.ENTITY_MAPPING_OUTBOXEVENT_TABLE, config.entityMapping().outboxEvent().table()); - addCoordinationConfig(propertyCollector, + HibernateSearchOutboxPollingConfigUtil.addCoordinationConfig(propertyCollector, HibernateOrmMapperOutboxPollingSettings.CoordinationRadicals.ENTITY_MAPPING_OUTBOXEVENT_UUID_GEN_STRATEGY, config.entityMapping().outboxEvent().uuidGenStrategy()); - addCoordinationConfig(propertyCollector, + HibernateSearchOutboxPollingConfigUtil.addCoordinationConfig(propertyCollector, HibernateOrmMapperOutboxPollingSettings.CoordinationRadicals.ENTITY_MAPPING_OUTBOXEVENT_UUID_TYPE, config.entityMapping().outboxEvent().uuidType()); } @@ -110,48 +109,49 @@ public void contributeRuntimeProperties(BiConsumer propertyColle contributeCoordinationRuntimeProperties(propertyCollector, null, runtimeConfig.coordination().defaults()); - for (Entry tenantEntry : runtimeConfig.coordination().tenants().entrySet()) { + for (Entry tenantEntry : runtimeConfig + .coordination().tenants().entrySet()) { contributeCoordinationRuntimeProperties(propertyCollector, tenantEntry.getKey(), tenantEntry.getValue()); } } private void contributeCoordinationRuntimeProperties(BiConsumer propertyCollector, String tenantId, - AgentsConfig agentsConfig) { - addCoordinationConfig(propertyCollector, tenantId, + HibernateSearchOutboxPollingRuntimeConfigPersistenceUnit.AgentsConfig agentsConfig) { + HibernateSearchOutboxPollingConfigUtil.addCoordinationConfig(propertyCollector, tenantId, HibernateOrmMapperOutboxPollingSettings.CoordinationRadicals.EVENT_PROCESSOR_ENABLED, agentsConfig.eventProcessor().enabled()); - addCoordinationConfig(propertyCollector, tenantId, + HibernateSearchOutboxPollingConfigUtil.addCoordinationConfig(propertyCollector, tenantId, HibernateOrmMapperOutboxPollingSettings.CoordinationRadicals.EVENT_PROCESSOR_SHARDS_TOTAL_COUNT, agentsConfig.eventProcessor().shards().totalCount()); - addCoordinationConfig(propertyCollector, tenantId, + HibernateSearchOutboxPollingConfigUtil.addCoordinationConfig(propertyCollector, tenantId, HibernateOrmMapperOutboxPollingSettings.CoordinationRadicals.EVENT_PROCESSOR_SHARDS_ASSIGNED, agentsConfig.eventProcessor().shards().assigned()); - addCoordinationConfig(propertyCollector, tenantId, + HibernateSearchOutboxPollingConfigUtil.addCoordinationConfig(propertyCollector, tenantId, HibernateOrmMapperOutboxPollingSettings.CoordinationRadicals.EVENT_PROCESSOR_POLLING_INTERVAL, agentsConfig.eventProcessor().pollingInterval().toMillis()); - addCoordinationConfig(propertyCollector, tenantId, + HibernateSearchOutboxPollingConfigUtil.addCoordinationConfig(propertyCollector, tenantId, HibernateOrmMapperOutboxPollingSettings.CoordinationRadicals.EVENT_PROCESSOR_PULSE_INTERVAL, agentsConfig.eventProcessor().pulseInterval().toMillis()); - addCoordinationConfig(propertyCollector, tenantId, + HibernateSearchOutboxPollingConfigUtil.addCoordinationConfig(propertyCollector, tenantId, HibernateOrmMapperOutboxPollingSettings.CoordinationRadicals.EVENT_PROCESSOR_PULSE_EXPIRATION, agentsConfig.eventProcessor().pulseExpiration().toMillis()); - addCoordinationConfig(propertyCollector, tenantId, + HibernateSearchOutboxPollingConfigUtil.addCoordinationConfig(propertyCollector, tenantId, HibernateOrmMapperOutboxPollingSettings.CoordinationRadicals.EVENT_PROCESSOR_BATCH_SIZE, agentsConfig.eventProcessor().batchSize()); - addCoordinationConfig(propertyCollector, tenantId, + HibernateSearchOutboxPollingConfigUtil.addCoordinationConfig(propertyCollector, tenantId, HibernateOrmMapperOutboxPollingSettings.CoordinationRadicals.EVENT_PROCESSOR_TRANSACTION_TIMEOUT, agentsConfig.eventProcessor().transactionTimeout(), Optional::isPresent, d -> d.get().toSeconds()); - addCoordinationConfig(propertyCollector, tenantId, + HibernateSearchOutboxPollingConfigUtil.addCoordinationConfig(propertyCollector, tenantId, HibernateOrmMapperOutboxPollingSettings.CoordinationRadicals.EVENT_PROCESSOR_RETRY_DELAY, agentsConfig.eventProcessor().retryDelay().toSeconds()); - addCoordinationConfig(propertyCollector, tenantId, + HibernateSearchOutboxPollingConfigUtil.addCoordinationConfig(propertyCollector, tenantId, HibernateOrmMapperOutboxPollingSettings.CoordinationRadicals.MASS_INDEXER_POLLING_INTERVAL, agentsConfig.massIndexer().pollingInterval().toMillis()); - addCoordinationConfig(propertyCollector, tenantId, + HibernateSearchOutboxPollingConfigUtil.addCoordinationConfig(propertyCollector, tenantId, HibernateOrmMapperOutboxPollingSettings.CoordinationRadicals.MASS_INDEXER_PULSE_INTERVAL, agentsConfig.massIndexer().pulseInterval().toMillis()); - addCoordinationConfig(propertyCollector, tenantId, + HibernateSearchOutboxPollingConfigUtil.addCoordinationConfig(propertyCollector, tenantId, HibernateOrmMapperOutboxPollingSettings.CoordinationRadicals.MASS_INDEXER_PULSE_EXPIRATION, agentsConfig.massIndexer().pulseExpiration().toMillis()); } diff --git a/extensions/hibernate-search-orm-coordination-outbox-polling/runtime/src/main/java/io/quarkus/hibernate/search/orm/coordination/outboxpolling/runtime/HibernateSearchOutboxPollingRuntimeConfig.java b/extensions/hibernate-search-orm-outbox-polling/runtime/src/main/java/io/quarkus/hibernate/search/orm/outboxpolling/runtime/HibernateSearchOutboxPollingRuntimeConfig.java similarity index 91% rename from extensions/hibernate-search-orm-coordination-outbox-polling/runtime/src/main/java/io/quarkus/hibernate/search/orm/coordination/outboxpolling/runtime/HibernateSearchOutboxPollingRuntimeConfig.java rename to extensions/hibernate-search-orm-outbox-polling/runtime/src/main/java/io/quarkus/hibernate/search/orm/outboxpolling/runtime/HibernateSearchOutboxPollingRuntimeConfig.java index 31ed8c13bbcf1..738f695f7648f 100644 --- a/extensions/hibernate-search-orm-coordination-outbox-polling/runtime/src/main/java/io/quarkus/hibernate/search/orm/coordination/outboxpolling/runtime/HibernateSearchOutboxPollingRuntimeConfig.java +++ b/extensions/hibernate-search-orm-outbox-polling/runtime/src/main/java/io/quarkus/hibernate/search/orm/outboxpolling/runtime/HibernateSearchOutboxPollingRuntimeConfig.java @@ -1,4 +1,4 @@ -package io.quarkus.hibernate.search.orm.coordination.outboxpolling.runtime; +package io.quarkus.hibernate.search.orm.outboxpolling.runtime; import java.util.Map; diff --git a/extensions/hibernate-search-orm-coordination-outbox-polling/runtime/src/main/java/io/quarkus/hibernate/search/orm/coordination/outboxpolling/runtime/HibernateSearchOutboxPollingRuntimeConfigPersistenceUnit.java b/extensions/hibernate-search-orm-outbox-polling/runtime/src/main/java/io/quarkus/hibernate/search/orm/outboxpolling/runtime/HibernateSearchOutboxPollingRuntimeConfigPersistenceUnit.java similarity index 99% rename from extensions/hibernate-search-orm-coordination-outbox-polling/runtime/src/main/java/io/quarkus/hibernate/search/orm/coordination/outboxpolling/runtime/HibernateSearchOutboxPollingRuntimeConfigPersistenceUnit.java rename to extensions/hibernate-search-orm-outbox-polling/runtime/src/main/java/io/quarkus/hibernate/search/orm/outboxpolling/runtime/HibernateSearchOutboxPollingRuntimeConfigPersistenceUnit.java index 99841da547be9..f88b0faf70a12 100644 --- a/extensions/hibernate-search-orm-coordination-outbox-polling/runtime/src/main/java/io/quarkus/hibernate/search/orm/coordination/outboxpolling/runtime/HibernateSearchOutboxPollingRuntimeConfigPersistenceUnit.java +++ b/extensions/hibernate-search-orm-outbox-polling/runtime/src/main/java/io/quarkus/hibernate/search/orm/outboxpolling/runtime/HibernateSearchOutboxPollingRuntimeConfigPersistenceUnit.java @@ -1,4 +1,4 @@ -package io.quarkus.hibernate.search.orm.coordination.outboxpolling.runtime; +package io.quarkus.hibernate.search.orm.outboxpolling.runtime; import java.time.Duration; import java.util.List; diff --git a/extensions/hibernate-search-orm-coordination-outbox-polling/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/hibernate-search-orm-outbox-polling/runtime/src/main/resources/META-INF/quarkus-extension.yaml similarity index 93% rename from extensions/hibernate-search-orm-coordination-outbox-polling/runtime/src/main/resources/META-INF/quarkus-extension.yaml rename to extensions/hibernate-search-orm-outbox-polling/runtime/src/main/resources/META-INF/quarkus-extension.yaml index 8491a792d7a64..abea6a0dfc07c 100644 --- a/extensions/hibernate-search-orm-coordination-outbox-polling/runtime/src/main/resources/META-INF/quarkus-extension.yaml +++ b/extensions/hibernate-search-orm-outbox-polling/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -5,6 +5,7 @@ metadata: keywords: - "hibernate-search-orm-elasticsearch" - "hibernate-search-elasticsearch" + - "hibernate-search-orm-outbox-polling" - "hibernate-search-orm-coordination-outbox-polling" - "search" - "full-text" diff --git a/extensions/infinispan-client/deployment/src/main/java/io/quarkus/infinispan/client/deployment/InfinispanClientProcessor.java b/extensions/infinispan-client/deployment/src/main/java/io/quarkus/infinispan/client/deployment/InfinispanClientProcessor.java index 58201a89f0b4f..7e11774606f90 100644 --- a/extensions/infinispan-client/deployment/src/main/java/io/quarkus/infinispan/client/deployment/InfinispanClientProcessor.java +++ b/extensions/infinispan-client/deployment/src/main/java/io/quarkus/infinispan/client/deployment/InfinispanClientProcessor.java @@ -205,7 +205,6 @@ InfinispanPropertiesBuildItem setup(ApplicationArchivesBuildItem applicationArch additionalBeans.produce(AdditionalBeanBuildItem.builder().addBeanClass(InfinispanClientName.class).build()); additionalBeans.produce(AdditionalBeanBuildItem.builder().addBeanClass(Remote.class).build()); - systemProperties.produce(new SystemPropertyBuildItem("io.netty.noUnsafe", "true")); hotDeployment .produce(new HotDeploymentWatchedFileBuildItem(META_INF + File.separator + DEFAULT_HOTROD_CLIENT_PROPERTIES)); diff --git a/extensions/jaxb/runtime/pom.xml b/extensions/jaxb/runtime/pom.xml index 6498c57c868e2..591b9481a3e67 100644 --- a/extensions/jaxb/runtime/pom.xml +++ b/extensions/jaxb/runtime/pom.xml @@ -53,6 +53,9 @@ javax.xml.bind:jaxb-api org.jboss.spec.javax.xml.bind:jboss-jaxb-api_2.3_spec + + io.quarkus.jaxb + diff --git a/extensions/jaxp/runtime/pom.xml b/extensions/jaxp/runtime/pom.xml index 3b8b0f44298e6..c670dfaf44b4d 100644 --- a/extensions/jaxp/runtime/pom.xml +++ b/extensions/jaxp/runtime/pom.xml @@ -24,6 +24,11 @@ io.quarkus quarkus-extension-maven-plugin + + + io.quarkus.jaxp + + maven-compiler-plugin diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/AddServiceMonitorResourceDecorator.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/AddServiceMonitorResourceDecorator.java index bc75666b48f15..425377c822541 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/AddServiceMonitorResourceDecorator.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/AddServiceMonitorResourceDecorator.java @@ -36,7 +36,7 @@ public void visit(KubernetesListBuilder list) { .endSelector() .addNewEndpoint() .withScheme(scheme) - .withNewTargetPort(targetPort) + .withNewTargetPort(Integer.parseInt(targetPort)) //This needs to be passed as int .withPath(path) .withInterval(interval + "s") .withHonorLabels(honorLabels) diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/ContainerImageUtil.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/ContainerImageUtil.java deleted file mode 100644 index aec5fef91db35..0000000000000 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/ContainerImageUtil.java +++ /dev/null @@ -1,25 +0,0 @@ -package io.quarkus.kubernetes.deployment; - -import java.util.Optional; - -import io.quarkus.container.image.deployment.ContainerImageCapabilitiesUtil; -import io.quarkus.container.spi.ContainerImageInfoBuildItem; -import io.quarkus.deployment.Capabilities; -import io.quarkus.deployment.Capability; - -final class ContainerImageUtil { - - private ContainerImageUtil() { - } - - static boolean isRegistryMissingAndNotS2I(Capabilities capabilities, ContainerImageInfoBuildItem containerImageInfo) { - Optional activeContainerImageCapability = ContainerImageCapabilitiesUtil - .getActiveContainerImageCapability(capabilities); - if (!activeContainerImageCapability.isPresent()) { // shouldn't ever happen when this method is called - return false; - } - - return !containerImageInfo.getRegistry().isPresent() - && !Capability.CONTAINER_IMAGE_S2I.equals(activeContainerImageCapability.get()); - } -} diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/OpenshiftProcessor.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/OpenshiftProcessor.java index 6e8747568b32a..18fa42dbc4e26 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/OpenshiftProcessor.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/OpenshiftProcessor.java @@ -157,8 +157,7 @@ public List createConfigurators(ApplicationInfoBuildItem result.add(new ConfiguratorBuildItem(new AddPortToOpenshiftConfig(config.remoteDebug.buildDebugPort()))); } - if (!capabilities.isPresent(Capability.CONTAINER_IMAGE_S2I) - && !capabilities.isPresent("io.quarkus.openshift") + if (!capabilities.isPresent("io.quarkus.openshift") && !capabilities.isPresent(Capability.CONTAINER_IMAGE_OPENSHIFT)) { result.add(new ConfiguratorBuildItem(new DisableS2iConfigurator())); diff --git a/extensions/liquibase-mongodb/deployment/src/main/java/io/quarkus/liquibase/mongodb/deployment/LiquibaseMongodbProcessor.java b/extensions/liquibase-mongodb/deployment/src/main/java/io/quarkus/liquibase/mongodb/deployment/LiquibaseMongodbProcessor.java index f043f34fb046f..001ec6d6da15a 100644 --- a/extensions/liquibase-mongodb/deployment/src/main/java/io/quarkus/liquibase/mongodb/deployment/LiquibaseMongodbProcessor.java +++ b/extensions/liquibase-mongodb/deployment/src/main/java/io/quarkus/liquibase/mongodb/deployment/LiquibaseMongodbProcessor.java @@ -153,6 +153,7 @@ void nativeImageConfiguration( Stream.of(liquibase.change.Change.class, liquibase.changelog.ChangeLogHistoryService.class, + liquibase.changeset.ChangeSetService.class, liquibase.database.Database.class, liquibase.database.DatabaseConnection.class, liquibase.datatype.LiquibaseDataType.class, @@ -164,9 +165,11 @@ void nativeImageConfiguration( liquibase.lockservice.LockService.class, liquibase.logging.LogService.class, liquibase.parser.ChangeLogParser.class, + liquibase.parser.LiquibaseSqlParser.class, liquibase.parser.NamespaceDetails.class, liquibase.parser.SnapshotParser.class, liquibase.precondition.Precondition.class, + liquibase.report.ShowSummaryGenerator.class, liquibase.serializer.ChangeLogSerializer.class, liquibase.serializer.SnapshotSerializer.class, liquibase.servicelocator.ServiceLocator.class, @@ -202,6 +205,8 @@ void nativeImageConfiguration( "www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.21.xsd", "www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.22.xsd", "www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.23.xsd", + "www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.24.xsd", + "www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.25.xsd", "www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd", "www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd", "liquibase.build.properties")); diff --git a/extensions/liquibase/deployment/src/main/java/io/quarkus/liquibase/deployment/LiquibaseProcessor.java b/extensions/liquibase/deployment/src/main/java/io/quarkus/liquibase/deployment/LiquibaseProcessor.java index 52b2e76afd401..8c2a597126e5f 100644 --- a/extensions/liquibase/deployment/src/main/java/io/quarkus/liquibase/deployment/LiquibaseProcessor.java +++ b/extensions/liquibase/deployment/src/main/java/io/quarkus/liquibase/deployment/LiquibaseProcessor.java @@ -53,7 +53,6 @@ import io.quarkus.deployment.builditem.nativeimage.NativeImageResourceBundleBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; import io.quarkus.deployment.builditem.nativeimage.RuntimeInitializedClassBuildItem; -import io.quarkus.deployment.builditem.nativeimage.RuntimeReinitializedClassBuildItem; import io.quarkus.deployment.builditem.nativeimage.ServiceProviderBuildItem; import io.quarkus.deployment.pkg.steps.NativeOrNativeSourcesBuild; import io.quarkus.deployment.util.ServiceUtil; @@ -106,7 +105,6 @@ void nativeImageConfiguration( BuildProducer resource, BuildProducer services, BuildProducer runtimeInitialized, - BuildProducer runtimeReInitialized, BuildProducer resourceBundle) { runtimeInitialized.produce(new RuntimeInitializedClassBuildItem(liquibase.diff.compare.CompareControl.class.getName())); @@ -170,6 +168,7 @@ void nativeImageConfiguration( Stream.of(liquibase.change.Change.class, liquibase.changelog.ChangeLogHistoryService.class, + liquibase.changeset.ChangeSetService.class, liquibase.database.Database.class, liquibase.database.DatabaseConnection.class, liquibase.datatype.LiquibaseDataType.class, @@ -181,9 +180,11 @@ void nativeImageConfiguration( liquibase.lockservice.LockService.class, liquibase.logging.LogService.class, liquibase.parser.ChangeLogParser.class, + liquibase.parser.LiquibaseSqlParser.class, liquibase.parser.NamespaceDetails.class, liquibase.parser.SnapshotParser.class, liquibase.precondition.Precondition.class, + liquibase.report.ShowSummaryGenerator.class, liquibase.serializer.ChangeLogSerializer.class, liquibase.serializer.SnapshotSerializer.class, liquibase.servicelocator.ServiceLocator.class, @@ -236,6 +237,8 @@ void nativeImageConfiguration( "www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.21.xsd", "www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.22.xsd", "www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.23.xsd", + "www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.24.xsd", + "www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.25.xsd", "www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd", "www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd", "liquibase.build.properties")); diff --git a/extensions/micrometer/deployment/src/main/java/io/quarkus/micrometer/deployment/MicrometerProcessor.java b/extensions/micrometer/deployment/src/main/java/io/quarkus/micrometer/deployment/MicrometerProcessor.java index 256f6fbd6a624..d1f95bd0bd34d 100644 --- a/extensions/micrometer/deployment/src/main/java/io/quarkus/micrometer/deployment/MicrometerProcessor.java +++ b/extensions/micrometer/deployment/src/main/java/io/quarkus/micrometer/deployment/MicrometerProcessor.java @@ -51,6 +51,7 @@ import io.quarkus.micrometer.runtime.MeterRegistryCustomizer; import io.quarkus.micrometer.runtime.MeterRegistryCustomizerConstraint; import io.quarkus.micrometer.runtime.MeterRegistryCustomizerConstraints; +import io.quarkus.micrometer.runtime.MeterTagsSupport; import io.quarkus.micrometer.runtime.MicrometerCounted; import io.quarkus.micrometer.runtime.MicrometerCountedInterceptor; import io.quarkus.micrometer.runtime.MicrometerRecorder; @@ -73,6 +74,7 @@ public class MicrometerProcessor { private static final DotName COUNTED_INTERCEPTOR = DotName.createSimple(MicrometerCountedInterceptor.class.getName()); private static final DotName TIMED_ANNOTATION = DotName.createSimple(Timed.class.getName()); private static final DotName TIMED_INTERCEPTOR = DotName.createSimple(MicrometerTimedInterceptor.class.getName()); + private static final DotName METER_TAG_SUPPORT = DotName.createSimple(MeterTagsSupport.class.getName()); public static class MicrometerEnabled implements BooleanSupplier { MicrometerConfig mConfig; @@ -123,6 +125,7 @@ UnremovableBeanBuildItem registerAdditionalBeans(CombinedIndexBuildItem indexBui .addBeanClass(COUNTED_ANNOTATION.toString()) .addBeanClass(COUNTED_BINDING.toString()) .addBeanClass(COUNTED_INTERCEPTOR.toString()) + .addBeanClass(METER_TAG_SUPPORT.toString()) .build()); // @Timed is registered as an additional interceptor binding diff --git a/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/runtime/MicrometerCounterInterceptorTest.java b/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/runtime/MicrometerCounterInterceptorTest.java index 7364893851673..1e2cb176cc451 100644 --- a/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/runtime/MicrometerCounterInterceptorTest.java +++ b/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/runtime/MicrometerCounterInterceptorTest.java @@ -16,6 +16,7 @@ import io.micrometer.core.instrument.simple.SimpleMeterRegistry; import io.quarkus.micrometer.test.CountedResource; import io.quarkus.micrometer.test.GuardedResult; +import io.quarkus.micrometer.test.TestValueResolver; import io.quarkus.micrometer.test.TimedResource; import io.quarkus.test.QuarkusUnitTest; import io.smallrye.mutiny.Uni; @@ -30,6 +31,7 @@ public class MicrometerCounterInterceptorTest { .overrideConfigKey("quarkus.micrometer.registry-enabled-default", "false") .overrideConfigKey("quarkus.redis.devservices.enabled", "false") .withApplicationRoot((jar) -> jar + .addClass(TestValueResolver.class) .addClass(CountedResource.class) .addClass(TimedResource.class) .addClass(GuardedResult.class)); @@ -58,6 +60,7 @@ void testCountAllMetrics_MetricsOnSuccess() { .tag("method", "countAllInvocations") .tag("class", "io.quarkus.micrometer.test.CountedResource") .tag("extra", "tag") + .tag("do_fail", "prefix_false") .tag("exception", "none") .tag("result", "success").counter(); Assertions.assertNotNull(counter); @@ -71,6 +74,7 @@ void testCountAllMetrics_MetricsOnFailure() { .tag("method", "countAllInvocations") .tag("class", "io.quarkus.micrometer.test.CountedResource") .tag("extra", "tag") + .tag("do_fail", "prefix_true") .tag("exception", "NullPointerException") .tag("result", "failure").counter(); Assertions.assertNotNull(counter); @@ -85,6 +89,7 @@ void testCountEmptyMetricName_Success() { .tag("method", "emptyMetricName") .tag("class", "io.quarkus.micrometer.test.CountedResource") .tag("exception", "none") + .tag("fail", "false") .tag("result", "success").counter(); Assertions.assertNotNull(counter); Assertions.assertEquals(1, counter.count()); @@ -98,6 +103,7 @@ void testCountEmptyMetricName_Failure() { .tag("method", "emptyMetricName") .tag("class", "io.quarkus.micrometer.test.CountedResource") .tag("exception", "NullPointerException") + .tag("fail", "true") .tag("result", "failure").counter(); Assertions.assertNotNull(counter); Assertions.assertEquals(1, counter.count()); diff --git a/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/test/CountedResource.java b/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/test/CountedResource.java index add85789e2b3b..ddc7a1078aaf1 100644 --- a/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/test/CountedResource.java +++ b/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/test/CountedResource.java @@ -7,6 +7,7 @@ import jakarta.enterprise.context.ApplicationScoped; import io.micrometer.core.annotation.Counted; +import io.micrometer.core.aop.MeterTag; import io.smallrye.mutiny.Uni; @ApplicationScoped @@ -16,14 +17,14 @@ public void onlyCountFailures() { } @Counted(value = "metric.all", extraTags = { "extra", "tag" }) - public void countAllInvocations(boolean fail) { + public void countAllInvocations(@MeterTag(key = "do_fail", resolver = TestValueResolver.class) boolean fail) { if (fail) { throw new NullPointerException("Failed on purpose"); } } @Counted(description = "nice description") - public void emptyMetricName(boolean fail) { + public void emptyMetricName(@MeterTag boolean fail) { if (fail) { throw new NullPointerException("Failed on purpose"); } diff --git a/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/test/TestValueResolver.java b/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/test/TestValueResolver.java new file mode 100644 index 0000000000000..aace9841984a7 --- /dev/null +++ b/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/test/TestValueResolver.java @@ -0,0 +1,13 @@ +package io.quarkus.micrometer.test; + +import jakarta.inject.Singleton; + +import io.micrometer.common.annotation.ValueResolver; + +@Singleton +public class TestValueResolver implements ValueResolver { + @Override + public String resolve(Object parameter) { + return "prefix_" + parameter; + } +} diff --git a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/MeterTagsSupport.java b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/MeterTagsSupport.java new file mode 100644 index 0000000000000..14399c6f536e2 --- /dev/null +++ b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/MeterTagsSupport.java @@ -0,0 +1,105 @@ +package io.quarkus.micrometer.runtime; + +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import jakarta.enterprise.inject.Instance; +import jakarta.inject.Singleton; + +import io.micrometer.common.annotation.NoOpValueResolver; +import io.micrometer.common.annotation.ValueExpressionResolver; +import io.micrometer.common.annotation.ValueResolver; +import io.micrometer.common.util.StringUtils; +import io.micrometer.core.aop.MeterTag; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Tags; +import io.quarkus.arc.All; +import io.quarkus.arc.ArcInvocationContext; +import io.quarkus.arc.ClientProxy; + +@Singleton +public class MeterTagsSupport { + private final Map, ValueResolver> valueResolvers; + private final ValueExpressionResolver valueExpressionResolver; + + public MeterTagsSupport(@All List valueResolvers, + Instance valueExpressionResolver) { + this.valueResolvers = createValueResolverMap(valueResolvers); + this.valueExpressionResolver = valueExpressionResolver.isUnsatisfied() ? null : valueExpressionResolver.get(); + } + + Tags getTags(ArcInvocationContext context) { + return getCommonTags(context) + .and(getMeterTags(context)); + } + + private Tags getMeterTags(ArcInvocationContext context) { + List tags = new ArrayList<>(); + Method method = context.getMethod(); + Parameter[] parameters = method.getParameters(); + for (int i = 0; i < parameters.length; i++) { + Parameter methodParameter = parameters[i]; + MeterTag annotation = methodParameter.getAnnotation(MeterTag.class); + if (annotation != null) { + Object parameterValue = context.getParameters()[i]; + + tags.add(Tag.of( + resolveTagKey(annotation, methodParameter.getName()), + resolveTagValue(annotation, parameterValue))); + } + } + return Tags.of(tags); + } + + private static Tags getCommonTags(ArcInvocationContext context) { + Method method = context.getMethod(); + String className = method.getDeclaringClass().getName(); + String methodName = method.getName(); + return Tags.of("class", className, "method", methodName); + } + + /* + * Precedence copied from MeterTagAnnotationHandler + */ + private String resolveTagValue(MeterTag annotation, Object parameterValue) { + if (annotation.resolver() != NoOpValueResolver.class) { + ValueResolver valueResolver = valueResolvers.get(annotation.resolver()); + return valueResolver.resolve(parameterValue); + } else if (StringUtils.isNotBlank(annotation.expression())) { + if (valueExpressionResolver == null) { + throw new IllegalArgumentException("No valueExpressionResolver is defined"); + } + return valueExpressionResolver.resolve(annotation.expression(), parameterValue); + } else if (parameterValue != null) { + return parameterValue.toString(); + } else { + return ""; + } + } + + /* + * Precedence copied from MeterTagAnnotationHandler + */ + private static String resolveTagKey(MeterTag annotation, String parameterName) { + if (StringUtils.isNotBlank(annotation.value())) { + return annotation.value(); + } else if (StringUtils.isNotBlank(annotation.key())) { + return annotation.key(); + } else { + return parameterName; + } + } + + private static Map, ValueResolver> createValueResolverMap(List valueResolvers) { + Map, ValueResolver> valueResolverMap = new HashMap<>(); + for (ValueResolver valueResolver : valueResolvers) { + ValueResolver instance = ClientProxy.unwrap(valueResolver); + valueResolverMap.put(instance.getClass(), valueResolver); + } + return valueResolverMap; + } +} diff --git a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/MicrometerCountedInterceptor.java b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/MicrometerCountedInterceptor.java index 152a36486b456..4b2230d53cf0b 100644 --- a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/MicrometerCountedInterceptor.java +++ b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/MicrometerCountedInterceptor.java @@ -32,9 +32,11 @@ public class MicrometerCountedInterceptor { public final String RESULT_TAG_SUCCESS_VALUE = "success"; private final MeterRegistry meterRegistry; + private final MeterTagsSupport meterTagsSupport; - public MicrometerCountedInterceptor(MeterRegistry meterRegistry) { + public MicrometerCountedInterceptor(MeterRegistry meterRegistry, MeterTagsSupport meterTagsSupport) { this.meterRegistry = meterRegistry; + this.meterTagsSupport = meterTagsSupport; } /** @@ -61,7 +63,7 @@ Object countedMethod(ArcInvocationContext context) throws Exception { return context.proceed(); } Method method = context.getMethod(); - Tags commonTags = getCommonTags(method.getDeclaringClass().getName(), method.getName()); + Tags tags = meterTagsSupport.getTags(context); Class returnType = method.getReturnType(); if (TypesUtil.isCompletionStage(returnType)) { @@ -69,11 +71,11 @@ Object countedMethod(ArcInvocationContext context) throws Exception { return ((CompletionStage) context.proceed()).whenComplete(new BiConsumer() { @Override public void accept(Object o, Throwable throwable) { - recordCompletionResult(counted, commonTags, throwable); + recordCompletionResult(counted, tags, throwable); } }); } catch (Throwable e) { - record(counted, commonTags, e); + record(counted, tags, e); } } else if (TypesUtil.isUni(returnType)) { try { @@ -81,22 +83,22 @@ public void accept(Object o, Throwable throwable) { new Functions.TriConsumer<>() { @Override public void accept(Object o, Throwable throwable, Boolean cancelled) { - recordCompletionResult(counted, commonTags, throwable); + recordCompletionResult(counted, tags, throwable); } }); } catch (Throwable e) { - record(counted, commonTags, e); + record(counted, tags, e); } } try { Object result = context.proceed(); if (!counted.recordFailuresOnly()) { - record(counted, commonTags, null); + record(counted, tags, null); } return result; } catch (Throwable e) { - record(counted, commonTags, e); + record(counted, tags, e); throw e; } } @@ -122,8 +124,4 @@ private void record(MicrometerCounted counted, Tags commonTags, Throwable throwa builder.register(meterRegistry).increment(); } - private Tags getCommonTags(String className, String methodName) { - return Tags.of("class", className, "method", methodName); - } - } diff --git a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/MicrometerTimedInterceptor.java b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/MicrometerTimedInterceptor.java index 7d6ff73b77260..76415a669883e 100644 --- a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/MicrometerTimedInterceptor.java +++ b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/MicrometerTimedInterceptor.java @@ -1,6 +1,5 @@ package io.quarkus.micrometer.runtime; -import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -33,9 +32,11 @@ public class MicrometerTimedInterceptor { public static final String DEFAULT_METRIC_NAME = "method.timed"; private final MeterRegistry meterRegistry; + private final MeterTagsSupport meterTagsSupport; - public MicrometerTimedInterceptor(MeterRegistry meterRegistry) { + public MicrometerTimedInterceptor(MeterRegistry meterRegistry, MeterTagsSupport meterTagsSupport) { this.meterRegistry = meterRegistry; + this.meterTagsSupport = meterTagsSupport; } @AroundInvoke @@ -85,18 +86,17 @@ public void accept(Object o, Throwable throwable, Boolean cancelled) { } private List getSamples(ArcInvocationContext context) { - Method method = context.getMethod(); - Tags commonTags = getCommonTags(method.getDeclaringClass().getName(), method.getName()); List timed = context.findIterceptorBindings(Timed.class); if (timed.isEmpty()) { return Collections.emptyList(); } + Tags tags = meterTagsSupport.getTags(context); List samples = new ArrayList<>(timed.size()); for (Timed t : timed) { if (t.longTask()) { - samples.add(new LongTimerSample(t, commonTags)); + samples.add(new LongTimerSample(t, tags)); } else { - samples.add(new TimerSample(t, commonTags)); + samples.add(new TimerSample(t, tags)); } } return samples; diff --git a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/mpmetrics/MetricRegistryAdapter.java b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/mpmetrics/MetricRegistryAdapter.java index be354011e3b89..4bffa61ee7830 100644 --- a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/mpmetrics/MetricRegistryAdapter.java +++ b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/mpmetrics/MetricRegistryAdapter.java @@ -90,13 +90,13 @@ public Counter counter(Metadata metadata, Tag... tags) { Counter interceptorCounter(Metadata metadata, String... tags) { return internalCounter(internalGetMetadata(metadata, MetricType.COUNTER), - new MetricDescriptor(metadata.getName(), tags)); + new MetricDescriptor(metadata.getName(), scopeTags(tags))); } Counter injectedCounter(org.eclipse.microprofile.metrics.annotation.Metric annotation) { return internalCounter( internalGetMetadata(annotation.name(), MetricType.COUNTER).merge(annotation), - new MetricDescriptor(annotation.name(), annotation.tags())); + new MetricDescriptor(annotation.name(), scopeTags(annotation.tags()))); } CounterAdapter internalCounter(MpMetadata metadata, MetricDescriptor id) { @@ -138,13 +138,13 @@ public ConcurrentGauge concurrentGauge(Metadata metadata, Tag... tags) { ConcurrentGaugeImpl interceptorConcurrentGauge(Metadata metadata, String... tags) { return internalConcurrentGauge(internalGetMetadata(metadata, MetricType.CONCURRENT_GAUGE), - new MetricDescriptor(metadata.getName(), tags)); + new MetricDescriptor(metadata.getName(), scopeTags(tags))); } ConcurrentGaugeImpl injectedConcurrentGauge(org.eclipse.microprofile.metrics.annotation.Metric annotation) { return internalConcurrentGauge( internalGetMetadata(annotation.name(), MetricType.CONCURRENT_GAUGE).merge(annotation), - new MetricDescriptor(annotation.name(), annotation.tags())); + new MetricDescriptor(annotation.name(), scopeTags(annotation.tags()))); } ConcurrentGaugeImpl internalConcurrentGauge(MpMetadata metadata, MetricDescriptor id) { @@ -276,7 +276,7 @@ public Histogram histogram(Metadata metadata, Tag... tags) { HistogramAdapter injectedHistogram(org.eclipse.microprofile.metrics.annotation.Metric annotation) { return internalHistogram( internalGetMetadata(annotation.name(), MetricType.HISTOGRAM).merge(annotation), - new MetricDescriptor(annotation.name(), annotation.tags())); + new MetricDescriptor(annotation.name(), scopeTags(annotation.tags()))); } HistogramAdapter internalHistogram(MpMetadata metadata, MetricDescriptor id) { @@ -319,7 +319,7 @@ public Meter meter(Metadata metadata, Tag... tags) { MeterAdapter injectedMeter(org.eclipse.microprofile.metrics.annotation.Metric annotation) { return internalMeter( internalGetMetadata(annotation.name(), MetricType.METERED).merge(annotation), - new MetricDescriptor(annotation.name(), annotation.tags())); + new MetricDescriptor(annotation.name(), scopeTags(annotation.tags()))); } MeterAdapter internalMeter(MpMetadata metadata, MetricDescriptor id) { @@ -363,12 +363,12 @@ public Timer timer(Metadata metadata, Tag... tags) { TimerAdapter injectedTimer(org.eclipse.microprofile.metrics.annotation.Metric annotation) { return internalTimer( internalGetMetadata(annotation.name(), MetricType.TIMER).merge(annotation), - new MetricDescriptor(annotation.name(), annotation.tags())); + new MetricDescriptor(annotation.name(), scopeTags(annotation.tags()))); } TimerAdapter interceptorTimer(Metadata metadata, String... tags) { return internalTimer(internalGetMetadata(metadata, MetricType.TIMER), - new MetricDescriptor(metadata.getName(), tags)); + new MetricDescriptor(metadata.getName(), scopeTags(tags))); } TimerAdapter internalTimer(MpMetadata metadata, MetricDescriptor id) { @@ -465,7 +465,7 @@ public Metadata getMetadata(String name) { TimerAdapter injectedSimpleTimer(org.eclipse.microprofile.metrics.annotation.Metric annotation) { return internalSimpleTimer( internalGetMetadata(annotation.name(), MetricType.SIMPLE_TIMER).merge(annotation), - new MetricDescriptor(annotation.name(), annotation.tags())); + new MetricDescriptor(annotation.name(), scopeTags(annotation.tags()))); } TimerAdapter internalSimpleTimer(MpMetadata metadata, MetricDescriptor id) { @@ -657,14 +657,23 @@ public Type getType() { return null; } + Tags scopeTags() { + return Tags.of("scope", this.type.getName()); + } + Tags scopeTags(Tag... tags) { - Tags out = Tags.of("scope", this.type.getName()); + Tags out = scopeTags(); for (Tag t : tags) { out = out.and(t.getTagName(), t.getTagValue()); } return out; } + Tags scopeTags(String... tags) { + Tags in = Tags.of(tags); + return scopeTags().and(in); + } + private MpMetadata internalGetMetadata(String name, MetricType type) { MpMetadata result = metadataMap.computeIfAbsent(name, k -> new MpMetadata(name, type)); if (result.type != type) { diff --git a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/vertx/VertxHttpServerMetrics.java b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/vertx/VertxHttpServerMetrics.java index 4fbb473487541..6f22060edc250 100644 --- a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/vertx/VertxHttpServerMetrics.java +++ b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/vertx/VertxHttpServerMetrics.java @@ -28,9 +28,9 @@ /** * HttpServerMetrics *

      - *
    • R for Request metric -- RequestMetricContext
    • + *
    • R for Request metric -- HttpRequestMetric
    • *
    • W for Websocket metric -- LongTaskTimer sample
    • - *
    • S for Socket metric -- Map
    • + *
    • S for Socket metric -- LongTaskTimer sample
    • *
    */ public class VertxHttpServerMetrics extends VertxTcpServerMetrics diff --git a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/vertx/VertxMeterBinderAdapter.java b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/vertx/VertxMeterBinderAdapter.java index e3a4b7de2a890..384f10ed0bc43 100644 --- a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/vertx/VertxMeterBinderAdapter.java +++ b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/vertx/VertxMeterBinderAdapter.java @@ -10,6 +10,7 @@ import io.vertx.core.datagram.DatagramSocketOptions; import io.vertx.core.http.HttpClientOptions; import io.vertx.core.http.HttpServerOptions; +import io.vertx.core.impl.NoStackTraceException; import io.vertx.core.metrics.MetricsOptions; import io.vertx.core.net.NetClientOptions; import io.vertx.core.net.NetServerOptions; @@ -60,7 +61,7 @@ public MetricsOptions newOptions() { @Override public HttpServerMetrics createHttpServerMetrics(HttpServerOptions options, SocketAddress localAddress) { if (httpBinderConfiguration == null) { - throw new IllegalStateException("HttpBinderConfiguration was not found"); + throw new NoStackTraceException("HttpBinderConfiguration was not found"); } if (httpBinderConfiguration.isServerEnabled()) { log.debugf("Create HttpServerMetrics with options %s and address %s", options, localAddress); diff --git a/extensions/narayana-jta/deployment/src/test/java/io/quarkus/narayana/quarkus/QuarkusTransactionTest.java b/extensions/narayana-jta/deployment/src/test/java/io/quarkus/narayana/quarkus/QuarkusTransactionTest.java index f895d26c31fe3..4f7f8c0cfdb9a 100644 --- a/extensions/narayana-jta/deployment/src/test/java/io/quarkus/narayana/quarkus/QuarkusTransactionTest.java +++ b/extensions/narayana-jta/deployment/src/test/java/io/quarkus/narayana/quarkus/QuarkusTransactionTest.java @@ -3,22 +3,34 @@ import static io.quarkus.narayana.jta.QuarkusTransaction.beginOptions; import static io.quarkus.narayana.jta.QuarkusTransaction.runOptions; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.IntStream; import jakarta.enterprise.context.ContextNotActiveException; import jakarta.enterprise.context.control.ActivateRequestContext; import jakarta.enterprise.inject.Produces; import jakarta.inject.Inject; import jakarta.inject.Singleton; +import jakarta.transaction.HeuristicMixedException; +import jakarta.transaction.HeuristicRollbackException; +import jakarta.transaction.NotSupportedException; import jakarta.transaction.RollbackException; import jakarta.transaction.Status; import jakarta.transaction.Synchronization; import jakarta.transaction.SystemException; +import jakarta.transaction.Transaction; import jakarta.transaction.TransactionManager; import jakarta.transaction.TransactionScoped; +import jakarta.transaction.TransactionSynchronizationRegistry; import org.eclipse.microprofile.context.ThreadContext; import org.jboss.shrinkwrap.api.ShrinkWrap; @@ -35,7 +47,10 @@ public class QuarkusTransactionTest { - private static AtomicInteger counter = new AtomicInteger(); + private static final AtomicInteger counter = new AtomicInteger(); + + @Inject + TransactionSynchronizationRegistry tsr; @Inject TransactionManager transactionManager; @@ -241,6 +256,8 @@ public void testCallJoinExisting() throws SystemException { @Test public void testConcurrentTransactionScopedBeanCreation() { + counter.set(0); + // 1. A Transaction is activated in a parent thread. QuarkusTransaction.run(() -> { ExecutorService executor = Executors.newCachedThreadPool(); @@ -264,6 +281,86 @@ public void testConcurrentTransactionScopedBeanCreation() { Assertions.assertEquals(1, counter.get()); } + @Test + public void testConcurrentTransactionScopedBeanCreationWithSynchronization() { + // test that propagating a transaction to other threads and use of Synchronizations do not interfere + counter.set(0); + + // 1. A Transaction is activated in a parent thread. + QuarkusTransaction.run(() -> { + ExecutorService executor = Executors.newCachedThreadPool(); + + try { + Transaction txn = testBean.doWorkWithSynchronization(tsr, transactionManager); + + // 2. The parent thread starts 2 child threads, and propagates the transaction. + // 3. The child threads access a @TransactionScoped bean concurrently, + Future f1 = executor + .submit(threadContext + .contextualCallable(() -> testBean.doWorkWithSynchronization(tsr, transactionManager))); + Future f2 = executor + .submit(threadContext + .contextualCallable(() -> testBean.doWorkWithSynchronization(tsr, transactionManager))); + + Transaction t1 = f1.get(); + Transaction t2 = f2.get(); + + // the Synchronization callbacks for the parent thread and the two child threads should + // all have run with the same transaction context + Assertions.assertEquals(t1, txn); + Assertions.assertEquals(t2, txn); + } catch (Throwable e) { + throw new AssertionError("Should not have thrown", e); + } finally { + executor.shutdownNow(); + } + }); + } + + @Test + public void testConcurrentWithSynchronization() { + // test that Synchronizations registered with concurrent transactions do not interfere + Collection> callables = new ArrayList<>(); + IntStream.rangeClosed(1, 8) + .forEach(i -> callables.add(() -> { + try { + // start a txn + // then register an interposed Synchronization + // then commit the txn + // and then verify the Synchronization ran with the same transaction + TestInterposedSync t = new TestInterposedSync(tsr, transactionManager); + transactionManager.begin(); + Transaction txn = transactionManager.getTransaction(); + tsr.registerInterposedSynchronization(t); + transactionManager.commit(); + // check that the transaction seen by the Synchronization is same as the one we just started + Assertions.assertEquals(txn, t.getContext(), "Synchronization ran with the wrong context"); + } catch (NotSupportedException | SystemException | RollbackException | HeuristicMixedException + | HeuristicRollbackException | SecurityException | IllegalStateException e) { + throw new RuntimeException(e); + } + return null; + })); + + ExecutorService executor = Executors.newCachedThreadPool(); + + try { + List> futures = executor.invokeAll(callables); + futures.forEach(f -> { + try { + // verify that the task did not throw an exception + f.get(); + } catch (InterruptedException | ExecutionException e) { + throw new AssertionError("Should not have thrown", e); + } + }); + } catch (InterruptedException e) { + throw new AssertionError("Should not have thrown", e); + } finally { + executor.shutdownNow(); + } + } + TestSync register() { TestSync t = new TestSync(); try { @@ -289,10 +386,51 @@ public void afterCompletion(int status) { } } + static class TestInterposedSync implements Synchronization { + private final TransactionManager tm; + private Transaction context; + int completionStatus = -1; + + public TestInterposedSync(TransactionSynchronizationRegistry tsr, TransactionManager tm) { + this.tm = tm; + } + + @Override + public void beforeCompletion() { + try { + // remember the transaction context used to run the Synchronization + context = tm.getTransaction(); + } catch (SystemException e) { + throw new RuntimeException(e); + } + } + + @Override + public void afterCompletion(int status) { + this.completionStatus = status; + } + + public Transaction getContext() { + // report the transaction context used to run the Synchronization + return context; + } + } + static class TransactionScopedTestBean { public void doWork() { } + + public Transaction doWorkWithSynchronization(TransactionSynchronizationRegistry tsr, TransactionManager tm) { + TestInterposedSync t = new TestInterposedSync(tsr, tm); + + try { + tsr.registerInterposedSynchronization(t); + return tm.getTransaction(); + } catch (Exception e) { + throw new AssertionError("Should not have thrown", e); + } + } } @Singleton diff --git a/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/NarayanaJtaProducers.java b/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/NarayanaJtaProducers.java index b784d5b92e6df..327acd72af42c 100644 --- a/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/NarayanaJtaProducers.java +++ b/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/NarayanaJtaProducers.java @@ -16,6 +16,7 @@ import com.arjuna.ats.internal.jta.transaction.arjunacore.TransactionSynchronizationRegistryImple; import io.quarkus.arc.Unremovable; +import io.quarkus.narayana.jta.runtime.internal.tsr.TransactionSynchronizationRegistryWrapper; @Dependent public class NarayanaJtaProducers { @@ -50,7 +51,7 @@ public XAResourceRecoveryRegistry xaResourceRecoveryRegistry() { @ApplicationScoped @Unremovable public TransactionSynchronizationRegistry transactionSynchronizationRegistry() { - return new TransactionSynchronizationRegistryImple(); + return new TransactionSynchronizationRegistryWrapper(new TransactionSynchronizationRegistryImple()); } @Produces diff --git a/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/internal/tsr/AgroalOrderedLastSynchronizationList.java b/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/internal/tsr/AgroalOrderedLastSynchronizationList.java new file mode 100644 index 0000000000000..51ee738db44b6 --- /dev/null +++ b/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/internal/tsr/AgroalOrderedLastSynchronizationList.java @@ -0,0 +1,190 @@ +package io.quarkus.narayana.jta.runtime.internal.tsr; + +import java.util.ArrayList; +import java.util.List; + +import jakarta.transaction.Status; +import jakarta.transaction.Synchronization; +import jakarta.transaction.TransactionSynchronizationRegistry; + +import org.jboss.logging.Logger; + +public class AgroalOrderedLastSynchronizationList implements Synchronization { + private static final Logger LOGGER = Logger.getLogger(AgroalOrderedLastSynchronizationList.class); + private static final String ADD_SYNC_ERROR = "Syncs are not allowed because the group of synchronizations to which this sync belongs has already ran"; + private static final String REGISTER_SYNC_ERROR = "Syncs are not allowed to be registered when the transaction is in state "; + + // order the groups of synchronization as follows (Agroal is last since it needs to validate that + // connection wrappers are closed at the right time): + private static final String[] PKG_PREFIXES = { "", "org.hibernate", "io.agroal.narayana" }; + + private final List synchGroups = new ArrayList<>(); + private SynchronizationGroup otherSynchs; + private final TransactionSynchronizationRegistry tsr; + private volatile Throwable deferredThrowable; // remember the first beforeCompletion exception + + /* + * Keep track of whether a synchronization group has been processed. + * If a group of synchs has already been processed then do not allow further synchs to be registered in that group. + * If a group of synchs is currently being processed then allow it to be registered. + * But note that no synchronizations can be registered after the transaction has finished preparing. + */ + private enum ExecutionStatus { + PENDING, // the synchronization has not started executing + RUNNING, // the synchronization is executing + FINISHED // the synchronization has executed + } + + /* + * Synchronizations are grouped by package prefix and these groups are ordered such that the + * synchronizations in the first group execute first, then the second group is processed, etc. + * In particular, the Agroal synchronization group runs last. + * + * The beforeCompletion methods within a group are called in the order they were added, + * and the afterCompletion methods are ran in the reverse order + */ + private class SynchronizationGroup implements Synchronization { + String packagePrefix; // Synchronizations with this package prefix belong to this group + final List synchs; // the Synchronizations in the group + volatile ExecutionStatus status; // track the status to decide when it's too late to allow more registrations + + public SynchronizationGroup(String packagePrefix) { + this.packagePrefix = packagePrefix; + this.synchs = new ArrayList<>(); + this.status = ExecutionStatus.PENDING; + } + + public void add(Synchronization synchronization) { + if (status == ExecutionStatus.FINISHED) { + // this group of syncs have already ran + throw new IllegalStateException(ADD_SYNC_ERROR); + } + synchs.add(synchronization); + } + + @Override + public void beforeCompletion() { + status = ExecutionStatus.RUNNING; + + // Note that because synchronizations can register other synchronizations + // we cannot use enhanced for loops as that could cause a concurrency exception + for (int i = 0; i < synchs.size(); i++) { + Synchronization sync = synchs.get(i); + + try { + sync.beforeCompletion(); + } catch (Exception e) { + if (LOGGER.isDebugEnabled()) { + LOGGER.debugf( + "The synchronization %s associated with tx key %s failed during beforeCompletion: %s", + sync, tsr.getTransactionKey(), e.getMessage()); + } + + if (deferredThrowable == null) { + // only save the first failure + deferredThrowable = e; + } + } + } + + status = ExecutionStatus.FINISHED; + } + + @Override + public void afterCompletion(int status) { + // The list should be iterated in reverse order + for (int i = synchs.size(); i-- > 0;) { + synchs.get(i).afterCompletion(status); + } + } + + // does packageName belong to this group of synchronizations + private boolean shouldAdd(String packageName) { + return !packagePrefix.isEmpty() && packageName.startsWith(packagePrefix); + } + } + + public AgroalOrderedLastSynchronizationList( + TransactionSynchronizationRegistryWrapper transactionSynchronizationRegistryWrapper) { + + this.tsr = transactionSynchronizationRegistryWrapper; + + for (var packagePrefix : PKG_PREFIXES) { + var synchronizationGroup = new SynchronizationGroup(packagePrefix); + + synchGroups.add(synchronizationGroup); + + if (packagePrefix.isEmpty()) { + otherSynchs = synchronizationGroup; // the catch-all group + } + } + } + + /** + * Register an interposed synchronization. Note that synchronizations are not allowed if: + *

    + * + * @param synchronization The synchronization to register + * @throws IllegalStateException if the transaction is in the wrong state: + *

      + *
    1. the transaction has already prepared; + *
    2. the transaction is marked rollback only + *
    3. the group that the synchronization should belong to has already been processed + *
    + */ + public void registerInterposedSynchronization(Synchronization synchronization) { + int status = tsr.getTransactionStatus(); + + switch (status) { + case Status.STATUS_ACTIVE: + case Status.STATUS_PREPARING: + break; + default: + throw new IllegalStateException(REGISTER_SYNC_ERROR + status); + } + + // add the synchronization to the group that matches this package and, if there is no such group + // then add it to the catch-all group (otherSyncs) + String packageName = synchronization.getClass().getName(); + SynchronizationGroup synchGroup = otherSynchs; + + for (SynchronizationGroup g : synchGroups) { + if (g.shouldAdd(packageName)) { + synchGroup = g; + break; + } + } + + synchGroup.add(synchronization); + } + + /** + * Exceptions from beforeCompletion Synchronizations are not caught because such errors should cause the + * transaction to roll back. + */ + @Override + public void beforeCompletion() { + // run each group of synchs according to the order they were added to the list + for (SynchronizationGroup g : synchGroups) { + g.beforeCompletion(); + } + + if (deferredThrowable != null) { + /* + * If any Synchronization threw an exception then only report the first one. + * + * Cause the transaction to rollback. The underlying transaction manager will catch the runtime + * exception and re-throw it when it does the rollback + */ + throw new RuntimeException(deferredThrowable); + } + } + + @Override + public void afterCompletion(int status) { + // run each group of synchs according to the order they were added to the list + for (SynchronizationGroup g : synchGroups) { + g.afterCompletion(status); + } + } +} diff --git a/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/internal/tsr/TransactionSynchronizationRegistryWrapper.java b/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/internal/tsr/TransactionSynchronizationRegistryWrapper.java new file mode 100644 index 0000000000000..3c88840159e7d --- /dev/null +++ b/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/internal/tsr/TransactionSynchronizationRegistryWrapper.java @@ -0,0 +1,87 @@ +package io.quarkus.narayana.jta.runtime.internal.tsr; + +import jakarta.transaction.Synchronization; +import jakarta.transaction.TransactionSynchronizationRegistry; + +import org.jboss.logging.Logger; + +import com.arjuna.ats.internal.jta.transaction.arjunacore.TransactionSynchronizationRegistryImple; + +/** + * Agroal registers an interposed synchronization which validates that connections have been released. + * Components such as hibernate release connections in an interposed synchronization. + * Therefore, we must ensure that Agroal runs last. + *

    + * + * This wrapper re-orders interposed synchronizations as follows: [other, hibernate-orm, agroal]. + *

    + * + * Synchronizations are placed into groups according to their package name and the groups are ordered which means + * that all hibernate synchronizations run before Agroal ones and all other synchs run before the hibernate ones. + *

    + * + * See {@code AgroalOrderedLastSynchronizationList} for details of the re-ordering. + */ +public class TransactionSynchronizationRegistryWrapper implements TransactionSynchronizationRegistry { + private final Object key = new Object(); + private static final Logger LOG = Logger.getLogger(TransactionSynchronizationRegistryWrapper.class); + + private final TransactionSynchronizationRegistryImple tsr; + private transient com.arjuna.ats.internal.jta.transaction.arjunacore.TransactionManagerImple delegate; + + public TransactionSynchronizationRegistryWrapper( + TransactionSynchronizationRegistryImple transactionSynchronizationRegistryImple) { + this.tsr = transactionSynchronizationRegistryImple; + } + + @Override + public void registerInterposedSynchronization(Synchronization sync) { + AgroalOrderedLastSynchronizationList agroalOrderedLastSynchronization = (AgroalOrderedLastSynchronizationList) tsr + .getResource(key); + + if (agroalOrderedLastSynchronization == null) { + synchronized (key) { + agroalOrderedLastSynchronization = (AgroalOrderedLastSynchronizationList) tsr.getResource(key); + if (agroalOrderedLastSynchronization == null) { + agroalOrderedLastSynchronization = new AgroalOrderedLastSynchronizationList(this); + + tsr.putResource(key, agroalOrderedLastSynchronization); + tsr.registerInterposedSynchronization(agroalOrderedLastSynchronization); + } + } + } + + // add the synchronization to the list that does the reordering + agroalOrderedLastSynchronization.registerInterposedSynchronization(sync); + } + + @Override + public Object getTransactionKey() { + return tsr.getTransactionKey(); + } + + @Override + public int getTransactionStatus() { + return tsr.getTransactionStatus(); + } + + @Override + public boolean getRollbackOnly() { + return tsr.getRollbackOnly(); + } + + @Override + public void setRollbackOnly() { + tsr.setRollbackOnly(); + } + + @Override + public Object getResource(Object key) { + return tsr.getResource(key); + } + + @Override + public void putResource(Object key, Object value) { + tsr.putResource(key, value); + } +} diff --git a/extensions/netty/deployment/src/main/java/io/quarkus/netty/deployment/NettyProcessor.java b/extensions/netty/deployment/src/main/java/io/quarkus/netty/deployment/NettyProcessor.java index 766d2f8c00800..8d0f294af25fe 100644 --- a/extensions/netty/deployment/src/main/java/io/quarkus/netty/deployment/NettyProcessor.java +++ b/extensions/netty/deployment/src/main/java/io/quarkus/netty/deployment/NettyProcessor.java @@ -162,6 +162,29 @@ NativeImageConfigBuildItem build( log.debug("Not registering Netty native kqueue classes as they were not found"); } + builder.addRuntimeReinitializedClass("io.netty.util.internal.PlatformDependent") + .addRuntimeReinitializedClass("io.netty.util.internal.PlatformDependent0") + .addRuntimeReinitializedClass("io.netty.buffer.PooledByteBufAllocator"); + + if (QuarkusClassLoader.isClassPresentAtRuntime("io.netty.buffer.UnpooledByteBufAllocator")) { + builder.addRuntimeReinitializedClass("io.netty.buffer.UnpooledByteBufAllocator") + .addRuntimeReinitializedClass("io.netty.buffer.Unpooled") + .addRuntimeReinitializedClass("io.vertx.core.http.impl.Http1xServerResponse") + .addRuntimeReinitializedClass("io.netty.handler.codec.http.HttpObjectAggregator") + .addRuntimeReinitializedClass("io.netty.handler.codec.ReplayingDecoderByteBuf") + .addRuntimeReinitializedClass("io.vertx.core.parsetools.impl.RecordParserImpl"); + + if (QuarkusClassLoader.isClassPresentAtRuntime("io.vertx.ext.web.client.impl.MultipartFormUpload")) { + builder.addRuntimeReinitializedClass("io.vertx.ext.web.client.impl.MultipartFormUpload"); + } + + if (QuarkusClassLoader + .isClassPresentAtRuntime("org.jboss.resteasy.reactive.client.impl.multipart.QuarkusMultipartFormUpload")) { + builder.addRuntimeReinitializedClass( + "org.jboss.resteasy.reactive.client.impl.multipart.QuarkusMultipartFormUpload"); + } + } + return builder //TODO: make configurable .build(); } diff --git a/extensions/oidc-client-filter/runtime/src/main/java/io/quarkus/oidc/client/filter/runtime/AbstractOidcClientRequestFilter.java b/extensions/oidc-client-filter/runtime/src/main/java/io/quarkus/oidc/client/filter/runtime/AbstractOidcClientRequestFilter.java index 5b2a0f25f67ac..458fa15740778 100644 --- a/extensions/oidc-client-filter/runtime/src/main/java/io/quarkus/oidc/client/filter/runtime/AbstractOidcClientRequestFilter.java +++ b/extensions/oidc-client-filter/runtime/src/main/java/io/quarkus/oidc/client/filter/runtime/AbstractOidcClientRequestFilter.java @@ -25,8 +25,8 @@ public void filter(ClientRequestContext requestContext) throws IOException { final String accessToken = getAccessToken(); requestContext.getHeaders().add(HttpHeaders.AUTHORIZATION, BEARER_SCHEME_WITH_SPACE + accessToken); } catch (DisabledOidcClientException ex) { - LOG.debug("Client is disabled, aborting the request"); - throw ex; + LOG.debug("Client is disabled, acquiring and propagating the token is not necessary"); + return; } catch (Exception ex) { LOG.debugf("Access token is not available, cause: %s, aborting the request", ex.getMessage()); throw (ex instanceof RuntimeException) ? (RuntimeException) ex : new RuntimeException(ex); diff --git a/extensions/oidc-client-reactive-filter/runtime/src/main/java/io/quarkus/oidc/client/reactive/filter/runtime/AbstractOidcClientRequestReactiveFilter.java b/extensions/oidc-client-reactive-filter/runtime/src/main/java/io/quarkus/oidc/client/reactive/filter/runtime/AbstractOidcClientRequestReactiveFilter.java index 9c266fb892baf..5bff7d719403d 100644 --- a/extensions/oidc-client-reactive-filter/runtime/src/main/java/io/quarkus/oidc/client/reactive/filter/runtime/AbstractOidcClientRequestReactiveFilter.java +++ b/extensions/oidc-client-reactive-filter/runtime/src/main/java/io/quarkus/oidc/client/reactive/filter/runtime/AbstractOidcClientRequestReactiveFilter.java @@ -39,11 +39,12 @@ public void accept(Tokens tokens) { @Override public void accept(Throwable t) { if (t instanceof DisabledOidcClientException) { - LOG.debug("Client is disabled, aborting the request"); + LOG.debug("Client is disabled, acquiring and propagating the token is not necessary"); + requestContext.resume(); } else { LOG.debugf("Access token is not available, cause: %s, aborting the request", t.getMessage()); + requestContext.resume((t instanceof RuntimeException) ? t : new RuntimeException(t)); } - requestContext.resume((t instanceof RuntimeException) ? t : new RuntimeException(t)); } }); } 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 0f0252a3a003e..8dcf143c6cadb 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,6 +17,8 @@ import io.quarkus.oidc.client.OidcClientConfig; import io.quarkus.oidc.client.OidcClientException; import io.quarkus.oidc.client.Tokens; +import io.quarkus.oidc.common.OidcEndpoint; +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; @@ -46,12 +48,12 @@ public class OidcClientImpl implements OidcClient { private final String clientSecretBasicAuthScheme; private final Key clientJwtKey; private final OidcClientConfig oidcConfig; - private final List filters; + private final Map> filters; private volatile boolean closed; public OidcClientImpl(WebClient client, String tokenRequestUri, String tokenRevokeUri, String grantType, MultiMap tokenGrantParams, MultiMap commonRefreshGrantParams, OidcClientConfig oidcClientConfig, - List filters) { + Map> filters) { this.client = client; this.tokenRequestUri = tokenRequestUri; this.tokenRevokeUri = tokenRevokeUri; @@ -71,7 +73,7 @@ public Uni getTokens(Map additionalGrantParameters) { throw new OidcClientException( "Only 'refresh_token' grant is supported, please call OidcClient#refreshTokens method instead"); } - return getJsonResponse(tokenGrantParams, additionalGrantParameters, false); + return getJsonResponse(OidcEndpoint.Type.TOKEN, tokenGrantParams, additionalGrantParameters, false); } @Override @@ -82,7 +84,7 @@ public Uni refreshTokens(String refreshToken, Map additi } MultiMap refreshGrantParams = copyMultiMap(commonRefreshGrantParams); refreshGrantParams.add(OidcConstants.REFRESH_TOKEN_VALUE, refreshToken); - return getJsonResponse(refreshGrantParams, additionalGrantParameters, true); + return getJsonResponse(OidcEndpoint.Type.TOKEN, refreshGrantParams, additionalGrantParameters, true); } @Override @@ -94,7 +96,8 @@ public Uni revokeAccessToken(String accessToken, Map ad if (tokenRevokeUri != null) { MultiMap tokenRevokeParams = new MultiMap(io.vertx.core.MultiMap.caseInsensitiveMultiMap()); tokenRevokeParams.set(OidcConstants.REVOCATION_TOKEN, accessToken); - return postRequest(client.postAbs(tokenRevokeUri), tokenRevokeParams, additionalParameters, false) + return postRequest(OidcEndpoint.Type.TOKEN_REVOCATION, client.postAbs(tokenRevokeUri), tokenRevokeParams, + additionalParameters, false) .transform(resp -> toRevokeResponse(resp)); } else { LOG.debugf("%s OidcClient can not revoke the access token because the revocation endpoint URL is not set"); @@ -111,20 +114,23 @@ private Boolean toRevokeResponse(HttpResponse resp) { return resp.statusCode() == 503 ? false : true; } - private Uni getJsonResponse(MultiMap formBody, Map additionalGrantParameters, boolean refresh) { + private Uni getJsonResponse(OidcEndpoint.Type endpointType, MultiMap formBody, + Map additionalGrantParameters, + boolean refresh) { //Uni needs to be lazy by default, we don't send the request unless //something has subscribed to it. This is important for the CAS state //management in TokensHelper return Uni.createFrom().deferred(new Supplier>() { @Override public Uni get() { - return postRequest(client.postAbs(tokenRequestUri), formBody, additionalGrantParameters, refresh) + return postRequest(endpointType, client.postAbs(tokenRequestUri), formBody, additionalGrantParameters, refresh) .transform(resp -> emitGrantTokens(resp, refresh)); } }); } - private UniOnItem> postRequest(HttpRequest request, MultiMap formBody, + private UniOnItem> postRequest(OidcEndpoint.Type endpointType, HttpRequest request, + MultiMap formBody, Map additionalGrantParameters, boolean refresh) { MultiMap body = formBody; @@ -165,7 +171,7 @@ private UniOnItem> postRequest(HttpRequest request, } // Retry up to three times with a one-second delay between the retries if the connection is closed Buffer buffer = OidcCommonUtils.encodeForm(body); - Uni> response = filter(request, buffer).sendBuffer(buffer) + Uni> response = filter(endpointType, request, buffer).sendBuffer(buffer) .onFailure(ConnectException.class) .retry() .atMost(oidcConfig.connectionRetryCount) @@ -259,9 +265,12 @@ private void checkClosed() { } } - private HttpRequest filter(HttpRequest request, Buffer body) { - for (OidcRequestFilter filter : filters) { - filter.filter(request, body, null); + private HttpRequest filter(OidcEndpoint.Type endpointType, HttpRequest request, Buffer body) { + if (!filters.isEmpty()) { + OidcRequestContextProperties props = new OidcRequestContextProperties(); + for (OidcRequestFilter filter : OidcCommonUtils.getMatchingOidcRequestFilters(filters, endpointType)) { + filter.filter(request, body, props); + } } 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 88004463f2e5f..cff9f35a930cc 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,6 +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.OidcEndpoint; import io.quarkus.oidc.common.OidcRequestFilter; import io.quarkus.oidc.common.runtime.OidcCommonUtils; import io.quarkus.oidc.common.runtime.OidcConstants; @@ -122,7 +123,7 @@ protected static Uni createOidcClientUni(OidcClientConfig oidcConfig WebClient client = WebClient.create(new io.vertx.mutiny.core.Vertx(vertx.get()), options); - List clientRequestFilters = OidcCommonUtils.getClientRequestCustomizer(); + Map> oidcRequestFilters = OidcCommonUtils.getOidcRequestFilters(); Uni tokenUrisUni = null; if (OidcCommonUtils.isAbsoluteUrl(oidcConfig.tokenPath)) { @@ -137,7 +138,7 @@ protected static Uni createOidcClientUni(OidcClientConfig oidcConfig OidcCommonUtils.getOidcEndpointUrl(authServerUriString, oidcConfig.tokenPath), OidcCommonUtils.getOidcEndpointUrl(authServerUriString, oidcConfig.revokePath))); } else { - tokenUrisUni = discoverTokenUris(client, clientRequestFilters, authServerUriString.toString(), oidcConfig); + tokenUrisUni = discoverTokenUris(client, oidcRequestFilters, authServerUriString.toString(), oidcConfig); } } return tokenUrisUni.onItemOrFailure() @@ -193,7 +194,7 @@ public OidcClient apply(OidcConfigurationMetadata metadata, Throwable t) { tokenGrantParams, commonRefreshGrantParams, oidcConfig, - clientRequestFilters); + oidcRequestFilters); } }); @@ -211,10 +212,10 @@ private static void setGrantClientParams(OidcClientConfig oidcConfig, MultiMap g } private static Uni discoverTokenUris(WebClient client, - List clientRequestFilters, + Map> oidcRequestFilters, String authServerUrl, OidcClientConfig oidcConfig) { final long connectionDelayInMillisecs = OidcCommonUtils.getConnectionDelayInMillis(oidcConfig); - return OidcCommonUtils.discoverMetadata(client, clientRequestFilters, authServerUrl, connectionDelayInMillisecs) + return OidcCommonUtils.discoverMetadata(client, oidcRequestFilters, authServerUrl, connectionDelayInMillisecs) .onItem().transform(json -> new OidcConfigurationMetadata(json.getString("token_endpoint"), json.getString("revocation_endpoint"))); } diff --git a/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/OidcEndpoint.java b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/OidcEndpoint.java new file mode 100644 index 0000000000000..2707f8f3bb09c --- /dev/null +++ b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/OidcEndpoint.java @@ -0,0 +1,52 @@ +package io.quarkus.oidc.common; + +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * Annotation that can be used to restrict {@link OidcRequestFilter} to specific OIDC endpoints + */ +@Target({ TYPE }) +@Retention(RUNTIME) +public @interface OidcEndpoint { + + enum Type { + ALL, + + /** + * Applies to OIDC discovery requests + */ + DISCOVERY, + + /** + * Applies to OIDC token endpoint requests + */ + TOKEN, + + /** + * Applies to OIDC token revocation endpoint requests + */ + TOKEN_REVOCATION, + + /** + * Applies to OIDC token introspection requests + */ + INTROSPECTION, + /** + * Applies to OIDC JSON Web Key Set endpoint requests + */ + JWKS, + /** + * Applies to OIDC UserInfo endpoint requests + */ + USERINFO + } + + /** + * Identifies an OIDC tenant to which a given feature applies. + */ + Type value() default Type.ALL; +} 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 index d7a1f620a48af..e5dee80db7fe3 100644 --- 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 @@ -1,20 +1,28 @@ package io.quarkus.oidc.common; +import java.util.Collections; import java.util.Map; public class OidcRequestContextProperties { public static String TOKEN = "token"; public static String TOKEN_CREDENTIAL = "token_credential"; + public static String DISCOVERY_ENDPOINT = "discovery_endpoint"; private final Map properties; + public OidcRequestContextProperties() { + this(Map.of()); + } + public OidcRequestContextProperties(Map properties) { this.properties = properties; } - public Object get(String name) { - return properties.get(name); + public T get(String name) { + @SuppressWarnings("unchecked") + T value = (T) properties.get(name); + return value; } public String getString(String name) { @@ -25,4 +33,8 @@ public T get(String name, Class type) { return type.cast(get(name)); } + public Map getAll() { + return Collections.unmodifiableMap(properties); + } + } 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 index 7318f34eff3b1..93834a53fb41e 100644 --- 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 @@ -5,15 +5,18 @@ /** * 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 + * which are made from the OIDC adapter to the OIDC provider. + *

    + * Filter can be restricted to a specific OIDC endpoint with a {@link OidcEndpoint} annotation. */ 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 + * @param contextProperties context properties that can be available in context of some requests */ 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 45b1923d5d805..f3810d610a001 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 @@ -180,7 +180,7 @@ public static enum Method { } /** - * The client secret value - it will be ignored if 'secret.key' is set + * The client secret value - it will be ignored if 'credentials.secret' is set */ @ConfigItem public Optional value = Optional.empty(); 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 3b0e9dbdc8f76..92b12d8ed569e 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 @@ -13,7 +13,9 @@ import java.security.KeyStore; import java.security.PrivateKey; import java.time.Duration; +import java.util.ArrayList; import java.util.Base64; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; @@ -30,6 +32,8 @@ import io.quarkus.arc.ArcContainer; import io.quarkus.credentials.CredentialsProvider; import io.quarkus.credentials.runtime.CredentialsProviderFinder; +import io.quarkus.oidc.common.OidcEndpoint; +import io.quarkus.oidc.common.OidcRequestContextProperties; import io.quarkus.oidc.common.OidcRequestFilter; import io.quarkus.oidc.common.runtime.OidcCommonConfig.Credentials; import io.quarkus.oidc.common.runtime.OidcCommonConfig.Credentials.Provider; @@ -137,7 +141,7 @@ public static void setHttpClientOptions(OidcCommonConfig oidcConfig, TlsConfig t .setPassword(oidcConfig.tls.getTrustStorePassword().orElse("password")) .setAlias(oidcConfig.tls.getTrustStoreCertAlias().orElse(null)) .setValue(io.vertx.core.buffer.Buffer.buffer(trustStoreData)) - .setType(getStoreType(oidcConfig.tls.trustStoreFileType, oidcConfig.tls.trustStoreFile.get())) + .setType(getKeyStoreType(oidcConfig.tls.trustStoreFileType, oidcConfig.tls.trustStoreFile.get())) .setProvider(oidcConfig.tls.trustStoreProvider.orElse(null)); options.setTrustOptions(trustStoreOptions); if (Verification.CERTIFICATE_VALIDATION == oidcConfig.tls.verification.orElse(Verification.REQUIRED)) { @@ -156,7 +160,7 @@ public static void setHttpClientOptions(OidcCommonConfig oidcConfig, TlsConfig t .setAlias(oidcConfig.tls.keyStoreKeyAlias.orElse(null)) .setAliasPassword(oidcConfig.tls.keyStoreKeyPassword.orElse(null)) .setValue(io.vertx.core.buffer.Buffer.buffer(keyStoreData)) - .setType(getStoreType(oidcConfig.tls.keyStoreFileType, oidcConfig.tls.keyStoreFile.get())) + .setType(getKeyStoreType(oidcConfig.tls.keyStoreFileType, oidcConfig.tls.keyStoreFile.get())) .setProvider(oidcConfig.tls.keyStoreProvider.orElse(null)); if (oidcConfig.tls.keyStorePassword.isPresent()) { @@ -184,7 +188,7 @@ public static void setHttpClientOptions(OidcCommonConfig oidcConfig, TlsConfig t options.setConnectTimeout((int) oidcConfig.getConnectionTimeout().toMillis()); } - private static String getStoreType(Optional fileType, Path storePath) { + public static String getKeyStoreType(Optional fileType, Path storePath) { if (fileType.isPresent()) { return fileType.get().toUpperCase(); } @@ -427,12 +431,16 @@ 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, Map> filters, String authServerUrl, long connectionDelayInMillisecs) { - final String discoveryUrl = authServerUrl + OidcConstants.WELL_KNOWN_CONFIGURATION; + final String discoveryUrl = getDiscoveryUri(authServerUrl); HttpRequest request = client.getAbs(discoveryUrl); - for (OidcRequestFilter filter : filters) { - filter.filter(request, null, null); + if (!filters.isEmpty()) { + OidcRequestContextProperties requestProps = new OidcRequestContextProperties( + Map.of(OidcRequestContextProperties.DISCOVERY_ENDPOINT, discoveryUrl)); + for (OidcRequestFilter filter : getMatchingOidcRequestFilters(filters, OidcEndpoint.Type.DISCOVERY)) { + filter.filter(request, null, requestProps); + } } return request.send().onItem().transform(resp -> { if (resp.statusCode() == 200) { @@ -452,6 +460,10 @@ public static Uni discoverMetadata(WebClient client, List getClientRequestCustomizer() { + public static Map> getOidcRequestFilters() { ArcContainer container = Arc.container(); if (container != null) { - return container.listAll(OidcRequestFilter.class).stream().map(handle -> handle.get()) - .collect(Collectors.toList()); + Map> map = new HashMap<>(); + for (OidcRequestFilter filter : container.listAll(OidcRequestFilter.class).stream().map(handle -> handle.get()) + .collect(Collectors.toList())) { + OidcEndpoint endpoint = filter.getClass().getAnnotation(OidcEndpoint.class); + OidcEndpoint.Type type = endpoint != null ? endpoint.value() : OidcEndpoint.Type.ALL; + map.computeIfAbsent(type, k -> new ArrayList()).add(filter); + } + return map; + } + return Map.of(); + } + + public static List getMatchingOidcRequestFilters(Map> filters, + OidcEndpoint.Type type) { + List typeSpecific = filters.get(type); + List all = filters.get(OidcEndpoint.Type.ALL); + if (typeSpecific == null && all == null) { + return List.of(); } - return List.of(); + if (typeSpecific != null && all == null) { + return typeSpecific; + } else if (typeSpecific == null && all != null) { + return all; + } else { + List combined = new ArrayList<>(typeSpecific.size() + all.size()); + combined.addAll(typeSpecific); + combined.addAll(all); + return combined; + } + } } diff --git a/extensions/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 05a69f6df1015..6a82d0475c8a3 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 @@ -35,7 +35,7 @@ public class DevServicesConfig { * string. * Set 'quarkus.keycloak.devservices.keycloak-x-image' to override this check. */ - @ConfigItem(defaultValue = "quay.io/keycloak/keycloak:22.0.5") + @ConfigItem(defaultValue = "quay.io/keycloak/keycloak:23.0.1") public String imageName; /** diff --git a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakDevServicesProcessor.java b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakDevServicesProcessor.java index 23ffebaa2e6a2..019a3e4fad943 100644 --- a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakDevServicesProcessor.java +++ b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakDevServicesProcessor.java @@ -108,7 +108,7 @@ public class KeycloakDevServicesProcessor { private static final String KEYCLOAK_QUARKUS_HOSTNAME = "KC_HOSTNAME"; private static final String KEYCLOAK_QUARKUS_ADMIN_PROP = "KEYCLOAK_ADMIN"; private static final String KEYCLOAK_QUARKUS_ADMIN_PASSWORD_PROP = "KEYCLOAK_ADMIN_PASSWORD"; - private static final String KEYCLOAK_QUARKUS_START_CMD = "start --storage=chm --http-enabled=true --hostname-strict=false --hostname-strict-https=false"; + private static final String KEYCLOAK_QUARKUS_START_CMD = "start --http-enabled=true --hostname-strict=false --hostname-strict-https=false"; private static final String JAVA_OPTS = "JAVA_OPTS"; private static final String OIDC_USERS = "oidc.users"; diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcConfigurationMetadata.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcConfigurationMetadata.java index 4e7795a802431..aee8379f99360 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcConfigurationMetadata.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcConfigurationMetadata.java @@ -17,6 +17,7 @@ public class OidcConfigurationMetadata { private static final String END_SESSION_ENDPOINT = "end_session_endpoint"; private static final String SCOPES_SUPPORTED = "scopes_supported"; + private final String discoveryUri; private final String tokenUri; private final String introspectionUri; private final String authorizationUri; @@ -33,6 +34,7 @@ public OidcConfigurationMetadata(String tokenUri, String userInfoUri, String endSessionUri, String issuer) { + this.discoveryUri = null; this.tokenUri = tokenUri; this.introspectionUri = introspectionUri; this.authorizationUri = authorizationUri; @@ -44,10 +46,12 @@ public OidcConfigurationMetadata(String tokenUri, } public OidcConfigurationMetadata(JsonObject wellKnownConfig) { - this(wellKnownConfig, null); + this(wellKnownConfig, null, null); } - public OidcConfigurationMetadata(JsonObject wellKnownConfig, OidcConfigurationMetadata localMetadataConfig) { + public OidcConfigurationMetadata(JsonObject wellKnownConfig, OidcConfigurationMetadata localMetadataConfig, + String discoveryUri) { + this.discoveryUri = discoveryUri; this.tokenUri = getMetadataValue(wellKnownConfig, TOKEN_ENDPOINT, localMetadataConfig == null ? null : localMetadataConfig.tokenUri); this.introspectionUri = getMetadataValue(wellKnownConfig, INTROSPECTION_ENDPOINT, @@ -69,6 +73,10 @@ private static String getMetadataValue(JsonObject wellKnownConfig, String proper return localValue != null ? localValue : wellKnownConfig.getString(propertyName); } + public String getDiscoveryUri() { + return discoveryUri; + } + public String getTokenUri() { return tokenUri; } 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 731715560132d..6b913536b709f 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 @@ -1,5 +1,6 @@ package io.quarkus.oidc; +import java.nio.file.Path; import java.time.Duration; import java.util.Arrays; import java.util.HashMap; @@ -170,6 +171,66 @@ public void setIncludeClientId(boolean includeClientId) { @ConfigItem public Logout logout = new Logout(); + /** + * Configuration of the certificate chain which can be used to verify tokens. + * If the certificate chain trusstore is configured then the tokens can be verified using the certificate + * chain inlined in the Base64-encoded format as an `x5c` header in the token itself. + */ + @ConfigItem + public CertificateChain certificateChain = new CertificateChain(); + + @ConfigGroup + public static class CertificateChain { + /** + * Truststore file which keeps thumbprints of the trusted certificates + */ + @ConfigItem + public Optional trustStoreFile = Optional.empty(); + + /** + * A parameter to specify the password of the truststore file if it is configured with {@link #trustStoreFile}. + */ + @ConfigItem + public Optional trustStorePassword; + + /** + * A parameter to specify the alias of the truststore certificate. + */ + @ConfigItem + public Optional trustStoreCertAlias = Optional.empty(); + + /** + * 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(); + + public Optional getTrustStoreFile() { + return trustStoreFile; + } + + public void setTrustStoreFile(Path trustStoreFile) { + this.trustStoreFile = Optional.of(trustStoreFile); + } + + public Optional getTrustStoreCertAlias() { + return trustStoreCertAlias; + } + + public void setTrustStoreCertAlias(String trustStoreCertAlias) { + this.trustStoreCertAlias = Optional.of(trustStoreCertAlias); + } + + public Optional getTrustStoreFileType() { + return trustStoreFileType; + } + + public void setTrustStoreFileType(Optional trustStoreFileType) { + this.trustStoreFileType = trustStoreFileType; + } + } + /** * Different options to configure authorization requests */ @@ -1811,4 +1872,12 @@ public CodeGrant getCodeGrant() { public void setCodeGrant(CodeGrant codeGrant) { this.codeGrant = codeGrant; } + + public CertificateChain getCertificateChain() { + return certificateChain; + } + + public void setCertificateChain(CertificateChain certificateChain) { + this.certificateChain = certificateChain; + } } diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CertChainPublicKeyResolver.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CertChainPublicKeyResolver.java new file mode 100644 index 0000000000000..ae0105fce3bcf --- /dev/null +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CertChainPublicKeyResolver.java @@ -0,0 +1,57 @@ +package io.quarkus.oidc.runtime; + +import java.security.Key; +import java.security.cert.X509Certificate; +import java.util.List; +import java.util.Set; + +import org.jboss.logging.Logger; +import org.jose4j.jws.JsonWebSignature; +import org.jose4j.jwx.JsonWebStructure; +import org.jose4j.lang.UnresolvableKeyException; + +import io.quarkus.oidc.OidcTenantConfig.CertificateChain; +import io.quarkus.runtime.configuration.ConfigurationException; +import io.vertx.ext.auth.impl.CertificateHelper; + +public class CertChainPublicKeyResolver implements RefreshableVerificationKeyResolver { + private static final Logger LOG = Logger.getLogger(OidcProvider.class); + final Set thumbprints; + + public CertChainPublicKeyResolver(CertificateChain chain) { + if (chain.trustStorePassword.isEmpty()) { + throw new ConfigurationException( + "Truststore with configured password which keeps thumbprints of the trusted certificates must be present"); + } + this.thumbprints = TrustStoreUtils.getTrustedCertificateThumbprints(chain.trustStoreFile.get(), + chain.trustStorePassword.get(), chain.trustStoreCertAlias, chain.getTrustStoreFileType()); + } + + @Override + public Key resolveKey(JsonWebSignature jws, List nestingContext) + throws UnresolvableKeyException { + + try { + List chain = jws.getCertificateChainHeaderValue(); + if (chain == null) { + LOG.debug("Token does not have an 'x5c' certificate chain header"); + return null; + } + String thumbprint = TrustStoreUtils.calculateThumprint(chain.get(0)); + if (!thumbprints.contains(thumbprint)) { + throw new UnresolvableKeyException("Certificate chain thumprint is invalid"); + } + //TODO: support revocation lists + CertificateHelper.checkValidity(chain, null); + if (chain.size() == 1) { + // CertificateHelper.checkValidity does not currently + // verify the certificate signature if it is a single certificate chain + final X509Certificate root = chain.get(0); + root.verify(root.getPublicKey()); + } + return chain.get(0).getPublicKey(); + } catch (Exception ex) { + throw new UnresolvableKeyException("Invalid certificate chain", ex); + } + } +} \ No newline at end of file diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/DefaultTenantConfigResolver.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/DefaultTenantConfigResolver.java index 1448c112aa928..ef07097a85bbc 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/DefaultTenantConfigResolver.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/DefaultTenantConfigResolver.java @@ -152,9 +152,12 @@ private TenantConfigContext getStaticTenantContext(RoutingContext context) { if (tenantId == null && context.get(CURRENT_STATIC_TENANT_ID_NULL) == null) { if (tenantResolver.isResolvable()) { tenantId = tenantResolver.get().resolve(context); - } else if (tenantConfigBean.getStaticTenantsConfig().size() > 0) { + } + + if (tenantId == null && tenantConfigBean.getStaticTenantsConfig().size() > 0) { tenantId = defaultStaticTenantResolver.resolve(context); } + if (tenantId == null) { tenantId = context.get(OidcUtils.TENANT_ID_ATTRIBUTE); } 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 index f9b9eb7b2a03e..3844bd400cbfd 100644 --- 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 @@ -27,11 +27,17 @@ public class DynamicVerificationKeyResolver { private final OidcProviderClient client; private final MemoryCache cache; + final CertChainPublicKeyResolver chainResolverFallback; public DynamicVerificationKeyResolver(OidcProviderClient client, OidcTenantConfig config) { this.client = client; this.cache = new MemoryCache(client.getVertx(), config.jwks.cleanUpTimerInterval, config.jwks.cacheTimeToLive, config.jwks.cacheSize); + if (config.certificateChain.trustStoreFile.isPresent()) { + chainResolverFallback = new CertChainPublicKeyResolver(config.certificateChain); + } else { + chainResolverFallback = null; + } } public Uni resolve(TokenCredential tokenCred) { @@ -98,11 +104,15 @@ public Uni apply(JsonWebKeySet jwks) { newKey = jwks.getKeyWithoutKeyIdAndThumbprint("RSA"); } + if (newKey == null && chainResolverFallback != null) { + LOG.debug("JWK is not available, neither 'kid' nor 'x5t#S256' nor 'x5t' token headers are set," + + " falling back to the certificate chain resolver"); + return Uni.createFrom().item(chainResolverFallback); + } + 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))); + "JWK is not available, neither 'kid' nor 'x5t#S256' nor 'x5t' token headers are set")); } else { return Uni.createFrom().item(new SingleKeyVerificationKeyResolver(newKey)); } 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 4cc968508c72f..711baa25e5435 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 @@ -102,6 +102,9 @@ private Uni authenticate(TokenAuthenticationRequest request, M if (resolvedContext.oidcConfig.publicKey.isPresent()) { LOG.debug("Performing token verification with a configured public key"); return validateTokenWithoutOidcServer(request, resolvedContext); + } else if (resolvedContext.oidcConfig.getCertificateChain().trustStoreFile.isPresent()) { + LOG.debug("Performing token verification with a public key inlined in the certificate chain"); + return validateTokenWithoutOidcServer(request, resolvedContext); } else { return validateAllTokensWithOidcServer(requestData, request, resolvedContext); } @@ -557,7 +560,8 @@ private static Uni validateTokenWithoutOidcServer(TokenAuthent TenantConfigContext resolvedContext) { try { - TokenVerificationResult result = resolvedContext.provider.verifyJwtToken(request.getToken().getToken(), false, + TokenVerificationResult result = resolvedContext.provider.verifyJwtToken(request.getToken().getToken(), + resolvedContext.oidcConfig.token.subjectRequired, false, null); return Uni.createFrom() .item(validateAndCreateIdentity(Map.of(), request.getToken(), resolvedContext, 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 8d26b8aae936c..0dce102574eb8 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 @@ -35,6 +35,7 @@ import io.quarkus.oidc.OIDCException; import io.quarkus.oidc.OidcConfigurationMetadata; import io.quarkus.oidc.OidcTenantConfig; +import io.quarkus.oidc.OidcTenantConfig.CertificateChain; import io.quarkus.oidc.TokenCustomizer; import io.quarkus.oidc.TokenIntrospection; import io.quarkus.oidc.UserInfo; @@ -86,7 +87,7 @@ public OidcProvider(OidcProviderClient client, OidcTenantConfig oidcConfig, Json this.oidcConfig = oidcConfig; this.tokenCustomizer = tokenCustomizer; this.asymmetricKeyResolver = jwks == null ? null - : new JsonWebKeyResolver(jwks, oidcConfig.token.forcedJwkRefreshInterval); + : new JsonWebKeyResolver(jwks, oidcConfig.token.forcedJwkRefreshInterval, oidcConfig.certificateChain); if (client != null && oidcConfig != null && !oidcConfig.jwks.resolveEarly) { this.keyResolverProvider = new DynamicVerificationKeyResolver(client, oidcConfig); } else { @@ -103,7 +104,13 @@ public OidcProvider(String publicKeyEnc, OidcTenantConfig oidcConfig, Key tokenD this.client = null; this.oidcConfig = oidcConfig; this.tokenCustomizer = TokenCustomizerFinder.find(oidcConfig); - this.asymmetricKeyResolver = new LocalPublicKeyResolver(publicKeyEnc); + if (publicKeyEnc != null) { + this.asymmetricKeyResolver = new LocalPublicKeyResolver(publicKeyEnc); + } else if (oidcConfig.certificateChain.trustStoreFile.isPresent()) { + this.asymmetricKeyResolver = new CertChainPublicKeyResolver(oidcConfig.certificateChain); + } else { + throw new IllegalStateException("Neither public key nor certificate chain verification modes are enabled"); + } this.keyResolverProvider = null; this.issuer = checkIssuerProp(); this.audience = checkAudienceProp(); @@ -399,10 +406,16 @@ private class JsonWebKeyResolver implements RefreshableVerificationKeyResolver { volatile JsonWebKeySet jwks; volatile long lastForcedRefreshTime; volatile long forcedJwksRefreshIntervalMilliSecs; + final CertChainPublicKeyResolver chainResolverFallback; - JsonWebKeyResolver(JsonWebKeySet jwks, Duration forcedJwksRefreshInterval) { + JsonWebKeyResolver(JsonWebKeySet jwks, Duration forcedJwksRefreshInterval, CertificateChain chain) { this.jwks = jwks; this.forcedJwksRefreshIntervalMilliSecs = forcedJwksRefreshInterval.toMillis(); + if (chain.trustStoreFile.isPresent()) { + chainResolverFallback = new CertChainPublicKeyResolver(chain); + } else { + chainResolverFallback = null; + } } @Override @@ -453,10 +466,15 @@ public Key resolveKey(JsonWebSignature jws, List nestingContex } } + if (key == null && chainResolverFallback != null) { + LOG.debug("JWK is not available, neither 'kid' nor 'x5t#S256' nor 'x5t' token headers are set," + + " falling back to the certificate chain resolver"); + key = chainResolverFallback.resolveKey(jws, nestingContext); + } + if (key == null) { throw new UnresolvableKeyException( - String.format("JWK is not available, neither 'kid' nor 'x5t#S256' nor 'x5t' token headers are set", - kid)); + "JWK is not available, neither 'kid' nor 'x5t#S256' nor 'x5t' token headers are set"); } else { return key; } @@ -541,12 +559,6 @@ public OidcConfigurationMetadata getMetadata() { return client.getMetadata(); } - private static interface RefreshableVerificationKeyResolver extends VerificationKeyResolver { - default Uni refresh() { - return Uni.createFrom().voidItem(); - } - } - private static class CustomClaimsValidator implements Validator { private final Map customClaims; 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 204c38984259c..4aad502590622 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 @@ -4,6 +4,7 @@ import java.net.ConnectException; import java.nio.charset.StandardCharsets; import java.security.Key; +import java.util.HashMap; import java.util.List; import java.util.Map; @@ -15,6 +16,7 @@ import io.quarkus.oidc.OidcTenantConfig; import io.quarkus.oidc.TokenIntrospection; import io.quarkus.oidc.UserInfo; +import io.quarkus.oidc.common.OidcEndpoint; import io.quarkus.oidc.common.OidcRequestContextProperties; import io.quarkus.oidc.common.OidcRequestFilter; import io.quarkus.oidc.common.runtime.OidcCommonUtils; @@ -48,13 +50,13 @@ public class OidcProviderClient implements Closeable { private final String clientSecretBasicAuthScheme; private final String introspectionBasicAuthScheme; private final Key clientJwtKey; - private final List filters; + private final Map> filters; public OidcProviderClient(WebClient client, Vertx vertx, OidcConfigurationMetadata metadata, OidcTenantConfig oidcConfig, - List filters) { + Map> filters) { this.client = client; this.vertx = vertx; this.metadata = metadata; @@ -80,13 +82,14 @@ public OidcConfigurationMetadata getMetadata() { } public Uni getJsonWebKeySet(OidcRequestContextProperties contextProperties) { - return filter(client.getAbs(metadata.getJsonWebKeySetUri()), null, contextProperties).send().onItem() + return filter(OidcEndpoint.Type.JWKS, 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, null) + return filter(OidcEndpoint.Type.USERINFO, client.getAbs(metadata.getUserInfoUri()), null, null) .putHeader(AUTHORIZATION_HEADER, OidcConstants.BEARER_SCHEME + " " + token) .send().onItem().transform(resp -> getUserInfo(resp)); } @@ -168,7 +171,9 @@ 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, null).sendBuffer(buffer) + + OidcEndpoint.Type endpoint = introspect ? OidcEndpoint.Type.INTROSPECTION : OidcEndpoint.Type.TOKEN; + Uni> response = filter(endpoint, request, buffer, null).sendBuffer(buffer) .onFailure(ConnectException.class) .retry() .atMost(oidcConfig.connectionRetryCount).onFailure().transform(t -> t.getCause()); @@ -224,10 +229,16 @@ public Key getClientJwtKey() { return clientJwtKey; } - private HttpRequest filter(HttpRequest request, Buffer body, + private HttpRequest filter(OidcEndpoint.Type endpointType, HttpRequest request, Buffer body, OidcRequestContextProperties contextProperties) { - for (OidcRequestFilter filter : filters) { - filter.filter(request, body, contextProperties); + if (!filters.isEmpty()) { + Map newProperties = contextProperties == null ? new HashMap<>() + : new HashMap<>(contextProperties.getAll()); + newProperties.put(OidcConfigurationMetadata.class.getName(), metadata); + OidcRequestContextProperties newContextProperties = new OidcRequestContextProperties(newProperties); + for (OidcRequestFilter filter : OidcCommonUtils.getMatchingOidcRequestFilters(filters, endpointType)) { + filter.filter(request, body, newContextProperties); + } } return request; } 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 50264f617dfd5..a159b320f6a0c 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,6 +30,7 @@ import io.quarkus.oidc.OidcTenantConfig.TokenStateManager.Strategy; import io.quarkus.oidc.TenantConfigResolver; import io.quarkus.oidc.TenantIdentityProvider; +import io.quarkus.oidc.common.OidcEndpoint; import io.quarkus.oidc.common.OidcRequestFilter; import io.quarkus.oidc.common.runtime.OidcCommonConfig; import io.quarkus.oidc.common.runtime.OidcCommonUtils; @@ -180,8 +181,17 @@ private Uni createTenantContext(Vertx vertx, OidcTenantConf return Uni.createFrom().item(new TenantConfigContext(new OidcProvider(null, null, null, null), oidcConfig)); } - if (oidcConfig.getPublicKey().isPresent()) { - return Uni.createFrom().item(createTenantContextFromPublicKey(oidcConfig)); + if (!oidcConfig.getAuthServerUrl().isPresent()) { + if (oidcConfig.getPublicKey().isPresent() && oidcConfig.certificateChain.trustStoreFile.isPresent()) { + throw new ConfigurationException("Both public key and certificate chain verification modes are enabled"); + } + if (oidcConfig.getPublicKey().isPresent()) { + return Uni.createFrom().item(createTenantContextFromPublicKey(oidcConfig)); + } + + if (oidcConfig.certificateChain.trustStoreFile.isPresent()) { + return Uni.createFrom().item(createTenantContextToVerifyCertChain(oidcConfig)); + } } try { @@ -332,6 +342,16 @@ private static TenantConfigContext createTenantContextFromPublicKey(OidcTenantCo new OidcProvider(oidcConfig.publicKey.get(), oidcConfig, readTokenDecryptionKey(oidcConfig)), oidcConfig); } + private static TenantConfigContext createTenantContextToVerifyCertChain(OidcTenantConfig oidcConfig) { + if (!OidcUtils.isServiceApp(oidcConfig)) { + throw new ConfigurationException( + "Currently only 'service' applications can be used to verify tokens with inlined certificate chains"); + } + + return new TenantConfigContext( + new OidcProvider(null, oidcConfig, readTokenDecryptionKey(oidcConfig)), oidcConfig); + } + public void setSecurityEventObserved(boolean isSecurityEventObserved) { DefaultTenantConfigResolver bean = Arc.container().instance(DefaultTenantConfigResolver.class).get(); bean.setSecurityEventObserved(isSecurityEventObserved); @@ -433,7 +453,7 @@ protected static Uni createOidcClientUni(OidcTenantConfig oi WebClient client = WebClient.create(new io.vertx.mutiny.core.Vertx(vertx), options); - List clientRequestFilters = OidcCommonUtils.getClientRequestCustomizer(); + Map> oidcRequestFilters = OidcCommonUtils.getOidcRequestFilters(); Uni metadataUni = null; if (!oidcConfig.discoveryEnabled.orElse(true)) { @@ -441,12 +461,13 @@ protected static Uni createOidcClientUni(OidcTenantConfig oi } else { final long connectionDelayInMillisecs = OidcCommonUtils.getConnectionDelayInMillis(oidcConfig); metadataUni = OidcCommonUtils - .discoverMetadata(client, clientRequestFilters, authServerUriString, connectionDelayInMillisecs) + .discoverMetadata(client, oidcRequestFilters, authServerUriString, connectionDelayInMillisecs) .onItem() .transform(new Function() { @Override public OidcConfigurationMetadata apply(JsonObject json) { - return new OidcConfigurationMetadata(json, createLocalMetadata(oidcConfig, authServerUriString)); + return new OidcConfigurationMetadata(json, createLocalMetadata(oidcConfig, authServerUriString), + OidcCommonUtils.getDiscoveryUri(authServerUriString)); } }); } @@ -478,7 +499,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, vertx, metadata, oidcConfig, clientRequestFilters)); + .item(new OidcProviderClient(client, vertx, metadata, oidcConfig, oidcRequestFilters)); } }); diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/RefreshableVerificationKeyResolver.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/RefreshableVerificationKeyResolver.java new file mode 100644 index 0000000000000..de0209f332ec1 --- /dev/null +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/RefreshableVerificationKeyResolver.java @@ -0,0 +1,11 @@ +package io.quarkus.oidc.runtime; + +import org.jose4j.keys.resolvers.VerificationKeyResolver; + +import io.smallrye.mutiny.Uni; + +public interface RefreshableVerificationKeyResolver extends VerificationKeyResolver { + default Uni refresh() { + return Uni.createFrom().voidItem(); + } +} \ No newline at end of file diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/TrustStoreUtils.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/TrustStoreUtils.java new file mode 100644 index 0000000000000..d29c54ffb6754 --- /dev/null +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/TrustStoreUtils.java @@ -0,0 +1,89 @@ +package io.quarkus.oidc.runtime; + +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.KeyStore; +import java.security.KeyStore.Entry; +import java.security.KeyStore.TrustedCertificateEntry; +import java.security.cert.X509Certificate; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; + +import org.jboss.logging.Logger; +import org.jose4j.keys.X509Util; + +import io.quarkus.oidc.common.runtime.OidcCommonUtils; +import io.quarkus.runtime.util.ClassPathUtils; + +public class TrustStoreUtils { + private static final Logger LOGGER = Logger.getLogger(TrustStoreUtils.class.getName()); + + public static Set getTrustedCertificateThumbprints(Path path, String trustStoreSecret, + Optional trustStoreCertAlias, Optional trustStoreFileType) { + URL trustStoreFileUrl = null; + if ((trustStoreFileUrl = Thread.currentThread().getContextClassLoader() + .getResource(ClassPathUtils.toResourceName(path))) != null) { + return readTrustStore(path, trustStoreFileUrl, trustStoreSecret, trustStoreCertAlias, trustStoreFileType); + } else if (Files.exists(path)) { + try { + return readTrustStore(path, path.toUri().toURL(), trustStoreSecret, trustStoreCertAlias, trustStoreFileType); + } catch (MalformedURLException e) { + LOGGER.errorf("Keystore %s location is not a valid URL", path.toUri()); + throw new RuntimeException(e); + } + } else { + LOGGER.errorf("Keystore %s can not be found on the classpath and the file system", path.toUri()); + throw new RuntimeException(); + } + } + + private static Set readTrustStore(Path path, URL trustStoreFileUrl, String trustStoreSecret, + Optional trustStoreCertAlias, Optional trustStoreFileType) { + + try (InputStream fis = trustStoreFileUrl.openStream()) { + KeyStore keyStore = KeyStore.getInstance(OidcCommonUtils.getKeyStoreType(trustStoreFileType, path)); + keyStore.load(fis, trustStoreSecret.toCharArray()); + + Set allThumbprints = new HashSet<>(); + if (trustStoreCertAlias.isPresent()) { + addThumbprints(keyStore, allThumbprints, trustStoreCertAlias.get()); + } else { + for (Enumeration aliases = keyStore.aliases(); aliases.hasMoreElements();) { + addThumbprints(keyStore, allThumbprints, aliases.nextElement()); + } + } + + if (allThumbprints.isEmpty()) { + LOGGER.errorf("Keystore %s entries can not be loaded", trustStoreFileUrl.toString()); + throw new RuntimeException(); + } + + return allThumbprints; + } catch (IOException e) { + LOGGER.errorf("Keystore %s can not be loaded", trustStoreFileUrl.toString()); + throw new RuntimeException(e); + } catch (Exception e) { + LOGGER.errorf("Keystore %s entries can not be loaded", trustStoreFileUrl.toString()); + throw new RuntimeException(e); + } + } + + private static void addThumbprints(KeyStore keyStore, Set allThumbprints, String alias) + throws Exception { + Entry entry = keyStore.getEntry(alias, null); + if (entry instanceof TrustedCertificateEntry) { + X509Certificate cert = (X509Certificate) ((TrustedCertificateEntry) entry).getTrustedCertificate(); + allThumbprints.add(calculateThumprint(cert)); + } + } + + public static String calculateThumprint(X509Certificate cert) { + return X509Util.x5tS256(cert); + } +} \ No newline at end of file diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/devui/OidcDevJsonRpcService.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/devui/OidcDevJsonRpcService.java index e54c728dbe077..0c26e16a76d05 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/devui/OidcDevJsonRpcService.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/devui/OidcDevJsonRpcService.java @@ -45,7 +45,15 @@ public OidcDevJsonRpcService(HttpConfiguration httpConfiguration, SmallRyeConfig // we must always produce it when in DEV mode because we can't check for 'KeycloakDevServicesConfigBuildItem' // due to circular reference: JSON RPC provider is additional bean and 'LoggingSetupBuildItem' used by // 'KeycloakDevServicesProcessor' is created with combined index - OidcDevUiRpcSvcPropertiesBean props = Arc.container().instance(OidcDevUiRpcSvcPropertiesBean.class).get(); + final var propsInstanceHandle = Arc.container().instance(OidcDevUiRpcSvcPropertiesBean.class); + final OidcDevUiRpcSvcPropertiesBean props; + if (propsInstanceHandle.isAvailable()) { + props = propsInstanceHandle.get(); + } else { + // OIDC Dev UI is disabled, but this RPC service still gets initialized by Quarkus DEV UI + props = new OidcDevUiRpcSvcPropertiesBean(null, null, null, null, Map.of(), Map.of(), null, null, null, false, null, + List.of(), false, false, null, null, false); + } this.httpPort = httpConfiguration.port; this.config = config; diff --git a/extensions/openshift-client/runtime/pom.xml b/extensions/openshift-client/runtime/pom.xml index 18fa13c77c232..fd62cdfb36faa 100644 --- a/extensions/openshift-client/runtime/pom.xml +++ b/extensions/openshift-client/runtime/pom.xml @@ -41,6 +41,11 @@ + + org.graalvm.sdk + graal-sdk + provided + @@ -70,4 +75,4 @@ - \ No newline at end of file + diff --git a/extensions/openshift-client/runtime/src/main/java/io/quarkus/it/openshift/client/runtime/graal/MiscellaneousSubstitutions.java b/extensions/openshift-client/runtime/src/main/java/io/quarkus/it/openshift/client/runtime/graal/MiscellaneousSubstitutions.java new file mode 100644 index 0000000000000..3d1284aee3c8d --- /dev/null +++ b/extensions/openshift-client/runtime/src/main/java/io/quarkus/it/openshift/client/runtime/graal/MiscellaneousSubstitutions.java @@ -0,0 +1,97 @@ +package io.quarkus.it.openshift.client.runtime.graal; + +import java.util.Arrays; +import java.util.function.BooleanSupplier; + +import com.oracle.svm.core.annotate.Substitute; +import com.oracle.svm.core.annotate.TargetClass; + +import io.fabric8.kubernetes.client.dsl.MixedOperation; +import io.fabric8.kubernetes.client.dsl.NonNamespaceOperation; +import io.fabric8.kubernetes.client.dsl.Resource; +import io.fabric8.openshift.api.model.miscellaneous.apiserver.v1.APIRequestCount; +import io.fabric8.openshift.api.model.miscellaneous.apiserver.v1.APIRequestCountList; +import io.fabric8.openshift.api.model.miscellaneous.cloudcredential.v1.CredentialsRequest; +import io.fabric8.openshift.api.model.miscellaneous.cloudcredential.v1.CredentialsRequestList; +import io.fabric8.openshift.api.model.miscellaneous.cncf.cni.v1.NetworkAttachmentDefinition; +import io.fabric8.openshift.api.model.miscellaneous.cncf.cni.v1.NetworkAttachmentDefinitionList; +import io.fabric8.openshift.api.model.miscellaneous.imageregistry.operator.v1.Config; +import io.fabric8.openshift.api.model.miscellaneous.imageregistry.operator.v1.ConfigList; +import io.fabric8.openshift.api.model.miscellaneous.metal3.v1alpha1.BareMetalHost; +import io.fabric8.openshift.api.model.miscellaneous.metal3.v1alpha1.BareMetalHostList; +import io.fabric8.openshift.api.model.miscellaneous.metal3.v1beta1.Metal3Remediation; +import io.fabric8.openshift.api.model.miscellaneous.metal3.v1beta1.Metal3RemediationList; +import io.fabric8.openshift.api.model.miscellaneous.metal3.v1beta1.Metal3RemediationTemplate; +import io.fabric8.openshift.api.model.miscellaneous.metal3.v1beta1.Metal3RemediationTemplateList; +import io.fabric8.openshift.api.model.miscellaneous.network.operator.v1.EgressRouter; +import io.fabric8.openshift.api.model.miscellaneous.network.operator.v1.EgressRouterList; +import io.fabric8.openshift.api.model.miscellaneous.network.operator.v1.OperatorPKI; +import io.fabric8.openshift.api.model.miscellaneous.network.operator.v1.OperatorPKIList; + +/** + * Allows the exclusion of the openshift-model-miscellaneous model without breaking the --link-at-build-time check. + */ +@TargetClass(className = "io.fabric8.openshift.client.impl.OpenShiftClientImpl", onlyWith = MiscellaneousSubstitutions.NoOpenShiftMiscellaneousModel.class) +public final class MiscellaneousSubstitutions { + + @Substitute + public NonNamespaceOperation> apiRequestCounts() { + throw new RuntimeException(Constants.ERROR_MESSAGE); + } + + @Substitute + public MixedOperation> bareMetalHosts() { + throw new RuntimeException(Constants.ERROR_MESSAGE); + } + + @Substitute + public MixedOperation> credentialsRequests() { + throw new RuntimeException(Constants.ERROR_MESSAGE); + } + + @Substitute + public MixedOperation> egressRouters() { + throw new RuntimeException(Constants.ERROR_MESSAGE); + } + + @Substitute + public NonNamespaceOperation> imageRegistryOperatorConfigs() { + throw new RuntimeException(Constants.ERROR_MESSAGE); + } + + @Substitute + public MixedOperation> metal3Remediations() { + throw new RuntimeException(Constants.ERROR_MESSAGE); + } + + @Substitute + public MixedOperation> metal3RemediationTemplates() { + throw new RuntimeException(Constants.ERROR_MESSAGE); + } + + @Substitute + public MixedOperation> networkAttachmentDefinitions() { + throw new RuntimeException(Constants.ERROR_MESSAGE); + } + + @Substitute + public MixedOperation> operatorPKIs() { + throw new RuntimeException(Constants.ERROR_MESSAGE); + } + + static final class Constants { + private static final String ERROR_MESSAGE = "OpenShift Miscellaneous API is not available, please add the openshift-model-miscellaneous module to your classpath"; + } + + static final class NoOpenShiftMiscellaneousModel implements BooleanSupplier { + + private static final String OPENSHIFT_MODEL_MISCELLANEOUS_PACKAGE = "io.fabric8.openshift.api.model.miscellaneous."; + static final Boolean OPENSHIFT_MODEL_MISCELLANEOUS_PRESENT = Arrays.stream(Package.getPackages()) + .map(Package::getName).anyMatch(p -> p.startsWith(OPENSHIFT_MODEL_MISCELLANEOUS_PACKAGE)); + + @Override + public boolean getAsBoolean() { + return !OPENSHIFT_MODEL_MISCELLANEOUS_PRESENT; + } + } +} diff --git a/extensions/openshift-client/runtime/src/main/java/io/quarkus/it/openshift/client/runtime/graal/OperatorSubstitutions.java b/extensions/openshift-client/runtime/src/main/java/io/quarkus/it/openshift/client/runtime/graal/OperatorSubstitutions.java new file mode 100644 index 0000000000000..3730608f29f24 --- /dev/null +++ b/extensions/openshift-client/runtime/src/main/java/io/quarkus/it/openshift/client/runtime/graal/OperatorSubstitutions.java @@ -0,0 +1,34 @@ +package io.quarkus.it.openshift.client.runtime.graal; + +import java.util.Arrays; +import java.util.function.BooleanSupplier; + +import com.oracle.svm.core.annotate.Substitute; +import com.oracle.svm.core.annotate.TargetClass; + +import io.fabric8.openshift.client.dsl.OpenShiftOperatorAPIGroupDSL; + +/** + * Allows the exclusion of the openshift-model-operator model without breaking the --link-at-build-time check. + */ +@TargetClass(className = "io.fabric8.openshift.client.impl.OpenShiftClientImpl", onlyWith = OperatorSubstitutions.NoOpenShiftOperatorModel.class) +public final class OperatorSubstitutions { + + @Substitute + public OpenShiftOperatorAPIGroupDSL operator() { + throw new RuntimeException( + "OpenShift Operator API is not available, please add the openshift-model-operator module to your classpath"); + } + + static final class NoOpenShiftOperatorModel implements BooleanSupplier { + + private static final String OPENSHIFT_MODEL_OPERATOR_PACKAGE = "io.fabric8.openshift.api.model.operator."; + static final Boolean OPENSHIFT_MODEL_OPERATOR_PRESENT = Arrays.stream(Package.getPackages()) + .map(Package::getName).anyMatch(p -> p.startsWith(OPENSHIFT_MODEL_OPERATOR_PACKAGE)); + + @Override + public boolean getAsBoolean() { + return !OPENSHIFT_MODEL_OPERATOR_PRESENT; + } + } +} diff --git a/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/scheduler/OpenTelemetrySchedulerProcessor.java b/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/scheduler/OpenTelemetrySchedulerProcessor.java new file mode 100644 index 0000000000000..ecb8dbdd113f3 --- /dev/null +++ b/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/scheduler/OpenTelemetrySchedulerProcessor.java @@ -0,0 +1,20 @@ +package io.quarkus.opentelemetry.deployment.scheduler; + +import io.quarkus.arc.deployment.AdditionalBeanBuildItem; +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.opentelemetry.deployment.OpenTelemetryEnabled; +import io.quarkus.opentelemetry.runtime.scheduler.OpenTelemetryJobInstrumenter; + +public class OpenTelemetrySchedulerProcessor { + + @BuildStep(onlyIf = OpenTelemetryEnabled.class) + void registerJobInstrumenter(Capabilities capabilities, BuildProducer beans) { + if (capabilities.isPresent(Capability.SCHEDULER)) { + beans.produce(new AdditionalBeanBuildItem(OpenTelemetryJobInstrumenter.class)); + } + } + +} diff --git a/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/tracing/instrumentation/InstrumentationProcessor.java b/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/tracing/instrumentation/InstrumentationProcessor.java index 83101515d740c..d8aa5e59bd0cb 100644 --- a/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/tracing/instrumentation/InstrumentationProcessor.java +++ b/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/tracing/instrumentation/InstrumentationProcessor.java @@ -146,5 +146,4 @@ void resteasyReactiveIntegration( preExceptionMapperHandlerBuildItemBuildProducer .produce(new PreExceptionMapperHandlerBuildItem(new AttachExceptionHandler())); } - } diff --git a/extensions/opentelemetry/runtime/pom.xml b/extensions/opentelemetry/runtime/pom.xml index dd7d2fda4acde..9d47d52ade00a 100644 --- a/extensions/opentelemetry/runtime/pom.xml +++ b/extensions/opentelemetry/runtime/pom.xml @@ -61,6 +61,11 @@ quarkus-smallrye-reactive-messaging true + + io.quarkus + quarkus-scheduler-spi + true + diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/scheduler/OpenTelemetryJobInstrumenter.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/scheduler/OpenTelemetryJobInstrumenter.java new file mode 100644 index 0000000000000..3a1a84c111bbb --- /dev/null +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/scheduler/OpenTelemetryJobInstrumenter.java @@ -0,0 +1,51 @@ +package io.quarkus.opentelemetry.runtime.scheduler; + +import java.util.concurrent.CompletionStage; + +import jakarta.inject.Singleton; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.api.instrumenter.ErrorCauseExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder; +import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; +import io.quarkus.scheduler.spi.JobInstrumenter; + +@Singleton +public class OpenTelemetryJobInstrumenter implements JobInstrumenter { + + private final Instrumenter instrumenter; + + public OpenTelemetryJobInstrumenter(OpenTelemetry openTelemetry) { + InstrumenterBuilder instrumenterBuilder = Instrumenter.builder( + openTelemetry, "io.quarkus.opentelemetry", + new SpanNameExtractor() { + @Override + public String extract(JobInstrumentationContext context) { + return context.getSpanName(); + } + }); + instrumenterBuilder.setErrorCauseExtractor(new ErrorCauseExtractor() { + @Override + public Throwable extract(Throwable throwable) { + return throwable; + } + }); + this.instrumenter = instrumenterBuilder.buildInstrumenter(); + } + + @Override + public CompletionStage instrument(JobInstrumentationContext instrumentationContext) { + Context parentCtx = Context.current(); + Context context = instrumenter.start(parentCtx, instrumentationContext); + try (Scope scope = context.makeCurrent()) { + return instrumentationContext + .executeJob() + .whenComplete( + (result, throwable) -> instrumenter.end(context, instrumentationContext, null, throwable)); + } + } + +} diff --git a/extensions/panache/hibernate-orm-panache-common/runtime/src/main/java/io/quarkus/hibernate/orm/panache/common/runtime/NamedQueryUtil.java b/extensions/panache/hibernate-orm-panache-common/runtime/src/main/java/io/quarkus/hibernate/orm/panache/common/runtime/NamedQueryUtil.java index 5eb9eaf69e31d..bb241d2ece45f 100644 --- a/extensions/panache/hibernate-orm-panache-common/runtime/src/main/java/io/quarkus/hibernate/orm/panache/common/runtime/NamedQueryUtil.java +++ b/extensions/panache/hibernate-orm-panache-common/runtime/src/main/java/io/quarkus/hibernate/orm/panache/common/runtime/NamedQueryUtil.java @@ -4,6 +4,8 @@ import java.util.Map; import org.hibernate.query.SemanticException; +import org.hibernate.query.SyntaxException; +import org.hibernate.query.sqm.ParsingException; import io.quarkus.panache.common.exception.PanacheQueryException; @@ -43,7 +45,8 @@ private static boolean isNamedQuery(String namedQuery) { public static RuntimeException checkForNamedQueryMistake(IllegalArgumentException x, String originalQuery) { if (originalQuery != null - && x.getCause() instanceof SemanticException + && (x.getCause() instanceof SemanticException || x.getCause() instanceof ParsingException + || x.getCause() instanceof SyntaxException) && isNamedQuery(originalQuery)) { return new PanacheQueryException("Invalid query '" + originalQuery + "' but it matches a known @NamedQuery, perhaps you should prefix it with a '#' to use it as a named query: '#" diff --git a/extensions/panache/hibernate-orm-panache-kotlin/runtime/pom.xml b/extensions/panache/hibernate-orm-panache-kotlin/runtime/pom.xml index 46b1e19bde994..466e953432d4e 100644 --- a/extensions/panache/hibernate-orm-panache-kotlin/runtime/pom.xml +++ b/extensions/panache/hibernate-orm-panache-kotlin/runtime/pom.xml @@ -74,22 +74,6 @@ quarkus-panache-common-deployment test - - org.hibernate.orm - hibernate-jpamodelgen - provided - - - - javax.xml.bind - jaxb-api - - - javax.activation - javax.activation-api - - - @@ -122,6 +106,13 @@ kapt + + + org.hibernate.orm + hibernate-jpamodelgen + ${hibernate-orm.version} + + org.hibernate.jpamodelgen.JPAMetaModelEntityProcessor diff --git a/extensions/panache/hibernate-orm-panache/deployment/src/test/java/io/quarkus/hibernate/orm/panache/deployment/test/JpaOperationsSortTest.java b/extensions/panache/hibernate-orm-panache/deployment/src/test/java/io/quarkus/hibernate/orm/panache/deployment/test/JpaOperationsSortTest.java index 30e13c7a45c5f..39e14f149d201 100644 --- a/extensions/panache/hibernate-orm-panache/deployment/src/test/java/io/quarkus/hibernate/orm/panache/deployment/test/JpaOperationsSortTest.java +++ b/extensions/panache/hibernate-orm-panache/deployment/src/test/java/io/quarkus/hibernate/orm/panache/deployment/test/JpaOperationsSortTest.java @@ -2,9 +2,11 @@ import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import io.quarkus.panache.common.Sort; +import io.quarkus.panache.common.exception.PanacheQueryException; import io.quarkus.panache.hibernate.common.runtime.PanacheJpaUtil; public class JpaOperationsSortTest { @@ -18,7 +20,7 @@ public void testEmptySortByYieldsEmptyString() { @Test public void testSortBy() { Sort sort = Sort.by("foo", "bar"); - assertEquals(" ORDER BY foo , bar", PanacheJpaUtil.toOrderBy(sort)); + assertEquals(" ORDER BY `foo` , `bar`", PanacheJpaUtil.toOrderBy(sort)); } @Test @@ -29,14 +31,27 @@ public void testEmptySortEmptyYieldsEmptyString() { @Test public void testSortByNullsFirst() { - Sort emptySort = Sort.by("foo", Sort.Direction.Ascending, Sort.NullPrecedence.NULLS_FIRST); - assertEquals(" ORDER BY foo NULLS FIRST", PanacheJpaUtil.toOrderBy(emptySort)); + Sort sort = Sort.by("foo", Sort.Direction.Ascending, Sort.NullPrecedence.NULLS_FIRST); + assertEquals(" ORDER BY `foo` NULLS FIRST", PanacheJpaUtil.toOrderBy(sort)); } @Test public void testSortByNullsLast() { - Sort emptySort = Sort.by("foo", Sort.Direction.Descending, Sort.NullPrecedence.NULLS_LAST); - assertEquals(" ORDER BY foo DESC NULLS LAST", PanacheJpaUtil.toOrderBy(emptySort)); + Sort sort = Sort.by("foo", Sort.Direction.Descending, Sort.NullPrecedence.NULLS_LAST); + assertEquals(" ORDER BY `foo` DESC NULLS LAST", PanacheJpaUtil.toOrderBy(sort)); + } + + @Test + public void testSortByColumnWithBacktick() { + Sort sort = Sort.by("jeanne", "d`arc"); + Assertions.assertThrowsExactly(PanacheQueryException.class, () -> PanacheJpaUtil.toOrderBy(sort), + "Sort column name cannot have backticks"); + } + + @Test + public void testSortByQuotedColumn() { + Sort sort = Sort.by("`foo`", "bar"); + assertEquals(" ORDER BY `foo` , `bar`", PanacheJpaUtil.toOrderBy(sort)); } } diff --git a/integration-tests/java-17/src/main/java/io/quarkus/it/hibernate/panache/person/Person.java b/extensions/panache/hibernate-orm-panache/deployment/src/test/java/io/quarkus/hibernate/orm/panache/deployment/test/record/Person.java similarity index 79% rename from integration-tests/java-17/src/main/java/io/quarkus/it/hibernate/panache/person/Person.java rename to extensions/panache/hibernate-orm-panache/deployment/src/test/java/io/quarkus/hibernate/orm/panache/deployment/test/record/Person.java index c93442cdc243a..d75f1988fbd53 100644 --- a/integration-tests/java-17/src/main/java/io/quarkus/it/hibernate/panache/person/Person.java +++ b/extensions/panache/hibernate-orm-panache/deployment/src/test/java/io/quarkus/hibernate/orm/panache/deployment/test/record/Person.java @@ -1,4 +1,4 @@ -package io.quarkus.it.hibernate.panache.person; +package io.quarkus.hibernate.orm.panache.deployment.test.record; import jakarta.persistence.Entity; diff --git a/integration-tests/java-17/src/main/java/io/quarkus/it/hibernate/panache/person/PersonName.java b/extensions/panache/hibernate-orm-panache/deployment/src/test/java/io/quarkus/hibernate/orm/panache/deployment/test/record/PersonName.java similarity index 69% rename from integration-tests/java-17/src/main/java/io/quarkus/it/hibernate/panache/person/PersonName.java rename to extensions/panache/hibernate-orm-panache/deployment/src/test/java/io/quarkus/hibernate/orm/panache/deployment/test/record/PersonName.java index 6dd48b17fda5d..395bdd5e214a7 100644 --- a/integration-tests/java-17/src/main/java/io/quarkus/it/hibernate/panache/person/PersonName.java +++ b/extensions/panache/hibernate-orm-panache/deployment/src/test/java/io/quarkus/hibernate/orm/panache/deployment/test/record/PersonName.java @@ -1,4 +1,4 @@ -package io.quarkus.it.hibernate.panache.person; +package io.quarkus.hibernate.orm.panache.deployment.test.record; import io.quarkus.runtime.annotations.RegisterForReflection; diff --git a/extensions/panache/hibernate-orm-panache/deployment/src/test/java/io/quarkus/hibernate/orm/panache/deployment/test/record/RecordInPanacheTest.java b/extensions/panache/hibernate-orm-panache/deployment/src/test/java/io/quarkus/hibernate/orm/panache/deployment/test/record/RecordInPanacheTest.java new file mode 100644 index 0000000000000..f8f9e5d196490 --- /dev/null +++ b/extensions/panache/hibernate-orm-panache/deployment/src/test/java/io/quarkus/hibernate/orm/panache/deployment/test/record/RecordInPanacheTest.java @@ -0,0 +1,72 @@ +package io.quarkus.hibernate.orm.panache.deployment.test.record; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import jakarta.transaction.Transactional; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +class RecordInPanacheTest { + + @RegisterExtension + static QuarkusUnitTest runner = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addAsResource("application-test.properties", "application.properties") + .addClasses(Person.class, PersonName.class, Status.class)); + + @Test + @Transactional + void testRecordInPanache() { + var person1 = new Person(); + person1.firstname = "Loïc"; + person1.lastname = "Mathieu"; + person1.status = Status.ALIVE; + person1.persist(); + + var person2 = new Person(); + person1.firstname = "Zombie"; + person2.lastname = "Zombie"; + person2.status = Status.DEAD; + person2.persist(); + + assertEquals(2L, Person.count()); + + } + + @Test + @Transactional + void testHqlPanacheProject() { + var mark = new Person(); + mark.firstname = "Mark"; + mark.lastname = "Mark"; + mark.persistAndFlush(); + + var hqlWithoutSpace = """ + select + firstname, + lastname + from + io.quarkus.hibernate.orm.panache.deployment.test.record.Person + where + firstname = ?1 + """; + var persistedWithoutSpace = Person.find(hqlWithoutSpace, "Mark").project(PersonName.class).firstResult(); + assertEquals("Mark", persistedWithoutSpace.firstname()); + + // We need to escape the whitespace in Java otherwise the compiler removes it. + var hqlWithSpace = """ + select\s + firstname, + lastname + from + io.quarkus.hibernate.orm.panache.deployment.test.record.Person + where + firstname = ?1 + """; + var persistedWithSpace = Person.find(hqlWithSpace, "Mark").project(PersonName.class).firstResult(); + assertEquals("Mark", persistedWithSpace.firstname()); + } +} diff --git a/integration-tests/java-17/src/main/java/io/quarkus/it/hibernate/panache/person/Status.java b/extensions/panache/hibernate-orm-panache/deployment/src/test/java/io/quarkus/hibernate/orm/panache/deployment/test/record/Status.java similarity index 82% rename from integration-tests/java-17/src/main/java/io/quarkus/it/hibernate/panache/person/Status.java rename to extensions/panache/hibernate-orm-panache/deployment/src/test/java/io/quarkus/hibernate/orm/panache/deployment/test/record/Status.java index 1f2b06dbc4f57..c0c9d50209cf7 100644 --- a/integration-tests/java-17/src/main/java/io/quarkus/it/hibernate/panache/person/Status.java +++ b/extensions/panache/hibernate-orm-panache/deployment/src/test/java/io/quarkus/hibernate/orm/panache/deployment/test/record/Status.java @@ -1,4 +1,4 @@ -package io.quarkus.it.hibernate.panache.person; +package io.quarkus.hibernate.orm.panache.deployment.test.record; public enum Status { DEAD("I'm a Zombie"), diff --git a/extensions/panache/hibernate-orm-panache/runtime/pom.xml b/extensions/panache/hibernate-orm-panache/runtime/pom.xml index 2520949da5759..170a7624aadd5 100644 --- a/extensions/panache/hibernate-orm-panache/runtime/pom.xml +++ b/extensions/panache/hibernate-orm-panache/runtime/pom.xml @@ -48,36 +48,6 @@ mockito-core test - - - org.hibernate.orm - hibernate-jpamodelgen - provided - - - - javax.xml.bind - jaxb-api - - - javax.activation - javax.activation-api - - - - - - - jakarta.xml.bind - jakarta.xml.bind-api - provided - - - - org.eclipse.angus - angus-activation - provided - @@ -85,6 +55,13 @@ maven-compiler-plugin + + + org.hibernate.orm + hibernate-jpamodelgen + ${hibernate-orm.version} + + org.hibernate.jpamodelgen.JPAMetaModelEntityProcessor diff --git a/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/NamedQueryUtil.java b/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/NamedQueryUtil.java index 0aee47decd816..ed5a92b8ae25d 100644 --- a/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/NamedQueryUtil.java +++ b/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/NamedQueryUtil.java @@ -4,6 +4,7 @@ import java.util.Map; import org.hibernate.query.SemanticException; +import org.hibernate.query.SyntaxException; import org.hibernate.query.sqm.ParsingException; import io.quarkus.panache.common.exception.PanacheQueryException; @@ -54,7 +55,8 @@ static String getNamedQuery(String namedQuery) { public static RuntimeException checkForNamedQueryMistake(IllegalArgumentException x, String originalQuery) { if (originalQuery != null - && (x.getCause() instanceof SemanticException || x.getCause() instanceof ParsingException) + && (x.getCause() instanceof SemanticException || x.getCause() instanceof ParsingException + || x.getCause() instanceof SyntaxException) && isNamedQuery(originalQuery)) { return new PanacheQueryException("Invalid query '" + originalQuery + "' but it matches a known @NamedQuery, perhaps you should prefix it with a '#' to use it as a named query: '#" diff --git a/extensions/panache/hibernate-reactive-panache-kotlin/runtime/pom.xml b/extensions/panache/hibernate-reactive-panache-kotlin/runtime/pom.xml index 759631b16e091..e007a8da611b8 100644 --- a/extensions/panache/hibernate-reactive-panache-kotlin/runtime/pom.xml +++ b/extensions/panache/hibernate-reactive-panache-kotlin/runtime/pom.xml @@ -69,39 +69,15 @@ test - - org.hibernate.orm - hibernate-jpamodelgen - provided - - - - jakarta.xml.bind - jakarta.xml.bind-api - - - javax.xml.bind - jaxb-api - - - javax.activation - javax.activation-api - - - io.quarkus quarkus-panache-common-deployment test - - - jakarta.xml.bind - jakarta.xml.bind-api - provided - - + org.eclipse.angus angus-activation @@ -139,6 +115,13 @@ kapt + + + org.hibernate.orm + hibernate-jpamodelgen + ${hibernate-orm.version} + + org.hibernate.jpamodelgen.JPAMetaModelEntityProcessor diff --git a/extensions/panache/hibernate-reactive-panache/runtime/pom.xml b/extensions/panache/hibernate-reactive-panache/runtime/pom.xml index 2071edb9247e8..7c3a24f07a37d 100644 --- a/extensions/panache/hibernate-reactive-panache/runtime/pom.xml +++ b/extensions/panache/hibernate-reactive-panache/runtime/pom.xml @@ -52,40 +52,6 @@ mockito-core test - - - org.hibernate.orm - hibernate-jpamodelgen - provided - - - - jakarta.xml.bind - jakarta.xml.bind-api - - - javax.xml.bind - jaxb-api - - - javax.activation - javax.activation-api - - - - - - - jakarta.xml.bind - jakarta.xml.bind-api - provided - - - - org.eclipse.angus - angus-activation - provided - @@ -93,6 +59,13 @@ maven-compiler-plugin + + + org.hibernate.orm + hibernate-jpamodelgen + ${hibernate-orm.version} + + org.hibernate.jpamodelgen.JPAMetaModelEntityProcessor diff --git a/extensions/panache/panache-hibernate-common/deployment/src/main/java/io/quarkus/panache/common/deployment/PanacheHibernateCommonResourceProcessor.java b/extensions/panache/panache-hibernate-common/deployment/src/main/java/io/quarkus/panache/common/deployment/PanacheHibernateCommonResourceProcessor.java index 93037abea56c9..6127afcc2955b 100644 --- a/extensions/panache/panache-hibernate-common/deployment/src/main/java/io/quarkus/panache/common/deployment/PanacheHibernateCommonResourceProcessor.java +++ b/extensions/panache/panache-hibernate-common/deployment/src/main/java/io/quarkus/panache/common/deployment/PanacheHibernateCommonResourceProcessor.java @@ -157,9 +157,12 @@ private EntityModel createEntityModel(ClassInfo classInfo) { // so we need to be careful when we enhance private fields, // because the corresponding `$_hibernate_{read/write}_*()` methods // will only be generated for classes mapped through *annotations*. - boolean willBeEnhancedByHibernateOrm = classInfo.hasAnnotation(DOTNAME_ENTITY) + boolean isManaged = classInfo.hasAnnotation(DOTNAME_ENTITY) || classInfo.hasAnnotation(DOTNAME_MAPPED_SUPERCLASS) || classInfo.hasAnnotation(DOTNAME_EMBEDDABLE); + boolean willBeEnhancedByHibernateOrm = isManaged + // Records are immutable, thus never enhanced + && !classInfo.isRecord(); for (FieldInfo fieldInfo : classInfo.fields()) { String name = fieldInfo.name(); if (!Modifier.isStatic(fieldInfo.flags()) diff --git a/extensions/panache/panache-hibernate-common/runtime/src/main/java/io/quarkus/panache/hibernate/common/runtime/PanacheJpaUtil.java b/extensions/panache/panache-hibernate-common/runtime/src/main/java/io/quarkus/panache/hibernate/common/runtime/PanacheJpaUtil.java index 3029a33398242..14edc447ad802 100644 --- a/extensions/panache/panache-hibernate-common/runtime/src/main/java/io/quarkus/panache/hibernate/common/runtime/PanacheJpaUtil.java +++ b/extensions/panache/panache-hibernate-common/runtime/src/main/java/io/quarkus/panache/hibernate/common/runtime/PanacheJpaUtil.java @@ -175,7 +175,7 @@ public static String toOrderBy(Sort sort) { Sort.Column column = sort.getColumns().get(i); if (i > 0) sb.append(" , "); - sb.append(column.getName()); + sb.append('`').append(unquoteColumnName(column)).append('`'); if (column.getDirection() != Sort.Direction.Ascending) { sb.append(" DESC"); } @@ -191,4 +191,20 @@ public static String toOrderBy(Sort sort) { } return sb.toString(); } + + private static String unquoteColumnName(Sort.Column column) { + String columnName = column.getName(); + String unquotedColumnName; + //Note HQL uses backticks to escape/quote special words that are used as identifiers + if (columnName.charAt(0) == '`' && columnName.charAt(columnName.length() - 1) == '`') { + unquotedColumnName = columnName.substring(1, columnName.length() - 1); + } else { + unquotedColumnName = columnName; + } + // Note we're not dealing with columns but with entity attributes so no backticks expected in unquoted column name + if (unquotedColumnName.indexOf('`') >= 0) { + throw new PanacheQueryException("Sort column name cannot have backticks"); + } + return unquotedColumnName; + } } diff --git a/extensions/pom.xml b/extensions/pom.xml index acc804301bc04..f405528ad1782 100644 --- a/extensions/pom.xml +++ b/extensions/pom.xml @@ -96,7 +96,7 @@ hibernate-validator panache hibernate-search-orm-elasticsearch - hibernate-search-orm-coordination-outbox-polling + hibernate-search-orm-outbox-polling elasticsearch-rest-client-common elasticsearch-rest-client elasticsearch-java-client diff --git a/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/ApplicationNotRunningPredicateTest.java b/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/ApplicationNotRunningPredicateTest.java new file mode 100644 index 0000000000000..8cb4bcda0915d --- /dev/null +++ b/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/ApplicationNotRunningPredicateTest.java @@ -0,0 +1,57 @@ +package io.quarkus.quartz.test; + +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import jakarta.enterprise.event.Observes; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.runtime.StartupEvent; +import io.quarkus.scheduler.FailedExecution; +import io.quarkus.scheduler.Scheduled; +import io.quarkus.scheduler.SuccessfulExecution; +import io.quarkus.test.QuarkusUnitTest; + +public class ApplicationNotRunningPredicateTest { + + @RegisterExtension + static final QuarkusUnitTest test = new QuarkusUnitTest().withApplicationRoot((jar) -> jar.addClasses(Jobs.class)); + + static final CountDownLatch SUCCESS_LATCH = new CountDownLatch(1); + static volatile FailedExecution failedExecution; + + @Test + public void testTriggerErrorStatus() throws InterruptedException { + assertTrue(SUCCESS_LATCH.await(5, TimeUnit.SECONDS)); + assertNull(failedExecution); + } + + void observeSuccessfulExecution(@Observes SuccessfulExecution successfulExecution) { + SUCCESS_LATCH.countDown(); + } + + void observeFailedExecution(@Observes FailedExecution failedExecution) { + ApplicationNotRunningPredicateTest.failedExecution = failedExecution; + } + + static class Jobs { + + volatile boolean started; + + void started(@Observes StartupEvent event) { + started = true; + } + + @Scheduled(every = "0.2s", skipExecutionIf = Scheduled.ApplicationNotRunning.class) + void scheduleAfterStarted() { + if (!started) { + throw new IllegalStateException(); + } + } + } +} diff --git a/extensions/quartz/runtime/pom.xml b/extensions/quartz/runtime/pom.xml index 0540be355fbae..1b7818b00dfa1 100644 --- a/extensions/quartz/runtime/pom.xml +++ b/extensions/quartz/runtime/pom.xml @@ -55,6 +55,17 @@ --> + + + io.opentelemetry.instrumentation + opentelemetry-instrumentation-api + true + + + io.opentelemetry.instrumentation + opentelemetry-instrumentation-api-semconv + true + diff --git a/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/InstrumentedJob.java b/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/InstrumentedJob.java new file mode 100644 index 0000000000000..f7744c282895a --- /dev/null +++ b/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/InstrumentedJob.java @@ -0,0 +1,51 @@ +package io.quarkus.quartz.runtime; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; + +import org.quartz.Job; +import org.quartz.JobExecutionContext; +import org.quartz.JobExecutionException; +import org.quartz.JobKey; + +import io.quarkus.scheduler.spi.JobInstrumenter; +import io.quarkus.scheduler.spi.JobInstrumenter.JobInstrumentationContext; + +/** + * + * @see JobInstrumenter + */ +class InstrumentedJob implements Job { + + private final Job delegate; + private final JobInstrumenter instrumenter; + + InstrumentedJob(Job delegate, JobInstrumenter instrumenter) { + this.delegate = delegate; + this.instrumenter = instrumenter; + } + + @Override + public void execute(JobExecutionContext context) throws JobExecutionException { + instrumenter.instrument(new JobInstrumentationContext() { + + @Override + public CompletionStage executeJob() { + try { + delegate.execute(context); + return CompletableFuture.completedFuture(null); + } catch (Exception e) { + return CompletableFuture.failedFuture(e); + } + + } + + @Override + public String getSpanName() { + JobKey key = context.getJobDetail().getKey(); + return key.getGroup() + '.' + key.getName(); + } + }); + } + +} diff --git a/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzSchedulerImpl.java b/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzSchedulerImpl.java index ce07d6ee5914c..cedfa97b9cfac 100644 --- a/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzSchedulerImpl.java +++ b/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzSchedulerImpl.java @@ -85,9 +85,11 @@ import io.quarkus.scheduler.common.runtime.SchedulerContext; import io.quarkus.scheduler.common.runtime.SyntheticScheduled; import io.quarkus.scheduler.common.runtime.util.SchedulerUtils; +import io.quarkus.scheduler.runtime.SchedulerConfig; import io.quarkus.scheduler.runtime.SchedulerRuntimeConfig; import io.quarkus.scheduler.runtime.SchedulerRuntimeConfig.StartMode; import io.quarkus.scheduler.runtime.SimpleScheduler; +import io.quarkus.scheduler.spi.JobInstrumenter; import io.quarkus.vertx.core.runtime.context.VertxContextSafetyToggle; import io.quarkus.virtual.threads.VirtualThreadsRecorder; import io.smallrye.common.vertx.VertxContext; @@ -123,6 +125,8 @@ public class QuartzSchedulerImpl implements QuartzScheduler { private final Event scheduledJobPausedEvent; private final Event scheduledJobResumedEvent; private final QuartzRuntimeConfig runtimeConfig; + private final SchedulerConfig schedulerConfig; + private final Instance jobInstrumenter; public QuartzSchedulerImpl(SchedulerContext context, QuartzSupport quartzSupport, SchedulerRuntimeConfig schedulerRuntimeConfig, @@ -131,7 +135,8 @@ public QuartzSchedulerImpl(SchedulerContext context, QuartzSupport quartzSupport Event schedulerResumedEvent, Event scheduledJobPausedEvent, Event scheduledJobResumedEvent, Instance jobs, Instance userTransaction, - Vertx vertx) { + Vertx vertx, + SchedulerConfig schedulerConfig, Instance jobInstrumenter) { this.shutdownWaitTime = quartzSupport.getRuntimeConfig().shutdownWaitTime; this.skippedExecutionEvent = skippedExecutionEvent; this.successExecutionEvent = successExecutionEvent; @@ -143,6 +148,8 @@ public QuartzSchedulerImpl(SchedulerContext context, QuartzSupport quartzSupport this.runtimeConfig = quartzSupport.getRuntimeConfig(); this.enabled = schedulerRuntimeConfig.enabled; this.defaultOverdueGracePeriod = schedulerRuntimeConfig.overdueGracePeriod; + this.schedulerConfig = schedulerConfig; + this.jobInstrumenter = jobInstrumenter; StartMode startMode = initStartMode(schedulerRuntimeConfig, runtimeConfig); @@ -176,6 +183,11 @@ public QuartzSchedulerImpl(SchedulerContext context, QuartzSupport quartzSupport CronDefinition def = CronDefinitionBuilder.instanceDefinitionFor(cronType); cronParser = new CronParser(def); + JobInstrumenter instrumenter = null; + if (schedulerConfig.tracingEnabled && jobInstrumenter.isResolvable()) { + instrumenter = jobInstrumenter.get(); + } + if (!enabled) { LOGGER.info("Quartz scheduler is disabled by config property and will not be started"); this.scheduler = null; @@ -196,7 +208,8 @@ public QuartzSchedulerImpl(SchedulerContext context, QuartzSupport quartzSupport scheduler = schedulerFactory.getScheduler(); // Set custom job factory - scheduler.setJobFactory(new InvokerJobFactory(scheduledTasks, jobs, vertx)); + scheduler.setJobFactory( + new InvokerJobFactory(scheduledTasks, jobs, vertx, instrumenter)); if (transaction != null) { transaction.begin(); @@ -209,11 +222,12 @@ public QuartzSchedulerImpl(SchedulerContext context, QuartzSupport quartzSupport if (identity.isEmpty()) { identity = ++nameSequence + "_" + method.getInvokerClassName(); } + ScheduledInvoker invoker = SimpleScheduler.initInvoker( context.createInvoker(method.getInvokerClassName()), skippedExecutionEvent, successExecutionEvent, failedExecutionEvent, scheduled.concurrentExecution(), - SimpleScheduler.initSkipPredicate(scheduled.skipExecutionIf())); + SimpleScheduler.initSkipPredicate(scheduled.skipExecutionIf()), instrumenter); JobDetail jobDetail = createJobDetail(identity, method.getInvokerClassName()); Optional> triggerBuilder = createTrigger(identity, scheduled, cronType, runtimeConfig, @@ -791,6 +805,7 @@ public boolean isRunningOnVirtualThread() { }; } else { invoker = new DefaultInvoker() { + @Override public CompletionStage invokeBean(ScheduledExecution execution) { try { @@ -807,6 +822,7 @@ public boolean isBlocking() { }; } + Scheduled scheduled = new SyntheticScheduled(identity, cron, every, 0, TimeUnit.MINUTES, delayed, overdueGracePeriod, concurrentExecution, skipPredicate, timeZone); @@ -814,8 +830,12 @@ public boolean isBlocking() { Optional> triggerBuilder = createTrigger(identity, scheduled, cronType, runtimeConfig, jobDetail); if (triggerBuilder.isPresent()) { + JobInstrumenter instrumenter = null; + if (schedulerConfig.tracingEnabled && jobInstrumenter.isResolvable()) { + instrumenter = jobInstrumenter.get(); + } invoker = SimpleScheduler.initInvoker(invoker, skippedExecutionEvent, successExecutionEvent, - failedExecutionEvent, concurrentExecution, skipPredicate); + failedExecutionEvent, concurrentExecution, skipPredicate, instrumenter); org.quartz.Trigger trigger = triggerBuilder.get().build(); QuartzTrigger existing = scheduledTasks.putIfAbsent(identity, new QuartzTrigger(trigger.getKey(), new Function<>() { @@ -895,6 +915,7 @@ public void run() { }); } else { context.executeBlocking(new Callable() { + @Override public Object call() throws Exception { return trigger.invoker.invoke(new QuartzScheduledExecution(trigger, jobExecutionContext)); @@ -918,9 +939,9 @@ public void handle(Void event) { } } else { String jobName = jobExecutionContext.getJobDetail().getKey().getName(); - LOGGER.warnf("Unable to find corresponding Quartz trigger for job %s. " + - "Update your Quartz table by removing all phantom jobs or make sure that there is a " + - "Scheduled method with the identity matching the job's name", jobName); + LOGGER.warnf("Unable to find corresponding Quartz trigger for job %s. " + + "Update your Quartz table by removing all phantom jobs or make sure that there is a " + + "Scheduled method with the identity matching the job's name", jobName); } } } @@ -1018,11 +1039,15 @@ static class InvokerJobFactory extends SimpleJobFactory { final Map scheduledTasks; final Instance jobs; final Vertx vertx; + final JobInstrumenter instrumenter; - InvokerJobFactory(Map scheduledTasks, Instance jobs, Vertx vertx) { + InvokerJobFactory(Map scheduledTasks, Instance jobs, Vertx vertx, + JobInstrumenter instrumenter) { this.scheduledTasks = scheduledTasks; this.jobs = jobs; this.vertx = vertx; + this.instrumenter = instrumenter; + } @SuppressWarnings("unchecked") @@ -1039,9 +1064,16 @@ public Job newJob(TriggerFiredBundle bundle, org.quartz.Scheduler Scheduler) thr } Instance instance = jobs.select(jobClass); if (instance.isResolvable()) { - return (Job) instance.get(); + return jobWithSpanWrapper((Job) instance.get()); + } + return jobWithSpanWrapper(super.newJob(bundle, Scheduler)); + } + + private Job jobWithSpanWrapper(Job job) { + if (instrumenter != null) { + return new InstrumentedJob(job, instrumenter); } - return super.newJob(bundle, Scheduler); + return job; } } diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleProcessor.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleProcessor.java index 2a67ca6932137..12477cf517114 100644 --- a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleProcessor.java +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleProcessor.java @@ -435,7 +435,8 @@ void validateMessageBundleMethodsInTemplates(TemplatesAnalysisBuildItem analysis List checkedTemplates, BeanDiscoveryFinishedBuildItem beanDiscovery, List templateData, - QuteConfig config) { + QuteConfig config, + List globals) { IndexView index = beanArchiveIndex.getIndex(); Function templateIdToPathFun = new Function() { @@ -585,7 +586,7 @@ public String apply(String id) { implicitClassToMembersUsed, templateIdToPathFun, generatedIdsToMatches, extensionMethodExcludes, checkedTemplate, lookupConfig, namedBeans, namespaceTemplateData, regularExtensionMethods, namespaceExtensionMethods, - assignabilityCheck); + assignabilityCheck, globals); MatchResult match = results.get(param.toOriginalString()); if (match != null && !match.isEmpty() && !assignabilityCheck.isAssignableFrom(match.type(), methodParams.get(idx))) { diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java index adc3c78166a5c..c2fc7b7393167 100644 --- a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java @@ -153,6 +153,7 @@ public class QuteProcessor { public static final DotName LOCATION = Names.LOCATION; + public static final String GLOBAL_NAMESPACE = "global"; private static final Logger LOGGER = Logger.getLogger(QuteProcessor.class); @@ -922,7 +923,8 @@ void validateExpressions(TemplatesAnalysisBuildItem templatesAnalysis, List checkedTemplates, List templateData, QuteConfig config, - PackageConfig packageConfig) { + PackageConfig packageConfig, + List globals) { long start = System.nanoTime(); @@ -996,7 +998,7 @@ public String apply(String id) { incorrectExpressions, expression, index, implicitClassToMembersUsed, templateIdToPathFun, generatedIdsToMatches, extensionMethodExcludes, checkedTemplate, lookupConfig, namedBeans, namespaceTemplateData, regularExtensionMethods, - namespaceExtensionMethods, assignabilityCheck); + namespaceExtensionMethods, assignabilityCheck, globals); generatedIdsToMatches.put(expression.getGeneratedId(), match); } @@ -1130,7 +1132,8 @@ static MatchResult validateNestedExpressions(QuteConfig config, TemplateAnalysis Map namespaceTemplateData, List regularExtensionMethods, Map> namespaceToExtensionMethods, - AssignabilityCheck assignabilityCheck) { + AssignabilityCheck assignabilityCheck, + List globals) { LOGGER.debugf("Validate %s from %s", expression, expression.getOrigin()); @@ -1140,7 +1143,7 @@ static MatchResult validateNestedExpressions(QuteConfig config, TemplateAnalysis validateParametersOfNestedVirtualMethods(config, templateAnalysis, results, excludes, incorrectExpressions, expression, index, implicitClassToMembersUsed, templateIdToPathFun, generatedIdsToMatches, extensionMethodExcludes, checkedTemplate, lookupConfig, namedBeans, namespaceTemplateData, regularExtensionMethods, - namespaceToExtensionMethods, assignabilityCheck); + namespaceToExtensionMethods, assignabilityCheck, globals); MatchResult match = new MatchResult(assignabilityCheck); @@ -1148,7 +1151,8 @@ static MatchResult validateNestedExpressions(QuteConfig config, TemplateAnalysis // Process the namespace // ====================== NamespaceResult namespaceResult = processNamespace(expression, match, index, incorrectExpressions, namedBeans, results, - templateAnalysis, namespaceTemplateData, lookupConfig, namespaceToExtensionMethods, templateIdToPathFun); + templateAnalysis, namespaceTemplateData, lookupConfig, namespaceToExtensionMethods, templateIdToPathFun, + globals); if (namespaceResult.ignoring) { return match; } @@ -1326,19 +1330,44 @@ private static RootResult processRoot(Expression expression, MatchResult match, ignoring = true; } } else { - // No namespace extension method found - incorrect expression - incorrectExpressions.produce(new IncorrectExpressionBuildItem(expression.toOriginalString(), - String.format("No matching namespace [%s] extension method found", namespace.namespace), - expression.getOrigin())); - match.clearValues(); - putResult(match, results, expression); - ignoring = true; + if (namespace.hasGlobal()) { + ClassInfo variableClass = index.getClassByName(namespace.global.getVariableType().name()); + if (variableClass != null) { + match.setValues(variableClass, namespace.global.getVariableType()); + iterator = processHintsIfNeeded(root, iterator, parts, templateAnalysis, root.asHintInfo().hints, match, + index, expression, generatedIdsToMatches, incorrectExpressions); + } else { + // Global variable type not available + putResult(match, results, expression); + ignoring = true; + } + } else { + // No global and no namespace extension method found - incorrect expression + incorrectExpressions.produce(new IncorrectExpressionBuildItem(expression.toOriginalString(), + String.format("No matching namespace [%s] extension method found", namespace.namespace), + expression.getOrigin())); + match.clearValues(); + putResult(match, results, expression); + ignoring = true; + } } } else if (namespace.hasDataNamespaceInfo()) { // Validate as Data namespace expression has parameter declaration bound to the variable // Skip the first part, e.g. for {data:item.name} we start validation with "name" match.setValues(namespace.dataNamespaceExpTypeInfo.rawClass, namespace.dataNamespaceExpTypeInfo.resolvedType); + } else if (namespace.hasGlobal()) { + // "global:" namespace is used and no namespace extension methods exist + ClassInfo variableClass = index.getClassByName(namespace.global.getVariableType().name()); + if (variableClass != null) { + match.setValues(variableClass, namespace.global.getVariableType()); + iterator = processHintsIfNeeded(root, iterator, parts, templateAnalysis, root.asHintInfo().hints, match, + index, expression, generatedIdsToMatches, incorrectExpressions); + } else { + // Global variable type not available + putResult(match, results, expression); + ignoring = true; + } } else if (rootClazz == null) { // No namespace is used or no declarative resolver (extension methods, @TemplateData, etc.) if (root.isTypeInfo()) { @@ -1416,7 +1445,8 @@ private static NamespaceResult processNamespace(Expression expression, MatchResu Map results, TemplateAnalysis templateAnalysis, Map namespaceTemplateData, JavaMemberLookupConfig lookupConfig, Map> namespaceToExtensionMethods, - Function templateIdToPathFun) { + Function templateIdToPathFun, + List globals) { String namespace = expression.getNamespace(); if (namespace == null) { return NamespaceResult.EMPTY; @@ -1426,6 +1456,10 @@ private static NamespaceResult processNamespace(Expression expression, MatchResu TemplateDataBuildItem templateData = null; List namespaceExtensionMethods = null; boolean ignored = false; + TemplateGlobalBuildItem global = namespace.equals(GLOBAL_NAMESPACE) + ? globals.stream().filter(g -> g.getName().equals(expression.getParts().get(0).getName())).findFirst() + .orElse(null) + : null; if (namespace.equals(INJECT_NAMESPACE) || namespace.equals(CDI_NAMESPACE)) { // cdi:, inject: @@ -1475,22 +1509,26 @@ private static NamespaceResult processNamespace(Expression expression, MatchResu filter = filter.and(templateData::filter); lookupConfig = new FirstPassJavaMemberLookupConfig(lookupConfig, filter, true); } else { - // Extension methods exist for the given namespace + // Extension methods may exist for the given namespace namespaceExtensionMethods = namespaceToExtensionMethods.get(namespace); + if (namespaceExtensionMethods == null) { - // All other namespaces are ignored - putResult(match, results, expression); - ignored = true; + if (!namespace.equals(GLOBAL_NAMESPACE) || global == null) { + // Not "global:" with a matching global variable + // All other namespaces are ignored + putResult(match, results, expression); + ignored = true; + } } } } return new NamespaceResult(namespace, rootClazz, dataNamespaceTypeInfo, templateData, namespaceExtensionMethods, - ignored, lookupConfig); + ignored, lookupConfig, global); } private static class NamespaceResult { - static final NamespaceResult EMPTY = new NamespaceResult(null, null, null, null, null, false, null); + static final NamespaceResult EMPTY = new NamespaceResult(null, null, null, null, null, false, null, null); private final String namespace; private final ClassInfo rootClazz; @@ -1499,11 +1537,12 @@ private static class NamespaceResult { private final List extensionMethods; private final boolean ignoring; private final JavaMemberLookupConfig lookupConfig; + private final TemplateGlobalBuildItem global; NamespaceResult(String namespace, ClassInfo rootClazz, TypeInfo dataNamespaceExpTypeInfo, - TemplateDataBuildItem templateData, - List namespaceExtensionMethods, boolean ignoring, - JavaMemberLookupConfig lookupConfig) { + TemplateDataBuildItem templateData, List namespaceExtensionMethods, + boolean ignoring, + JavaMemberLookupConfig lookupConfig, TemplateGlobalBuildItem global) { this.namespace = namespace; this.rootClazz = rootClazz; this.dataNamespaceExpTypeInfo = dataNamespaceExpTypeInfo; @@ -1511,6 +1550,7 @@ private static class NamespaceResult { this.extensionMethods = namespaceExtensionMethods; this.ignoring = ignoring; this.lookupConfig = lookupConfig; + this.global = global; } boolean hasExtensionMethods() { @@ -1529,6 +1569,10 @@ boolean hasLookupConfig() { return lookupConfig != null; } + boolean hasGlobal() { + return global != null; + } + boolean isIn(String... values) { for (String value : values) { if (value.equals(namespace)) { @@ -1595,7 +1639,8 @@ private static void validateParametersOfNestedVirtualMethods(QuteConfig config, Map namespaceTemplateData, List regularExtensionMethods, Map> namespaceExtensionMethods, - AssignabilityCheck assignabilityCheck) { + AssignabilityCheck assignabilityCheck, + List globals) { for (Expression.Part part : expression.getParts()) { if (part.isVirtualMethod()) { for (Expression param : part.asVirtualMethod().getParameters()) { @@ -1607,7 +1652,8 @@ private static void validateParametersOfNestedVirtualMethods(QuteConfig config, validateNestedExpressions(config, templateAnalysis, null, results, excludes, incorrectExpressions, param, index, implicitClassToMembersUsed, templateIdToPathFun, generatedIdsToMatches, extensionMethodExcludes, checkedTemplate, lookupConfig, namedBeans, - namespaceTemplateData, regularExtensionMethods, namespaceExtensionMethods, assignabilityCheck); + namespaceTemplateData, regularExtensionMethods, namespaceExtensionMethods, assignabilityCheck, + globals); } } } @@ -1834,7 +1880,7 @@ void generateValueResolvers(QuteConfig config, BuildProducer generatedResolvers, BuildProducer reflectiveClass, - BuildProducer generatedInitializers) { + BuildProducer globalProviders) { if (!incorrectExpressions.isEmpty()) { // Skip generation if a validation error occurs @@ -2004,7 +2050,7 @@ public Function apply(ClassInfo clazz) { } if (!templateGlobals.isEmpty()) { - TemplateGlobalGenerator globalGenerator = new TemplateGlobalGenerator(classOutput); + TemplateGlobalGenerator globalGenerator = new TemplateGlobalGenerator(classOutput, GLOBAL_NAMESPACE, -1000, index); Map> classToTargets = new HashMap<>(); Map> classToGlobals = templateGlobals.stream() @@ -2019,7 +2065,7 @@ public Function apply(ClassInfo clazz) { } for (String generatedType : globalGenerator.getGeneratedTypes()) { - generatedInitializers.produce(new GeneratedTemplateInitializerBuildItem(generatedType)); + globalProviders.produce(new TemplateGlobalProviderBuildItem(generatedType)); reflectiveClass.produce(ReflectiveClassBuildItem.builder(generatedType).build()); } } @@ -2399,7 +2445,7 @@ public boolean test(TypeCheck check) { void initialize(BuildProducer syntheticBeans, QuteRecorder recorder, List generatedValueResolvers, List templatePaths, Optional templateVariants, - List templateInitializers, + List templateInitializers, TemplateRootsBuildItem templateRoots) { List templates = new ArrayList<>(); @@ -2424,7 +2470,7 @@ void initialize(BuildProducer syntheticBeans, QuteRecord .supplier(recorder.createContext(generatedValueResolvers.stream() .map(GeneratedValueResolverBuildItem::getClassName).collect(Collectors.toList()), templates, tags, variants, templateInitializers.stream() - .map(GeneratedTemplateInitializerBuildItem::getClassName).collect(Collectors.toList()), + .map(TemplateGlobalProviderBuildItem::getClassName).collect(Collectors.toList()), templateRoots.getPaths().stream().map(p -> p + "/").collect(Collectors.toSet()))) .done()); } diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/GeneratedTemplateInitializerBuildItem.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplateGlobalProviderBuildItem.java similarity index 60% rename from extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/GeneratedTemplateInitializerBuildItem.java rename to extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplateGlobalProviderBuildItem.java index 3101fa3a59c34..d02a84ee06bef 100644 --- a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/GeneratedTemplateInitializerBuildItem.java +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplateGlobalProviderBuildItem.java @@ -2,11 +2,11 @@ import io.quarkus.builder.item.MultiBuildItem; -public final class GeneratedTemplateInitializerBuildItem extends MultiBuildItem { +public final class TemplateGlobalProviderBuildItem extends MultiBuildItem { private final String className; - public GeneratedTemplateInitializerBuildItem(String className) { + public TemplateGlobalProviderBuildItem(String className) { this.className = className; } diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/PropertyNotFoundDevModeTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/PropertyNotFoundDevModeTest.java index 3e32d1500e078..89cabb8733609 100644 --- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/PropertyNotFoundDevModeTest.java +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/PropertyNotFoundDevModeTest.java @@ -27,7 +27,7 @@ public class PropertyNotFoundDevModeTest { @Test public void testExceptionIsThrown() { assertEquals( - "Rendering error in template [foo.html] line 1: Entry \"foo\" not found in the data map in expression {foo.surname}", + "Rendering error in template [foo.html] line 1: Key \"foo\" not found in the template data map with keys [] in expression {foo.surname}", RestAssured.get("test-foo").then().statusCode(200).extract().body().asString()); assertEquals( "Rendering error in template [bar.html] line 1: Property \"name\" not found on the base object \"java.lang.String\" in expression {bar.name}", diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/extensions/TimeTemplateExtensionsTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/extensions/TimeTemplateExtensionsTest.java index ec758c43a46f3..9186d303a8fa1 100644 --- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/extensions/TimeTemplateExtensionsTest.java +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/extensions/TimeTemplateExtensionsTest.java @@ -59,7 +59,8 @@ public void testInvalidParameter() { engine.parse("{time:format(input.birthday, 'uuuu')}").data("input", Map.of("name", "Quarkus Qute")).render(); fail(); } catch (TemplateException expected) { - assertTrue(expected.getMessage().startsWith("Rendering error: Property \"birthday\" not found on the base object"), + assertTrue( + expected.getMessage().startsWith("Rendering error: Key \"birthday\" not found in the map with keys [name]"), expected.getMessage()); } } diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/globals/TemplateGlobalNamespaceValidationFailureTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/globals/TemplateGlobalNamespaceValidationFailureTest.java new file mode 100644 index 0000000000000..5e07de37c8945 --- /dev/null +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/globals/TemplateGlobalNamespaceValidationFailureTest.java @@ -0,0 +1,55 @@ +package io.quarkus.qute.deployment.globals; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.qute.TemplateException; +import io.quarkus.qute.TemplateGlobal; +import io.quarkus.test.QuarkusUnitTest; + +public class TemplateGlobalNamespaceValidationFailureTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot(root -> root + .addClasses(Globals.class) + .addAsResource(new StringAsset( + "Hello {global:user.name}!"), + "templates/hello.txt")) + .assertException(t -> { + Throwable e = t; + TemplateException te = null; + while (e != null) { + if (e instanceof TemplateException) { + te = (TemplateException) e; + break; + } + e = e.getCause(); + } + assertNotNull(te); + assertTrue( + te.getMessage().contains("Found incorrect expressions (1)"), te.getMessage()); + assertTrue( + te.getMessage().contains( + "Property/method [name] not found on class [java.lang.String] nor handled by an extension method"), + te.getMessage()); + }); + + @Test + public void test() { + fail(); + } + + public static class Globals { + + @TemplateGlobal + static String user = "Fu"; + + } + +} diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/globals/TemplateGlobalTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/globals/TemplateGlobalTest.java index de372ee5c691a..b5b0388ecca85 100644 --- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/globals/TemplateGlobalTest.java +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/globals/TemplateGlobalTest.java @@ -20,7 +20,7 @@ public class TemplateGlobalTest { .withApplicationRoot(root -> root .addClasses(Globals.class, NextGlobals.class) .addAsResource(new StringAsset( - "Hello {currentUser}! Your name is {_name}. You're {age} years old."), + "Hello {currentUser}|{global:currentUser}! Your name is {_name}|{global:_name}. You're {age}|{global:age} years old."), "templates/hello.txt")); @Inject @@ -28,15 +28,19 @@ public class TemplateGlobalTest { @Test public void testTemplateData() { - assertEquals("Hello Fu! Your name is Lu. You're 40 years old.", hello.render()); - assertEquals("Hello Fu! Your name is Lu. You're 40 years old.", - Qute.fmt("Hello {currentUser}! Your name is {_name}. You're {age} years old.").render()); + assertEquals("Hello Fu|Fu! Your name is Lu|Lu. You're 40|40 years old.", hello.render()); + assertEquals("Hello Fu|Fu! Your name is Lu|Lu. You're 40|40 years old.", + Qute.fmt( + "Hello {currentUser}|{global:currentUser}! Your name is {_name}|{global:_name}. You're {age}|{global:age} years old.") + .render()); Globals.user = "Hu"; - assertEquals("Hello Hu! Your name is Lu. You're 20 years old.", hello.render()); - assertEquals("Hello Hu! Your name is Lu. You're 20 years old.", - Qute.fmt("Hello {currentUser}! Your name is {_name}. You're {age} years old.").render()); + assertEquals("Hello Hu|Hu! Your name is Lu|Lu. You're 20|20 years old.", hello.render()); + assertEquals("Hello Hu|Hu! Your name is Lu|Lu. You're 20|20 years old.", + Qute.fmt( + "Hello {currentUser}|{global:currentUser}! Your name is {_name}|{global:_name}. You're {age}|{global:age} years old.") + .render()); - assertEquals("First color is: RED", Qute.fmt("First color is: {colors[0]}").render()); + assertEquals("First color is: RED|RED", Qute.fmt("First color is: {colors[0]}|{global:colors[0]}").render()); } public static class Globals { diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/propertynotfound/PropertyNotFoundThrowExceptionTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/propertynotfound/PropertyNotFoundThrowExceptionTest.java index 789b4edb4b351..ef234400539a5 100644 --- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/propertynotfound/PropertyNotFoundThrowExceptionTest.java +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/propertynotfound/PropertyNotFoundThrowExceptionTest.java @@ -36,7 +36,8 @@ public void testException() { } catch (Exception expected) { Throwable rootCause = ExceptionUtil.getRootCause(expected); assertEquals(TemplateException.class, rootCause.getClass()); - assertTrue(rootCause.getMessage().contains("Entry \"foos\" not found in the data map"), rootCause.getMessage()); + assertTrue(rootCause.getMessage().contains("Key \"foos\" not found in the template data map with keys []"), + rootCause.getMessage()); } } 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 9bcf522449dc6..48a5360c3e92e 100644 --- a/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/EngineProducer.java +++ b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/EngineProducer.java @@ -46,6 +46,7 @@ import io.quarkus.qute.Results; import io.quarkus.qute.SectionHelperFactory; import io.quarkus.qute.Template; +import io.quarkus.qute.TemplateGlobalProvider; import io.quarkus.qute.TemplateInstance; import io.quarkus.qute.TemplateInstance.Initializer; import io.quarkus.qute.TemplateLocator; @@ -204,9 +205,11 @@ public EngineProducer(QuteContext context, QuteConfig config, QuteRuntimeConfig // Add a special parser hook for Qute.fmt() methods builder.addParserHook(new Qute.IndexedArgumentsParserHook()); - // Add template initializers - for (String initializerClass : context.getTemplateInstanceInitializerClasses()) { - builder.addTemplateInstanceInitializer(createInitializer(initializerClass)); + // Add global providers + for (String globalProviderClass : context.getTemplateGlobalProviderClasses()) { + TemplateGlobalProvider provider = createGlobalProvider(globalProviderClass); + builder.addTemplateInstanceInitializer(provider); + builder.addNamespaceResolver(provider); } // Add a special initializer for templates that contain an inject/cdi namespace expressions @@ -313,17 +316,17 @@ private Resolver createResolver(String resolverClassName) { } } - private TemplateInstance.Initializer createInitializer(String initializerClassName) { + private TemplateGlobalProvider createGlobalProvider(String initializerClassName) { try { Class initializerClazz = Thread.currentThread() .getContextClassLoader().loadClass(initializerClassName); - if (TemplateInstance.Initializer.class.isAssignableFrom(initializerClazz)) { - return (TemplateInstance.Initializer) initializerClazz.getDeclaredConstructor().newInstance(); + if (TemplateGlobalProvider.class.isAssignableFrom(initializerClazz)) { + return (TemplateGlobalProvider) initializerClazz.getDeclaredConstructor().newInstance(); } - throw new IllegalStateException("Not an initializer: " + initializerClazz); + throw new IllegalStateException("Not a global provider: " + initializerClazz); } catch (InstantiationException | IllegalAccessException | ClassNotFoundException | IllegalArgumentException | InvocationTargetException | NoSuchMethodException | SecurityException e) { - throw new IllegalStateException("Unable to create initializer: " + initializerClassName, e); + throw new IllegalStateException("Unable to create global provider: " + initializerClassName, e); } } diff --git a/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/QuteRecorder.java b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/QuteRecorder.java index 17a2caee2ddba..0fff3270c5735 100644 --- a/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/QuteRecorder.java +++ b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/QuteRecorder.java @@ -12,7 +12,7 @@ public class QuteRecorder { public Supplier createContext(List resolverClasses, List templatePaths, List tags, Map> variants, - List templateInstanceInitializerClasses, Set templateRoots) { + List templateGlobalProviderClasses, Set templateRoots) { return new Supplier() { @Override @@ -40,8 +40,8 @@ public Map> getVariants() { } @Override - public List getTemplateInstanceInitializerClasses() { - return templateInstanceInitializerClasses; + public List getTemplateGlobalProviderClasses() { + return templateGlobalProviderClasses; } @Override @@ -63,7 +63,7 @@ public interface QuteContext { Map> getVariants(); - List getTemplateInstanceInitializerClasses(); + List getTemplateGlobalProviderClasses(); Set getTemplateRoots(); diff --git a/extensions/reactive-routes/runtime/src/main/java/io/quarkus/vertx/web/runtime/RouteHandlers.java b/extensions/reactive-routes/runtime/src/main/java/io/quarkus/vertx/web/runtime/RouteHandlers.java index 6303c0be1c785..185a7e23db834 100644 --- a/extensions/reactive-routes/runtime/src/main/java/io/quarkus/vertx/web/runtime/RouteHandlers.java +++ b/extensions/reactive-routes/runtime/src/main/java/io/quarkus/vertx/web/runtime/RouteHandlers.java @@ -1,5 +1,7 @@ package io.quarkus.vertx.web.runtime; +import static io.vertx.core.http.HttpHeaders.CONTENT_TYPE; + import io.vertx.core.Handler; import io.vertx.core.http.HttpServerResponse; import io.vertx.ext.web.RoutingContext; @@ -9,20 +11,20 @@ public final class RouteHandlers { private RouteHandlers() { } - static final String CONTENT_TYPE = "content-type"; - public static void setContentType(RoutingContext context, String defaultContentType) { HttpServerResponse response = context.response(); context.addHeadersEndHandler(new Handler() { @Override public void handle(Void aVoid) { + var headers = response.headers(); //use a listener to set the content type if it has not been set - if (response.headers().get(CONTENT_TYPE) == null) { + if (!headers.contains(CONTENT_TYPE)) { String acceptableContentType = context.getAcceptableContentType(); + // we can use add because we know already there's no content type if (acceptableContentType != null) { - response.putHeader(CONTENT_TYPE, acceptableContentType); + headers.add(CONTENT_TYPE, acceptableContentType); } else if (defaultContentType != null) { - response.putHeader(CONTENT_TYPE, defaultContentType); + headers.add(CONTENT_TYPE, defaultContentType); } } } diff --git a/extensions/reactive-routes/runtime/src/main/java/io/quarkus/vertx/web/runtime/ValidationSupport.java b/extensions/reactive-routes/runtime/src/main/java/io/quarkus/vertx/web/runtime/ValidationSupport.java index 25755f5595113..552e2170bbb21 100644 --- a/extensions/reactive-routes/runtime/src/main/java/io/quarkus/vertx/web/runtime/ValidationSupport.java +++ b/extensions/reactive-routes/runtime/src/main/java/io/quarkus/vertx/web/runtime/ValidationSupport.java @@ -10,6 +10,7 @@ import jakarta.validation.Validator; import io.quarkus.arc.ArcContainer; +import io.vertx.core.http.HttpHeaders; import io.vertx.core.http.HttpServerResponse; import io.vertx.core.json.JsonArray; import io.vertx.core.json.JsonObject; @@ -70,7 +71,7 @@ private static JsonObject generateJsonResponse(Set> viola public static void handleViolationException(ConstraintViolationException ex, RoutingContext rc, boolean forceJsonEncoding) { String accept = rc.request().getHeader(ACCEPT_HEADER); if (forceJsonEncoding || accept != null && accept.contains(APPLICATION_JSON)) { - rc.response().putHeader(RouteHandlers.CONTENT_TYPE, APPLICATION_JSON); + rc.response().putHeader(HttpHeaders.CONTENT_TYPE, APPLICATION_JSON); JsonObject json = generateJsonResponse(ex.getConstraintViolations(), false); rc.response().setStatusCode(json.getInteger(PROBLEM_STATUS)); rc.response().end(json.encode()); diff --git a/extensions/redis-cache/runtime/src/main/java/io/quarkus/cache/redis/runtime/RedisCacheInfoBuilder.java b/extensions/redis-cache/runtime/src/main/java/io/quarkus/cache/redis/runtime/RedisCacheInfoBuilder.java index 80113eee560ba..7fd99591c1ba9 100644 --- a/extensions/redis-cache/runtime/src/main/java/io/quarkus/cache/redis/runtime/RedisCacheInfoBuilder.java +++ b/extensions/redis-cache/runtime/src/main/java/io/quarkus/cache/redis/runtime/RedisCacheInfoBuilder.java @@ -31,7 +31,7 @@ public static Set build(Set cacheNames, RedisCachesBuild if (namedRuntimeConfig != null && namedRuntimeConfig.expireAfterWrite.isPresent()) { cacheInfo.expireAfterWrite = namedRuntimeConfig.expireAfterWrite; - } else if (defaultRuntimeConfig.expireAfterAccess.isPresent()) { + } else if (defaultRuntimeConfig.expireAfterWrite.isPresent()) { cacheInfo.expireAfterWrite = defaultRuntimeConfig.expireAfterWrite; } diff --git a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/SortedSetCommandsTest.java b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/SortedSetCommandsTest.java index b5d791247b17e..6f0e3959b033c 100644 --- a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/SortedSetCommandsTest.java +++ b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/SortedSetCommandsTest.java @@ -388,7 +388,7 @@ void zrangebyscoreWithScoresInfinity() { @Test @RequiresRedis6OrHigher void zrangestorebylex() { - setOfStrings.zadd(key, Map.of("a", 1.0, "b", 2.0, "c", 3.0, "d", 4.0)); + setOfStrings.zadd(key, Map.of("a", 1.0, "b", 1.0, "c", 1.0, "d", 1.0)); assertThat(setOfStrings.zrangestorebylex("key1", key, new Range<>("b", "d"), new ZRangeArgs().limit(0, 4))) .isEqualTo(3); assertThat(setOfStrings.zrange("key1", 0, 1)).isEqualTo(List.of("b", "c")); @@ -489,7 +489,7 @@ void zrevrangeWithScores() { @Test @RequiresRedis6OrHigher void zrevrangebylex() { - populateManyStringEntries(); + populateManyStringEntriesForLex(); assertThat(setOfStrings.zrangebylex(key, Range.unbounded(), new ZRangeArgs().rev())).hasSize(100); assertThat(setOfStrings.zrangebylex(key, new Range<>("value", "zzz"), new ZRangeArgs().rev())).hasSize(100); assertThat(setOfStrings.zrangebylex(key, new Range<>("value98", true, "value99", true), @@ -748,7 +748,7 @@ void zscanMatch() { @Test void zlexcount() { - populateManyStringEntries(); + populateManyStringEntriesForLex(); assertThat(setOfStrings.zlexcount(key, new Range<>("-", "+"))).isEqualTo(100); assertThat(setOfStrings.zlexcount(key, new Range<>("value", "zzz"))).isEqualTo(100); @@ -854,7 +854,7 @@ public void bzmpopMax() { @Test @RequiresRedis6OrHigher void zrangebylex() { - populateManyStringEntries(); + populateManyStringEntriesForLex(); assertThat(setOfStrings.zrangebylex(key, new Range<>("-", "+"))).hasSize(100); assertThat(setOfStrings.zrangebylex(key, new Range<>("-", "+"), new ZRangeArgs().limit(10, 10))).hasSize(10); @@ -870,10 +870,10 @@ void zrangebylex() { @Test @RequiresRedis6OrHigher void zremrangebylex() { - populateManyStringEntries(); + populateManyStringEntriesForLex(); assertThat(setOfStrings.zremrangebylex(key, new Range<>("aaa", false, "zzz", true))).isEqualTo(100); - populateManyStringEntries(); + populateManyStringEntriesForLex(); assertThat(setOfStrings.zremrangebylex(key, new Range<>("aaa", "zzz"))).isEqualTo(100); } @@ -967,6 +967,13 @@ private void populateManyStringEntries() { } } + private void populateManyStringEntriesForLex() { + for (int i = 0; i < 100; i++) { + setOfStrings.zadd(key + 1, 1.0, value + i); + setOfStrings.zadd(key, 1.0, value + i); + } + } + @Test @RequiresRedis6OrHigher void sort() { diff --git a/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientBuildConfig.java b/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientBuildConfig.java new file mode 100644 index 0000000000000..8dac7273fe320 --- /dev/null +++ b/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientBuildConfig.java @@ -0,0 +1,25 @@ +package io.quarkus.restclient.config; + +import java.util.Optional; + +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; + +import io.quarkus.runtime.annotations.ConfigGroup; +import io.quarkus.runtime.annotations.ConfigItem; + +@ConfigGroup +public class RestClientBuildConfig { + + /** + * The CDI scope to use for injection. This property can contain either a fully qualified class name of a CDI scope + * annotation (such as "jakarta.enterprise.context.ApplicationScoped") or its simple name (such as + * "ApplicationScoped"). + * By default, this is not set which means the interface is not registered as a bean unless it is annotated with + * {@link RegisterRestClient}. + * If an interface is not annotated with {@link RegisterRestClient} and this property is set, then Quarkus will make the + * interface + * a bean of the configured scope. + */ + @ConfigItem + public Optional scope; +} diff --git a/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientConfig.java b/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientConfig.java index c838bb62dbaa8..00d050a52db7d 100644 --- a/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientConfig.java +++ b/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientConfig.java @@ -21,7 +21,6 @@ public class RestClientConfig { EMPTY = new RestClientConfig(); EMPTY.url = Optional.empty(); EMPTY.uri = Optional.empty(); - EMPTY.scope = Optional.empty(); EMPTY.providers = Optional.empty(); EMPTY.connectTimeout = Optional.empty(); EMPTY.readTimeout = Optional.empty(); @@ -70,14 +69,6 @@ public class RestClientConfig { @ConfigItem public Optional uri; - /** - * The CDI scope to use for injection. This property can contain either a fully qualified class name of a CDI scope - * annotation (such as "jakarta.enterprise.context.ApplicationScoped") or its simple name (such as - * "ApplicationScoped"). - */ - @ConfigItem - public Optional scope; - /** * Map where keys are fully-qualified provider classnames to include in the client, and values are their integer * priorities. The equivalent of the `@RegisterProvider` annotation. @@ -280,7 +271,6 @@ public static RestClientConfig load(String configKey) { instance.url = getConfigValue(configKey, "url", String.class); instance.uri = getConfigValue(configKey, "uri", String.class); - instance.scope = getConfigValue(configKey, "scope", String.class); instance.providers = getConfigValue(configKey, "providers", String.class); instance.connectTimeout = getConfigValue(configKey, "connect-timeout", Long.class); instance.readTimeout = getConfigValue(configKey, "read-timeout", Long.class); @@ -320,7 +310,6 @@ public static RestClientConfig load(Class interfaceClass) { instance.url = getConfigValue(interfaceClass, "url", String.class); instance.uri = getConfigValue(interfaceClass, "uri", String.class); - instance.scope = getConfigValue(interfaceClass, "scope", String.class); instance.providers = getConfigValue(interfaceClass, "providers", String.class); instance.connectTimeout = getConfigValue(interfaceClass, "connect-timeout", Long.class); instance.readTimeout = getConfigValue(interfaceClass, "read-timeout", Long.class); diff --git a/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientsBuildTimeConfig.java b/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientsBuildTimeConfig.java new file mode 100644 index 0000000000000..16ac5b0776c4e --- /dev/null +++ b/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientsBuildTimeConfig.java @@ -0,0 +1,18 @@ +package io.quarkus.restclient.config; + +import java.util.Collections; +import java.util.Map; + +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.annotations.ConfigRoot; + +@ConfigRoot(name = "rest-client", phase = ConfigPhase.BUILD_TIME) +public class RestClientsBuildTimeConfig { + + /** + * Configurations of REST client instances. + */ + @ConfigItem(name = ConfigItem.PARENT) + public Map configs = Collections.emptyMap(); +} diff --git a/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientsConfig.java b/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientsConfig.java index 51a7aa1a377de..fd79160d021c5 100644 --- a/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientsConfig.java +++ b/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientsConfig.java @@ -2,6 +2,7 @@ import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import jakarta.enterprise.inject.CreationException; @@ -326,6 +327,10 @@ public void putClientConfig(Class clientInterface, RestClientConfig clientCon configs.put(clientInterface.getName(), clientConfig); } + public Set getConfigKeys() { + return configs.keySet(); + } + public static RestClientsConfig getInstance() { InstanceHandle configHandle; try { diff --git a/extensions/resteasy-classic/rest-client-config/runtime/src/test/java/io/quarkus/restclient/config/RestClientConfigTest.java b/extensions/resteasy-classic/rest-client-config/runtime/src/test/java/io/quarkus/restclient/config/RestClientConfigTest.java index 051d4db37e3a1..03fd6335dd430 100644 --- a/extensions/resteasy-classic/rest-client-config/runtime/src/test/java/io/quarkus/restclient/config/RestClientConfigTest.java +++ b/extensions/resteasy-classic/rest-client-config/runtime/src/test/java/io/quarkus/restclient/config/RestClientConfigTest.java @@ -33,8 +33,6 @@ private void verifyConfig(RestClientConfig config) { assertThat(config.url.get()).isEqualTo("http://localhost:8080"); assertThat(config.uri).isPresent(); assertThat(config.uri.get()).isEqualTo("http://localhost:8081"); - assertThat(config.scope).isPresent(); - assertThat(config.scope.get()).isEqualTo("Singleton"); assertThat(config.providers).isPresent(); assertThat(config.providers.get()).isEqualTo("io.quarkus.restclient.configuration.MyResponseFilter"); assertThat(config.connectTimeout).isPresent(); diff --git a/extensions/resteasy-classic/rest-client-jackson/deployment/src/test/java/io/quarkus/restclient/jackson/deployment/ZonedDateTimeObjectMapperCustomizer.java b/extensions/resteasy-classic/rest-client-jackson/deployment/src/test/java/io/quarkus/restclient/jackson/deployment/ZonedDateTimeObjectMapperCustomizer.java index 950ecc5c60c96..a9ffa8bb49077 100644 --- a/extensions/resteasy-classic/rest-client-jackson/deployment/src/test/java/io/quarkus/restclient/jackson/deployment/ZonedDateTimeObjectMapperCustomizer.java +++ b/extensions/resteasy-classic/rest-client-jackson/deployment/src/test/java/io/quarkus/restclient/jackson/deployment/ZonedDateTimeObjectMapperCustomizer.java @@ -13,7 +13,7 @@ import com.fasterxml.jackson.databind.JsonDeserializer; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.datatype.jsr310.ser.ZonedDateTimeSerializer; import io.quarkus.jackson.ObjectMapperCustomizer; @@ -21,9 +21,14 @@ @Singleton public class ZonedDateTimeObjectMapperCustomizer implements ObjectMapperCustomizer { + @Override + public int priority() { + return MINIMUM_PRIORITY; + } + @Override public void customize(ObjectMapper objectMapper) { - JavaTimeModule customDateModule = new JavaTimeModule(); + SimpleModule customDateModule = new SimpleModule(); customDateModule.addSerializer(ZonedDateTime.class, new ZonedDateTimeSerializer( new DateTimeFormatterBuilder().appendInstant(0).toFormatter().withZone(ZoneId.of("Z")))); customDateModule.addDeserializer(ZonedDateTime.class, new ZonedDateTimeEuropeLondonDeserializer()); diff --git a/extensions/resteasy-classic/rest-client/deployment/src/test/java/io/quarkus/restclient/configuration/QuarkusConfigurationTest.java b/extensions/resteasy-classic/rest-client/deployment/src/test/java/io/quarkus/restclient/configuration/QuarkusConfigurationTest.java index 8927496bdc37a..a333943a45e00 100644 --- a/extensions/resteasy-classic/rest-client/deployment/src/test/java/io/quarkus/restclient/configuration/QuarkusConfigurationTest.java +++ b/extensions/resteasy-classic/rest-client/deployment/src/test/java/io/quarkus/restclient/configuration/QuarkusConfigurationTest.java @@ -55,8 +55,6 @@ void configurationsShouldBeLoaded() { void verifyClientConfig(RestClientConfig clientConfig, boolean verifyNonStandardProperties) { assertThat(clientConfig.url).isPresent(); assertThat(clientConfig.url.get()).contains("localhost"); - assertThat(clientConfig.scope).isPresent(); - assertThat(clientConfig.scope.get()).isEqualTo("Singleton"); assertThat(clientConfig.providers).isPresent(); assertThat(clientConfig.providers.get()) .isEqualTo("io.quarkus.restclient.configuration.MyResponseFilter"); diff --git a/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/RemoteUserHttpAccessLogTest.java b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/RemoteUserHttpAccessLogTest.java new file mode 100644 index 0000000000000..e1d331227436b --- /dev/null +++ b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/RemoteUserHttpAccessLogTest.java @@ -0,0 +1,112 @@ +package io.quarkus.resteasy.test.security; + +import static io.quarkus.vertx.http.runtime.attribute.RemoteUserAttribute.REMOTE_USER_SHORT; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.security.Principal; +import java.util.logging.Level; +import java.util.logging.LogRecord; +import java.util.stream.Collectors; + +import jakarta.ws.rs.container.ContainerRequestContext; +import jakarta.ws.rs.container.ContainerRequestFilter; +import jakarta.ws.rs.container.PreMatching; +import jakarta.ws.rs.core.SecurityContext; +import jakarta.ws.rs.ext.Provider; + +import org.hamcrest.Matchers; +import org.jboss.shrinkwrap.api.asset.StringAsset; +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 RemoteUserHttpAccessLogTest { + + @RegisterExtension + public static QuarkusUnitTest unitTest = new QuarkusUnitTest() + .withApplicationRoot(jar -> jar + .addClasses(RolesAllowedResource.class, SecurityOverrideFilter.class) + .addClasses(TestIdentityController.class, TestIdentityProvider.class) + .add(new StringAsset("quarkus.http.access-log.enabled=true\n" + + "quarkus.http.access-log.pattern=%h %t " + REMOTE_USER_SHORT), "application.properties")) + .setLogRecordPredicate(logRecord -> logRecord.getLevel().equals(Level.INFO) + && logRecord.getLoggerName().equals("io.quarkus.http.access-log")) + .assertLogRecords(logRecords -> { + var accessLogRecords = logRecords + .stream() + .map(LogRecord::getMessage) + .collect(Collectors.toList()); + assertTrue(accessLogRecords.stream().anyMatch(msg -> msg.endsWith("admin"))); + assertFalse(accessLogRecords.stream().anyMatch(msg -> msg.endsWith("user"))); + assertTrue(accessLogRecords.stream().anyMatch(msg -> msg.endsWith("Charlie"))); + }); + + @BeforeAll + public static void setupUsers() { + TestIdentityController.resetRoles() + .add("admin", "admin", "admin") + .add("user", "user", "user"); + } + + @Test + public void testAuthRemoteUserLogged() { + RestAssured + .given() + .auth().preemptive().basic("admin", "admin") + .get("/roles") + .then() + .statusCode(200) + .body(Matchers.is("default")); + RestAssured + .given() + .auth().preemptive().basic("user", "user") + .get("/roles") + .then() + .statusCode(200) + .body(Matchers.is("default")); + } + + @Provider + @PreMatching + public static class SecurityOverrideFilter implements ContainerRequestFilter { + @Override + public void filter(ContainerRequestContext requestContext) throws IOException { + if (requestContext.getSecurityContext().getUserPrincipal().getName().equals("user")) { + requestContext.setSecurityContext(new SecurityContext() { + @Override + public Principal getUserPrincipal() { + return new Principal() { + @Override + public String getName() { + return "Charlie"; + } + }; + } + + @Override + public boolean isUserInRole(String r) { + return "user".equals(r); + } + + @Override + public boolean isSecure() { + return false; + } + + @Override + public String getAuthenticationScheme() { + return "basic"; + } + }); + } + + } + } +} diff --git a/extensions/resteasy-classic/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/SecurityContextFilter.java b/extensions/resteasy-classic/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/SecurityContextFilter.java index 50b9e743f7c24..adebeceec2645 100644 --- a/extensions/resteasy-classic/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/SecurityContextFilter.java +++ b/extensions/resteasy-classic/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/SecurityContextFilter.java @@ -21,7 +21,9 @@ import io.quarkus.security.credential.Credential; import io.quarkus.security.identity.CurrentIdentityAssociation; import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.vertx.http.runtime.security.QuarkusHttpUser; import io.smallrye.mutiny.Uni; +import io.vertx.ext.web.RoutingContext; @PreMatching @Priority(Priorities.USER + 1) @@ -34,6 +36,9 @@ public class SecurityContextFilter implements ContainerRequestFilter { @Inject CurrentIdentityAssociation currentIdentityAssociation; + @Inject + RoutingContext routingContext; + @Override public void filter(ContainerRequestContext requestContext) throws IOException { SecurityContext modified = requestContext.getSecurityContext(); @@ -95,6 +100,7 @@ public Uni checkPermission(Permission permission) { return Uni.createFrom().nullItem(); } }; + routingContext.setUser(new QuarkusHttpUser(newIdentity)); currentIdentityAssociation.setIdentity(newIdentity); } } diff --git a/extensions/resteasy-reactive/jaxrs-client-reactive/deployment/src/main/java/io/quarkus/jaxrs/client/reactive/deployment/JaxrsClientReactiveProcessor.java b/extensions/resteasy-reactive/jaxrs-client-reactive/deployment/src/main/java/io/quarkus/jaxrs/client/reactive/deployment/JaxrsClientReactiveProcessor.java index b96c574255bfa..cea258ce2771c 100644 --- a/extensions/resteasy-reactive/jaxrs-client-reactive/deployment/src/main/java/io/quarkus/jaxrs/client/reactive/deployment/JaxrsClientReactiveProcessor.java +++ b/extensions/resteasy-reactive/jaxrs-client-reactive/deployment/src/main/java/io/quarkus/jaxrs/client/reactive/deployment/JaxrsClientReactiveProcessor.java @@ -47,7 +47,6 @@ import java.util.function.Predicate; import java.util.function.Supplier; import java.util.regex.Pattern; -import java.util.stream.Collectors; import jakarta.ws.rs.ProcessingException; import jakarta.ws.rs.RuntimeType; @@ -287,9 +286,8 @@ void setupClientProxies(JaxrsClientReactiveRecorder recorder, scannedParameterContainers.addAll(parameterContainersBuildItem.getClassNames()); } reflectiveClassBuildItemBuildProducer.produce(ReflectiveClassBuildItem - .builder(scannedParameterContainers.stream().map(name -> name.toString()).collect(Collectors.toSet()) - .toArray(new String[0])) - .fields().build()); + .builder(scannedParameterContainers.stream().map(DotName::toString).distinct().toArray(String[]::new)) + .methods().fields().build()); if (resourceScanningResultBuildItem.isEmpty() || resourceScanningResultBuildItem.get().getResult().getClientInterfaces().isEmpty()) { diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin-serialization-common/runtime/src/main/java/io/quarkus/resteasy/reactive/kotlin/serialization/common/runtime/JsonConfig.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin-serialization-common/runtime/src/main/java/io/quarkus/resteasy/reactive/kotlin/serialization/common/runtime/JsonConfig.java index df47f1c32e7c0..bc64347ff3d76 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin-serialization-common/runtime/src/main/java/io/quarkus/resteasy/reactive/kotlin/serialization/common/runtime/JsonConfig.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin-serialization-common/runtime/src/main/java/io/quarkus/resteasy/reactive/kotlin/serialization/common/runtime/JsonConfig.java @@ -129,6 +129,12 @@ public class JsonConfig { @ConfigItem(defaultValue = "false") public boolean decodeEnumsCaseInsensitive = false; + /** + * Specifies if trailing comma is allowed. + */ + @ConfigItem(defaultValue = "false") + public boolean allowTrailingComma = false; + @Override public String toString() { return new StringJoiner(", ", JsonConfig.class.getSimpleName() + "[", "]") @@ -144,6 +150,7 @@ public String toString() { .add("allowSpecialFloatingPointValues=" + allowSpecialFloatingPointValues) .add("useAlternativeNames=" + useAlternativeNames) .add("decodeEnumsCaseInsensitive=" + decodeEnumsCaseInsensitive) + .add("allowTrailingComma=" + allowTrailingComma) .toString(); } } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin-serialization-common/runtime/src/main/kotlin/io/quarkus/resteasy/reactive/kotlin/serialization/common/runtime/JsonProducer.kt b/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin-serialization-common/runtime/src/main/kotlin/io/quarkus/resteasy/reactive/kotlin/serialization/common/runtime/JsonProducer.kt index 45ea46cde67a8..89dc0289b1104 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin-serialization-common/runtime/src/main/kotlin/io/quarkus/resteasy/reactive/kotlin/serialization/common/runtime/JsonProducer.kt +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin-serialization-common/runtime/src/main/kotlin/io/quarkus/resteasy/reactive/kotlin/serialization/common/runtime/JsonProducer.kt @@ -38,6 +38,7 @@ class JsonProducer { useAlternativeNames = configuration.json.useAlternativeNames useArrayPolymorphism = configuration.json.useArrayPolymorphism decodeEnumsCaseInsensitive = configuration.json.decodeEnumsCaseInsensitive + allowTrailingComma = configuration.json.allowTrailingComma configuration.json.namingStrategy.ifPresent { strategy -> loadStrategy(this, strategy, this@JsonProducer) diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/providers/FileTestCase.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/providers/FileTestCase.java index 80f99ddf1105d..a7feae8824879 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/providers/FileTestCase.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/providers/FileTestCase.java @@ -45,17 +45,37 @@ public void testFiles() throws Exception { .then() .statusCode(206) .header(HttpHeaders.CONTENT_LENGTH, "10") + .header("Content-Range", "bytes 0-9/" + contentLength) .body(Matchers.equalTo(content.substring(0, 10))); RestAssured.given().header("Range", "bytes=10-19").get("/providers/file/file") .then() .statusCode(206) .header(HttpHeaders.CONTENT_LENGTH, "10") + .header("Content-Range", "bytes 10-19/" + contentLength) .body(Matchers.equalTo(content.substring(10, 20))); RestAssured.given().header("Range", "bytes=10-").get("/providers/file/file") .then() .statusCode(206) .header(HttpHeaders.CONTENT_LENGTH, String.valueOf(content.length() - 10)) + .header("Content-Range", "bytes 10-" + (content.length() - 1) + "/" + contentLength) .body(Matchers.equalTo(content.substring(10))); + RestAssured.given().header("Range", "bytes=-10").get("/providers/file/file") + .then() + .statusCode(206) + .header(HttpHeaders.CONTENT_LENGTH, "10") + .header("Content-Range", + "bytes " + (content.length() - 10) + "-" + (content.length() - 1) + "/" + contentLength) + .body(Matchers.equalTo(content.substring((content.length() - 10)))); + RestAssured.given().header("Range", "bytes=" + (content.length() + 1) + "-").get("/providers/file/file") + .then() + .statusCode(200) + .header(HttpHeaders.CONTENT_LENGTH, contentLength) + .body(Matchers.equalTo(content)); + RestAssured.given().header("Range", "bytes=0-1, 3-4").get("/providers/file/file") + .then() + .statusCode(200) + .header(HttpHeaders.CONTENT_LENGTH, contentLength) + .body(Matchers.equalTo(content)); RestAssured.get("/providers/file/file-partial") .then() .statusCode(200) diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/RemoteUserHttpAccessLogTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/RemoteUserHttpAccessLogTest.java new file mode 100644 index 0000000000000..4651072c3474c --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/RemoteUserHttpAccessLogTest.java @@ -0,0 +1,112 @@ +package io.quarkus.resteasy.reactive.server.test.security; + +import static io.quarkus.vertx.http.runtime.attribute.RemoteUserAttribute.REMOTE_USER_SHORT; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.security.Principal; +import java.util.logging.Level; +import java.util.logging.LogRecord; +import java.util.stream.Collectors; + +import jakarta.ws.rs.container.ContainerRequestContext; +import jakarta.ws.rs.container.ContainerRequestFilter; +import jakarta.ws.rs.container.PreMatching; +import jakarta.ws.rs.core.SecurityContext; +import jakarta.ws.rs.ext.Provider; + +import org.hamcrest.Matchers; +import org.jboss.shrinkwrap.api.asset.StringAsset; +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 RemoteUserHttpAccessLogTest { + + @RegisterExtension + public static QuarkusUnitTest unitTest = new QuarkusUnitTest() + .withApplicationRoot(jar -> jar + .addClasses(RolesAllowedResource.class, SecurityOverrideFilter.class) + .addClasses(TestIdentityController.class, TestIdentityProvider.class) + .add(new StringAsset("quarkus.http.access-log.enabled=true\n" + + "quarkus.http.access-log.pattern=%h %t " + REMOTE_USER_SHORT), "application.properties")) + .setLogRecordPredicate(logRecord -> logRecord.getLevel().equals(Level.INFO) + && logRecord.getLoggerName().equals("io.quarkus.http.access-log")) + .assertLogRecords(logRecords -> { + var accessLogRecords = logRecords + .stream() + .map(LogRecord::getMessage) + .collect(Collectors.toList()); + assertTrue(accessLogRecords.stream().anyMatch(msg -> msg.endsWith("admin"))); + assertFalse(accessLogRecords.stream().anyMatch(msg -> msg.endsWith("user"))); + assertTrue(accessLogRecords.stream().anyMatch(msg -> msg.endsWith("Charlie"))); + }); + + @BeforeAll + public static void setupUsers() { + TestIdentityController.resetRoles() + .add("admin", "admin", "admin") + .add("user", "user", "user"); + } + + @Test + public void testAuthRemoteUserLogged() { + RestAssured + .given() + .auth().preemptive().basic("admin", "admin") + .get("/roles") + .then() + .statusCode(200) + .body(Matchers.is("default")); + RestAssured + .given() + .auth().preemptive().basic("user", "user") + .get("/roles") + .then() + .statusCode(200) + .body(Matchers.is("default")); + } + + @Provider + @PreMatching + public static class SecurityOverrideFilter implements ContainerRequestFilter { + @Override + public void filter(ContainerRequestContext requestContext) throws IOException { + if (requestContext.getSecurityContext().getUserPrincipal().getName().equals("user")) { + requestContext.setSecurityContext(new SecurityContext() { + @Override + public Principal getUserPrincipal() { + return new Principal() { + @Override + public String getName() { + return "Charlie"; + } + }; + } + + @Override + public boolean isUserInRole(String r) { + return "user".equals(r); + } + + @Override + public boolean isSecure() { + return false; + } + + @Override + public String getAuthenticationScheme() { + return "basic"; + } + }); + } + + } + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/security/SecurityContextOverrideHandler.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/security/SecurityContextOverrideHandler.java index 64e04f1eb5c92..c526bc1b7846f 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/security/SecurityContextOverrideHandler.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/security/SecurityContextOverrideHandler.java @@ -22,7 +22,9 @@ import io.quarkus.security.credential.Credential; import io.quarkus.security.identity.CurrentIdentityAssociation; import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.vertx.http.runtime.security.QuarkusHttpUser; import io.smallrye.mutiny.Uni; +import io.vertx.ext.web.RoutingContext; public class SecurityContextOverrideHandler implements ServerRestHandler { @@ -46,6 +48,7 @@ private void updateIdentity(ResteasyReactiveRequestContext requestContext, Secur requestContext.requireCDIRequestScope(); InjectableInstance instance = getCurrentIdentityAssociation(); if (instance.isResolvable()) { + RoutingContext routingContext = requestContext.unwrap(RoutingContext.class); CurrentIdentityAssociation currentIdentityAssociation = instance.get(); Uni oldIdentity = currentIdentityAssociation.getDeferredIdentity(); currentIdentityAssociation.setIdentity(oldIdentity.map(new Function() { @@ -53,7 +56,7 @@ private void updateIdentity(ResteasyReactiveRequestContext requestContext, Secur public SecurityIdentity apply(SecurityIdentity old) { Set oldCredentials = old.getCredentials(); Map oldAttributes = old.getAttributes(); - return new SecurityIdentity() { + SecurityIdentity newIdentity = new SecurityIdentity() { @Override public Principal getPrincipal() { return modified.getUserPrincipal(); @@ -107,6 +110,10 @@ public Uni checkPermission(Permission permission) { return Uni.createFrom().nullItem(); } }; + if (routingContext != null) { + routingContext.setUser(new QuarkusHttpUser(newIdentity)); + } + return newIdentity; } })); } 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 22a4b76f9b69e..839f1de001b1b 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 @@ -9,6 +9,8 @@ import static io.quarkus.rest.client.reactive.deployment.DotNames.CLIENT_QUERY_PARAM; import static io.quarkus.rest.client.reactive.deployment.DotNames.CLIENT_QUERY_PARAMS; import static io.quarkus.rest.client.reactive.deployment.DotNames.CLIENT_REDIRECT_HANDLER; +import static io.quarkus.rest.client.reactive.deployment.DotNames.CLIENT_REQUEST_FILTER; +import static io.quarkus.rest.client.reactive.deployment.DotNames.CLIENT_RESPONSE_FILTER; import static io.quarkus.rest.client.reactive.deployment.DotNames.REGISTER_CLIENT_HEADERS; import static io.quarkus.rest.client.reactive.deployment.DotNames.REGISTER_PROVIDER; import static io.quarkus.rest.client.reactive.deployment.DotNames.REGISTER_PROVIDERS; @@ -32,6 +34,7 @@ import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.stream.Collectors; import jakarta.enterprise.context.SessionScoped; import jakarta.enterprise.inject.Typed; @@ -101,6 +104,7 @@ import io.quarkus.rest.client.reactive.runtime.RestClientReactiveConfig; import io.quarkus.rest.client.reactive.runtime.RestClientRecorder; import io.quarkus.rest.client.reactive.spi.RestClientAnnotationsTransformerBuildItem; +import io.quarkus.restclient.config.RestClientsBuildTimeConfig; import io.quarkus.restclient.config.RestClientsConfig; import io.quarkus.restclient.config.deployment.RestClientConfigUtils; import io.quarkus.resteasy.reactive.spi.ContainerRequestFilterBuildItem; @@ -297,18 +301,10 @@ void registerProvidersFromAnnotations(CombinedIndexBuildItem indexBuildItem, } } - List providerInterfaceNames = providerClass.interfaceNames(); - // don't register server specific types - if (providerInterfaceNames.contains(ResteasyReactiveDotNames.CONTAINER_REQUEST_FILTER) - || providerInterfaceNames.contains(ResteasyReactiveDotNames.CONTAINER_RESPONSE_FILTER) - || providerInterfaceNames.contains(ResteasyReactiveDotNames.EXCEPTION_MAPPER)) { + if (skipAutoDiscoveredProvider(providerClass.interfaceNames())) { continue; } - 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); @@ -416,10 +412,13 @@ void addRestClientBeans(Capabilities capabilities, List restClientAnnotationsTransformerBuildItem, BuildProducer generatedBeans, RestClientReactiveConfig clientConfig, + RestClientsBuildTimeConfig clientsBuildConfig, RestClientRecorder recorder) { CompositeIndex index = CompositeIndex.create(combinedIndexBuildItem.getIndex()); - Set registerRestClientAnnos = new HashSet<>(index.getAnnotations(REGISTER_REST_CLIENT)); + + Set registerRestClientAnnos = determineRegisterRestClientInstances(clientsBuildConfig, index); + Map configKeys = new HashMap<>(); var annotationsStore = new AnnotationStore(restClientAnnotationsTransformerBuildItem.stream() .map(RestClientAnnotationsTransformerBuildItem::getAnnotationsTransformer).collect(toList())); @@ -580,6 +579,57 @@ && isImplementorOf(index, target.asClass(), RESPONSE_EXCEPTION_MAPPER, Set.of(AP } } + private Set determineRegisterRestClientInstances(RestClientsBuildTimeConfig clientsConfig, + CompositeIndex index) { + // these are the actual instances + Set registerRestClientAnnos = new HashSet<>(index.getAnnotations(REGISTER_REST_CLIENT)); + // a set of the original target class + Set registerRestClientTargets = registerRestClientAnnos.stream().map(ai -> ai.target().asClass()).collect( + Collectors.toSet()); + + // now we go through the keys and if any of them correspond to classes that don't have a @RegisterRestClient annotation, we fake that annotation + Set configKeyNames = clientsConfig.configs.keySet(); + for (String configKeyName : configKeyNames) { + ClassInfo classInfo = index.getClassByName(configKeyName); + if (classInfo == null) { + continue; + } + if (registerRestClientTargets.contains(classInfo)) { + continue; + } + Optional cdiScope = clientsConfig.configs.get(configKeyName).scope; + if (cdiScope.isEmpty()) { + continue; + } + registerRestClientAnnos.add(AnnotationInstance.builder(REGISTER_REST_CLIENT).add("configKey", configKeyName) + .buildWithTarget(classInfo)); + } + return registerRestClientAnnos; + } + + /** + * Based on a list of interfaces implemented by @Provider class, determine if registration + * should be skipped or not. Server-specific types should be omitted unless implementation + * of a ClientRequestFilter exists on the same class explicitly. + * Features should always be omitted. + */ + private boolean skipAutoDiscoveredProvider(List providerInterfaceNames) { + if (providerInterfaceNames.contains(ResteasyReactiveDotNames.FEATURE)) { + return true; + } + if (providerInterfaceNames.contains(ResteasyReactiveDotNames.CONTAINER_REQUEST_FILTER) + || providerInterfaceNames.contains(ResteasyReactiveDotNames.CONTAINER_RESPONSE_FILTER) + || providerInterfaceNames.contains(ResteasyReactiveDotNames.EXCEPTION_MAPPER)) { + if (providerInterfaceNames.contains(CLIENT_REQUEST_FILTER) + || providerInterfaceNames.contains(CLIENT_RESPONSE_FILTER)) { + return false; + } else { + return true; + } + } + return false; + } + private Map populateClientExceptionMapperFromAnnotations( BuildProducer generatedClasses, BuildProducer reflectiveClasses, IndexView index) { diff --git a/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/BeanFromConfigTest.java b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/BeanFromConfigTest.java new file mode 100644 index 0000000000000..4fadb3d3ae01e --- /dev/null +++ b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/BeanFromConfigTest.java @@ -0,0 +1,59 @@ +package io.quarkus.rest.client.reactive; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Set; + +import jakarta.enterprise.context.Dependent; +import jakarta.enterprise.inject.spi.Bean; +import jakarta.enterprise.inject.spi.BeanManager; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +import org.eclipse.microprofile.rest.client.inject.RestClient; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.arc.Arc; +import io.quarkus.test.QuarkusUnitTest; + +public class BeanFromConfigTest { + + @RegisterExtension + static final QuarkusUnitTest TEST = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(HelloClient.class, HelloResource.class)) + .overrideConfigKey("quarkus.rest-client.\"io.quarkus.rest.client.reactive.HelloClient\".scope", "Dependent") + .overrideRuntimeConfigKey("quarkus.rest-client.\"io.quarkus.rest.client.reactive.HelloClient\".url", + "http://localhost:${quarkus.http.test-port:8081}"); + + @RestClient + HelloClient client; + + @Test + void shouldHello() { + assertThat(client.echo("w0rld")).isEqualTo("hello, w0rld"); + } + + @Test + void shouldHaveDependentScope() { + BeanManager beanManager = Arc.container().beanManager(); + Set> beans = beanManager.getBeans(HelloClient.class, RestClient.LITERAL); + Bean resolvedBean = beanManager.resolve(beans); + assertThat(resolvedBean.getScope()).isEqualTo(Dependent.class); + } + + @Path("/hello") + @Produces(MediaType.TEXT_PLAIN) + @Consumes(MediaType.TEXT_PLAIN) + public static class HelloResource { + + @POST + public String echo(String name) { + return "hello, " + name; + } + } +} diff --git a/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/ConfigurationTest.java b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/ConfigurationTest.java index 35c8f6e0f8c23..eb6011ebd3ff0 100644 --- a/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/ConfigurationTest.java +++ b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/ConfigurationTest.java @@ -74,8 +74,6 @@ void emptyPathAnnotationShouldWork() { private void verifyClientConfig(RestClientConfig clientConfig, boolean checkExtraProperties) { assertThat(clientConfig.url).isPresent(); assertThat(clientConfig.url.get()).endsWith("/hello"); - assertThat(clientConfig.scope).isPresent(); - assertThat(clientConfig.scope.get()).isEqualTo("Singleton"); assertThat(clientConfig.providers).isPresent(); assertThat(clientConfig.providers.get()) .isEqualTo("io.quarkus.rest.client.reactive.HelloClientWithBaseUri$MyResponseFilter"); diff --git a/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/multipart/MultipartEncoderModeTest.java b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/multipart/MultipartEncoderModeTest.java new file mode 100644 index 0000000000000..62f18083af66f --- /dev/null +++ b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/multipart/MultipartEncoderModeTest.java @@ -0,0 +1,94 @@ +package io.quarkus.rest.client.reactive.multipart; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.File; +import java.io.IOException; +import java.net.URI; + +import jakarta.inject.Inject; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.FormParam; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.client.ClientRequestContext; +import jakarta.ws.rs.client.ClientRequestFilter; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +import org.eclipse.microprofile.rest.client.annotation.RegisterProvider; +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; +import org.eclipse.microprofile.rest.client.inject.RestClient; +import org.jboss.resteasy.reactive.ResponseStatus; +import org.jboss.resteasy.reactive.RestForm; +import org.jboss.resteasy.reactive.client.api.QuarkusRestClientProperties; +import org.jboss.resteasy.reactive.client.impl.multipart.PausableHttpPostRequestEncoder; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.api.io.TempDir; + +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.test.common.http.TestHTTPResource; + +public class MultipartEncoderModeTest { + @TestHTTPResource + URI baseUri; + + @RegisterExtension + static final QuarkusUnitTest TEST = new QuarkusUnitTest() + .withApplicationRoot(jar -> jar.addClasses(Resource.class, Client.class)) + .withConfigurationResource("multipart-encoder-mode-test.properties"); + + @Inject + @RestClient + Client client; + + @Test + void shouldPassUsingCustomMultipartPostEncoderMode(@TempDir File tempDir) throws IOException { + File file = File.createTempFile("MultipartTest", ".txt", tempDir); + assertThat(client.upload(file)).isEqualTo("OK"); + } + + /** + * This filter is present to check in advance if property {@link QuarkusRestClientProperties#MULTIPART_ENCODER_MODE} + * is of the right type. + */ + static class MultipartEncoderModeCheck implements ClientRequestFilter { + @Override + public void filter(ClientRequestContext requestContext) throws IOException { + Object mode = requestContext.getConfiguration().getProperty(QuarkusRestClientProperties.MULTIPART_ENCODER_MODE); + if (mode == null) { + requestContext.abortWith(Response.serverError().entity("encoderMode is null").build()); + return; + } + if (mode.getClass() != PausableHttpPostRequestEncoder.EncoderMode.class) { + requestContext.abortWith(Response.serverError().entity("encoderMode illegal type").build()); + return; + } + } + } + + @Path("resource") + static public class Resource { + @Path("upload") + @POST + @Consumes(MediaType.MULTIPART_FORM_DATA) + @Produces(MediaType.TEXT_PLAIN) + @ResponseStatus(200) + public String upload(@RestForm File file) { + return "OK"; + } + } + + @Path("resource") + @RegisterRestClient(configKey = "client") + @RegisterProvider(MultipartEncoderModeCheck.class) + static public interface Client { + @Path("upload") + @POST + @Consumes(MediaType.MULTIPART_FORM_DATA) + @Produces(MediaType.TEXT_PLAIN) + String upload(@FormParam("file") File file); + } +} diff --git a/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/resources/multipart-encoder-mode-test.properties b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/resources/multipart-encoder-mode-test.properties new file mode 100644 index 0000000000000..8b0eef3890109 --- /dev/null +++ b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/resources/multipart-encoder-mode-test.properties @@ -0,0 +1,2 @@ +quarkus.rest-client.multipart-post-encoder-mode=HTML5 +quarkus.rest-client.client.url=http://localhost:${quarkus.http.test-port} \ No newline at end of file diff --git a/extensions/resteasy-reactive/rest-client-reactive/runtime/pom.xml b/extensions/resteasy-reactive/rest-client-reactive/runtime/pom.xml index f92fe41fad669..46b69aeb98394 100644 --- a/extensions/resteasy-reactive/rest-client-reactive/runtime/pom.xml +++ b/extensions/resteasy-reactive/rest-client-reactive/runtime/pom.xml @@ -106,8 +106,6 @@ ${project.version} - 11 - 11 diff --git a/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientBuilderImpl.java b/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientBuilderImpl.java index 657849440d32e..9e2bb6ace28be 100644 --- a/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientBuilderImpl.java +++ b/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientBuilderImpl.java @@ -319,7 +319,8 @@ public T build(Class aClass) throws IllegalStateException, RestClientDefi ArcContainer arcContainer = Arc.container(); if (arcContainer == null) { - throw new IllegalStateException("The Reactive REST Client is not meant to be used outside of Quarkus"); + throw new IllegalStateException( + "The Reactive REST Client needs to be built within the context of a Quarkus application with a valid ArC (CDI) context running."); } RestClientListeners.get().forEach(listener -> listener.onNewClient(aClass, this)); diff --git a/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientCDIDelegateBuilder.java b/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientCDIDelegateBuilder.java index e6966a2ffae71..e10be163fb08b 100644 --- a/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientCDIDelegateBuilder.java +++ b/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientCDIDelegateBuilder.java @@ -23,8 +23,8 @@ import org.eclipse.microprofile.rest.client.ext.QueryParamStyle; import org.jboss.resteasy.reactive.client.api.QuarkusRestClientProperties; +import org.jboss.resteasy.reactive.client.impl.multipart.PausableHttpPostRequestEncoder; -import io.netty.handler.codec.http.multipart.HttpPostRequestEncoder; import io.quarkus.rest.client.reactive.QuarkusRestClientBuilder; import io.quarkus.restclient.config.RestClientConfig; import io.quarkus.restclient.config.RestClientsConfig; @@ -77,7 +77,7 @@ void configureBuilder(QuarkusRestClientBuilder builder) { private void configureCustomProperties(QuarkusRestClientBuilder builder) { Optional encoder = configRoot.multipartPostEncoderMode; if (encoder != null && encoder.isPresent()) { - HttpPostRequestEncoder.EncoderMode mode = HttpPostRequestEncoder.EncoderMode + PausableHttpPostRequestEncoder.EncoderMode mode = PausableHttpPostRequestEncoder.EncoderMode .valueOf(encoder.get().toUpperCase(Locale.ROOT)); builder.property(QuarkusRestClientProperties.MULTIPART_ENCODER_MODE, mode); } diff --git a/extensions/resteasy-reactive/rest-client-reactive/runtime/src/test/java/io/quarkus/rest/client/reactive/runtime/RestClientCDIDelegateBuilderTest.java b/extensions/resteasy-reactive/rest-client-reactive/runtime/src/test/java/io/quarkus/rest/client/reactive/runtime/RestClientCDIDelegateBuilderTest.java index 10948c051213d..6f72c3b6ded94 100644 --- a/extensions/resteasy-reactive/rest-client-reactive/runtime/src/test/java/io/quarkus/rest/client/reactive/runtime/RestClientCDIDelegateBuilderTest.java +++ b/extensions/resteasy-reactive/rest-client-reactive/runtime/src/test/java/io/quarkus/rest/client/reactive/runtime/RestClientCDIDelegateBuilderTest.java @@ -20,12 +20,12 @@ import org.eclipse.microprofile.rest.client.ext.QueryParamStyle; import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; import org.jboss.resteasy.reactive.client.api.QuarkusRestClientProperties; +import org.jboss.resteasy.reactive.client.impl.multipart.PausableHttpPostRequestEncoder; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.mockito.Mockito; -import io.netty.handler.codec.http.multipart.HttpPostRequestEncoder; import io.quarkus.restclient.config.RestClientConfig; import io.quarkus.restclient.config.RestClientMultipartConfig; import io.quarkus.restclient.config.RestClientsConfig; @@ -99,7 +99,7 @@ public void testClientSpecificConfigs() { Mockito.verify(restClientBuilderMock).property(QuarkusRestClientProperties.SHARED, true); Mockito.verify(restClientBuilderMock).property(QuarkusRestClientProperties.NAME, "my-client"); Mockito.verify(restClientBuilderMock).property(QuarkusRestClientProperties.MULTIPART_ENCODER_MODE, - HttpPostRequestEncoder.EncoderMode.HTML5); + PausableHttpPostRequestEncoder.EncoderMode.HTML5); Mockito.verify(restClientBuilderMock).proxyAddress("host1", 123); Mockito.verify(restClientBuilderMock).proxyUser("proxyUser1"); @@ -141,7 +141,7 @@ public void testGlobalConfigs() { Mockito.verify(restClientBuilderMock).baseUri(URI.create("http://localhost:8080")); Mockito.verify(restClientBuilderMock) - .property(QuarkusRestClientProperties.MULTIPART_ENCODER_MODE, HttpPostRequestEncoder.EncoderMode.HTML5); + .property(QuarkusRestClientProperties.MULTIPART_ENCODER_MODE, PausableHttpPostRequestEncoder.EncoderMode.HTML5); Mockito.verify(restClientBuilderMock).property(QuarkusRestClientProperties.DISABLE_CONTEXTUAL_ERROR_MESSAGES, true); Mockito.verify(restClientBuilderMock).proxyAddress("host2", 123); diff --git a/extensions/scheduler/api/src/main/java/io/quarkus/scheduler/Scheduled.java b/extensions/scheduler/api/src/main/java/io/quarkus/scheduler/Scheduled.java index 2fe195219ad13..6a1e12c82e70e 100644 --- a/extensions/scheduler/api/src/main/java/io/quarkus/scheduler/Scheduled.java +++ b/extensions/scheduler/api/src/main/java/io/quarkus/scheduler/Scheduled.java @@ -88,12 +88,15 @@ String cron() default ""; /** - * Defines a period between invocations. + * Defines the period between invocations. *

    * The value is parsed with {@link Duration#parse(CharSequence)}. However, if an expression starts with a digit, "PT" prefix * is added automatically, so for example, {@code 15m} can be used instead of {@code PT15M} and is parsed as "15 minutes". * Note that the absolute value of the value is always used. *

    + * A value less than one second may not be supported by the underlying scheduler implementation. In that case a warning + * message is logged during build and application start. + *

    * The value can be a property expression. In this case, the scheduler attempts to use the configured value instead: * {@code @Scheduled(every = "${myJob.everyExpression}")}. * Additionally, the property expression can specify a default value: {@code @Scheduled(every = diff --git a/extensions/scheduler/api/src/main/java/io/quarkus/scheduler/ScheduledExecution.java b/extensions/scheduler/api/src/main/java/io/quarkus/scheduler/ScheduledExecution.java index 4dd7d401f5f94..5f69240af667c 100644 --- a/extensions/scheduler/api/src/main/java/io/quarkus/scheduler/ScheduledExecution.java +++ b/extensions/scheduler/api/src/main/java/io/quarkus/scheduler/ScheduledExecution.java @@ -3,19 +3,20 @@ import java.time.Instant; /** - * Scheduled execution metadata. + * Execution metadata of a specific scheduled job. */ public interface ScheduledExecution { /** * - * @return the trigger that fired the event + * @return the trigger that fired the execution */ Trigger getTrigger(); /** + * Unlike {@link Trigger#getPreviousFireTime()} this method always returns the same value. * - * @return the time the event was fired + * @return the time the associated trigger was fired */ Instant getFireTime(); diff --git a/extensions/scheduler/api/src/main/java/io/quarkus/scheduler/Scheduler.java b/extensions/scheduler/api/src/main/java/io/quarkus/scheduler/Scheduler.java index 86a506b94d108..a6f2fffde045d 100644 --- a/extensions/scheduler/api/src/main/java/io/quarkus/scheduler/Scheduler.java +++ b/extensions/scheduler/api/src/main/java/io/quarkus/scheduler/Scheduler.java @@ -111,6 +111,9 @@ interface JobDefinition { * The schedule is defined either by {@link #setCron(String)} or by {@link #setInterval(String)}. If both methods are * used, then the cron expression takes precedence. *

    + * A value less than one second may not be supported by the underlying scheduler implementation. In that case a warning + * message is logged immediately. + *

    * {@link Scheduled#every()} * * @param every diff --git a/extensions/scheduler/api/src/main/java/io/quarkus/scheduler/Trigger.java b/extensions/scheduler/api/src/main/java/io/quarkus/scheduler/Trigger.java index f8d2037b40e2f..c076e5712bc0e 100644 --- a/extensions/scheduler/api/src/main/java/io/quarkus/scheduler/Trigger.java +++ b/extensions/scheduler/api/src/main/java/io/quarkus/scheduler/Trigger.java @@ -3,7 +3,10 @@ import java.time.Instant; /** - * Trigger is bound to a scheduled task. + * Trigger is bound to a scheduled job. + *

    + * It represents the logic that is used to test if a scheduled job should be executed + * at a specific time, i.e. the trigger is "fired". * * @see Scheduled */ @@ -11,8 +14,9 @@ public interface Trigger { /** * - * @return the identifier + * @return the identifier of the job * @see Scheduled#identity() + * @see Scheduler#newJob(String) */ String getId(); diff --git a/extensions/scheduler/common/pom.xml b/extensions/scheduler/common/pom.xml index 7da88cfc94fe6..126368f70c885 100644 --- a/extensions/scheduler/common/pom.xml +++ b/extensions/scheduler/common/pom.xml @@ -17,6 +17,14 @@ io.quarkus quarkus-scheduler-api + + io.quarkus + quarkus-scheduler-spi + + + io.opentelemetry.instrumentation + opentelemetry-instrumentation-api + com.cronutils cron-utils diff --git a/extensions/scheduler/common/src/main/java/io/quarkus/scheduler/common/runtime/InstrumentedInvoker.java b/extensions/scheduler/common/src/main/java/io/quarkus/scheduler/common/runtime/InstrumentedInvoker.java new file mode 100644 index 0000000000000..c0e330001f436 --- /dev/null +++ b/extensions/scheduler/common/src/main/java/io/quarkus/scheduler/common/runtime/InstrumentedInvoker.java @@ -0,0 +1,43 @@ +package io.quarkus.scheduler.common.runtime; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; + +import io.quarkus.scheduler.ScheduledExecution; +import io.quarkus.scheduler.spi.JobInstrumenter; +import io.quarkus.scheduler.spi.JobInstrumenter.JobInstrumentationContext; + +/** + * + * @see JobInstrumenter + */ +public class InstrumentedInvoker extends DelegateInvoker { + + private final JobInstrumenter instrumenter; + + public InstrumentedInvoker(ScheduledInvoker delegate, JobInstrumenter instrumenter) { + super(delegate); + this.instrumenter = instrumenter; + } + + @Override + public CompletionStage invoke(ScheduledExecution execution) throws Exception { + return instrumenter.instrument(new JobInstrumentationContext() { + + @Override + public CompletionStage executeJob() { + try { + return delegate.invoke(execution); + } catch (Exception e) { + return CompletableFuture.failedFuture(e); + } + } + + @Override + public String getSpanName() { + return execution.getTrigger().getId(); + } + }); + } + +} diff --git a/extensions/scheduler/deployment/pom.xml b/extensions/scheduler/deployment/pom.xml index 8271a16616d4a..8082201f010c5 100644 --- a/extensions/scheduler/deployment/pom.xml +++ b/extensions/scheduler/deployment/pom.xml @@ -50,6 +50,11 @@ quarkus-junit5-internal test + + org.assertj + assertj-core + test + io.rest-assured rest-assured diff --git a/extensions/scheduler/deployment/src/main/java/io/quarkus/scheduler/deployment/SchedulerProcessor.java b/extensions/scheduler/deployment/src/main/java/io/quarkus/scheduler/deployment/SchedulerProcessor.java index 60196c55e4190..3b3e2e5f03b3e 100644 --- a/extensions/scheduler/deployment/src/main/java/io/quarkus/scheduler/deployment/SchedulerProcessor.java +++ b/extensions/scheduler/deployment/src/main/java/io/quarkus/scheduler/deployment/SchedulerProcessor.java @@ -98,8 +98,9 @@ public class SchedulerProcessor { @BuildStep void beans(Capabilities capabilities, BuildProducer additionalBeans) { + additionalBeans.produce(new AdditionalBeanBuildItem(Scheduled.ApplicationNotRunning.class)); if (capabilities.isMissing(Capability.QUARTZ)) { - additionalBeans.produce(new AdditionalBeanBuildItem(SimpleScheduler.class, Scheduled.ApplicationNotRunning.class)); + additionalBeans.produce(new AdditionalBeanBuildItem(SimpleScheduler.class)); } } @@ -186,7 +187,8 @@ private void collectScheduledMethods(IndexView index, TransformedAnnotationsBuil @BuildStep void validateScheduledBusinessMethods(SchedulerConfig config, List scheduledMethods, - ValidationPhaseBuildItem validationPhase, BuildProducer validationErrors) { + ValidationPhaseBuildItem validationPhase, BuildProducer validationErrors, + Capabilities capabilities) { List errors = new ArrayList<>(); Map encounteredIdentities = new HashMap<>(); Set methodDescriptions = new HashSet<>(); @@ -239,9 +241,11 @@ void validateScheduledBusinessMethods(SchedulerConfig config, List annotationsTransformer) { - - if (config.tracingEnabled && capabilities.isPresent(Capability.OPENTELEMETRY_TRACER)) { - DotName withSpan = DotName.createSimple("io.opentelemetry.instrumentation.annotations.WithSpan"); - DotName legacyWithSpan = DotName.createSimple("io.opentelemetry.extension.annotations.WithSpan"); - - annotationsTransformer.produce(new AnnotationsTransformerBuildItem(AnnotationsTransformer.builder() - .appliesTo(METHOD) - .whenContainsAny(List.of(SchedulerDotNames.SCHEDULED_NAME, SchedulerDotNames.SCHEDULES_NAME)) - .whenContainsNone(List.of(withSpan, legacyWithSpan)) - .transform(context -> { - MethodInfo scheduledMethod = context.getTarget().asMethod(); - context.transform() - .add(withSpan) - .done(); - LOGGER.debugf("Added OpenTelemetry @WithSpan to a @Scheduled method %s#%s()", - scheduledMethod.declaringClass().name(), - scheduledMethod.name()); - }))); - } - } - private String generateInvoker(ScheduledBusinessMethodItem scheduledMethod, ClassOutput classOutput) { BeanInfo bean = scheduledMethod.getBean(); @@ -532,7 +512,7 @@ private String generateInvoker(ScheduledBusinessMethodItem scheduledMethod, Clas private Throwable validateScheduled(CronParser parser, AnnotationInstance schedule, Map encounteredIdentities, - BeanDeploymentValidator.ValidationContext validationContext) { + BeanDeploymentValidator.ValidationContext validationContext, long checkPeriod) { MethodInfo method = schedule.target().asMethod(); AnnotationValue cronValue = schedule.value("cron"); AnnotationValue everyValue = schedule.value("every"); @@ -542,7 +522,7 @@ private Throwable validateScheduled(CronParser parser, AnnotationInstance schedu try { parser.parse(cron).validate(); } catch (IllegalArgumentException e) { - return new IllegalStateException("Invalid cron() expression on: " + schedule, e); + return new IllegalStateException(errorMessage("Invalid cron() expression", schedule, method), e); } if (everyValue != null && !everyValue.asString().trim().isEmpty()) { LOGGER.warnf( @@ -558,7 +538,7 @@ private Throwable validateScheduled(CronParser parser, AnnotationInstance schedu try { ZoneId.of(timeZone); } catch (Exception e) { - return new IllegalStateException("Invalid timeZone() on " + schedule, e); + return new IllegalStateException(errorMessage("Invalid timeZone()", schedule, method), e); } } } @@ -571,9 +551,14 @@ private Throwable validateScheduled(CronParser parser, AnnotationInstance schedu every = "PT" + every; } try { - Duration.parse(every); + Duration period = Duration.parse(every); + if (period.toMillis() < checkPeriod) { + LOGGER.warnf( + "An every() value less than %s ms is not supported - the scheduled job will be executed with a delay: %s declared on %s#%s()", + checkPeriod, schedule, method.declaringClass().name(), method.name()); + } } catch (Exception e) { - return new IllegalStateException("Invalid every() expression on: " + schedule, e); + return new IllegalStateException(errorMessage("Invalid every() expression", schedule, method), e); } } } else { @@ -592,7 +577,7 @@ private Throwable validateScheduled(CronParser parser, AnnotationInstance schedu try { Duration.parse(delayed); } catch (Exception e) { - return new IllegalStateException("Invalid delayed() expression on: " + schedule, e); + return new IllegalStateException(errorMessage("Invalid delayed() expression", schedule, method), e); } } @@ -632,6 +617,10 @@ private Throwable validateScheduled(CronParser parser, AnnotationInstance schedu return null; } + private static String errorMessage(String base, AnnotationInstance scheduled, MethodInfo method) { + return String.format("%s: %s declared on %s#%s()", base, scheduled, method.declaringClass().name(), method.name()); + } + @BuildStep UnremovableBeanBuildItem unremoveableSkipPredicates() { return new UnremovableBeanBuildItem(new UnremovableBeanBuildItem.BeanTypeExclusion(SchedulerDotNames.SKIP_PREDICATE)); diff --git a/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/ApplicationNotRunningPredicateTest.java b/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/ApplicationNotRunningPredicateTest.java index 6318d8ecb0d31..c7b246ebe2b0b 100644 --- a/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/ApplicationNotRunningPredicateTest.java +++ b/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/ApplicationNotRunningPredicateTest.java @@ -6,9 +6,7 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; -import jakarta.annotation.Priority; import jakarta.enterprise.event.Observes; -import jakarta.interceptor.Interceptor; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -43,15 +41,15 @@ void observeFailedExecution(@Observes FailedExecution failedExecution) { static class Jobs { - volatile boolean preStart; + volatile boolean started; - void started(@Observes @Priority(Interceptor.Priority.PLATFORM_BEFORE) StartupEvent event) { - preStart = true; + void started(@Observes StartupEvent event) { + started = true; } @Scheduled(every = "0.2s", skipExecutionIf = Scheduled.ApplicationNotRunning.class) void scheduleAfterStarted() { - if (!preStart) { + if (!started) { throw new IllegalStateException(); } } diff --git a/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/InvalidCronExpressionTest.java b/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/InvalidCronExpressionTest.java index 1b0b1beaae199..fdfde8e468851 100644 --- a/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/InvalidCronExpressionTest.java +++ b/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/InvalidCronExpressionTest.java @@ -1,6 +1,6 @@ package io.quarkus.scheduler.test; -import jakarta.enterprise.inject.spi.DeploymentException; +import static org.assertj.core.api.Assertions.assertThat; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -12,7 +12,10 @@ public class InvalidCronExpressionTest { @RegisterExtension static final QuarkusUnitTest test = new QuarkusUnitTest() - .setExpectedException(DeploymentException.class) + .assertException(t -> { + assertThat(t).cause().isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Invalid cron() expression"); + }) .withApplicationRoot((jar) -> jar .addClasses(InvalidBean.class)); diff --git a/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/InvalidDelayedExpressionTest.java b/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/InvalidDelayedExpressionTest.java index de243471726e9..c348e9789336e 100644 --- a/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/InvalidDelayedExpressionTest.java +++ b/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/InvalidDelayedExpressionTest.java @@ -1,6 +1,6 @@ package io.quarkus.scheduler.test; -import jakarta.enterprise.inject.spi.DeploymentException; +import static org.assertj.core.api.Assertions.assertThat; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -12,7 +12,10 @@ public class InvalidDelayedExpressionTest { @RegisterExtension static final QuarkusUnitTest test = new QuarkusUnitTest() - .setExpectedException(DeploymentException.class) + .assertException(t -> { + assertThat(t).cause().isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Invalid delayed() expression"); + }) .withApplicationRoot((jar) -> jar .addClasses(InvalidBean.class)); diff --git a/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/InvalidEveryExpressionTest.java b/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/InvalidEveryExpressionTest.java index 56cbc8dffa64f..332e9dd4db59d 100644 --- a/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/InvalidEveryExpressionTest.java +++ b/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/InvalidEveryExpressionTest.java @@ -1,6 +1,6 @@ package io.quarkus.scheduler.test; -import jakarta.enterprise.inject.spi.DeploymentException; +import static org.assertj.core.api.Assertions.assertThat; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -12,7 +12,10 @@ public class InvalidEveryExpressionTest { @RegisterExtension static final QuarkusUnitTest test = new QuarkusUnitTest() - .setExpectedException(DeploymentException.class) + .assertException(t -> { + assertThat(t).cause().isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Invalid every() expression"); + }) .withApplicationRoot((jar) -> jar .addClasses(InvalidEveryExpressionTest.InvalidBean.class)); diff --git a/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/InvalidTimeZoneTest.java b/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/InvalidTimeZoneTest.java index fa6c98bcbd437..1e059125cd18b 100644 --- a/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/InvalidTimeZoneTest.java +++ b/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/InvalidTimeZoneTest.java @@ -1,6 +1,6 @@ package io.quarkus.scheduler.test; -import jakarta.enterprise.inject.spi.DeploymentException; +import static org.assertj.core.api.Assertions.assertThat; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -12,7 +12,9 @@ public class InvalidTimeZoneTest { @RegisterExtension static final QuarkusUnitTest test = new QuarkusUnitTest() - .setExpectedException(DeploymentException.class) + .assertException(t -> { + assertThat(t).cause().isInstanceOf(IllegalStateException.class).hasMessageContaining("Invalid timeZone()"); + }) .withApplicationRoot((jar) -> jar .addClasses(InvalidBean.class)); diff --git a/extensions/scheduler/pom.xml b/extensions/scheduler/pom.xml index a763e4e95e6d8..babdb6acf34c2 100644 --- a/extensions/scheduler/pom.xml +++ b/extensions/scheduler/pom.xml @@ -17,6 +17,7 @@ deployment api + spi common kotlin runtime diff --git a/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/runtime/SchedulerConfig.java b/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/runtime/SchedulerConfig.java index 9e6274a255de5..23ce44a235145 100644 --- a/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/runtime/SchedulerConfig.java +++ b/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/runtime/SchedulerConfig.java @@ -25,9 +25,9 @@ public class SchedulerConfig { public boolean metricsEnabled; /** - * Tracing will be enabled if the OpenTelemetry extension is present and this value is true. + * Controls whether tracing is enabled. If set to true and the OpenTelemetry extension is present, + * tracing will be enabled, creating automatic Spans for each scheduled task. */ @ConfigItem(name = "tracing.enabled") public boolean tracingEnabled; - } diff --git a/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/runtime/SimpleScheduler.java b/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/runtime/SimpleScheduler.java index d69c97d0d3ba1..2a493c5d9170f 100644 --- a/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/runtime/SimpleScheduler.java +++ b/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/runtime/SimpleScheduler.java @@ -24,6 +24,7 @@ import jakarta.enterprise.event.Event; import jakarta.enterprise.event.Observes; import jakarta.enterprise.inject.Any; +import jakarta.enterprise.inject.Instance; import jakarta.enterprise.inject.Typed; import jakarta.inject.Singleton; import jakarta.interceptor.Interceptor; @@ -55,6 +56,7 @@ import io.quarkus.scheduler.common.runtime.AbstractJobDefinition; import io.quarkus.scheduler.common.runtime.DefaultInvoker; import io.quarkus.scheduler.common.runtime.Events; +import io.quarkus.scheduler.common.runtime.InstrumentedInvoker; import io.quarkus.scheduler.common.runtime.ScheduledInvoker; import io.quarkus.scheduler.common.runtime.ScheduledMethod; import io.quarkus.scheduler.common.runtime.SchedulerContext; @@ -64,6 +66,7 @@ import io.quarkus.scheduler.common.runtime.SyntheticScheduled; import io.quarkus.scheduler.common.runtime.util.SchedulerUtils; import io.quarkus.scheduler.runtime.SchedulerRuntimeConfig.StartMode; +import io.quarkus.scheduler.spi.JobInstrumenter; import io.quarkus.vertx.core.runtime.context.VertxContextSafetyToggle; import io.quarkus.virtual.threads.VirtualThreadsRecorder; import io.smallrye.common.vertx.VertxContext; @@ -78,7 +81,7 @@ public class SimpleScheduler implements Scheduler { private static final Logger LOG = Logger.getLogger(SimpleScheduler.class); // milliseconds - private static final long CHECK_PERIOD = 1000L; + public static final long CHECK_PERIOD = 1000L; private final ScheduledExecutorService scheduledExecutor; private final Vertx vertx; @@ -94,12 +97,15 @@ public class SimpleScheduler implements Scheduler { private final Event schedulerResumedEvent; private final Event scheduledJobPausedEvent; private final Event scheduledJobResumedEvent; + private final SchedulerConfig schedulerConfig; + private final Instance jobInstrumenter; public SimpleScheduler(SchedulerContext context, SchedulerRuntimeConfig schedulerRuntimeConfig, Event skippedExecutionEvent, Event successExecutionEvent, Event failedExecutionEvent, Event schedulerPausedEvent, Event schedulerResumedEvent, Event scheduledJobPausedEvent, - Event scheduledJobResumedEvent, Vertx vertx) { + Event scheduledJobResumedEvent, Vertx vertx, SchedulerConfig schedulerConfig, + Instance jobInstrumenter) { this.running = true; this.enabled = schedulerRuntimeConfig.enabled; this.scheduledTasks = new ConcurrentHashMap<>(); @@ -111,6 +117,8 @@ public SimpleScheduler(SchedulerContext context, SchedulerRuntimeConfig schedule this.schedulerResumedEvent = schedulerResumedEvent; this.scheduledJobPausedEvent = scheduledJobPausedEvent; this.scheduledJobResumedEvent = scheduledJobResumedEvent; + this.schedulerConfig = schedulerConfig; + this.jobInstrumenter = jobInstrumenter; CronDefinition definition = CronDefinitionBuilder.instanceDefinitionFor(context.getCronType()); this.cronParser = new CronParser(definition); @@ -153,9 +161,13 @@ public void run() { Optional trigger = createTrigger(id, method.getMethodDescription(), cronParser, scheduled, defaultOverdueGracePeriod); if (trigger.isPresent()) { + JobInstrumenter instrumenter = null; + if (schedulerConfig.tracingEnabled && jobInstrumenter.isResolvable()) { + instrumenter = jobInstrumenter.get(); + } ScheduledInvoker invoker = initInvoker(context.createInvoker(method.getInvokerClassName()), skippedExecutionEvent, successExecutionEvent, failedExecutionEvent, - scheduled.concurrentExecution(), initSkipPredicate(scheduled.skipExecutionIf())); + scheduled.concurrentExecution(), initSkipPredicate(scheduled.skipExecutionIf()), instrumenter); scheduledTasks.put(trigger.get().id, new ScheduledTask(trigger.get(), invoker, false)); } } @@ -168,7 +180,7 @@ public JobDefinition newJob(String identity) { if (scheduledTasks.containsKey(identity)) { throw new IllegalStateException("A job with this identity is already scheduled: " + identity); } - return new SimpleJobDefinition(identity); + return new SimpleJobDefinition(identity, schedulerConfig); } @Override @@ -339,7 +351,7 @@ Optional createTrigger(String id, String methodDescription, CronP SchedulerUtils.parseCronTimeZone(scheduled), methodDescription)); } else if (!scheduled.every().isEmpty()) { final OptionalLong everyMillis = SchedulerUtils.parseEveryAsMillis(scheduled); - if (!everyMillis.isPresent()) { + if (everyMillis.isEmpty()) { return Optional.empty(); } return Optional.of(new IntervalTrigger(id, start, everyMillis.getAsLong(), @@ -352,7 +364,7 @@ Optional createTrigger(String id, String methodDescription, CronP public static ScheduledInvoker initInvoker(ScheduledInvoker invoker, Event skippedExecutionEvent, Event successExecutionEvent, Event failedExecutionEvent, ConcurrentExecution concurrentExecution, - Scheduled.SkipPredicate skipPredicate) { + Scheduled.SkipPredicate skipPredicate, JobInstrumenter instrumenter) { invoker = new StatusEmitterInvoker(invoker, successExecutionEvent, failedExecutionEvent); if (concurrentExecution == ConcurrentExecution.SKIP) { invoker = new SkipConcurrentExecutionInvoker(invoker, skippedExecutionEvent); @@ -360,6 +372,9 @@ public static ScheduledInvoker initInvoker(ScheduledInvoker invoker, Event + + + io.quarkus + quarkus-scheduler-parent + 999-SNAPSHOT + + 4.0.0 + + quarkus-scheduler-spi + Quarkus - Scheduler - SPI + + + + io.quarkus + quarkus-scheduler-api + + + diff --git a/extensions/scheduler/spi/src/main/java/io/quarkus/scheduler/spi/JobInstrumenter.java b/extensions/scheduler/spi/src/main/java/io/quarkus/scheduler/spi/JobInstrumenter.java new file mode 100644 index 0000000000000..d6df78b5691c8 --- /dev/null +++ b/extensions/scheduler/spi/src/main/java/io/quarkus/scheduler/spi/JobInstrumenter.java @@ -0,0 +1,23 @@ +package io.quarkus.scheduler.spi; + +import java.util.concurrent.CompletionStage; + +/** + * Instruments a scheduled job. + *

    + * Telemetry extensions can provide exactly one CDI bean of this type. The scope must be either {@link jakarta.inject.Singleton} + * or {@link jakarta.enterprise.context.ApplicationScoped}. + */ +public interface JobInstrumenter { + + CompletionStage instrument(JobInstrumentationContext context); + + interface JobInstrumentationContext { + + String getSpanName(); + + CompletionStage executeJob(); + + } + +} diff --git a/extensions/security/deployment/src/test/java/io/quarkus/security/test/rolesallowed/RolesAllowedExpressionTest.java b/extensions/security/deployment/src/test/java/io/quarkus/security/test/rolesallowed/RolesAllowedExpressionTest.java index 7af69cfa4c78e..f29bc1f0c6f4e 100644 --- a/extensions/security/deployment/src/test/java/io/quarkus/security/test/rolesallowed/RolesAllowedExpressionTest.java +++ b/extensions/security/deployment/src/test/java/io/quarkus/security/test/rolesallowed/RolesAllowedExpressionTest.java @@ -40,7 +40,8 @@ public class RolesAllowedExpressionTest { "%test.test-profile-admin=admin\n" + "missing-profile-profile-admin=superman\n" + "%missing-profile.missing-profile-profile-admin=admin\n" + - "all-roles=Administrator,Software,Tester,User\n"; + "all-roles=Administrator,Software,Tester,User\n" + + "ldap-roles=cn=Administrator\\\\,ou=Software\\\\,dc=Tester\\\\,dc=User\n"; @RegisterExtension static final QuarkusUnitTest config = new QuarkusUnitTest() @@ -90,6 +91,10 @@ public void shouldRestrictAccessToSpecificRole() { assertSuccess(() -> bean.list(), "list", new AuthData(Set.of("Administrator", "Software", "Tester", "User"), false, "list")); assertFailureFor(() -> bean.list(), ForbiddenException.class, ADMIN); + + // property expression with escaped collection separator should not be treated as list + assertSuccess(() -> bean.ldap(), "ldap", + new AuthData(Set.of("cn=Administrator,ou=Software,dc=Tester,dc=User"), false, "ldap")); } @Singleton @@ -141,6 +146,11 @@ public final String list() { return "list"; } + @RolesAllowed("${ldap-roles}") + public final String ldap() { + return "ldap"; + } + } } diff --git a/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/SecurityCheckRecorder.java b/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/SecurityCheckRecorder.java index ee6639d2ef495..8661d06f3f4bd 100644 --- a/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/SecurityCheckRecorder.java +++ b/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/SecurityCheckRecorder.java @@ -98,15 +98,17 @@ public String[] get() { // @RolesAllowed({"${my.roles}"}) => my.roles=one,two <=> @RolesAllowed({"one", "two"}) if (strVal != null && strVal.contains(",")) { var strArr = StringUtil.split(strVal); - if (strArr.length > 1) { + if (strArr.length >= 1) { // role order is irrelevant as logical operator between them is OR - // first role will go to the original place + // first role will go to the original place, double escaped comma will be parsed correctly strVal = strArr[0]; - // the rest of the roles will be appended at the end - for (int i1 = 1; i1 < strArr.length; i1++) { - roles.add(strArr[i1]); + if (strArr.length > 1) { + // the rest of the roles will be appended at the end + for (int i1 = 1; i1 < strArr.length; i1++) { + roles.add(strArr[i1]); + } } } } diff --git a/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/X509IdentityProvider.java b/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/X509IdentityProvider.java index ae1ff274da163..d7bcff7deb67c 100644 --- a/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/X509IdentityProvider.java +++ b/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/X509IdentityProvider.java @@ -1,6 +1,13 @@ package io.quarkus.security.runtime; import java.security.cert.X509Certificate; +import java.util.Map; +import java.util.Set; + +import javax.naming.InvalidNameException; +import javax.naming.ldap.LdapName; +import javax.naming.ldap.Rdn; +import javax.security.auth.x500.X500Principal; import jakarta.inject.Singleton; @@ -12,6 +19,8 @@ @Singleton public class X509IdentityProvider implements IdentityProvider { + private static final String COMMON_NAME = "CN"; + private static final String ROLES_ATTRIBUTE = "roles"; @Override public Class getRequestType() { @@ -21,10 +30,51 @@ public Class getRequestType() { @Override public Uni authenticate(CertificateAuthenticationRequest request, AuthenticationRequestContext context) { X509Certificate certificate = request.getCertificate().getCertificate(); - + Map> roles = request.getAttribute(ROLES_ATTRIBUTE); return Uni.createFrom().item(QuarkusSecurityIdentity.builder() .setPrincipal(certificate.getSubjectX500Principal()) .addCredential(request.getCertificate()) + .addRoles(extractRoles(certificate, roles)) .build()); } + + private Set extractRoles(X509Certificate certificate, Map> roles) { + if (roles == null) { + return Set.of(); + } + X500Principal principal = certificate.getSubjectX500Principal(); + if (principal == null || principal.getName() == null) { + return Set.of(); + } + Set matchedRoles = roles.get(principal.getName()); + if (matchedRoles != null) { + return matchedRoles; + } + String commonName = getCommonName(principal); + if (commonName != null) { + matchedRoles = roles.get(commonName); + if (matchedRoles != null) { + return matchedRoles; + } + } + return Set.of(); + } + + private static String getCommonName(X500Principal principal) { + try { + LdapName ldapDN = new LdapName(principal.getName()); + + // Apparently for some CN variations it might not produce correct results + // Can be tuned as necessary. + for (Rdn rdn : ldapDN.getRdns()) { + if (COMMON_NAME.equals(rdn.getType())) { + return rdn.getValue().toString(); + } + } + } catch (InvalidNameException ex) { + // Failing the augmentation process because of this exception seems unnecessary + // The common name my include some characters unexpected by the legacy LdapName API specification. + } + return null; + } } diff --git a/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/graal/BouncyCastleSubstitutions.java b/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/graal/BouncyCastleSubstitutions.java index b647e93b36ccc..7fad7acc4698d 100644 --- a/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/graal/BouncyCastleSubstitutions.java +++ b/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/graal/BouncyCastleSubstitutions.java @@ -95,10 +95,6 @@ final class Target_org_bouncycastle_jcajce_provider_BouncyCastleFipsProvider { @Alias @RecomputeFieldValue(kind = RecomputeFieldValue.Kind.Reset) // private SecureRandom entropySource; - - @Alias - @RecomputeFieldValue(kind = RecomputeFieldValue.Kind.Reset) // - private SecureRandom providerDefaultRandom; } @com.oracle.svm.core.annotate.TargetClass(className = "org.bouncycastle.math.ec.ECPoint", onlyWith = BouncyCastleCryptoFips.class) diff --git a/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/SmallRyeOpenApiProcessor.java b/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/SmallRyeOpenApiProcessor.java index 851e82a60e179..f73cfa65aeeeb 100644 --- a/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/SmallRyeOpenApiProcessor.java +++ b/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/SmallRyeOpenApiProcessor.java @@ -244,6 +244,7 @@ void registerAutoSecurityFilter(BuildProducer syntheticB @BuildStep @Record(ExecutionTime.STATIC_INIT) void registerAnnotatedUserDefinedRuntimeFilters(BuildProducer syntheticBeans, + BuildProducer reflectiveClass, OpenApiFilteredIndexViewBuildItem apiFilteredIndexViewBuildItem, OpenApiRecorder recorder) { Config config = ConfigProvider.getConfig(); @@ -256,6 +257,7 @@ void registerAnnotatedUserDefinedRuntimeFilters(BuildProducer ShrinkWrap.create(JavaArchive.class) - .addClasses(WithDoubleValue.class, BasicTypeData.class, BasicTypeDataRepository.class)) + .addClasses(WithDoubleValue.class, BasicTypeData.class, BasicTypeDataRepository.class, + FixedLocaleJavaType.class)) .withConfigurationResource("application.properties"); private static final UUID uuid = UUID.randomUUID(); diff --git a/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/FixedLocaleJavaType.java b/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/FixedLocaleJavaType.java new file mode 100644 index 0000000000000..bc3819dc7287c --- /dev/null +++ b/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/FixedLocaleJavaType.java @@ -0,0 +1,53 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package io.quarkus.spring.data.deployment; + +import java.util.Locale; + +import org.hibernate.type.SqlTypes; +import org.hibernate.type.descriptor.WrapperOptions; +import org.hibernate.type.descriptor.java.LocaleJavaType; +import org.hibernate.type.descriptor.jdbc.JdbcType; +import org.hibernate.type.descriptor.jdbc.JdbcTypeIndicators; + +/** + * Workaround for https://hibernate.atlassian.net/browse/HHH-17466 + */ +public class FixedLocaleJavaType extends LocaleJavaType { + public FixedLocaleJavaType() { + super(); + } + + @Override + public JdbcType getRecommendedJdbcType(JdbcTypeIndicators indicators) { + return indicators.getJdbcType(SqlTypes.VARCHAR); + } + + @SuppressWarnings("unchecked") + @Override + public X unwrap(Locale value, Class type, WrapperOptions options) { + if (value == null) { + return null; + } + if (Locale.class.isAssignableFrom(type)) { + return (X) value; + } + return super.unwrap(value, type, options); + } + + @Override + public Locale wrap(X value, WrapperOptions options) { + if (value == null) { + return null; + } + if (value instanceof Locale) { + return (Locale) value; + } + return super.wrap(value, options); + } + +} diff --git a/extensions/spring-security/deployment/src/main/java/io/quarkus/spring/security/deployment/SpringSecurityProcessor.java b/extensions/spring-security/deployment/src/main/java/io/quarkus/spring/security/deployment/SpringSecurityProcessor.java index 70d16090e0090..d5d70eb2fbace 100644 --- a/extensions/spring-security/deployment/src/main/java/io/quarkus/spring/security/deployment/SpringSecurityProcessor.java +++ b/extensions/spring-security/deployment/src/main/java/io/quarkus/spring/security/deployment/SpringSecurityProcessor.java @@ -48,6 +48,7 @@ import io.quarkus.spring.security.runtime.interceptor.SpringPreauthorizeInterceptor; import io.quarkus.spring.security.runtime.interceptor.SpringSecuredInterceptor; import io.quarkus.spring.security.runtime.interceptor.SpringSecurityRecorder; +import io.quarkus.spring.security.runtime.interceptor.check.PrincipalNameFromParameterObjectSecurityCheck; import io.quarkus.spring.security.runtime.interceptor.check.PrincipalNameFromParameterSecurityCheck; class SpringSecurityProcessor { @@ -466,13 +467,18 @@ void addSpringPreAuthorizeSecurityCheck(CombinedIndexBuildItem index, propertyName, index.getIndex(), part); + PrincipalNameFromParameterObjectSecurityCheck.CheckType checkType = part.contains("==") + ? PrincipalNameFromParameterObjectSecurityCheck.CheckType.EQ + : PrincipalNameFromParameterObjectSecurityCheck.CheckType.NEQ; + securityChecks.add(springSecurityRecorder.principalNameFromParameterObjectSecurityCheck( parameterNameAndIndex.getIndex(), stringPropertyAccessorData.getMatchingParameterClassInfo().name().toString(), StringPropertyAccessorGenerator .getAccessorClassName( stringPropertyAccessorData.getMatchingParameterClassInfo().name()), - stringPropertyAccessorData.getMatchingParameterFieldInfo().name())); + stringPropertyAccessorData.getMatchingParameterFieldInfo().name(), + checkType)); } } else if (part.matches(SpringSecurityProcessorUtil.BASIC_BEAN_METHOD_INVOCATION_REGEX)) { diff --git a/extensions/spring-security/deployment/src/test/java/io/quarkus/spring/security/deployment/SpringPreAuthorizeTest.java b/extensions/spring-security/deployment/src/test/java/io/quarkus/spring/security/deployment/SpringPreAuthorizeTest.java index eaab4280b3647..c7dc3deff1f77 100644 --- a/extensions/spring-security/deployment/src/test/java/io/quarkus/spring/security/deployment/SpringPreAuthorizeTest.java +++ b/extensions/spring-security/deployment/src/test/java/io/quarkus/spring/security/deployment/SpringPreAuthorizeTest.java @@ -116,6 +116,16 @@ public void testPrincipalNameFromObject() { assertSuccess(() -> springComponent.principalNameFromObject(new Person("user")), "user", USER); } + @Test + public void testPrincipalNameFromObjectIsNot() { + assertFailureFor(() -> springComponent.principalNameFromObjectIsNot(new Person("whatever")), + UnauthorizedException.class, + ANONYMOUS); + assertSuccess(() -> springComponent.principalNameFromObjectIsNot(new Person("whatever")), "whatever", USER); + assertFailureFor(() -> springComponent.principalNameFromObjectIsNot(new Person("user")), ForbiddenException.class, + USER); + } + @Test public void testNotSecured() { assertSuccess(() -> springComponent.notSecured(), "notSecured", ANONYMOUS); diff --git a/extensions/spring-security/deployment/src/test/java/io/quarkus/spring/security/deployment/springapp/SpringComponent.java b/extensions/spring-security/deployment/src/test/java/io/quarkus/spring/security/deployment/springapp/SpringComponent.java index 0d86e8406fcf3..68a8af6552c6d 100644 --- a/extensions/spring-security/deployment/src/test/java/io/quarkus/spring/security/deployment/springapp/SpringComponent.java +++ b/extensions/spring-security/deployment/src/test/java/io/quarkus/spring/security/deployment/springapp/SpringComponent.java @@ -37,6 +37,11 @@ public String principalNameFromObject(Person person) { return person.getName(); } + @PreAuthorize("#person.name != authentication.principal.username") + public String principalNameFromObjectIsNot(Person person) { + return person.getName(); + } + public String notSecured() { return "notSecured"; } diff --git a/extensions/spring-security/runtime/src/main/java/io/quarkus/spring/security/runtime/interceptor/SpringSecurityRecorder.java b/extensions/spring-security/runtime/src/main/java/io/quarkus/spring/security/runtime/interceptor/SpringSecurityRecorder.java index 8101bdd0991b6..591c08d8bd892 100644 --- a/extensions/spring-security/runtime/src/main/java/io/quarkus/spring/security/runtime/interceptor/SpringSecurityRecorder.java +++ b/extensions/spring-security/runtime/src/main/java/io/quarkus/spring/security/runtime/interceptor/SpringSecurityRecorder.java @@ -70,8 +70,9 @@ public SecurityCheck fromGeneratedClass(String generatedClassName) { } public SecurityCheck principalNameFromParameterObjectSecurityCheck(int index, String expectedParameterClass, - String stringPropertyAccessorClass, String propertyName) { + String stringPropertyAccessorClass, String propertyName, + PrincipalNameFromParameterObjectSecurityCheck.CheckType checkType) { return PrincipalNameFromParameterObjectSecurityCheck.of(index, expectedParameterClass, stringPropertyAccessorClass, - propertyName); + propertyName, checkType); } } diff --git a/extensions/spring-security/runtime/src/main/java/io/quarkus/spring/security/runtime/interceptor/check/PrincipalNameFromParameterObjectSecurityCheck.java b/extensions/spring-security/runtime/src/main/java/io/quarkus/spring/security/runtime/interceptor/check/PrincipalNameFromParameterObjectSecurityCheck.java index c34e97b6d5f7b..63da575e48187 100644 --- a/extensions/spring-security/runtime/src/main/java/io/quarkus/spring/security/runtime/interceptor/check/PrincipalNameFromParameterObjectSecurityCheck.java +++ b/extensions/spring-security/runtime/src/main/java/io/quarkus/spring/security/runtime/interceptor/check/PrincipalNameFromParameterObjectSecurityCheck.java @@ -23,22 +23,24 @@ public class PrincipalNameFromParameterObjectSecurityCheck implements SecurityCh private final Class expectedParameterClass; private final Class stringPropertyAccessorClass; private final String propertyName; + private final CheckType checkType; private PrincipalNameFromParameterObjectSecurityCheck(int index, String expectedParameterClass, - String stringPropertyAccessorClass, String propertyName) throws ClassNotFoundException { + String stringPropertyAccessorClass, String propertyName, CheckType checkType) throws ClassNotFoundException { this.index = index; this.expectedParameterClass = Class.forName(expectedParameterClass, false, Thread.currentThread().getContextClassLoader()); this.stringPropertyAccessorClass = (Class) Class.forName(stringPropertyAccessorClass, false, Thread.currentThread().getContextClassLoader()); this.propertyName = propertyName; + this.checkType = checkType; } public static PrincipalNameFromParameterObjectSecurityCheck of(int index, String expectedParameterClass, - String stringPropertyAccessorClass, String propertyName) { + String stringPropertyAccessorClass, String propertyName, CheckType checkType) { try { return new PrincipalNameFromParameterObjectSecurityCheck(index, expectedParameterClass, stringPropertyAccessorClass, - propertyName); + propertyName, checkType); } catch (ClassNotFoundException e) { throw new RuntimeException(e); } @@ -70,8 +72,14 @@ private void doApply(SecurityIdentity identity, Object[] parameters, String clas } String name = identity.getPrincipal().getName(); - if (!name.equals(parameterValueStr)) { - throw new ForbiddenException(); + if (checkType == CheckType.EQ) { + if (!name.equals(parameterValueStr)) { + throw new ForbiddenException(); + } + } else if (checkType == CheckType.NEQ) { + if (name.equals(parameterValueStr)) { + throw new ForbiddenException(); + } } } @@ -84,4 +92,9 @@ private IllegalStateException genericNotApplicableException(String className, St "PrincipalNameFromParameterObjectSecurityCheck with index " + index + " cannot be applied to '" + className + "#" + methodName + "'"); } + + public enum CheckType { + EQ, + NEQ + } } diff --git a/extensions/vertx-http/deployment/pom.xml b/extensions/vertx-http/deployment/pom.xml index ed3dd67a09031..14d44cbf77b92 100644 --- a/extensions/vertx-http/deployment/pom.xml +++ b/extensions/vertx-http/deployment/pom.xml @@ -60,6 +60,10 @@ com.fasterxml.jackson.core jackson-databind + + com.fasterxml.jackson.datatype + jackson-datatype-jdk8 + diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/jsonrpc/DevUIDatabindCodec.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/jsonrpc/DevUIDatabindCodec.java index 877c25a895a90..be209fded25fa 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/jsonrpc/DevUIDatabindCodec.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/jsonrpc/DevUIDatabindCodec.java @@ -19,6 +19,7 @@ import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; import io.quarkus.devui.runtime.jsonrpc.json.JsonMapper; import io.quarkus.devui.runtime.jsonrpc.json.JsonTypeAdapter; @@ -141,6 +142,7 @@ public JsonMapper create(JsonTypeAdapter> jsonObjectAdapt module.addSerializer(ByteArrayInputStream.class, new ByteArrayInputStreamSerializer()); module.addDeserializer(ByteArrayInputStream.class, new ByteArrayInputStreamDeserializer()); mapper.registerModule(module); + mapper.registerModule(new Jdk8Module()); SimpleModule runtimeModule = new SimpleModule("vertx-module-runtime"); addAdapterToObject(runtimeModule, jsonObjectAdapter); 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 e414b69b3f7a9..2375e244ddb28 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 @@ -30,6 +30,7 @@ import io.quarkus.runtime.RuntimeValue; import io.quarkus.security.spi.runtime.MethodDescription; import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig; +import io.quarkus.vertx.http.runtime.HttpConfiguration; import io.quarkus.vertx.http.runtime.management.ManagementInterfaceBuildTimeConfig; import io.quarkus.vertx.http.runtime.security.BasicAuthenticationMechanism; import io.quarkus.vertx.http.runtime.security.EagerSecurityInterceptorStorage; @@ -92,6 +93,17 @@ AdditionalBeanBuildItem initMtlsClientAuth(HttpBuildTimeConfig buildTimeConfig) return null; } + @BuildStep + @Record(ExecutionTime.RUNTIME_INIT) + void setMtlsCertificateRoleProperties( + HttpSecurityRecorder recorder, + HttpConfiguration config, + HttpBuildTimeConfig buildTimeConfig) { + if (isMtlsClientAuthenticationEnabled(buildTimeConfig)) { + recorder.setMtlsCertificateRoleProperties(config); + } + } + @BuildStep(onlyIf = IsApplicationBasicAuthRequired.class) AdditionalBeanBuildItem initBasicAuth(HttpBuildTimeConfig buildTimeConfig, BuildProducer annotationsTransformerProducer, diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/http2/Http2RSTFloodProtectionConfigTest.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/http2/Http2RSTFloodProtectionConfigTest.java index 5f4091a2b7097..c3abccb5d5512 100644 --- a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/http2/Http2RSTFloodProtectionConfigTest.java +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/http2/Http2RSTFloodProtectionConfigTest.java @@ -14,6 +14,8 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledOnOs; +import org.junit.jupiter.api.condition.OS; import org.junit.jupiter.api.extension.RegisterExtension; import io.quarkus.test.QuarkusUnitTest; @@ -29,6 +31,7 @@ /** * Configuration of the RST flood protection (CVE-2023-44487) */ +@DisabledOnOs(OS.WINDOWS) public class Http2RSTFloodProtectionConfigTest { @TestHTTPResource(value = "/ping", ssl = true) diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/management/ManagementAndRootPathTest.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/management/ManagementAndRootPathTest.java index 1e9fe401909c9..7fc85520f8fb3 100644 --- a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/management/ManagementAndRootPathTest.java +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/management/ManagementAndRootPathTest.java @@ -2,9 +2,8 @@ import java.util.function.Consumer; -import javax.inject.Singleton; - import jakarta.enterprise.event.Observes; +import jakarta.inject.Singleton; import org.hamcrest.Matchers; import org.jboss.shrinkwrap.api.asset.StringAsset; diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/management/ManagementWithJksTest.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/management/ManagementWithJksTest.java index 580fd44b27066..1a0827d198fb4 100644 --- a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/management/ManagementWithJksTest.java +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/management/ManagementWithJksTest.java @@ -3,9 +3,8 @@ import java.io.File; import java.util.function.Consumer; -import javax.inject.Singleton; - import jakarta.enterprise.event.Observes; +import jakarta.inject.Singleton; import org.assertj.core.api.Assertions; import org.hamcrest.Matchers; diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/management/ManagementWithMainServerDisabledTest.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/management/ManagementWithMainServerDisabledTest.java index 9694283a305a9..13233ae9a8203 100644 --- a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/management/ManagementWithMainServerDisabledTest.java +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/management/ManagementWithMainServerDisabledTest.java @@ -2,9 +2,8 @@ import java.util.function.Consumer; -import javax.inject.Singleton; - import jakarta.enterprise.event.Observes; +import jakarta.inject.Singleton; import org.hamcrest.Matchers; import org.jboss.shrinkwrap.api.asset.StringAsset; diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/management/ManagementWithP12Test.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/management/ManagementWithP12Test.java index 0f730779f0856..a8e55cedb5608 100644 --- a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/management/ManagementWithP12Test.java +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/management/ManagementWithP12Test.java @@ -3,9 +3,8 @@ import java.io.File; import java.util.function.Consumer; -import javax.inject.Singleton; - import jakarta.enterprise.event.Observes; +import jakarta.inject.Singleton; import org.assertj.core.api.Assertions; import org.hamcrest.Matchers; diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/management/ManagementWithPemTest.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/management/ManagementWithPemTest.java index ef4b24d1638a1..bc86379e35909 100644 --- a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/management/ManagementWithPemTest.java +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/management/ManagementWithPemTest.java @@ -3,9 +3,8 @@ import java.io.File; import java.util.function.Consumer; -import javax.inject.Singleton; - import jakarta.enterprise.event.Observes; +import jakarta.inject.Singleton; import org.assertj.core.api.Assertions; import org.hamcrest.Matchers; diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/permission/AbstractHttpSecurityPolicyGrantingPermissionsTest.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/permission/AbstractHttpSecurityPolicyGrantingPermissionsTest.java index d7719a9996b39..0a2001a36a09c 100644 --- a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/permission/AbstractHttpSecurityPolicyGrantingPermissionsTest.java +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/permission/AbstractHttpSecurityPolicyGrantingPermissionsTest.java @@ -305,6 +305,7 @@ interface AuthenticatedUser { enum AuthenticatedUserImpl implements AuthenticatedUser { ADMIN(AuthenticatedUserImpl::useAdminRole), + ROOT(AuthenticatedUserImpl::useRootRole), USER(AuthenticatedUserImpl::useUserRole), TEST(AuthenticatedUserImpl::useTestRole), TEST2(AuthenticatedUserImpl::useTest2Role); @@ -331,6 +332,10 @@ private static void useTest2Role() { TestIdentityController.resetRoles().add("test2", "test2", "test2"); } + private static void useRootRole() { + TestIdentityController.resetRoles().add("root", "root", "root", "Admin1"); + } + private static void useAdminRole() { TestIdentityController.resetRoles().add("admin", "admin", "admin"); } diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/permission/HttpSecPolicyGrantingRolesTest.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/permission/HttpSecPolicyGrantingRolesTest.java new file mode 100644 index 0000000000000..d281193a43143 --- /dev/null +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/permission/HttpSecPolicyGrantingRolesTest.java @@ -0,0 +1,245 @@ +package io.quarkus.vertx.http.security.permission; + +import static io.quarkus.vertx.http.security.permission.AbstractHttpSecurityPolicyGrantingPermissionsTest.AuthenticatedUserImpl.ADMIN; +import static io.quarkus.vertx.http.security.permission.AbstractHttpSecurityPolicyGrantingPermissionsTest.AuthenticatedUserImpl.ROOT; +import static io.quarkus.vertx.http.security.permission.AbstractHttpSecurityPolicyGrantingPermissionsTest.AuthenticatedUserImpl.USER; + +import java.util.function.Supplier; + +import jakarta.annotation.security.RolesAllowed; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.event.Observes; +import jakarta.inject.Inject; + +import org.hamcrest.Matchers; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.arc.Arc; +import io.quarkus.security.AuthenticationFailedException; +import io.quarkus.security.ForbiddenException; +import io.quarkus.security.PermissionsAllowed; +import io.quarkus.security.UnauthorizedException; +import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.security.runtime.SecurityIdentityAssociation; +import io.quarkus.security.test.utils.TestIdentityController; +import io.quarkus.security.test.utils.TestIdentityProvider; +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.vertx.http.runtime.security.QuarkusHttpUser; +import io.quarkus.vertx.http.security.CustomPermission; +import io.quarkus.vertx.http.security.CustomPermissionWithActions; +import io.quarkus.vertx.http.security.permission.AbstractHttpSecurityPolicyGrantingPermissionsTest.AuthenticatedUser; +import io.quarkus.vertx.http.security.permission.AbstractHttpSecurityPolicyGrantingPermissionsTest.AuthenticatedUserImpl; +import io.restassured.RestAssured; +import io.smallrye.mutiny.Uni; +import io.vertx.core.Handler; +import io.vertx.ext.web.Router; +import io.vertx.ext.web.RoutingContext; + +public class HttpSecPolicyGrantingRolesTest { + + @RegisterExtension + static QuarkusUnitTest test = new QuarkusUnitTest().setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClasses(TestIdentityController.class, TestIdentityProvider.class, RolesPathHandler.class, + CDIBean.class, CustomPermission.class, CustomPermissionWithActions.class, AuthenticatedUser.class, + AuthenticatedUserImpl.class) + .addAsResource("conf/http-roles-grant-config.properties", "application.properties")); + + @Test + public void mapRolesToRolesSecuredWithRolesAllowed() { + assertSuccess(ADMIN, "/test/new-admin-roles-blocking"); + assertSuccess(ADMIN, "/test/new-admin-roles"); + assertSuccess(ADMIN, "/test/new-admin-roles2"); + assertSuccess(ADMIN, "/test/old-admin-roles-blocking"); + assertSuccess(ADMIN, "/test/old-admin-roles"); + assertSuccess(ADMIN, "/test/multiple-new-roles-1"); + assertSuccess(ADMIN, "/test/multiple-new-roles-2"); + assertSuccess(ADMIN, "/test/multiple-new-roles-3"); + assertForbidden(USER, "/test/new-admin-roles-blocking"); + assertForbidden(USER, "/test/new-admin-roles"); + assertForbidden(USER, "/test/new-admin-roles2"); + assertForbidden(USER, "/test/old-admin-roles-blocking"); + assertForbidden(USER, "/test/old-admin-roles"); + assertSuccess(USER, "/test/multiple-new-roles-1"); + assertSuccess(USER, "/test/multiple-new-roles-2"); + assertSuccess(USER, "/test/multiple-new-roles-3"); + } + + @Test + public void mapRolesToRolesNoSecurityAnnotation() { + assertSuccess(ADMIN, "/test/roles-allowed-path"); + assertForbidden(USER, "/test/roles-allowed-path"); + assertForbidden(ROOT, "/test/roles-allowed-path"); + assertSuccess(ADMIN, "/test/granted-and-checked-by-policy"); + assertForbidden(USER, "/test/granted-and-checked-by-policy"); + assertSuccess(ROOT, "/test/granted-and-checked-by-policy"); + } + + @Test + public void mapRolesToBothPermissionsAndRoles() { + assertSuccess(ADMIN, "/test/roles-and-perms-1"); + assertSuccess(ADMIN, "/test/roles-and-perms-2"); + assertForbidden(USER, "/test/roles-and-perms-1"); + assertForbidden(USER, "/test/roles-and-perms-2"); + } + + @ApplicationScoped + public static class RolesPathHandler { + + @Inject + CDIBean cdiBean; + + public void setup(@Observes Router router) { + router.route("/test/new-admin-roles-blocking").blockingHandler(new RouteHandler(() -> { + cdiBean.newRolesBlocking(); + return Uni.createFrom().nullItem(); + })); + router.route("/test/new-admin-roles").handler(new RouteHandler(cdiBean::newRoles)); + router.route("/test/new-admin-roles2").handler(new RouteHandler(cdiBean::newRoles2)); + router.route("/test/old-admin-roles-blocking").blockingHandler(new RouteHandler(() -> { + cdiBean.oldRolesBlocking(); + return Uni.createFrom().nullItem(); + })); + router.route("/test/old-admin-roles").handler(new RouteHandler(cdiBean::oldRoles)); + router.route("/test/multiple-new-roles-1").handler(new RouteHandler(cdiBean::multipleNewRoles1)); + router.route("/test/multiple-new-roles-2").handler(new RouteHandler(cdiBean::multipleNewRoles2)); + router.route("/test/multiple-new-roles-3").handler(new RouteHandler(cdiBean::multipleNewRoles3)); + router.route("/test/roles-allowed-path").handler(new RouteHandler(cdiBean::checkAdmin1Role)); + router.route("/test/granted-and-checked-by-policy").handler(new RouteHandler(cdiBean::checkAdmin1Role)); + router.route("/test/roles-and-perms-1").handler(new RouteHandler(cdiBean::rolesAndPermissions1)); + router.route("/test/roles-and-perms-2").handler(new RouteHandler(cdiBean::rolesAndPermissions2)); + } + } + + private static final class RouteHandler implements Handler { + + private final Supplier> callService; + + private RouteHandler(Supplier> callService) { + this.callService = callService; + } + + @Override + public void handle(RoutingContext event) { + // activate context so that we can use CDI beans + Arc.container().requestContext().activate(); + // set identity used by security checks performed by standard security interceptors + QuarkusHttpUser user = (QuarkusHttpUser) event.user(); + Arc.container().instance(SecurityIdentityAssociation.class).get().setIdentity(user.getSecurityIdentity()); + + callService.get().subscribe().with(unused -> { + String ret = user.getSecurityIdentity().getPrincipal().getName() + + ":" + event.normalizedPath(); + event.response().end(ret); + }, throwable -> { + if (throwable instanceof UnauthorizedException) { + event.response().setStatusCode(401); + } else if (throwable instanceof ForbiddenException) { + event.response().setStatusCode(403); + } else { + event.response().setStatusCode(500); + } + event.end(); + }); + } + } + + private void assertSuccess(AuthenticatedUser user, String... paths) { + user.authenticate(); + for (var path : paths) { + RestAssured + .given() + .auth() + .basic(user.role(), user.role()) + .get(path) + .then() + .statusCode(200) + .body(Matchers.is(user.role() + ":" + path)); + } + } + + private void assertForbidden(AuthenticatedUser user, String... paths) { + user.authenticate(); + for (var path : paths) { + RestAssured + .given() + .auth() + .basic(user.role(), user.role()) + .get(path) + .then() + .statusCode(403); + } + } + + @ApplicationScoped + public static class CDIBean { + + @Inject + SecurityIdentity identity; + + @RolesAllowed("Admin1") + public void newRolesBlocking() { + // NOTHING TO DO + } + + @RolesAllowed("Admin1") + public Uni newRoles() { + return Uni.createFrom().nullItem(); + } + + @RolesAllowed("Admin2") + public Uni newRoles2() { + return Uni.createFrom().nullItem(); + } + + @RolesAllowed("admin") + public void oldRolesBlocking() { + // NOTHING TO DO + } + + @RolesAllowed("admin") + public Uni oldRoles() { + return Uni.createFrom().nullItem(); + } + + @RolesAllowed("Janet") + public Uni multipleNewRoles1() { + return Uni.createFrom().nullItem(); + } + + @RolesAllowed("Monica") + public Uni multipleNewRoles2() { + return Uni.createFrom().nullItem(); + } + + @RolesAllowed("Robin") + public Uni multipleNewRoles3() { + return Uni.createFrom().nullItem(); + } + + @RolesAllowed("Admin3") + public Uni rolesAndPermissions1() { + return Uni.createFrom().nullItem(); + } + + @PermissionsAllowed("jump") + public Uni rolesAndPermissions2() { + return Uni.createFrom().nullItem(); + } + + public Uni checkAdmin1Role() { + if (identity.hasRole("Admin1")) { + if (identity.getPrincipal().getName().equals("root")) { + if (identity.hasRole("sudo")) { + return Uni.createFrom().nullItem(); + } + } else { + return Uni.createFrom().nullItem(); + } + } + return Uni.createFrom().failure(AuthenticationFailedException::new); + } + } +} diff --git a/extensions/vertx-http/deployment/src/test/resources/conf/http-roles-grant-config.properties b/extensions/vertx-http/deployment/src/test/resources/conf/http-roles-grant-config.properties new file mode 100644 index 0000000000000..25d32756ad392 --- /dev/null +++ b/extensions/vertx-http/deployment/src/test/resources/conf/http-roles-grant-config.properties @@ -0,0 +1,28 @@ +quarkus.http.auth.basic=true +quarkus.http.auth.policy.t1.roles.admin=Admin1 +quarkus.http.auth.permission.t1.paths=/* +quarkus.http.auth.permission.t1.policy=t1 +quarkus.http.auth.policy.t2.roles.admin=Admin2 +quarkus.http.auth.permission.t2.paths=/* +quarkus.http.auth.permission.t2.policy=t2 +quarkus.http.auth.policy.t3.roles.user=Janet,Robin +quarkus.http.auth.policy.t3.roles.admin=Janet,Robin +quarkus.http.auth.permission.t3.paths=/test/multiple-new-roles-1 +quarkus.http.auth.permission.t3.policy=t3 +quarkus.http.auth.policy.t4.roles.user=Monica,Robin +quarkus.http.auth.policy.t4.roles.admin=Monica,Robin +quarkus.http.auth.permission.t4.paths=/test/multiple-new-roles-2,/test/multiple-new-roles-3 +quarkus.http.auth.permission.t4.policy=t4 +quarkus.http.auth.policy.t5.roles-allowed=admin +quarkus.http.auth.policy.t5.roles.admin=Admin1 +quarkus.http.auth.permission.t5.paths=/test/roles-allowed-path +quarkus.http.auth.permission.t5.policy=t5 +quarkus.http.auth.policy.t6.roles-allowed=Admin1 +quarkus.http.auth.policy.t6.roles.admin=Admin1 +quarkus.http.auth.policy.t6.roles.root=sudo +quarkus.http.auth.permission.t6.paths=/test/granted-and-checked-by-policy +quarkus.http.auth.permission.t6.policy=t6 +quarkus.http.auth.policy.t7.roles.admin=Admin3 +quarkus.http.auth.policy.t7.permissions.admin=jump +quarkus.http.auth.permission.t7.paths=/test/roles-and-perms-1,/test/roles-and-perms-2 +quarkus.http.auth.permission.t7.policy=t7 \ No newline at end of file diff --git a/extensions/vertx-http/deployment/src/test/resources/conf/mtls/mtls-jks.conf b/extensions/vertx-http/deployment/src/test/resources/conf/mtls/mtls-jks.conf index 8a96de4a4137a..a78a34793096e 100644 --- a/extensions/vertx-http/deployment/src/test/resources/conf/mtls/mtls-jks.conf +++ b/extensions/vertx-http/deployment/src/test/resources/conf/mtls/mtls-jks.conf @@ -3,6 +3,7 @@ quarkus.http.ssl.certificate.key-store-password=secret quarkus.http.ssl.certificate.trust-store-file=server-truststore.jks quarkus.http.ssl.certificate.trust-store-password=password quarkus.http.ssl.client-auth=REQUIRED +quarkus.http.insecure-requests=enabled quarkus.http.auth.permission.default.paths=/* quarkus.http.auth.permission.default.policy=authenticated \ No newline at end of file diff --git a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-configuration.js b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-configuration.js index 0cb7923ad5746..917c076e6e655 100644 --- a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-configuration.js +++ b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-configuration.js @@ -13,6 +13,7 @@ import '@vaadin/integer-field'; import '@vaadin/text-field'; import '@vaadin/select'; import '@vaadin/details'; +import '@vaadin/combo-box'; import { notifier } from 'notifier'; import { unsafeHTML } from 'lit/directives/unsafe-html.js'; import { gridRowDetailsRenderer } from '@vaadin/grid/lit.js'; @@ -84,6 +85,10 @@ export class QwcConfiguration extends observeState(LitElement) { pointer-events: none; opacity: 0.4; } + .config-source-dropdown { + padding-left: 5px; + width: 300px; + } `; static properties = { @@ -93,17 +98,18 @@ export class QwcConfiguration extends observeState(LitElement) { _values: {state: true}, _detailsOpenedItem: {state: true, type: Array}, _busy: {state: true}, - _showOnlyOwnProperties: {state: true}, - _searchTerm: {state: true} + _showOnlyConfigSource: {state: true}, + _searchTerm: {state: true}, + _configSourceSet: {state: true} }; constructor() { super(); - + this._configSourceSet = new Map(); this._detailsOpenedItem = []; this._busy = null; - this._showOnlyOwnProperties = false; + this._showOnlyConfigSource = null; this._searchTerm = ''; } @@ -118,12 +124,39 @@ export class QwcConfiguration extends observeState(LitElement) { this._allConfiguration = e.result; this._visibleConfiguration = e.result; this._filtered = e.result; + + for (const configItem of this._allConfiguration) { + let configSourceName = this._getConfigSourceName(configItem.configValue); + if(configSourceName && !this._configSourceSet.has(configSourceName)){ + this._configSourceSet.set(configSourceName, this._createConfigSourceObject(configSourceName, configItem.configValue)); + } + } }); this.jsonRpc.getAllValues().then(e => { this._values = e.result; }); } + _getConfigSourceName(configValue){ + if(configValue.sourceName){ + return configValue.configSourceName; + } + return null; + } + + _createConfigSourceObject(configSourceName,configValue){ + + let displayName = configSourceName; + + if(configSourceName.startsWith("PropertiesConfigSource[source") + && configSourceName.endsWith("/application.properties]")){ + displayName = "My properties"; + } + + let configSourceObject = {name:configSourceName, display: displayName, position:configValue.configSourcePosition, ordinal:configValue.configSourceOrdinal}; + return configSourceObject; + } + render() { if (this._filtered && this._values) { return this._render(); @@ -177,25 +210,31 @@ export class QwcConfiguration extends observeState(LitElement) { ${this._filtered.length} - - + + + ${this._renderGrid()} `; } - _toggleShowOnlyOwnProperties(onlyMine){ - this._showOnlyOwnProperties = onlyMine; - if(this._showOnlyOwnProperties){ + _toggleFilterByConfigSource(event){ + if(event.target.value){ + this._showOnlyConfigSource = event.target.value; this._visibleConfiguration = this._allConfiguration.filter((prop) => { - return (prop.configValue.sourceName && prop.configValue.sourceName.startsWith("PropertiesConfigSource[source") - && prop.configValue.sourceName.endsWith("/application.properties]")); + return prop.configValue.sourceName && prop.configValue.sourceName === this._showOnlyConfigSource; }); - }else { + }else{ + this._showOnlyConfigSource = null; this._visibleConfiguration = this._allConfiguration; } return this._filterGrid(); @@ -383,7 +422,11 @@ export class QwcConfiguration extends observeState(LitElement) { if (prop.defaultValue) { def = "Default value: " + prop.defaultValue; } - let src = "Config source: " + prop.configValue.sourceName; + let configSourceName = "Unknown"; + if(prop.configValue.sourceName){ + configSourceName = prop.configValue.sourceName; + } + let src = "Config source: " + configSourceName; return html`

    ${unsafeHTML(prop.description)}

    diff --git a/extensions/vertx-http/dev-ui-tests/src/main/java/io/quarkus/devui/tests/DevUIJsonRPCTest.java b/extensions/vertx-http/dev-ui-tests/src/main/java/io/quarkus/devui/tests/DevUIJsonRPCTest.java index 3a18e793e5847..45791f5e27b86 100644 --- a/extensions/vertx-http/dev-ui-tests/src/main/java/io/quarkus/devui/tests/DevUIJsonRPCTest.java +++ b/extensions/vertx-http/dev-ui-tests/src/main/java/io/quarkus/devui/tests/DevUIJsonRPCTest.java @@ -59,7 +59,7 @@ public T executeJsonRPCMethod(TypeReference typeReference, String methodName } @SuppressWarnings("unchecked") - public T executeJsonRPCMethod(TypeReference typeReference, String methodName, Map params) + public T executeJsonRPCMethod(TypeReference typeReference, String methodName, Map params) throws Exception { int id = sendRequest(methodName, params); T response = getJsonRPCResponse(typeReference, id); @@ -71,7 +71,7 @@ public JsonNode executeJsonRPCMethod(String methodName) throws Exception { return executeJsonRPCMethod(methodName, null); } - public JsonNode executeJsonRPCMethod(String methodName, Map params) throws Exception { + public JsonNode executeJsonRPCMethod(String methodName, Map params) throws Exception { return executeJsonRPCMethod(JsonNode.class, methodName, params); } @@ -80,7 +80,7 @@ public T executeJsonRPCMethod(Class classType, String methodName) throws } @SuppressWarnings("unchecked") - public T executeJsonRPCMethod(Class classType, String methodName, Map params) throws Exception { + public T executeJsonRPCMethod(Class classType, String methodName, Map params) throws Exception { int id = sendRequest(methodName, params); T response = getJsonRPCResponse(classType, id); @@ -168,7 +168,7 @@ private JsonNode objectResultFromJsonRPC(int id, int loopCount) throws Interrupt } } - private String createJsonRPCRequest(int id, String methodName, Map params) throws IOException { + private String createJsonRPCRequest(int id, String methodName, Map params) throws IOException { ObjectNode request = mapper.createObjectNode(); @@ -177,15 +177,16 @@ private String createJsonRPCRequest(int id, String methodName, Map p : params.entrySet()) { - jsonParams.put(p.getKey(), p.getValue()); + for (Map.Entry p : params.entrySet()) { + JsonNode convertValue = mapper.convertValue(p.getValue(), JsonNode.class); + jsonParams.putIfAbsent(p.getKey(), convertValue); } } request.set("params", jsonParams); return mapper.writerWithDefaultPrettyPrinter().writeValueAsString(request); } - private int sendRequest(String methodName, Map params) throws IOException { + private int sendRequest(String methodName, Map params) throws IOException { int id = random.nextInt(Integer.MAX_VALUE); String request = createJsonRPCRequest(id, methodName, params); log.debug("request = " + request); diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/AuthRuntimeConfig.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/AuthRuntimeConfig.java index 8552e30ef6b6b..f601117ff07e9 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/AuthRuntimeConfig.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/AuthRuntimeConfig.java @@ -1,5 +1,6 @@ package io.quarkus.vertx.http.runtime; +import java.nio.file.Path; import java.util.Map; import java.util.Optional; @@ -24,6 +25,16 @@ public class AuthRuntimeConfig { @ConfigItem(name = "policy") public Map rolePolicy; + /** + * Properties file containing the client certificate common name (CN) to role mappings. + * Use it only if the mTLS authentication mechanism is enabled with either + * `quarkus.http.ssl.client-auth=required` or `quarkus.http.ssl.client-auth=request`. + *

    + * Properties file is expected to have the `CN=role1,role,...,roleN` format and should be encoded using UTF-8. + */ + @ConfigItem + public Optional certificateRoleProperties; + /** * The authentication realm */ diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/CertificateConfig.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/CertificateConfig.java index 9666a72d9f2db..377a59dc44a70 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/CertificateConfig.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/CertificateConfig.java @@ -14,7 +14,6 @@ * Provide either the certificate and key files or a keystore. */ @ConfigGroup -@SuppressWarnings("OptionalUsedAsFieldOrParameterType") public class CertificateConfig { /** diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpBuildTimeConfig.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpBuildTimeConfig.java index c1a2819bd3a88..5ca439bd79db9 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpBuildTimeConfig.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpBuildTimeConfig.java @@ -28,7 +28,11 @@ public class HttpBuildTimeConfig { /** * Configures the engine to require/request client authentication. - * NONE, REQUEST, REQUIRED + * {@code NONE, REQUEST, REQUIRED}. + *

    + * When set to {@code REQUIRED}, it's recommended to also set `quarkus.http.insecure-requests=disabled` to disable the + * plain HTTP port. If `quarkus.http.insecure-requests` is not set, but this parameter is set to {@code REQUIRED}, then, + * `quarkus.http.insecure-requests` is automatically set to `disabled`. */ @ConfigItem(name = "ssl.client-auth", defaultValue = "NONE") public ClientAuth tlsClientAuth; @@ -43,7 +47,7 @@ public class HttpBuildTimeConfig { /** * A common root path for non-application endpoints. Various extension-provided endpoints such as metrics, health, * and openapi are deployed under this path by default. - * + *

    * * Relative path (Default, `q`) -> * Non-application endpoints will be served from * `${quarkus.http.root-path}/${quarkus.http.non-application-root-path}`. @@ -51,7 +55,7 @@ public class HttpBuildTimeConfig { * Non-application endpoints will be served from the specified path. * * `${quarkus.http.root-path}` -> Setting this path to the same value as HTTP root path disables * this root path. All extension-provided endpoints will be served from `${quarkus.http.root-path}`. - * + *

    * If the management interface is enabled, the root path for the endpoints exposed on the management interface * is configured using the `quarkus.management.root-path` property instead of this property. * @@ -69,7 +73,7 @@ public class HttpBuildTimeConfig { /** * If enabled then the response body is compressed if the {@code Content-Type} header is set and the value is a compressed * media type as configured via {@link #compressMediaTypes}. - * + *

    * Note that the RESTEasy Reactive and Reactive Routes extensions also make it possible to enable/disable compression * declaratively using the annotations {@link io.quarkus.vertx.http.Compressed} and * {@link io.quarkus.vertx.http.Uncompressed}. @@ -79,7 +83,7 @@ public class HttpBuildTimeConfig { /** * When enabled, vert.x will decompress the request's body if it's compressed. - * + *

    * Note that the compression format (e.g., gzip) must be specified in the Content-Encoding header * in the request. */ 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 e726692e1952b..d7b8c233b5de6 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 @@ -39,9 +39,9 @@ public class HttpConfiguration { /** * The HTTP host - * + *

    * In dev/test mode this defaults to localhost, in prod mode this defaults to 0.0.0.0 - * + *

    * Defaulting to 0.0.0.0 makes it easier to deploy Quarkus to container, however it * is not suitable for dev/test mode as other people on the network can connect to your * development machine. @@ -86,13 +86,17 @@ public class HttpConfiguration { * then http works as normal. {@code redirect} will still open the http port, but * all requests will be redirected to the HTTPS port. {@code disabled} will prevent the HTTP * port from opening at all. + *

    + * Default is {@code enabled} except when client auth is set to {@code required} (configured using + * {@code quarkus.http.ssl.client-auth=required}). + * In this case, the default is {@code disabled}. */ - @ConfigItem(defaultValue = "enabled") - public InsecureRequests insecureRequests; + @ConfigItem + public Optional insecureRequests; /** * If this is true (the default) then HTTP/2 will be enabled. - * + *

    * Note that for browsers to be able to use it HTTPS must be enabled, * and you must be running on JDK11 or above, as JDK8 does not support * ALPN. @@ -134,7 +138,7 @@ public class HttpConfiguration { * The number if IO threads used to perform IO. This will be automatically set to a reasonable value based on * the number of CPU cores if it is not provided. If this is set to a higher value than the number of Vert.x event * loops then it will be capped at the number of event loops. - * + *

    * In general this should be controlled by setting quarkus.vertx.event-loops-pool-size, this setting should only * be used if you want to limit the number of HTTP io threads to a smaller number than the total number of IO threads. */ @@ -156,7 +160,6 @@ public class HttpConfiguration { * Http connection read timeout for blocking IO. This is the maximum amount of time * a thread will wait for data, before an IOException will be thrown and the connection * closed. - * */ @ConfigItem(defaultValue = "60s", name = "read-timeout") public Duration readTimeout; @@ -169,7 +172,7 @@ public class HttpConfiguration { /** * The encryption key that is used to store persistent logins (e.g. for form auth). Logins are stored in a persistent * cookie that is encrypted with AES-256 using a key derived from a SHA-256 hash of the key that is provided here. - * + *

    * If no key is provided then an in-memory one will be generated, this will change on every restart though so it * is not suitable for production environments. This must be more than 16 characters long for security reasons */ @@ -228,7 +231,7 @@ public class HttpConfiguration { /** * If this is true then the request start time will be recorded to enable logging of total request time. - * + *

    * This has a small performance penalty, so is disabled by default. */ @ConfigItem 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 6977b99f08770..be472e3ea47b7 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 @@ -21,6 +21,15 @@ public class PolicyConfig { @ConvertWith(TrimmedStringConverter.class) public List rolesAllowed; + /** + * Add roles granted to the `SecurityIdentity` based on the roles that the `SecurityIdentity` already have. + * For example, the Quarkus OIDC extension can map roles from the verified JWT access token, and you may want + * to remap them to a deployment specific roles. + */ + @ConfigDocMapKey("role1") + @ConfigItem + public Map> roles; + /** * Permissions granted to the `SecurityIdentity` if this policy is applied successfully * (the policy allows request to proceed) and the authenticated request has required role. diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/PolicyMappingConfig.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/PolicyMappingConfig.java index d78640f6913f6..68e0a2088939e 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/PolicyMappingConfig.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/PolicyMappingConfig.java @@ -20,7 +20,7 @@ public class PolicyMappingConfig { /** * The HTTP policy that this permission set is linked to. * - * There are 3 built in policies: permit, deny and authenticated. Role based + * There are three built-in policies: permit, deny and authenticated. Role based * policies can be defined, and extensions can add their own policies. */ @ConfigItem diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxHttpRecorder.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxHttpRecorder.java index a9d899f379bf2..5978fa7d2e819 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxHttpRecorder.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxHttpRecorder.java @@ -1,26 +1,17 @@ package io.quarkus.vertx.http.runtime; import static io.quarkus.vertx.core.runtime.context.VertxContextSafetyToggle.setContextSafe; +import static io.quarkus.vertx.http.runtime.options.HttpServerOptionsUtils.getInsecureRequestStrategy; import java.io.File; import java.io.IOException; import java.net.BindException; import java.net.URI; import java.net.URISyntaxException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.TreeMap; +import java.util.*; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicInteger; -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; +import java.util.function.*; import java.util.regex.Pattern; import jakarta.enterprise.event.Event; @@ -44,12 +35,7 @@ import io.quarkus.netty.runtime.virtual.VirtualAddress; import io.quarkus.netty.runtime.virtual.VirtualChannel; import io.quarkus.netty.runtime.virtual.VirtualServerChannel; -import io.quarkus.runtime.LaunchMode; -import io.quarkus.runtime.LiveReloadConfig; -import io.quarkus.runtime.QuarkusBindException; -import io.quarkus.runtime.RuntimeValue; -import io.quarkus.runtime.ShutdownContext; -import io.quarkus.runtime.ThreadPoolConfig; +import io.quarkus.runtime.*; import io.quarkus.runtime.annotations.Recorder; import io.quarkus.runtime.configuration.ConfigInstantiator; import io.quarkus.runtime.configuration.ConfigUtils; @@ -75,22 +61,8 @@ import io.quarkus.vertx.http.runtime.options.HttpServerCommonHandlers; import io.quarkus.vertx.http.runtime.options.HttpServerOptionsUtils; import io.smallrye.common.vertx.VertxContext; -import io.vertx.core.AbstractVerticle; -import io.vertx.core.AsyncResult; -import io.vertx.core.Context; -import io.vertx.core.DeploymentOptions; -import io.vertx.core.Handler; -import io.vertx.core.Promise; -import io.vertx.core.Verticle; -import io.vertx.core.Vertx; -import io.vertx.core.http.Cookie; -import io.vertx.core.http.CookieSameSite; -import io.vertx.core.http.HttpConnection; -import io.vertx.core.http.HttpHeaders; -import io.vertx.core.http.HttpMethod; -import io.vertx.core.http.HttpServer; -import io.vertx.core.http.HttpServerOptions; -import io.vertx.core.http.HttpServerRequest; +import io.vertx.core.*; +import io.vertx.core.http.*; import io.vertx.core.http.impl.Http1xServerConnection; import io.vertx.core.impl.ContextInternal; import io.vertx.core.impl.EventLoopContext; @@ -270,6 +242,7 @@ public static void startServerAfterFailedStart() { } rootHandler = root; + var insecureRequestStrategy = getInsecureRequestStrategy(buildConfig, config.insecureRequests); //we can't really do doServerStart(vertx, buildConfig, managementBuildTimeConfig, null, config, managementConfig, LaunchMode.DEVELOPMENT, new Supplier() { @@ -277,7 +250,7 @@ public static void startServerAfterFailedStart() { public Integer get() { return ProcessorInfo.availableProcessors(); //this is dev mode, so the number of IO threads not always being 100% correct does not really matter in this case } - }, null, false); + }, null, insecureRequestStrategy, false); } catch (Exception e) { throw new RuntimeException(e); } @@ -324,8 +297,11 @@ public void startServer(Supplier vertx, ShutdownContext shutdown, || managementConfig.hostEnabled || managementConfig.domainSocketEnabled)) { // Start the server if (closeTask == null) { + var insecureRequestStrategy = getInsecureRequestStrategy(httpBuildTimeConfig, + httpConfiguration.insecureRequests); doServerStart(vertx.get(), httpBuildTimeConfig, managementBuildTimeConfig, managementRouter, httpConfiguration, managementConfig, launchMode, ioThreads, websocketSubProtocols, + insecureRequestStrategy, auxiliaryApplication); if (launchMode != LaunchMode.DEVELOPMENT) { shutdown.addShutdownTask(closeTask); @@ -652,7 +628,8 @@ private static CompletableFuture initializeManagementInterface(Vertx private static CompletableFuture initializeMainHttpServer(Vertx vertx, HttpBuildTimeConfig httpBuildTimeConfig, HttpConfiguration httpConfiguration, LaunchMode launchMode, - Supplier eventLoops, List websocketSubProtocols) throws IOException { + Supplier eventLoops, List websocketSubProtocols, InsecureRequests insecureRequestStrategy) + throws IOException { if (!httpConfiguration.hostEnabled && !httpConfiguration.domainSocketEnabled) { return CompletableFuture.completedFuture(null); @@ -691,9 +668,9 @@ private static CompletableFuture initializeMainHttpServer(Vertx vertx, H } httpMainSslServerOptions = tmpSslConfig; - if (httpConfiguration.insecureRequests != HttpConfiguration.InsecureRequests.ENABLED + if (insecureRequestStrategy != HttpConfiguration.InsecureRequests.ENABLED && httpMainSslServerOptions == null) { - throw new IllegalStateException("Cannot set quarkus.http.redirect-insecure-requests without enabling SSL."); + throw new IllegalStateException("Cannot set quarkus.http.insecure-requests without enabling SSL."); } int eventLoopCount = eventLoops.get(); @@ -713,7 +690,7 @@ private static CompletableFuture initializeMainHttpServer(Vertx vertx, H public Verticle get() { return new WebDeploymentVerticle(httpMainServerOptions, httpMainSslServerOptions, httpMainDomainSocketOptions, launchMode, - httpConfiguration.insecureRequests, httpConfiguration, connectionCount); + insecureRequestStrategy, httpConfiguration, connectionCount); } }, new DeploymentOptions().setInstances(ioThreads), new Handler>() { @Override @@ -725,11 +702,11 @@ public void handle(AsyncResult event) { if ((httpMainSslServerOptions == null) && (httpMainServerOptions != null)) { portsUsed = List.of(httpMainServerOptions.getPort()); - } else if ((httpConfiguration.insecureRequests == InsecureRequests.DISABLED) + } else if ((insecureRequestStrategy == InsecureRequests.DISABLED) && (httpMainSslServerOptions != null)) { portsUsed = List.of(httpMainSslServerOptions.getPort()); } else if ((httpMainSslServerOptions != null) - && (httpConfiguration.insecureRequests == InsecureRequests.ENABLED) + && (insecureRequestStrategy == InsecureRequests.ENABLED) && (httpMainServerOptions != null)) { portsUsed = List.of(httpMainServerOptions.getPort(), httpMainSslServerOptions.getPort()); } @@ -750,10 +727,12 @@ private static void doServerStart(Vertx vertx, HttpBuildTimeConfig httpBuildTime ManagementInterfaceBuildTimeConfig managementBuildTimeConfig, Handler managementRouter, HttpConfiguration httpConfiguration, ManagementInterfaceConfiguration managementConfig, LaunchMode launchMode, - Supplier eventLoops, List websocketSubProtocols, boolean auxiliaryApplication) throws IOException { + Supplier eventLoops, List websocketSubProtocols, + InsecureRequests insecureRequestStrategy, + boolean auxiliaryApplication) throws IOException { var mainServerFuture = initializeMainHttpServer(vertx, httpBuildTimeConfig, httpConfiguration, launchMode, eventLoops, - websocketSubProtocols); + websocketSubProtocols, insecureRequestStrategy); var managementInterfaceFuture = initializeManagementInterface(vertx, managementBuildTimeConfig, managementRouter, managementConfig, launchMode, websocketSubProtocols); var managementInterfaceDomainSocketFuture = initializeManagementInterfaceWithDomainSocket(vertx, @@ -842,18 +821,19 @@ public void handle(AsyncResult event) { throw new RuntimeException("Unable to start HTTP server", e); } - setHttpServerTiming(httpConfiguration.insecureRequests, httpMainServerOptions, httpMainSslServerOptions, + setHttpServerTiming(insecureRequestStrategy == InsecureRequests.DISABLED, httpMainServerOptions, + httpMainSslServerOptions, httpMainDomainSocketOptions, auxiliaryApplication, httpManagementServerOptions); } - private static void setHttpServerTiming(InsecureRequests insecureRequests, HttpServerOptions httpServerOptions, + private static void setHttpServerTiming(boolean httpDisabled, HttpServerOptions httpServerOptions, HttpServerOptions sslConfig, HttpServerOptions domainSocketOptions, boolean auxiliaryApplication, HttpServerOptions managementConfig) { StringBuilder serverListeningMessage = new StringBuilder("Listening on: "); int socketCount = 0; - if (httpServerOptions != null && !InsecureRequests.DISABLED.equals(insecureRequests)) { + if (!httpDisabled && httpServerOptions != null) { serverListeningMessage.append(String.format( "http://%s:%s", httpServerOptions.getHost(), actualHttpPort)); socketCount++; diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/options/HttpServerOptionsUtils.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/options/HttpServerOptionsUtils.java index 36239a6ceaa1f..2895ca2149df0 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/options/HttpServerOptionsUtils.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/options/HttpServerOptionsUtils.java @@ -12,6 +12,8 @@ import java.util.Optional; import java.util.concurrent.TimeUnit; +import org.jboss.logging.Logger; + import io.quarkus.credentials.CredentialsProvider; import io.quarkus.credentials.runtime.CredentialsProviderFinder; import io.quarkus.runtime.LaunchMode; @@ -23,6 +25,7 @@ import io.quarkus.vertx.http.runtime.management.ManagementInterfaceBuildTimeConfig; import io.quarkus.vertx.http.runtime.management.ManagementInterfaceConfiguration; import io.vertx.core.buffer.Buffer; +import io.vertx.core.http.ClientAuth; import io.vertx.core.http.Http2Settings; import io.vertx.core.http.HttpServerOptions; import io.vertx.core.http.HttpVersion; @@ -428,4 +431,25 @@ private static void setIdleTimeout(HttpConfiguration httpConfiguration, HttpServ options.setIdleTimeout(idleTimeout); options.setIdleTimeoutUnit(TimeUnit.MILLISECONDS); } + + public static HttpConfiguration.InsecureRequests getInsecureRequestStrategy(HttpBuildTimeConfig buildTimeConfig, + Optional requests) { + if (requests.isPresent()) { + var value = requests.get(); + if (buildTimeConfig.tlsClientAuth == ClientAuth.REQUIRED && value == HttpConfiguration.InsecureRequests.ENABLED) { + Logger.getLogger(HttpServerOptionsUtils.class).warn( + "When configuring TLS client authentication to be required, it is recommended to **NOT** set `quarkus.http.insecure-requests` to `enabled`. " + + + "You can switch to `redirect` by setting `quarkus.http.insecure-requests=redirect`."); + } + return value; + } + if (buildTimeConfig.tlsClientAuth == ClientAuth.REQUIRED) { + Logger.getLogger(HttpServerOptionsUtils.class).info( + "TLS client authentication is required, thus disabling insecure requests. " + + "You can switch to `redirect` by setting `quarkus.http.insecure-requests=redirect`."); + return HttpConfiguration.InsecureRequests.DISABLED; + } + return HttpConfiguration.InsecureRequests.ENABLED; + } } 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 3371e6c365162..a1052fe8649d0 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 @@ -162,11 +162,12 @@ private static Map toNamedHttpSecPolicies(Map e : rolePolicies.entrySet()) { - PolicyConfig policyConfig = e.getValue(); + final PolicyConfig policyConfig = e.getValue(); + final Map> roleToPermissions; if (policyConfig.permissions.isEmpty()) { - namedPolicies.put(e.getKey(), new RolesAllowedHttpSecurityPolicy(policyConfig.rolesAllowed)); + roleToPermissions = null; } else { - final Map> roleToPermissions = new HashMap<>(); + roleToPermissions = new HashMap<>(); for (Map.Entry> roleToPermissionStr : policyConfig.permissions.entrySet()) { // collect permission actions @@ -190,9 +191,9 @@ private static Map toNamedHttpSecPolicies(Map mtls = Arc.container().instance(MtlsAuthenticationMechanism.class); + + if (mtls.isAvailable() && config.auth.certificateRoleProperties.isPresent()) { + Path rolesPath = config.auth.certificateRoleProperties.get(); + URL rolesResource = null; + if (Files.exists(rolesPath)) { + try { + rolesResource = rolesPath.toUri().toURL(); + } catch (MalformedURLException e) { + // The Files.exists(rolesPath) check has succeeded therefore this exception can't happen in this case + } + } else { + rolesResource = Thread.currentThread().getContextClassLoader().getResource(rolesPath.toString()); + } + if (rolesResource == null) { + throw new ConfigurationException( + "quarkus.http.auth.certificate-role-properties location can not be resolved", + Set.of("quarkus.http.auth.certificate-role-properties")); + } + + try (Reader reader = new BufferedReader( + new InputStreamReader(rolesResource.openStream(), StandardCharsets.UTF_8))) { + Properties rolesProps = new Properties(); + rolesProps.load(reader); + + Map> roles = new HashMap<>(); + for (Map.Entry e : rolesProps.entrySet()) { + log.debugf("Added role mapping for %s:%s", e.getKey(), e.getValue()); + roles.put((String) e.getKey(), parseRoles((String) e.getValue())); + } + + mtls.get().setRoleMappings(roles); + } catch (Exception e) { + log.warnf("Unable to read roles mappings from %s:%s", rolesPath, e.getMessage()); + } + } + } + + private static Set parseRoles(String value) { + Set roles = new HashSet<>(); + for (String s : value.split(",")) { + roles.add(s.trim()); + } + return Set.copyOf(roles); + } } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/ImmutablePathMatcher.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/ImmutablePathMatcher.java index 1778c24d81a94..45a2fcea2dac1 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/ImmutablePathMatcher.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/ImmutablePathMatcher.java @@ -150,7 +150,11 @@ public ImmutablePathMatcher build() { if (p.prefixPathHandler != null) { handler = p.prefixPathHandler; if (STRING_PATH_SEPARATOR.equals(p.path)) { - defaultHandler = p.prefixPathHandler; + if (defaultHandler == null) { + defaultHandler = p.prefixPathHandler; + } else { + handlerAccumulator.accept(defaultHandler, p.prefixPathHandler); + } } } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/MtlsAuthenticationMechanism.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/MtlsAuthenticationMechanism.java index 32ad48bdf07fe..ee95746c7006c 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/MtlsAuthenticationMechanism.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/MtlsAuthenticationMechanism.java @@ -20,6 +20,7 @@ import java.security.cert.Certificate; import java.security.cert.X509Certificate; import java.util.Collections; +import java.util.Map; import java.util.Set; import javax.net.ssl.SSLPeerUnverifiedException; @@ -38,6 +39,8 @@ * The authentication handler responsible for mTLS client authentication */ public class MtlsAuthenticationMechanism implements HttpAuthenticationMechanism { + private static final String ROLES_ATTRIBUTE = "roles"; + Map> roles = Map.of(); @Override public Uni authenticate(RoutingContext context, @@ -56,9 +59,12 @@ public Uni authenticate(RoutingContext context, return Uni.createFrom().nullItem(); } context.put(HttpAuthenticationMechanism.class.getName(), this); + + AuthenticationRequest authRequest = new CertificateAuthenticationRequest( + new CertificateCredential(X509Certificate.class.cast(certificate))); + authRequest.setAttribute(ROLES_ATTRIBUTE, roles); return identityProviderManager - .authenticate(HttpSecurityUtils.setRoutingContextAttribute(new CertificateAuthenticationRequest( - new CertificateCredential(X509Certificate.class.cast(certificate))), context)); + .authenticate(HttpSecurityUtils.setRoutingContextAttribute(authRequest, context)); } @Override @@ -76,4 +82,8 @@ public Set> getCredentialTypes() { public Uni getCredentialTransport(RoutingContext context) { return Uni.createFrom().item(new HttpCredentialTransport(HttpCredentialTransport.Type.X509, "X509")); } + + void setRoleMappings(Map> roles) { + this.roles = Collections.unmodifiableMap(roles); + } } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/RolesAllowedHttpSecurityPolicy.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/RolesAllowedHttpSecurityPolicy.java index 4b96c48c8786f..52061b6c41184 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/RolesAllowedHttpSecurityPolicy.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/RolesAllowedHttpSecurityPolicy.java @@ -17,34 +17,30 @@ * permission checker that handles role based permissions */ public class RolesAllowedHttpSecurityPolicy implements HttpSecurityPolicy { - private List rolesAllowed; + private static final String AUTHENTICATED = "**"; + private final String[] rolesAllowed; private final boolean grantPermissions; + private final boolean grantRoles; private final Map> roleToPermissions; - - public RolesAllowedHttpSecurityPolicy(List rolesAllowed) { - this.rolesAllowed = rolesAllowed; - this.grantPermissions = false; - this.roleToPermissions = null; - } - - public RolesAllowedHttpSecurityPolicy() { - this.grantPermissions = false; - this.roleToPermissions = null; - } - - public RolesAllowedHttpSecurityPolicy(List rolesAllowed, Map> roleToPermissions) { - this.rolesAllowed = rolesAllowed; - this.grantPermissions = true; - this.roleToPermissions = roleToPermissions; - } - - public List getRolesAllowed() { - return rolesAllowed; - } - - public RolesAllowedHttpSecurityPolicy setRolesAllowed(List rolesAllowed) { - this.rolesAllowed = rolesAllowed; - return this; + private final Map> roleToRoles; + + public RolesAllowedHttpSecurityPolicy(List rolesAllowed, Map> roleToPermissions, + Map> roleToRoles) { + if (roleToPermissions != null && !roleToPermissions.isEmpty()) { + this.grantPermissions = true; + this.roleToPermissions = Map.copyOf(roleToPermissions); + } else { + this.grantPermissions = false; + this.roleToPermissions = null; + } + if (roleToRoles != null && !roleToRoles.isEmpty()) { + this.grantRoles = true; + this.roleToRoles = Map.copyOf(roleToRoles); + } else { + this.grantRoles = false; + this.roleToRoles = null; + } + this.rolesAllowed = rolesAllowed.toArray(String[]::new); } @Override @@ -53,12 +49,19 @@ public Uni checkPermission(RoutingContext request, Uni() { @Override public CheckResult apply(SecurityIdentity securityIdentity) { - for (String i : rolesAllowed) { - if (securityIdentity.hasRole(i) || ("**".equals(i) && !securityIdentity.isAnonymous())) { - if (grantPermissions) { - // permit access and add augment security identity with additional permissions - return grantPermissions(securityIdentity); + if (grantPermissions || grantRoles) { + SecurityIdentity augmented = augmentIdentity(securityIdentity); + if (augmented != null) { + for (String i : rolesAllowed) { + if (augmented.hasRole(i) || (AUTHENTICATED.equals(i) && !augmented.isAnonymous())) { + return new CheckResult(true, augmented); + } } + return CheckResult.DENY; + } + } + for (String i : rolesAllowed) { + if (securityIdentity.hasRole(i) || (AUTHENTICATED.equals(i) && !securityIdentity.isAnonymous())) { return CheckResult.PERMIT; } } @@ -67,23 +70,36 @@ public CheckResult apply(SecurityIdentity securityIdentity) { }); } - private CheckResult grantPermissions(SecurityIdentity securityIdentity) { + private SecurityIdentity augmentIdentity(SecurityIdentity securityIdentity) { Set roles = securityIdentity.getRoles(); if (roles != null && !roles.isEmpty()) { - Set permissions = new HashSet<>(); + Set permissions = grantPermissions ? new HashSet<>() : null; + Set newRoles = grantRoles ? new HashSet<>() : null; for (String role : roles) { - if (roleToPermissions.containsKey(role)) { - permissions.addAll(roleToPermissions.get(role)); + if (grantPermissions) { + if (roleToPermissions.containsKey(role)) { + permissions.addAll(roleToPermissions.get(role)); + } + } + if (grantRoles) { + if (roleToRoles.containsKey(role)) { + newRoles.addAll(roleToRoles.get(role)); + } } } - if (!permissions.isEmpty()) { - return new CheckResult(true, augmentIdentity(securityIdentity, permissions)); + boolean addPerms = grantPermissions && !permissions.isEmpty(); + if (grantRoles && !newRoles.isEmpty()) { + newRoles.addAll(roles); + return augmentIdentity(securityIdentity, permissions, Set.copyOf(newRoles), addPerms); + } else if (addPerms) { + return augmentIdentity(securityIdentity, permissions, roles, true); } } - return CheckResult.PERMIT; + return null; } - private static SecurityIdentity augmentIdentity(SecurityIdentity securityIdentity, Set permissions) { + private static SecurityIdentity augmentIdentity(SecurityIdentity securityIdentity, Set permissions, + Set roles, boolean addPerms) { return new SecurityIdentity() { @Override public Principal getPrincipal() { @@ -97,12 +113,12 @@ public boolean isAnonymous() { @Override public Set getRoles() { - return securityIdentity.getRoles(); + return roles; } @Override public boolean hasRole(String s) { - return securityIdentity.hasRole(s); + return roles.contains(s); } @Override @@ -127,9 +143,11 @@ public Map getAttributes() { @Override public Uni checkPermission(Permission requiredPermission) { - for (Permission possessedPermission : permissions) { - if (possessedPermission.implies(requiredPermission)) { - return Uni.createFrom().item(true); + if (addPerms) { + for (Permission possessedPermission : permissions) { + if (possessedPermission.implies(requiredPermission)) { + return Uni.createFrom().item(true); + } } } @@ -138,9 +156,11 @@ public Uni checkPermission(Permission requiredPermission) { @Override public boolean checkPermissionBlocking(Permission requiredPermission) { - for (Permission possessedPermission : permissions) { - if (possessedPermission.implies(requiredPermission)) { - return true; + if (addPerms) { + for (Permission possessedPermission : permissions) { + if (possessedPermission.implies(requiredPermission)) { + return true; + } } } diff --git a/extensions/vertx-http/runtime/src/test/java/io/quarkus/vertx/http/runtime/PathMatcherTest.java b/extensions/vertx-http/runtime/src/test/java/io/quarkus/vertx/http/runtime/PathMatcherTest.java index 9cc89a3e3bd32..dfbfe2add67ed 100644 --- a/extensions/vertx-http/runtime/src/test/java/io/quarkus/vertx/http/runtime/PathMatcherTest.java +++ b/extensions/vertx-http/runtime/src/test/java/io/quarkus/vertx/http/runtime/PathMatcherTest.java @@ -340,6 +340,27 @@ public void testExactPathHandlerMerging() { assertEquals(1, handler.size()); } + @Test + public void testDefaultHandlerMerging() { + List handler1 = new ArrayList<>(); + handler1.add("Neo"); + List handler2 = new ArrayList<>(); + handler2.add("Trinity"); + List handler3 = new ArrayList<>(); + handler3.add("Morpheus"); + var matcher = ImmutablePathMatcher.> builder().handlerAccumulator(List::addAll) + .addPath("/*", handler1).addPath("/*", handler2) + .addPath("/", handler3).build(); + var handler = matcher.match("/default-path-handler").getValue(); + assertNotNull(handler); + assertTrue(handler.contains("Neo")); + assertTrue(handler.contains("Trinity")); + assertEquals(2, handler.size()); + handler = matcher.match("/").getValue(); + assertNotNull(handler); + assertEquals(1, handler.size()); + } + @Test public void testPrefixPathHandlerMerging() { List handler1 = new ArrayList<>(); diff --git a/extensions/vertx/deployment/src/test/java/io/quarkus/vertx/deployment/MessageConsumerFailureTest.java b/extensions/vertx/deployment/src/test/java/io/quarkus/vertx/deployment/MessageConsumerFailureTest.java index c282c6c1bb49a..888acb58d3d07 100644 --- a/extensions/vertx/deployment/src/test/java/io/quarkus/vertx/deployment/MessageConsumerFailureTest.java +++ b/extensions/vertx/deployment/src/test/java/io/quarkus/vertx/deployment/MessageConsumerFailureTest.java @@ -58,6 +58,12 @@ public void testFailure() throws InterruptedException { @Test public void testFailureNoReplyHandler() throws InterruptedException { + verifyFailureNoReply("foo", "Foo is dead", IllegalStateException.class); + verifyFailureNoReply("foo-blocking", "Red is dead", IllegalStateException.class); + } + + void verifyFailureNoReply(String address, String expectedMessage, Class expectedException) + throws InterruptedException { Handler oldHandler = vertx.exceptionHandler(); try { BlockingQueue synchronizer = new LinkedBlockingQueue<>(); @@ -71,10 +77,10 @@ public void handle(Throwable event) { } } }); - eventBus.send("foo", "bar"); + eventBus.send(address, "hello"); Object ret = synchronizer.poll(2, TimeUnit.SECONDS); - assertTrue(ret instanceof IllegalStateException); - assertEquals("Foo is dead", ((IllegalStateException) ret).getMessage()); + assertTrue(expectedException.isAssignableFrom(ret.getClass())); + assertEquals(expectedMessage, ((Throwable) ret).getMessage()); } finally { vertx.exceptionHandler(oldHandler); } diff --git a/extensions/vertx/runtime/src/main/java/io/quarkus/vertx/runtime/VertxEventBusConsumerRecorder.java b/extensions/vertx/runtime/src/main/java/io/quarkus/vertx/runtime/VertxEventBusConsumerRecorder.java index 71adb62608645..6c8e6633064a4 100644 --- a/extensions/vertx/runtime/src/main/java/io/quarkus/vertx/runtime/VertxEventBusConsumerRecorder.java +++ b/extensions/vertx/runtime/src/main/java/io/quarkus/vertx/runtime/VertxEventBusConsumerRecorder.java @@ -37,6 +37,7 @@ import io.smallrye.common.vertx.VertxContext; import io.vertx.core.AsyncResult; import io.vertx.core.Context; +import io.vertx.core.Future; import io.vertx.core.Handler; import io.vertx.core.Vertx; import io.vertx.core.eventbus.EventBus; @@ -147,7 +148,7 @@ public void run() { } }); } else { - dup.executeBlocking(new Callable() { + Future future = dup.executeBlocking(new Callable() { @Override public Void call() { try { @@ -163,6 +164,7 @@ public Void call() { return null; } }, invoker.isOrdered()); + future.onFailure(context::reportException); } } else { // Will run on the context used for the consumer registration. diff --git a/independent-projects/arc/pom.xml b/independent-projects/arc/pom.xml index fd08a481c9233..436b49a31206b 100644 --- a/independent-projects/arc/pom.xml +++ b/independent-projects/arc/pom.xml @@ -37,9 +37,6 @@ UTF-8 - 11 - 11 - 11 4.0.1 @@ -48,16 +45,16 @@ 2.0.1 1.7.0 - 3.1.5 + 3.1.6 3.5.3.Final 2.5.1 1.6.Final 3.24.2 - 5.10.0 - 1.9.10 + 5.10.1 + 1.9.21 1.7.3 - 5.4.0 + 5.8.0 1.7.0.Final 2.0.1 diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanDeployment.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanDeployment.java index f64a96a1c42f5..e96bbbe6df310 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanDeployment.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanDeployment.java @@ -64,6 +64,8 @@ public class BeanDeployment { private final IndexView beanArchiveImmutableIndex; private final IndexView applicationIndex; + private final Predicate applicationClassPredicate; + private final Map qualifiers; private final Map repeatingQualifierAnnotations; private final Map> qualifierNonbindingMembers; @@ -142,6 +144,7 @@ public class BeanDeployment { this.beanArchiveComputingIndex = builder.beanArchiveComputingIndex; this.beanArchiveImmutableIndex = Objects.requireNonNull(builder.beanArchiveImmutableIndex); this.applicationIndex = builder.applicationIndex; + this.applicationClassPredicate = builder.applicationClassPredicate; this.annotationStore = new AnnotationStore(initAndSort(builder.annotationTransformers, buildContext), buildContext); buildContext.putInternal(Key.ANNOTATION_STORE, annotationStore); @@ -283,7 +286,7 @@ BeanRegistrar.RegistrationContext registerBeans(List beanRegistra buildContext.putInternal(Key.INJECTION_POINTS, Collections.unmodifiableList(this.injectionPoints)); if (buildCompatibleExtensions != null) { - buildCompatibleExtensions.runRegistration(beanArchiveComputingIndex, beans, observers); + buildCompatibleExtensions.runRegistration(beanArchiveComputingIndex, beans, interceptors, observers); } return registerSyntheticBeans(beanRegistrars, buildContext); @@ -953,8 +956,14 @@ private List findBeans(Collection beanDefiningAnnotations, Li .map(StereotypeInfo::getName) .collect(Collectors.toSet()); + Set seenClasses = new HashSet<>(); + // If needed use the specialized immutable index to discover beans for (ClassInfo beanClass : beanArchiveImmutableIndex.getKnownClasses()) { + if (!seenClasses.add(beanClass.name())) { + // avoid discovering the same bean twice + continue; + } if (Modifier.isInterface(beanClass.flags()) || Modifier.isAbstract(beanClass.flags()) || beanClass.isAnnotation() || beanClass.isEnum()) { @@ -1141,8 +1150,9 @@ private List findBeans(Collection beanDefiningAnnotations, Li beanClasses.add(beanClass); } } - } else { + } else if (!annotationStore.hasAnnotation(field, DotNames.PRODUCES)) { // Verify that non-producer fields are not annotated with stereotypes + // (vetoed producers must _not_ be checked) for (AnnotationInstance i : annotationStore.getAnnotations(field)) { if (realStereotypes.contains(i.name())) { throw new DefinitionException( @@ -1383,7 +1393,7 @@ private RegistrationContext registerSyntheticBeans(List beanRegis } if (buildCompatibleExtensions != null) { buildCompatibleExtensions.runSynthesis(beanArchiveComputingIndex); - buildCompatibleExtensions.registerSyntheticBeans(context); + buildCompatibleExtensions.registerSyntheticBeans(context, applicationClassPredicate); } this.injectionPoints.addAll(context.syntheticInjectionPoints); return context; @@ -1398,7 +1408,7 @@ io.quarkus.arc.processor.ObserverRegistrar.RegistrationContext registerSynthetic context.extension = null; } if (buildCompatibleExtensions != null) { - buildCompatibleExtensions.registerSyntheticObservers(context); + buildCompatibleExtensions.registerSyntheticObservers(context, applicationClassPredicate); buildCompatibleExtensions.runRegistrationAgain(beanArchiveComputingIndex, beans, observers); } return context; @@ -1432,7 +1442,7 @@ private void addSyntheticObserver(ObserverConfigurator configurator) { configurator.observedQualifiers, Reception.ALWAYS, configurator.transactionPhase, configurator.isAsync, configurator.priority, observerTransformers, buildContext, - jtaCapabilities, configurator.notifyConsumer, configurator.params)); + jtaCapabilities, configurator.notifyConsumer, configurator.params, configurator.forceApplicationClass)); } static void processErrors(List errors) { @@ -1579,6 +1589,29 @@ private void validateBeans(List errors, Consumer } } } + + List>> duplicateBeanIds = beans.stream() + .collect(Collectors.groupingBy(BeanInfo::getIdentifier)) + .entrySet() + .stream() + .filter(entry -> entry.getValue().size() > 1) + .collect(Collectors.toList()); + if (!duplicateBeanIds.isEmpty()) { + String separator = "===================="; + StringBuilder error = new StringBuilder("\n") + .append(separator).append(separator).append(separator).append(separator).append("\n") + .append("Multiple beans with the same identifier found!\n") + .append("----------------------------------------------\n") + .append("This is an internal error. Please report a bug and attach the following listing.\n\n"); + for (Map.Entry> entry : duplicateBeanIds) { + error.append(entry.getKey()).append(" -> ").append(entry.getValue().size()).append(" beans:\n"); + for (BeanInfo bean : entry.getValue()) { + error.append("- ").append(bean).append("\n"); + } + } + error.append(separator).append(separator).append(separator).append(separator).append("\n"); + errors.add(new DeploymentException(error.toString())); + } } private void findNamespaces(BeanInfo bean, Set namespaces) { diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/ContextInstancesGenerator.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/ContextInstancesGenerator.java index 01a7ba04fc80a..81e66fc8f5e0f 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/ContextInstancesGenerator.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/ContextInstancesGenerator.java @@ -5,6 +5,7 @@ import static org.objectweb.asm.Opcodes.ACC_PUBLIC; import static org.objectweb.asm.Opcodes.ACC_VOLATILE; +import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; @@ -13,6 +14,7 @@ import java.util.Set; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Consumer; import java.util.function.Supplier; import org.jboss.jandex.DotName; @@ -88,7 +90,7 @@ Collection generate(DotName scope) { implementRemove(contextInstances, beans, idToField, lockField.getFieldDescriptor()); implementClear(contextInstances, idToField, lockField.getFieldDescriptor()); implementGetAllPresent(contextInstances, idToField, lockField.getFieldDescriptor()); - + implementForEach(contextInstances, idToField, lockField.getFieldDescriptor()); contextInstances.close(); return classOutput.getResources(); @@ -149,6 +151,31 @@ private void implementClear(ClassCreator applicationContextInstances, Map idToField, + FieldDescriptor lockField) { + MethodCreator forEach = contextInstances.getMethodCreator("forEach", void.class, Consumer.class) + .setModifiers(ACC_PUBLIC); + // lock.lock(); + // ContextInstanceHandle copy = this.1; + // lock.unlock(); + // if (copy != null) { + // consumer.accept(copy); + // } + ResultHandle lock = forEach.readInstanceField(lockField, forEach.getThis()); + forEach.invokeInterfaceMethod(MethodDescriptors.LOCK_LOCK, lock); + List results = new ArrayList<>(idToField.size()); + for (FieldDescriptor field : idToField.values()) { + results.add(forEach.readInstanceField(field, forEach.getThis())); + } + forEach.invokeInterfaceMethod(MethodDescriptors.LOCK_UNLOCK, lock); + for (int i = 0; i < results.size(); i++) { + ResultHandle copy = results.get(i); + BytecodeCreator isNotNull = forEach.ifNotNull(copy).trueBranch(); + isNotNull.invokeInterfaceMethod(MethodDescriptors.CONSUMER_ACCEPT, forEach.getMethodParam(0), copy); + } + forEach.returnVoid(); + } + private void implementRemove(ClassCreator contextInstances, List applicationScopedBeans, Map idToField, FieldDescriptor lockField) { MethodCreator remove = contextInstances diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/DecoratorGenerator.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/DecoratorGenerator.java index 6b465ba11a4ec..48c92af011388 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/DecoratorGenerator.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/DecoratorGenerator.java @@ -83,7 +83,8 @@ Collection generate(DecoratorInfo decorator) { return Collections.emptyList(); } - boolean isApplicationClass = applicationClassPredicate.test(decorator.getBeanClass()); + boolean isApplicationClass = applicationClassPredicate.test(decorator.getBeanClass()) + || decorator.isForceApplicationClass(); ResourceClassOutput classOutput = new ResourceClassOutput(isApplicationClass, name -> name.equals(generatedName) ? SpecialType.DECORATOR_BEAN : null, generateSources); diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/InterceptorGenerator.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/InterceptorGenerator.java index 57295af6ab5fc..5dfac488240a6 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/InterceptorGenerator.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/InterceptorGenerator.java @@ -108,7 +108,7 @@ private Collection generateSyntheticInterceptor(InterceptorInfo interc return Collections.emptyList(); } - boolean isApplicationClass = applicationClassPredicate.test(creatorClassName); + boolean isApplicationClass = applicationClassPredicate.test(creatorClassName) || interceptor.isForceApplicationClass(); ResourceClassOutput classOutput = new ResourceClassOutput(isApplicationClass, name -> name.equals(generatedName) ? SpecialType.INTERCEPTOR_BEAN : null, generateSources); @@ -168,7 +168,8 @@ private Collection generateClassInterceptor(InterceptorInfo intercepto return Collections.emptyList(); } - boolean isApplicationClass = applicationClassPredicate.test(interceptor.getBeanClass()); + boolean isApplicationClass = applicationClassPredicate.test(interceptor.getBeanClass()) + || interceptor.isForceApplicationClass(); ResourceClassOutput classOutput = new ResourceClassOutput(isApplicationClass, name -> name.equals(generatedName) ? SpecialType.INTERCEPTOR_BEAN : null, generateSources); diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/MethodDescriptors.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/MethodDescriptors.java index 8cc52eb815fb9..79b2f7af860b2 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/MethodDescriptors.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/MethodDescriptors.java @@ -11,6 +11,7 @@ import java.util.Set; import java.util.concurrent.locks.Lock; import java.util.function.BiFunction; +import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; @@ -63,6 +64,9 @@ public final class MethodDescriptors { public static final MethodDescriptor SUPPLIER_GET = MethodDescriptor.ofMethod(Supplier.class, "get", Object.class); + public static final MethodDescriptor CONSUMER_ACCEPT = MethodDescriptor.ofMethod(Consumer.class, "accept", + void.class, Object.class); + public static final MethodDescriptor CREATIONAL_CTX_CHILD = MethodDescriptor.ofMethod(CreationalContextImpl.class, "child", CreationalContextImpl.class, CreationalContext.class); diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/ObserverConfigurator.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/ObserverConfigurator.java index 4ab900d7b8d24..c65cf58bd845b 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/ObserverConfigurator.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/ObserverConfigurator.java @@ -35,6 +35,7 @@ public final class ObserverConfigurator extends ConfiguratorBase notifyConsumer; + boolean forceApplicationClass; public ObserverConfigurator(Consumer consumer) { this.consumer = consumer; @@ -118,6 +119,15 @@ public ObserverConfigurator notify(Consumer notifyConsumer) { return this; } + /** + * Forces the observer to be considered an 'application class', so it will be defined in the runtime + * ClassLoader and re-created on each redeployment. + */ + public ObserverConfigurator forceApplicationClass() { + this.forceApplicationClass = true; + return this; + } + public void done() { if (beanClass == null) { throw new IllegalStateException("Observer bean class must be set!"); diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/ObserverGenerator.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/ObserverGenerator.java index f0783ac0885c0..91b6fd82d202a 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/ObserverGenerator.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/ObserverGenerator.java @@ -181,7 +181,8 @@ Collection generate(ObserverInfo observer) { return Collections.emptyList(); } - boolean isApplicationClass = applicationClassPredicate.test(observer.getBeanClass()); + boolean isApplicationClass = applicationClassPredicate.test(observer.getBeanClass()) + || observer.isForceApplicationClass(); ResourceClassOutput classOutput = new ResourceClassOutput(isApplicationClass, name -> name.equals(generatedName) ? SpecialType.OBSERVER : null, generateSources); diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/ObserverInfo.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/ObserverInfo.java index ec23ce565663d..1c75166c852ed 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/ObserverInfo.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/ObserverInfo.java @@ -87,7 +87,7 @@ static ObserverInfo create(BeanInfo declaringBean, MethodInfo observerMethod, In initQualifiers(beanDeployment, observerMethod, eventParameter), reception, initTransactionPhase(isAsync, beanDeployment, observerMethod), isAsync, priority, transformers, - buildContext, jtaCapabilities, null, Collections.emptyMap()); + buildContext, jtaCapabilities, null, Collections.emptyMap(), false); } static ObserverInfo create(String id, BeanDeployment beanDeployment, DotName beanClass, BeanInfo declaringBean, @@ -95,7 +95,7 @@ static ObserverInfo create(String id, BeanDeployment beanDeployment, DotName bea MethodParameterInfo eventParameter, Type observedType, Set qualifiers, Reception reception, TransactionPhase transactionPhase, boolean isAsync, int priority, List transformers, BuildContext buildContext, boolean jtaCapabilities, - Consumer notify, Map params) { + Consumer notify, Map params, boolean forceApplicationClass) { if (!transformers.isEmpty()) { // Transform attributes if needed @@ -141,7 +141,8 @@ static ObserverInfo create(String id, BeanDeployment beanDeployment, DotName bea info, transactionPhase); } return new ObserverInfo(id, beanDeployment, beanClass, declaringBean, observerMethod, injection, eventParameter, - isAsync, priority, reception, transactionPhase, observedType, qualifiers, notify, params); + isAsync, priority, reception, transactionPhase, observedType, qualifiers, notify, params, + forceApplicationClass); } private final String id; @@ -178,11 +179,15 @@ static ObserverInfo create(String id, BeanDeployment beanDeployment, DotName bea private final Map params; - ObserverInfo(String id, BeanDeployment beanDeployment, DotName beanClass, BeanInfo declaringBean, MethodInfo observerMethod, + private final boolean forceApplicationClass; + + private ObserverInfo(String id, BeanDeployment beanDeployment, DotName beanClass, BeanInfo declaringBean, + MethodInfo observerMethod, Injection injection, MethodParameterInfo eventParameter, boolean isAsync, int priority, Reception reception, TransactionPhase transactionPhase, - Type observedType, Set qualifiers, Consumer notify, Map params) { + Type observedType, Set qualifiers, Consumer notify, + Map params, boolean forceApplicationClass) { this.id = id; this.beanDeployment = beanDeployment; this.beanClass = beanClass; @@ -199,6 +204,7 @@ static ObserverInfo create(String id, BeanDeployment beanDeployment, DotName bea this.qualifiers = qualifiers; this.notify = notify; this.params = params; + this.forceApplicationClass = forceApplicationClass; } @Override @@ -297,6 +303,10 @@ Map getParams() { return params; } + boolean isForceApplicationClass() { + return forceApplicationClass; + } + void init(List errors) { if (injection != null) { for (InjectionPointInfo injectionPoint : injection.injectionPoints) { diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/BeanInfoImpl.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/BeanInfoImpl.java index f4558bef257b9..7be224753d5cc 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/BeanInfoImpl.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/BeanInfoImpl.java @@ -105,7 +105,7 @@ public boolean isAlternative() { @Override public Integer priority() { - return arcBeanInfo.getAlternativePriority(); + return arcBeanInfo.getPriority(); } @Override diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ClassInfoImpl.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ClassInfoImpl.java index 4175392a021ce..d39cdd033e8e6 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ClassInfoImpl.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ClassInfoImpl.java @@ -166,7 +166,7 @@ private List allSupertypes() { alreadySeen.add(clazz.name()); DotName superClassName = clazz.superName(); - if (!DotNames.OBJECT.equals(superClassName)) { + if (superClassName != null && !DotNames.OBJECT.equals(superClassName)) { org.jboss.jandex.ClassInfo superClass = jandexIndex.getClassByName(superClassName); workQueue.add(superClass); } diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ExtensionInvoker.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ExtensionInvoker.java index a2e940cbe2216..24c4efb9d586d 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ExtensionInvoker.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ExtensionInvoker.java @@ -16,6 +16,7 @@ import jakarta.interceptor.Interceptor; import org.jboss.jandex.DotName; +import org.jboss.jandex.JandexReflection; // only this class uses reflection, everything else in this package is reflection-free class ExtensionInvoker { @@ -83,43 +84,8 @@ void callExtensionMethod(ExtensionMethod method, List arguments) throws ReflectiveOperationException { Class[] parameterTypes = new Class[arguments.size()]; - for (int i = 0; i < parameterTypes.length; i++) { - Object argument = arguments.get(i); - Class argumentClass = argument.getClass(); - - // beware of ordering! subtypes must precede supertypes - if (jakarta.enterprise.lang.model.declarations.ClassInfo.class.isAssignableFrom(argumentClass)) { - parameterTypes[i] = jakarta.enterprise.lang.model.declarations.ClassInfo.class; - } else if (jakarta.enterprise.lang.model.declarations.MethodInfo.class.isAssignableFrom(argumentClass)) { - parameterTypes[i] = jakarta.enterprise.lang.model.declarations.MethodInfo.class; - } else if (jakarta.enterprise.lang.model.declarations.FieldInfo.class.isAssignableFrom(argumentClass)) { - parameterTypes[i] = jakarta.enterprise.lang.model.declarations.FieldInfo.class; - } else if (jakarta.enterprise.inject.build.compatible.spi.ScannedClasses.class.isAssignableFrom(argumentClass)) { - parameterTypes[i] = jakarta.enterprise.inject.build.compatible.spi.ScannedClasses.class; - } else if (jakarta.enterprise.inject.build.compatible.spi.MetaAnnotations.class.isAssignableFrom(argumentClass)) { - parameterTypes[i] = jakarta.enterprise.inject.build.compatible.spi.MetaAnnotations.class; - } else if (jakarta.enterprise.inject.build.compatible.spi.ClassConfig.class.isAssignableFrom(argumentClass)) { - parameterTypes[i] = jakarta.enterprise.inject.build.compatible.spi.ClassConfig.class; - } else if (jakarta.enterprise.inject.build.compatible.spi.MethodConfig.class.isAssignableFrom(argumentClass)) { - parameterTypes[i] = jakarta.enterprise.inject.build.compatible.spi.MethodConfig.class; - } else if (jakarta.enterprise.inject.build.compatible.spi.FieldConfig.class.isAssignableFrom(argumentClass)) { - parameterTypes[i] = jakarta.enterprise.inject.build.compatible.spi.FieldConfig.class; - } else if (jakarta.enterprise.inject.build.compatible.spi.BeanInfo.class.isAssignableFrom(argumentClass)) { - parameterTypes[i] = jakarta.enterprise.inject.build.compatible.spi.BeanInfo.class; - } else if (jakarta.enterprise.inject.build.compatible.spi.ObserverInfo.class.isAssignableFrom(argumentClass)) { - parameterTypes[i] = jakarta.enterprise.inject.build.compatible.spi.ObserverInfo.class; - } else if (jakarta.enterprise.inject.build.compatible.spi.SyntheticComponents.class - .isAssignableFrom(argumentClass)) { - parameterTypes[i] = jakarta.enterprise.inject.build.compatible.spi.SyntheticComponents.class; - } else if (jakarta.enterprise.inject.build.compatible.spi.Messages.class.isAssignableFrom(argumentClass)) { - parameterTypes[i] = jakarta.enterprise.inject.build.compatible.spi.Messages.class; - } else if (jakarta.enterprise.inject.build.compatible.spi.Types.class.isAssignableFrom(argumentClass)) { - parameterTypes[i] = jakarta.enterprise.inject.build.compatible.spi.Types.class; - } else { - // should never happen, internal error (or missing error handling) if it does - throw new IllegalArgumentException("Unexpected extension method argument: " + argument); - } + parameterTypes[i] = JandexReflection.loadRawType(method.parameterType(i)); } Class extensionClass = extensionClasses.get(method.extensionClass.name().toString()); @@ -136,7 +102,6 @@ void invalidate() { } /** - * * @return {@code true} if no {@link BuildCompatibleExtension} was found, {@code false} otherwise */ boolean isEmpty() { diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ExtensionMethod.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ExtensionMethod.java index 49e51a2d7a7bc..d42faaee4906b 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ExtensionMethod.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ExtensionMethod.java @@ -25,6 +25,10 @@ List parameterTypes() { return jandex.parameterTypes(); } + org.jboss.jandex.Type parameterType(int index) { + return jandex.parameterType(index); + } + @Override public String toString() { return jandex.declaringClass().simpleName() + "." + jandex.name() + "()"; diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ExtensionPhaseRegistration.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ExtensionPhaseRegistration.java index 0e0b3513c8a72..a8758561b3fae 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ExtensionPhaseRegistration.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ExtensionPhaseRegistration.java @@ -6,23 +6,30 @@ import java.util.List; import java.util.Set; import java.util.stream.Collectors; +import java.util.stream.Stream; import jakarta.enterprise.inject.build.compatible.spi.BeanInfo; import jakarta.enterprise.inject.build.compatible.spi.ObserverInfo; import jakarta.enterprise.inject.spi.DefinitionException; +import org.jboss.jandex.IndexView; + +import io.quarkus.arc.processor.InterceptorInfo; + class ExtensionPhaseRegistration extends ExtensionPhaseBase { private final AllAnnotationOverlays annotationOverlays; private final Collection allBeans; + private final Collection allInterceptors; private final Collection allObservers; private final io.quarkus.arc.processor.AssignabilityCheck assignability; - ExtensionPhaseRegistration(ExtensionInvoker invoker, org.jboss.jandex.IndexView beanArchiveIndex, SharedErrors errors, + ExtensionPhaseRegistration(ExtensionInvoker invoker, IndexView beanArchiveIndex, SharedErrors errors, AllAnnotationOverlays annotationOverlays, Collection allBeans, - Collection allObservers) { + Collection allInterceptors, Collection allObservers) { super(ExtensionPhase.REGISTRATION, invoker, beanArchiveIndex, errors); this.annotationOverlays = annotationOverlays; this.allBeans = allBeans; + this.allInterceptors = allInterceptors; this.allObservers = allObservers; this.assignability = new io.quarkus.arc.processor.AssignabilityCheck(beanArchiveIndex, null); } @@ -84,7 +91,7 @@ private Set expectedTypes(org.jboss.jandex.MethodInfo jan private List matchingBeans(org.jboss.jandex.MethodInfo jandexMethod, boolean onlyInterceptors) { Set expectedTypes = expectedTypes(jandexMethod); - return allBeans.stream() + return Stream.concat(allBeans.stream(), allInterceptors.stream()) .filter(bean -> { if (onlyInterceptors && !bean.isInterceptor()) { return false; diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ExtensionsEntryPoint.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ExtensionsEntryPoint.java index cf490e5604ff8..a191021bc17b5 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ExtensionsEntryPoint.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ExtensionsEntryPoint.java @@ -12,6 +12,7 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Predicate; import java.util.stream.Collectors; import jakarta.enterprise.context.Dependent; @@ -272,6 +273,7 @@ public void runEnhancement(org.jboss.jandex.IndexView beanArchiveIndex, BeanProc */ public void runRegistration(org.jboss.jandex.IndexView beanArchiveIndex, Collection allBeans, + Collection allInterceptors, Collection allObservers) { if (invoker.isEmpty()) { return; @@ -281,7 +283,7 @@ public void runRegistration(org.jboss.jandex.IndexView beanArchiveIndex, try { new ExtensionPhaseRegistration(invoker, beanArchiveIndex, errors, annotationOverlays, - allBeans, allObservers).run(); + allBeans, allInterceptors, allObservers).run(); } finally { BuildServicesImpl.reset(); } @@ -312,7 +314,7 @@ public void runSynthesis(org.jboss.jandex.IndexView beanArchiveIndex) { *

    * It is a no-op if no {@link BuildCompatibleExtension} was found. */ - public void registerSyntheticBeans(BeanRegistrar.RegistrationContext context) { + public void registerSyntheticBeans(BeanRegistrar.RegistrationContext context, Predicate isApplicationClass) { if (invoker.isEmpty()) { return; } @@ -424,6 +426,16 @@ public void registerSyntheticBeans(BeanRegistrar.RegistrationContext context) { mc.returnValue(null); }); } + // the generated classes need to see the `creatorClass` and the `disposerClass`, + // so if they are application classes, the generated classes are forced to also + // be application classes, even if the `implementationClass` possibly isn't + if (isApplicationClass.test(DotName.createSimple(syntheticBean.creatorClass))) { + bean.forceApplicationClass(); + } + if (syntheticBean.disposerClass != null + && isApplicationClass.test(DotName.createSimple(syntheticBean.disposerClass))) { + bean.forceApplicationClass(); + } bean.done(); } } @@ -433,7 +445,8 @@ public void registerSyntheticBeans(BeanRegistrar.RegistrationContext context) { *

    * It is a no-op if no {@link BuildCompatibleExtension} was found. */ - public void registerSyntheticObservers(ObserverRegistrar.RegistrationContext context) { + public void registerSyntheticObservers(ObserverRegistrar.RegistrationContext context, + Predicate isApplicationClass) { if (invoker.isEmpty()) { return; } @@ -475,6 +488,12 @@ public void registerSyntheticObservers(ObserverRegistrar.RegistrationContext con // return type is void mc.returnValue(null); }); + // the generated classes need to see the `implementationClass`, so if it is + // an application class, the generated classes are forced to also be application + // classes, even if the `declaringClass` possibly isn't + if (isApplicationClass.test(DotName.createSimple(syntheticObserver.implementationClass))) { + observer.forceApplicationClass(); + } observer.done(); } } @@ -563,7 +582,7 @@ public void runRegistrationAgain(org.jboss.jandex.IndexView beanArchiveIndex, try { new ExtensionPhaseRegistration(invoker, beanArchiveIndex, errors, annotationOverlays, - syntheticBeans, syntheticObservers).run(); + syntheticBeans, Collections.emptyList(), syntheticObservers).run(); } finally { BuildServicesImpl.reset(); } diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/AbstractSharedContext.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/AbstractSharedContext.java index 748fae45d3fb7..111f18f174d1b 100644 --- a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/AbstractSharedContext.java +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/AbstractSharedContext.java @@ -1,9 +1,6 @@ package io.quarkus.arc.impl; -import java.util.Iterator; -import java.util.Map; -import java.util.Objects; -import java.util.Set; +import java.util.*; import java.util.function.Supplier; import java.util.stream.Collectors; @@ -87,7 +84,11 @@ public void destroy(Contextual contextual) { @Override public synchronized void destroy() { - Set> values = instances.getAllPresent(); + List> values = new LinkedList<>(); + instances.forEach(values::add); + if (values.isEmpty()) { + return; + } // Destroy the producers first for (Iterator> iterator = values.iterator(); iterator.hasNext();) { ContextInstanceHandle instanceHandle = iterator.next(); diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/ComputingCacheContextInstances.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/ComputingCacheContextInstances.java index fd89543cbc65b..5fdfdbb6d4761 100644 --- a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/ComputingCacheContextInstances.java +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/ComputingCacheContextInstances.java @@ -1,6 +1,7 @@ package io.quarkus.arc.impl; import java.util.Set; +import java.util.function.Consumer; import java.util.function.Supplier; import io.quarkus.arc.ContextInstanceHandle; @@ -38,4 +39,9 @@ public void clear() { instances.clear(); } + @Override + public void forEach(Consumer> handleConsumer) { + instances.getPresentValues().forEach(handleConsumer); + } + } diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/ContextInstances.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/ContextInstances.java index 76ca4ad531e6f..7c0557215ef57 100644 --- a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/ContextInstances.java +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/ContextInstances.java @@ -1,6 +1,7 @@ package io.quarkus.arc.impl; import java.util.Set; +import java.util.function.Consumer; import java.util.function.Supplier; import io.quarkus.arc.ContextInstanceHandle; @@ -17,4 +18,6 @@ public interface ContextInstances { void clear(); + void forEach(Consumer> handleConsumer); + } diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/RequestContext.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/RequestContext.java index 08e2043ff9179..40aa2e3240482 100644 --- a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/RequestContext.java +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/RequestContext.java @@ -210,7 +210,7 @@ public void destroy(ContextState state) { if (reqState.invalidate()) { // Fire an event with qualifier @BeforeDestroyed(RequestScoped.class) if there are any observers for it fireIfNotEmpty(beforeDestroyedNotifier); - reqState.contextInstances.getAllPresent().forEach(this::destroyContextElement); + reqState.contextInstances.forEach(this::destroyContextElement); reqState.contextInstances.clear(); // Fire an event with qualifier @Destroyed(RequestScoped.class) if there are any observers for it fireIfNotEmpty(destroyedNotifier); diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/cdi/bcextensions/RegistrationTest.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/cdi/bcextensions/RegistrationTest.java index 699c54c725d16..c5f166abfe6a8 100644 --- a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/cdi/bcextensions/RegistrationTest.java +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/cdi/bcextensions/RegistrationTest.java @@ -2,21 +2,30 @@ import static org.junit.jupiter.api.Assertions.assertEquals; +import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; import java.util.concurrent.atomic.AtomicInteger; +import jakarta.annotation.Priority; import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.context.Initialized; import jakarta.enterprise.event.Observes; import jakarta.enterprise.inject.Produces; import jakarta.enterprise.inject.build.compatible.spi.BeanInfo; import jakarta.enterprise.inject.build.compatible.spi.BuildCompatibleExtension; +import jakarta.enterprise.inject.build.compatible.spi.InterceptorInfo; +import jakarta.enterprise.inject.build.compatible.spi.Messages; import jakarta.enterprise.inject.build.compatible.spi.ObserverInfo; import jakarta.enterprise.inject.build.compatible.spi.Registration; import jakarta.enterprise.inject.build.compatible.spi.Types; import jakarta.inject.Qualifier; import jakarta.inject.Singleton; +import jakarta.interceptor.AroundInvoke; +import jakarta.interceptor.Interceptor; +import jakarta.interceptor.InterceptorBinding; +import jakarta.interceptor.InvocationContext; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -26,7 +35,8 @@ public class RegistrationTest { @RegisterExtension public ArcTestContainer container = ArcTestContainer.builder() - .beanClasses(MyQualifier.class, MyService.class, MyFooService.class, MyBarService.class, MyBarServiceProducer.class) + .beanClasses(MyQualifier.class, MyInterceptorBinding.class, MyInterceptor.class, MyService.class, + MyFooService.class, MyBarService.class, MyBarServiceProducer.class) .buildCompatibleExtensions(new MyExtension()) .build(); @@ -35,12 +45,14 @@ public void test() { assertEquals(2, MyExtension.beanCounter.get()); assertEquals(1, MyExtension.beanMyQualifierCounter.get()); assertEquals(1, MyExtension.observerQualifierCounter.get()); + assertEquals(2, MyExtension.interceptorCounter.get()); // one interceptor, counted twice } public static class MyExtension implements BuildCompatibleExtension { static final AtomicInteger beanCounter = new AtomicInteger(); static final AtomicInteger beanMyQualifierCounter = new AtomicInteger(); static final AtomicInteger observerQualifierCounter = new AtomicInteger(); + static final AtomicInteger interceptorCounter = new AtomicInteger(); @Registration(types = MyService.class) public void beans(BeanInfo bean) { @@ -57,6 +69,19 @@ public void observers(ObserverInfo observer, Types types) { observerQualifierCounter.addAndGet(observer.qualifiers().size()); } } + + @Registration(types = MyInterceptor.class) + public void interceptors(InterceptorInfo interceptor) { + interceptorCounter.incrementAndGet(); + } + + @Registration(types = MyInterceptor.class) + public void interceptorsAsBeans(BeanInfo interceptor, Messages msg) { + if (!interceptor.isInterceptor()) { + msg.error("Interceptor expected", interceptor); + } + interceptorCounter.incrementAndGet(); + } } // --- @@ -66,6 +91,22 @@ public void observers(ObserverInfo observer, Types types) { public @interface MyQualifier { } + @Target({ ElementType.TYPE, ElementType.METHOD }) + @Retention(RetentionPolicy.RUNTIME) + @InterceptorBinding + public @interface MyInterceptorBinding { + } + + @MyInterceptorBinding + @Interceptor + @Priority(1) + public static class MyInterceptor { + @AroundInvoke + public Object intercept(InvocationContext ctx) throws Exception { + return ctx.proceed(); + } + } + public interface MyService { String hello(); } diff --git a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/BootstrapModelResolver.java b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/BootstrapModelResolver.java index 97ee36094972c..14d4f5ef80f2b 100644 --- a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/BootstrapModelResolver.java +++ b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/BootstrapModelResolver.java @@ -38,7 +38,7 @@ public class BootstrapModelResolver implements ModelResolver { - public static ModelResolver newInstance(BootstrapMavenContext ctx, WorkspaceReader workspace) + public static BootstrapModelResolver newInstance(BootstrapMavenContext ctx, WorkspaceReader workspace) throws BootstrapMavenException { final RepositorySystem repoSystem = ctx.getRepositorySystem(); final RepositorySystemSession session = workspace == null @@ -102,6 +102,10 @@ private BootstrapModelResolver(BootstrapModelResolver original) { this.repositoryIds = new HashSet<>(original.repositoryIds); } + public RepositorySystemSession getSession() { + return session; + } + @Override public void addRepository(Repository repository) throws InvalidRepositoryException { diff --git a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/MavenArtifactResolver.java b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/MavenArtifactResolver.java index 0643165790f8e..ee8a85d4091b8 100644 --- a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/MavenArtifactResolver.java +++ b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/MavenArtifactResolver.java @@ -305,7 +305,7 @@ public CollectResult collectManagedDependencies(Artifact artifact, List moduleQueue = new ArrayList<>(); + private final Map loadedPoms = new HashMap<>(); + + private final Function modelProvider; + private final Map loadedModules = new HashMap<>(); + private final LocalWorkspace workspace = new LocalWorkspace(); - private final Map rawModelCache = new HashMap<>(); - private final Map projectCache = new HashMap<>(); private final Path currentProjectPom; - private Path workspaceRootPom; - private Function modelProvider; private ModelBuilder modelBuilder; - private ModelResolver modelResolver; + private BootstrapModelResolver modelResolver; private ModelCache modelCache; private List activeProfileIds; private List inactiveProfileIds; @@ -85,11 +71,20 @@ static Path locateCurrentProjectPom(Path path, boolean required) throws Bootstra WorkspaceLoader(BootstrapMavenContext ctx, Path currentProjectPom, Function modelProvider) throws BootstrapMavenException { - this.modelProvider = modelProvider; + try { + final BasicFileAttributes fileAttributes = Files.readAttributes(currentProjectPom, BasicFileAttributes.class); + this.currentProjectPom = fileAttributes.isDirectory() ? locateCurrentProjectPom(currentProjectPom) + : currentProjectPom; + } catch (IOException e) { + throw new IllegalArgumentException(currentProjectPom + " does not exist", e); + } + addModulePom(this.currentProjectPom); + this.modelProvider = modelProvider == null ? pom -> null : modelProvider; + if (ctx != null && ctx.isEffectiveModelBuilder()) { modelBuilder = BootstrapModelBuilderFactory.getDefaultModelBuilder(); modelResolver = BootstrapModelResolver.newInstance(ctx, this); - modelCache = new BootstrapModelCache(ctx.getRepositorySystemSession()); + modelCache = new BootstrapModelCache(modelResolver.getSession()); profiles = ctx.getActiveSettingsProfiles(); final BootstrapMavenOptions cliOptions = ctx.getCliOptions(); @@ -101,185 +96,126 @@ static Path locateCurrentProjectPom(Path path, boolean required) throws Bootstra inactiveProfileIds = cliOptions.getInactiveProfileIds(); } workspace.setBootstrapMavenContext(ctx); - this.currentProjectPom = isPom(currentProjectPom) ? currentProjectPom - : locateCurrentProjectPom(currentProjectPom, true); } - private boolean isPom(Path p) { - if (Files.exists(p) && !Files.isDirectory(p)) { - try { - rawModel(p); - return true; - } catch (BootstrapMavenException e) { - // not a POM file - } + private void addModulePom(Path pom) { + if (pom != null) { + moduleQueue.add(new RawModule(pom)); } - return false; } - private LocalProject project(Path pomFile) throws BootstrapMavenException { - final LocalProject project = projectCache.get(pomFile.getParent()); - return project == null ? loadAndCacheProject(pomFile) : project; + void setWorkspaceRootPom(Path rootPom) { + addModulePom(rootPom); } - private LocalProject loadAndCacheProject(Path pomFile) throws BootstrapMavenException { - final Model rawModel = rawModel(pomFile); - if (rawModel == null) { - return null; - } - final LocalProject project; - if (modelBuilder != null) { - ModelBuildingRequest req = new DefaultModelBuildingRequest(); - req.setPomFile(pomFile.toFile()); - req.setModelResolver(modelResolver); - req.setSystemProperties(System.getProperties()); - req.setUserProperties(System.getProperties()); - req.setModelCache(modelCache); - req.setActiveProfileIds(activeProfileIds); - req.setInactiveProfileIds(inactiveProfileIds); - req.setProfiles(profiles); - req.setRawModel(rawModel); - req.setWorkspaceModelResolver(this); - try { - project = new LocalProject(modelBuilder.build(req), workspace); - } catch (Exception e) { - throw new BootstrapMavenException("Failed to resolve the effective model for " + pomFile, e); - } + LocalProject load() throws BootstrapMavenException { + final AtomicReference currentProject = new AtomicReference<>(); + final Consumer processor; + if (modelBuilder == null) { + processor = rawModel -> { + var project = new LocalProject(rawModel, workspace); + if (currentProject.get() == null && project.getDir().equals(currentProjectPom.getParent())) { + currentProject.set(project); + } + }; } else { - project = new LocalProject(rawModel, workspace); + processor = rawModel -> { + var req = new DefaultModelBuildingRequest(); + req.setPomFile(rawModel.getPomFile()); + req.setModelResolver(modelResolver); + req.setSystemProperties(System.getProperties()); + req.setUserProperties(System.getProperties()); + req.setModelCache(modelCache); + req.setActiveProfileIds(activeProfileIds); + req.setInactiveProfileIds(inactiveProfileIds); + req.setProfiles(profiles); + req.setRawModel(rawModel); + req.setWorkspaceModelResolver(this); + LocalProject project; + try { + project = new LocalProject(modelBuilder.build(req), workspace); + } catch (Exception e) { + throw new RuntimeException("Failed to resolve the effective model for " + rawModel.getPomFile(), e); + } + if (currentProject.get() == null && project.getDir().equals(currentProjectPom.getParent())) { + currentProject.set(project); + } + for (var module : project.getModelBuildingResult().getEffectiveModel().getModules()) { + addModulePom(project.getDir().resolve(module).resolve(POM_XML)); + } + }; } - projectCache.put(pomFile.getParent(), project); - return project; - } - private Model rawModel(Path pomFile) throws BootstrapMavenException { - var moduleDir = pomFile.getParent(); - if (moduleDir != null) { - // the path might not be normalized, while the modelProvider below would typically recognize normalized absolute paths - moduleDir = moduleDir.normalize().toAbsolutePath(); - } - Model rawModel = rawModelCache.get(moduleDir); - if (rawModel != null) { - return rawModel; - } - rawModel = modelProvider == null ? null : modelProvider.apply(moduleDir); - if (rawModel == null) { - rawModel = readModel(pomFile); + int i = 0; + while (i < moduleQueue.size()) { + var newModules = new ArrayList(); + while (i < moduleQueue.size()) { + loadModule(moduleQueue.get(i++), newModules); + } + for (var newModule : newModules) { + newModule.process(processor); + } } - rawModelCache.put(moduleDir, rawModel); - return rawModel; - } - void setWorkspaceRootPom(Path rootPom) { - this.workspaceRootPom = rootPom; - } - - private LocalProject loadProject(Path projectPom, String skipModule) throws BootstrapMavenException { - final Model rawModel = rawModel(projectPom); - if (rawModel == null) { - return null; - } - final LocalProject parentProject = loadParentProject(projectPom, rawModel); - final LocalProject project = project(projectPom); - if (project == null) { - return null; + if (currentProject.get() == null) { + throw new BootstrapMavenException("Failed to load project " + currentProjectPom); } - if (parentProject != null) { - parentProject.modules.add(project); - } - loadProjectModules(project, skipModule); - return project; - } - - private LocalProject loadParentProject(Path projectPom, final Model rawModel) throws BootstrapMavenException { - final Path parentPom = getParentPom(projectPom, rawModel); - return parentPom == null || rawModelCache.containsKey(parentPom.getParent()) ? null - : loadProject(parentPom, parentPom.getParent().relativize(projectPom.getParent()).toString()); + return currentProject.get(); } - private Path getParentPom(Path projectPom, Model rawModel) { - if (rawModel == null) { - return null; + private void loadModule(RawModule rawModule, List newModules) { + var moduleDir = rawModule.pom.getParent(); + if (moduleDir == null) { + moduleDir = Path.of("/"); } - Path parentPom = null; - final Path projectDir = projectPom.getParent(); - final Parent parent = rawModel.getParent(); - if (parent != null && parent.getRelativePath() != null && !parent.getRelativePath().isEmpty()) { - parentPom = projectDir.resolve(parent.getRelativePath()).normalize(); - if (Files.isDirectory(parentPom)) { - parentPom = parentPom.resolve(POM_XML); - } - } else { - final Path parentDir = projectDir.getParent(); - if (parentDir != null) { - parentPom = parentDir.resolve(POM_XML); - } + if (loadedPoms.containsKey(moduleDir)) { + return; } - return parentPom != null && Files.exists(parentPom) ? parentPom : null; - } - private LocalProject loadProjectModules(LocalProject project, String skipModule) throws BootstrapMavenException { - final List modules; - if (project.getModelBuildingResult() == null) { - var projectModel = project.getRawModel(); - List combinedList = null; - for (var profile : projectModel.getProfiles()) { - if (!profile.getModules().isEmpty()) { - if (combinedList == null) { - combinedList = new ArrayList<>(projectModel.getModules()); - } - combinedList.addAll(profile.getModules()); - } - } - modules = combinedList == null ? projectModel.getModules() : combinedList; - } else { - modules = project.getModelBuildingResult().getEffectiveModel().getModules(); + rawModule.model = modelProvider == null ? null : modelProvider.apply(moduleDir); + if (rawModule.model == null) { + rawModule.model = readModel(rawModule.pom); } - if (!modules.isEmpty()) { - for (String module : modules) { - if (module.equals(skipModule)) { - continue; - } - final Path modulePom = project.getDir().resolve(module).resolve(POM_XML); - // some modules use different parent POMs than those that referred to them as their modules - // so make sure the parent project has been loaded, before resolving the effective model of the module - loadParentProject(modulePom, rawModel(modulePom)); - final LocalProject childProject = project(modulePom); - if (childProject != null) { - project.modules.add(loadProjectModules(childProject, null)); - } - } + loadedPoms.put(moduleDir, rawModule.model); + if (rawModule.model == null) { + return; } - return project; - } + newModules.add(rawModule); - LocalProject load() throws BootstrapMavenException { - if (workspaceRootPom != null) { - loadProject(workspaceRootPom, null); + var added = loadedModules.putIfAbsent( + new GAV(ModelUtils.getGroupId(rawModule.model), rawModule.model.getArtifactId(), + ModelUtils.getVersion(rawModule.model)), + rawModule.model); + if (added != null) { + return; + } + for (var module : rawModule.model.getModules()) { + moduleQueue.add( + new RawModule(rawModule, rawModule.model.getProjectDirectory().toPath().resolve(module).resolve(POM_XML))); } - LocalProject currentProject = projectCache.get(currentProjectPom.getParent()); - if (currentProject == null) { - currentProject = loadProject(currentProjectPom, null); + for (var profile : rawModule.model.getProfiles()) { + for (var module : profile.getModules()) { + moduleQueue.add(new RawModule(rawModule, + rawModule.model.getProjectDirectory().toPath().resolve(module).resolve(POM_XML))); + } } - if (workspace != null) { - workspace.setCurrentProject(currentProject); + if (rawModule.parent == null) { + final Path parentPom = rawModule.getParentPom(); + if (parentPom != null) { + var parent = new RawModule(parentPom); + rawModule.parent = parent; + moduleQueue.add(parent); + } } - return currentProject; } @Override - public Model resolveRawModel(String groupId, String artifactId, String versionConstraint) - throws UnresolvableModelException { - final LocalProject project = workspace.getProject(groupId, artifactId); - // we are comparing the raw version here because in case of a CI-friendly version (e.g. ${revision}) the versionConstraint will be an expression - return project != null && ModelUtils.getRawVersion(project.getRawModel()).equals(versionConstraint) - ? project.getRawModel() - : null; + public Model resolveRawModel(String groupId, String artifactId, String versionConstraint) { + return loadedModules.get(new GAV(groupId, artifactId, versionConstraint)); } @Override - public Model resolveEffectiveModel(String groupId, String artifactId, String versionConstraint) - throws UnresolvableModelException { + public Model resolveEffectiveModel(String groupId, String artifactId, String versionConstraint) { final LocalProject project = workspace.getProject(groupId, artifactId); return project != null && project.getVersion().equals(versionConstraint) ? project.getModelBuildingResult().getEffectiveModel() @@ -293,11 +229,81 @@ public WorkspaceRepository getRepository() { @Override public File findArtifact(Artifact artifact) { - return workspace.findArtifact(artifact); + if (!ArtifactCoords.TYPE_POM.equals(artifact.getExtension())) { + return null; + } + var model = loadedModules.get(new GAV(artifact.getGroupId(), artifact.getArtifactId(), artifact.getVersion())); + return model == null ? null : model.getPomFile(); } @Override public List findVersions(Artifact artifact) { - return workspace.findVersions(artifact); + var model = loadedModules.get(new GAV(artifact.getGroupId(), artifact.getArtifactId(), artifact.getVersion())); + return model == null ? List.of() : List.of(ModelUtils.getVersion(model)); + } + + private static Model readModel(Path pom) { + try { + final Model model = ModelUtils.readModel(pom); + model.setPomFile(pom.toFile()); + return model; + } catch (NoSuchFileException e) { + // some projects may be missing pom.xml relying on Maven extensions (e.g. tycho-maven-plugin) to build them, + // which we don't support in this workspace loader + log.warn("Module(s) under " + pom.getParent() + " will be handled as thirdparty dependencies because " + pom + + " does not exist"); + return null; + } catch (IOException e) { + throw new UncheckedIOException("Failed to load POM from " + pom, e); + } + } + + private static class RawModule { + final Path pom; + Model model; + RawModule parent; + boolean processed; + + private RawModule(Path pom) { + this(null, pom); + } + + private RawModule(RawModule parent, Path pom) { + this.pom = pom.normalize().toAbsolutePath(); + this.parent = parent; + } + + private Path getParentPom() { + if (model == null) { + return null; + } + Path parentPom = null; + final Parent parent = model.getParent(); + if (parent != null && parent.getRelativePath() != null && !parent.getRelativePath().isEmpty()) { + parentPom = pom.getParent().resolve(parent.getRelativePath()).normalize(); + if (Files.isDirectory(parentPom)) { + parentPom = parentPom.resolve(POM_XML); + } + } else { + final Path parentDir = pom.getParent().getParent(); + if (parentDir != null) { + parentPom = parentDir.resolve(POM_XML); + } + } + return parentPom != null && Files.exists(parentPom) ? parentPom : null; + } + + private void process(Consumer consumer) { + if (processed) { + return; + } + processed = true; + if (parent != null) { + parent.process(consumer); + } + if (model != null) { + consumer.accept(model); + } + } } } diff --git a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/util/DependencyUtils.java b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/util/DependencyUtils.java index 6bdd514695b05..41eac854f01cd 100644 --- a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/util/DependencyUtils.java +++ b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/util/DependencyUtils.java @@ -67,7 +67,7 @@ public static Artifact toArtifact(String str) { private static Artifact toArtifact(String str, int offset) { String groupId = null; String artifactId = null; - String classifier = ""; + String classifier = ArtifactCoords.DEFAULT_CLASSIFIER; String type = ArtifactCoords.TYPE_JAR; String version = null; diff --git a/independent-projects/bootstrap/maven-resolver/src/test/java/io/quarkus/bootstrap/workspace/test/LocalWorkspaceDiscoveryTest.java b/independent-projects/bootstrap/maven-resolver/src/test/java/io/quarkus/bootstrap/workspace/test/LocalWorkspaceDiscoveryTest.java index d284a1d0f2a83..e1b74d3a5b6ab 100644 --- a/independent-projects/bootstrap/maven-resolver/src/test/java/io/quarkus/bootstrap/workspace/test/LocalWorkspaceDiscoveryTest.java +++ b/independent-projects/bootstrap/maven-resolver/src/test/java/io/quarkus/bootstrap/workspace/test/LocalWorkspaceDiscoveryTest.java @@ -222,7 +222,8 @@ public void loadEffectiveModelBuilderModulesInProfiles() throws Exception { assertNotNull(ws.getProject("org.acme", "quarkus-quickstart-multimodule-rest")); assertNotNull(ws.getProject("org.acme", "acme-integration-tests")); assertNotNull(ws.getProject("org.acme", "acme-rest-tests")); - assertEquals(6, ws.getProjects().size()); + assertNotNull(ws.getProject("org.acme", "other")); + assertEquals(7, ws.getProjects().size()); } @Test @@ -535,7 +536,8 @@ public void loadWorkspaceForModuleWithNotDirectParentPath() throws Exception { @Test public void loadNonModuleChildProject() throws Exception { final LocalProject project = LocalProject - .loadWorkspace(workDir.resolve("root").resolve("non-module-child").resolve("target").resolve("classes")); + .loadWorkspace(IoUtils + .mkdirs(workDir.resolve("root").resolve("non-module-child").resolve("target").resolve("classes"))); assertNotNull(project); assertNotNull(project.getWorkspace()); assertEquals("non-module-child", project.getArtifactId()); diff --git a/independent-projects/bootstrap/pom.xml b/independent-projects/bootstrap/pom.xml index 0db20683a71fe..35b69b7fcd5d6 100644 --- a/independent-projects/bootstrap/pom.xml +++ b/independent-projects/bootstrap/pom.xml @@ -34,23 +34,20 @@ 1.3.2 1 UTF-8 - 11 - 11 - 11 3.11.0 3.2.1 3.1.2 - 3.1.5 + 3.1.6 3.24.2 0.9.5 3.5.3.Final - 5.10.0 - 3.9.5 - 0.3.5 - 3.7.1 - 1.9.13 + 5.10.1 + 3.9.6 + 0.9.0.M2 + 3.10.2 + 1.9.18 3.3.4 3.5.3 4.4.16 @@ -61,8 +58,8 @@ 4.0.1 2.0.1 1.16.0 - 2.15.0 - 3.13.0 + 2.15.1 + 3.14.0 32.1.3-jre 1.0.1 2.8 @@ -78,7 +75,7 @@ 3.5.1 2.1.2 1.3.2 - 8.4 + 8.5 0.0.9 0.1.3 2.23.0 diff --git a/independent-projects/enforcer-rules/pom.xml b/independent-projects/enforcer-rules/pom.xml index 3be3e3b40c112..73a26c45f61db 100644 --- a/independent-projects/enforcer-rules/pom.xml +++ b/independent-projects/enforcer-rules/pom.xml @@ -31,16 +31,13 @@ UTF-8 - 11 - 11 - 11 3.2.1 3.1.2 3.0.0-M3 3.6.0 - 3.9.5 + 3.9.6 11 - 11 11 - 3.9.5 + 3.9.6 3.11.0 3.2.1 3.1.2 - 3.8.1 - 2.15.3 + 3.10.2 + 2.16.0 1.3.2 - 5.10.0 + 5.10.1 @@ -393,7 +393,7 @@ org.mockito mockito-core - 5.4.0 + 5.8.0 test diff --git a/independent-projects/ide-config/pom.xml b/independent-projects/ide-config/pom.xml index 4437dc80e4461..2521b993cc08e 100644 --- a/independent-projects/ide-config/pom.xml +++ b/independent-projects/ide-config/pom.xml @@ -36,9 +36,6 @@ UTF-8 - 11 - 11 - 11 3.2.1 3.1.2 diff --git a/independent-projects/junit5-virtual-threads/pom.xml b/independent-projects/junit5-virtual-threads/pom.xml index 4b35067e4ac1e..1c417faab1b76 100644 --- a/independent-projects/junit5-virtual-threads/pom.xml +++ b/independent-projects/junit5-virtual-threads/pom.xml @@ -37,18 +37,15 @@ UTF-8 - 11 - 11 - 11 3.11.0 3.2.1 3.1.2 - 3.1.5 + 3.1.6 2.23.0 1.9.0 - 5.10.0 + 5.10.1 diff --git a/independent-projects/parent/pom.xml b/independent-projects/parent/pom.xml index be3075bf1548b..3f0164a735fbd 100644 --- a/independent-projects/parent/pom.xml +++ b/independent-projects/parent/pom.xml @@ -17,7 +17,7 @@ - 3.4.0 + 3.5.0 3.0.0 3.2.0 3.11.0 @@ -45,7 +45,9 @@ true - 11 + 17 + 17 + 17 ${maven.compiler.release} ${maven.compiler.release} ${maven.compiler.target} @@ -831,5 +833,30 @@ + + ci + + + env.CI + + + + + + + org.apache.maven.plugins + maven-gpg-plugin + + + + --pinentry-mode + loopback + + + + + + + diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/LoopSectionHelper.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/LoopSectionHelper.java index d5b38ce6cc2c3..a25d8b4b11932 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/LoopSectionHelper.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/LoopSectionHelper.java @@ -12,6 +12,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; import java.util.concurrent.CompletionStage; import java.util.stream.IntStream; import java.util.stream.Stream; @@ -317,6 +318,18 @@ public CompletionStage getAsync(String key) { } } + @Override + public Set mappedKeys() { + if (metadataPrefix != null) { + return Set.of(alias, metadataPrefix + "count", metadataPrefix + "index", metadataPrefix + "indexParity", + metadataPrefix + "hasNext", metadataPrefix + "isLast", metadataPrefix + "isFirst", + metadataPrefix + "isOdd", metadataPrefix + "odd", metadataPrefix + "isEven", metadataPrefix + "even"); + } else { + return Set.of(alias, "count", "index", "indexParity", "hasNext", "isLast", "isFirst", "isOdd", + "odd", "isEven", "even"); + } + } + } enum Code implements ErrorCode { diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/Mapper.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/Mapper.java index b8ac2888b9d0a..49bce16d53e5e 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/Mapper.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/Mapper.java @@ -1,6 +1,7 @@ package io.quarkus.qute; import java.util.Map; +import java.util.Set; import java.util.concurrent.CompletionStage; /** @@ -28,24 +29,22 @@ default boolean appliesTo(String key) { return true; } + /** + * The returned set may be a subset of the final set of all mapped keys. + * + * @return the set of known mapped keys + */ + default Set mappedKeys() { + return Set.of(); + } + /** * * @param map * @return a mapper that wraps the given map */ static Mapper wrap(Map map) { - return new Mapper() { - - @Override - public boolean appliesTo(String key) { - return map.containsKey(key); - } - - @Override - public Object get(String key) { - return map.get(key); - } - }; + return new MapperMapWrapper(map); } } diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/MapperMapWrapper.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/MapperMapWrapper.java new file mode 100644 index 0000000000000..762ebba454369 --- /dev/null +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/MapperMapWrapper.java @@ -0,0 +1,29 @@ +package io.quarkus.qute; + +import java.util.Map; +import java.util.Set; + +final class MapperMapWrapper implements Mapper { + + private final Map map; + + MapperMapWrapper(Map map) { + this.map = map; + } + + @Override + public boolean appliesTo(String key) { + return map.containsKey(key); + } + + @Override + public Object get(String key) { + return map.get(key); + } + + @Override + public Set mappedKeys() { + return map.keySet(); + } + +} diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/Results.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/Results.java index 079ceec677c3e..0cca2fb5418c5 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/Results.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/Results.java @@ -1,5 +1,7 @@ package io.quarkus.qute; +import static java.util.function.Predicate.not; + import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -135,29 +137,49 @@ public String asMessage() { if (name != null) { Object base = getBase().orElse(null); List params = getParams(); - boolean isDataMap = isDataMap(base); - // Entry "foo" not found in the data map - // Property "foo" not found on base object "org.acme.Bar" - // Method "getDiscount(value)" not found on base object "org.acme.Item" StringBuilder builder = new StringBuilder(); - if (isDataMap) { - builder.append("Entry "); - } else if (params.isEmpty()) { - builder.append("Property "); - } else { - builder.append("Method "); - } - builder.append("\"").append(name); - if (!params.isEmpty()) { - builder.append("("); - builder.append(params.stream().map(Expression::toOriginalString).collect(Collectors.joining(","))); - builder.append(")"); - } - builder.append("\" not found"); - if (isDataMap) { - builder.append(" in the data map"); + if (base instanceof Map || base instanceof Mapper) { + builder.append("Key ") + .append("\"") + .append(name) + .append("\" not found in the"); + if (isDataMap(base)) { + // Key "foo" not found in the template data map with keys [] + builder.append(" template data map with keys "); + if (base instanceof Map) { + builder.append(((Map) base).keySet().stream() + .filter(not(TemplateInstanceBase.DATA_MAP_KEY::equals)).collect(Collectors.toList())); + } else if (base instanceof Mapper) { + builder.append(((Mapper) base).mappedKeys().stream() + .filter(not(TemplateInstanceBase.DATA_MAP_KEY::equals)).collect(Collectors.toList())); + } + } else { + // Key "foo" not found in the map with keys [] + builder.append(" map with keys "); + if (base instanceof Map) { + builder.append(((Map) base).keySet()); + } else if (base instanceof Mapper) { + builder.append(((Mapper) base).mappedKeys()); + } + } + } else if (!params.isEmpty()) { + // Method "getDiscount(value)" not found on the base object "org.acme.Item" + builder.append("Method ") + .append("\"") + .append(name) + .append("(") + .append(params.stream().map(Expression::toOriginalString).collect(Collectors.joining(","))) + .append(")") + .append("\" not found") + .append(" on the base object \"").append(base == null ? "null" : base.getClass().getName()) + .append("\""); } else { - builder.append(" on the base object \"").append(base == null ? "null" : base.getClass().getName()) + // Property "foo" not found on the base object "org.acme.Bar" + builder.append("Property ") + .append("\"") + .append(name) + .append("\" not found") + .append(" on the base object \"").append(base == null ? "null" : base.getClass().getName()) .append("\""); } return builder.toString(); diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateGlobalProvider.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateGlobalProvider.java new file mode 100644 index 0000000000000..7918f914ac5c9 --- /dev/null +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateGlobalProvider.java @@ -0,0 +1,10 @@ +package io.quarkus.qute; + +/** + * An implementation is generated for each class declaring a template global. + * + * @see TemplateGlobal + */ +public interface TemplateGlobalProvider extends TemplateInstance.Initializer, NamespaceResolver { + +} diff --git a/independent-projects/qute/core/src/test/java/io/quarkus/qute/IfSectionTest.java b/independent-projects/qute/core/src/test/java/io/quarkus/qute/IfSectionTest.java index d5d9538ff086f..3f0d88c39f2a4 100644 --- a/independent-projects/qute/core/src/test/java/io/quarkus/qute/IfSectionTest.java +++ b/independent-projects/qute/core/src/test/java/io/quarkus/qute/IfSectionTest.java @@ -221,7 +221,8 @@ public void testSafeExpression() { engine.parse("{#if val.is.not.there}NOK{#else}OK{/if}").render(); fail(); } catch (TemplateException expected) { - assertEquals("Rendering error: Entry \"val\" not found in the data map in expression {val.is.not.there}", + assertEquals( + "Rendering error: Key \"val\" not found in the template data map with keys [] in expression {val.is.not.there}", expected.getMessage()); } assertEquals("OK", engine.parse("{#if val.is.not.there??}NOK{#else}OK{/if}").render()); diff --git a/independent-projects/qute/core/src/test/java/io/quarkus/qute/NotFoundResultTest.java b/independent-projects/qute/core/src/test/java/io/quarkus/qute/NotFoundResultTest.java new file mode 100644 index 0000000000000..24b14c90b4d3e --- /dev/null +++ b/independent-projects/qute/core/src/test/java/io/quarkus/qute/NotFoundResultTest.java @@ -0,0 +1,66 @@ +package io.quarkus.qute; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletionStage; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; + +import io.quarkus.qute.Results.NotFound; + +public class NotFoundResultTest { + + @Test + public void testAsMessage() { + assertEquals("Key \"foo\" not found in the template data map with keys []", + NotFound.from(evalContext(Map.of(TemplateInstanceBase.DATA_MAP_KEY, true))).asMessage()); + assertEquals("Key \"foo\" not found in the map with keys [baz]", + NotFound.from(evalContext(Map.of("baz", true))).asMessage()); + assertEquals("Property \"foo\" not found on the base object \"java.lang.Boolean\"", + NotFound.from(evalContext(Boolean.TRUE)).asMessage()); + assertEquals("Method \"foo(param)\" not found on the base object \"java.lang.Boolean\"", + NotFound.from(evalContext(Boolean.TRUE, "param")).asMessage()); + assertEquals("Key \"foo\" not found in the map with keys [baz]", + NotFound.from(evalContext(Mapper.wrap(Map.of("baz", false)))).asMessage()); + } + + EvalContext evalContext(Object base, Object... params) { + return new EvalContext() { + + @Override + public List getParams() { + return Arrays.stream(params).map(p -> ExpressionImpl.from(p.toString())).collect(Collectors.toList()); + } + + @Override + public String getName() { + return "foo"; + } + + @Override + public Object getBase() { + return base; + } + + @Override + public Object getAttribute(String key) { + return null; + } + + @Override + public CompletionStage evaluate(Expression expression) { + return null; + } + + @Override + public CompletionStage evaluate(String expression) { + return null; + } + }; + } + +} diff --git a/independent-projects/qute/core/src/test/java/io/quarkus/qute/SimpleTest.java b/independent-projects/qute/core/src/test/java/io/quarkus/qute/SimpleTest.java index 3e617c8229b88..642f7bf359058 100644 --- a/independent-projects/qute/core/src/test/java/io/quarkus/qute/SimpleTest.java +++ b/independent-projects/qute/core/src/test/java/io/quarkus/qute/SimpleTest.java @@ -153,7 +153,7 @@ public void testEmptySectionTag() { @Test public void testNotFound() { - assertEquals("Entry \"foo\" not found in the data map in foo.bar Collection size: 0", + assertEquals("Key \"foo\" not found in the template data map with keys [collection] in foo.bar Collection size: 0", Engine.builder().strictRendering(false).addDefaultValueResolvers() .addResultMapper(new ResultMapper() { diff --git a/independent-projects/qute/core/src/test/java/io/quarkus/qute/WhenSectionTest.java b/independent-projects/qute/core/src/test/java/io/quarkus/qute/WhenSectionTest.java index c8e4eba0be092..d6e428ecb85c4 100644 --- a/independent-projects/qute/core/src/test/java/io/quarkus/qute/WhenSectionTest.java +++ b/independent-projects/qute/core/src/test/java/io/quarkus/qute/WhenSectionTest.java @@ -59,7 +59,14 @@ public void testSwitchEnum() { try { fail(template.data("state", null).render()); } catch (TemplateException expected) { - assertEquals("Rendering error: Entry \"ON\" not found in the data map in expression {ON}", + // If state is null we can't detect it's an enum value, hence the weird error message + assertEquals("Rendering error: Key \"ON\" not found in the template data map with keys [state] in expression {ON}", + expected.getMessage()); + } + try { + fail(template.render()); + } catch (TemplateException expected) { + assertEquals("Rendering error: Key \"state\" not found in the template data map with keys [] in expression {state}", expected.getMessage()); } } @@ -75,7 +82,14 @@ public void testWhenEnum() { try { fail(template.data("state", null).render()); } catch (TemplateException expected) { - assertEquals("Rendering error: Entry \"ON\" not found in the data map in expression {ON}", + // If state is null we can't detect it's an enum value, hence the weird error message + assertEquals("Rendering error: Key \"ON\" not found in the template data map with keys [state] in expression {ON}", + expected.getMessage()); + } + try { + fail(template.render()); + } catch (TemplateException expected) { + assertEquals("Rendering error: Key \"state\" not found in the template data map with keys [] in expression {state}", expected.getMessage()); } } @@ -129,7 +143,8 @@ public void testWhenNotFound() { try { fail(template.render()); } catch (TemplateException expected) { - assertEquals("Rendering error: Entry \"testMe\" not found in the data map in expression {testMe}", + assertEquals( + "Rendering error: Key \"testMe\" not found in the template data map with keys [] in expression {testMe}", expected.getMessage()); } } diff --git a/independent-projects/qute/generator/src/main/java/io/quarkus/qute/generator/AbstractGenerator.java b/independent-projects/qute/generator/src/main/java/io/quarkus/qute/generator/AbstractGenerator.java new file mode 100644 index 0000000000000..84475ab5f42a5 --- /dev/null +++ b/independent-projects/qute/generator/src/main/java/io/quarkus/qute/generator/AbstractGenerator.java @@ -0,0 +1,180 @@ +package io.quarkus.qute.generator; + +import static org.objectweb.asm.Opcodes.ACC_PRIVATE; + +import java.util.HashSet; +import java.util.Set; + +import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.DotName; +import org.jboss.jandex.FieldInfo; +import org.jboss.jandex.IndexView; +import org.jboss.jandex.PrimitiveType.Primitive; +import org.jboss.jandex.Type; + +import io.quarkus.gizmo.BranchResult; +import io.quarkus.gizmo.BytecodeCreator; +import io.quarkus.gizmo.ClassCreator; +import io.quarkus.gizmo.ClassOutput; +import io.quarkus.gizmo.FieldDescriptor; +import io.quarkus.gizmo.Gizmo; +import io.quarkus.gizmo.IfThenElse; +import io.quarkus.gizmo.MethodCreator; +import io.quarkus.gizmo.ResultHandle; +import io.quarkus.qute.CompletedStage; + +public abstract class AbstractGenerator { + + protected final Set generatedTypes; + protected final IndexView index; + protected final ClassOutput classOutput; + + protected AbstractGenerator(IndexView index, ClassOutput classOutput) { + this.generatedTypes = new HashSet<>(); + this.index = index; + this.classOutput = classOutput; + } + + public Set getGeneratedTypes() { + return generatedTypes; + } + + protected void completeBoolean(BytecodeCreator bc, ResultHandle result) { + BranchResult isTrue = bc.ifTrue(result); + BytecodeCreator trueBranch = isTrue.trueBranch(); + trueBranch.returnValue(trueBranch.readStaticField(Descriptors.RESULTS_TRUE)); + BytecodeCreator falseBranch = isTrue.falseBranch(); + falseBranch.returnValue(falseBranch.readStaticField(Descriptors.RESULTS_FALSE)); + } + + protected boolean isEnum(Type returnType) { + if (returnType.kind() != org.jboss.jandex.Type.Kind.CLASS) { + return false; + } + ClassInfo maybeEnum = index.getClassByName(returnType.name()); + return maybeEnum != null && maybeEnum.isEnum(); + } + + protected boolean hasCompletionStage(Type type) { + return !skipMemberType(type) && hasCompletionStageInTypeClosure(index.getClassByName(type.name()), index); + } + + protected boolean hasCompletionStageInTypeClosure(ClassInfo classInfo, + IndexView index) { + return hasClassInTypeClosure(classInfo, DotNames.COMPLETION_STAGE, index); + } + + protected boolean hasClassInTypeClosure(ClassInfo classInfo, DotName className, + IndexView index) { + + if (classInfo == null) { + // TODO cannot perform analysis + return false; + } + if (classInfo.name().equals(className)) { + return true; + } + // Interfaces + for (Type interfaceType : classInfo.interfaceTypes()) { + ClassInfo interfaceClassInfo = index.getClassByName(interfaceType.name()); + if (interfaceClassInfo != null && hasCompletionStageInTypeClosure(interfaceClassInfo, index)) { + return true; + } + } + // Superclass + if (classInfo.superClassType() != null) { + ClassInfo superClassInfo = index.getClassByName(classInfo.superName()); + if (superClassInfo != null && hasClassInTypeClosure(superClassInfo, className, index)) { + return true; + } + } + return false; + } + + protected void processReturnVal(BytecodeCreator bc, Type type, ResultHandle ret, ClassCreator classCreator) { + if (hasCompletionStage(type)) { + bc.returnValue(ret); + } else { + // Try to use some shared CompletedStage constants + if (type.kind() == org.jboss.jandex.Type.Kind.PRIMITIVE + && type.asPrimitiveType().primitive() == Primitive.BOOLEAN) { + completeBoolean(bc, ret); + } else if (type.name().equals(DotNames.BOOLEAN)) { + BytecodeCreator isNull = bc.ifNull(ret).trueBranch(); + isNull.returnValue(isNull.readStaticField(Descriptors.RESULTS_NULL)); + completeBoolean(bc, bc.invokeVirtualMethod(Descriptors.BOOLEAN_VALUE, ret)); + } else if (isEnum(type)) { + BytecodeCreator isNull = bc.ifNull(ret).trueBranch(); + isNull.returnValue(isNull.readStaticField(Descriptors.RESULTS_NULL)); + completeEnum(index.getClassByName(type.name()), classCreator, ret, bc); + } else { + bc.returnValue(bc.invokeStaticMethod(Descriptors.COMPLETED_STAGE_OF, ret)); + } + } + } + + protected boolean completeEnum(ClassInfo enumClass, ClassCreator valueResolver, ResultHandle result, BytecodeCreator bc) { + IfThenElse ifThenElse = null; + for (FieldInfo enumConstant : enumClass.enumConstants()) { + String name = enumClass.name().toString().replace(".", "_") + "$$" + + enumConstant.name(); + FieldDescriptor enumConstantField = FieldDescriptor.of(enumClass.name().toString(), + enumConstant.name(), enumClass.name().toString()); + + // Additional methods and fields are generated for enums that are part of the index + // We don't care about visibility and atomicity here + // private CompletedStage org_acme_MyEnum$$CONSTANT; + FieldDescriptor csField = valueResolver + .getFieldCreator(name, CompletedStage.class).setModifiers(ACC_PRIVATE) + .getFieldDescriptor(); + // private CompletedStage org_acme_MyEnum$$CONSTANT() { + // if (org_acme_MyEnum$$CONSTANT == null) { + // org_acme_MyEnum$$CONSTANT = CompletedStage.of(MyEnum.CONSTANT); + // } + // return org_acme_MyEnum$$CONSTANT; + // } + MethodCreator enumConstantMethod = valueResolver.getMethodCreator(name, + CompletedStage.class).setModifiers(ACC_PRIVATE); + BytecodeCreator isNull = enumConstantMethod.ifNull(enumConstantMethod + .readInstanceField(csField, enumConstantMethod.getThis())) + .trueBranch(); + ResultHandle val = isNull.readStaticField(enumConstantField); + isNull.writeInstanceField(csField, enumConstantMethod.getThis(), + isNull.invokeStaticMethod(Descriptors.COMPLETED_STAGE_OF, val)); + enumConstantMethod.returnValue(enumConstantMethod + .readInstanceField(csField, enumConstantMethod.getThis())); + + // Unfortunately, we can't use the BytecodeCreator#enumSwitch() here because the enum class is not loaded + // if(val.equals(MyEnum.CONSTANT)) + // return org_acme_MyEnum$$CONSTANT(); + BytecodeCreator match; + if (ifThenElse == null) { + ifThenElse = bc.ifThenElse( + Gizmo.equals(bc, bc.readStaticField(enumConstantField), result)); + match = ifThenElse.then(); + } else { + match = ifThenElse.elseIf( + b -> Gizmo.equals(b, b.readStaticField(enumConstantField), result)); + } + match.returnValue(match.invokeVirtualMethod( + enumConstantMethod.getMethodDescriptor(), match.getThis())); + } + return true; + } + + protected boolean skipMemberType(Type type) { + switch (type.kind()) { + case VOID: + case PRIMITIVE: + case ARRAY: + case TYPE_VARIABLE: + case UNRESOLVED_TYPE_VARIABLE: + case TYPE_VARIABLE_REFERENCE: + case WILDCARD_TYPE: + return true; + default: + return false; + } + } + +} diff --git a/independent-projects/qute/generator/src/main/java/io/quarkus/qute/generator/ExtensionMethodGenerator.java b/independent-projects/qute/generator/src/main/java/io/quarkus/qute/generator/ExtensionMethodGenerator.java index a9448bf545ef3..1eea9f6d65045 100644 --- a/independent-projects/qute/generator/src/main/java/io/quarkus/qute/generator/ExtensionMethodGenerator.java +++ b/independent-projects/qute/generator/src/main/java/io/quarkus/qute/generator/ExtensionMethodGenerator.java @@ -14,7 +14,6 @@ import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; @@ -60,7 +59,7 @@ * @see ValueResolver * @see NamespaceResolver */ -public class ExtensionMethodGenerator { +public class ExtensionMethodGenerator extends AbstractGenerator { public static final DotName TEMPLATE_EXTENSION = DotName.createSimple(TemplateExtension.class.getName()); public static final DotName TEMPLATE_ATTRIBUTE = DotName.createSimple(TemplateExtension.TemplateAttribute.class.getName()); @@ -74,14 +73,8 @@ public class ExtensionMethodGenerator { public static final String NAMESPACE = "namespace"; public static final String PATTERN = "pattern"; - private final Set generatedTypes; - private final ClassOutput classOutput; - private final IndexView index; - public ExtensionMethodGenerator(IndexView index, ClassOutput classOutput) { - this.index = index; - this.classOutput = classOutput; - this.generatedTypes = new HashSet<>(); + super(index, classOutput); } public Set getGeneratedTypes() { @@ -235,8 +228,7 @@ private void implementResolve(ClassCreator valueResolver, ClassInfo declaringCla ResultHandle evalContext = resolve.getMethodParam(0); ResultHandle base = resolve.invokeInterfaceMethod(Descriptors.GET_BASE, evalContext); boolean isNameParamRequired = patternField != null || !matchNames.isEmpty() || matchName.equals(TemplateExtension.ANY); - boolean returnsCompletionStage = method.returnType().kind() != Kind.PRIMITIVE && ValueResolverGenerator - .hasCompletionStageInTypeClosure(index.getClassByName(method.returnType().name()), index); + boolean returnsCompletionStage = hasCompletionStage(method.returnType()); ResultHandle ret; if (!params.needsEvaluation()) { diff --git a/independent-projects/qute/generator/src/main/java/io/quarkus/qute/generator/TemplateGlobalGenerator.java b/independent-projects/qute/generator/src/main/java/io/quarkus/qute/generator/TemplateGlobalGenerator.java index e6a62d5122a37..6d259cb04ee9d 100644 --- a/independent-projects/qute/generator/src/main/java/io/quarkus/qute/generator/TemplateGlobalGenerator.java +++ b/independent-projects/qute/generator/src/main/java/io/quarkus/qute/generator/TemplateGlobalGenerator.java @@ -6,43 +6,50 @@ import static org.objectweb.asm.Opcodes.ACC_PUBLIC; import java.lang.reflect.Modifier; -import java.util.HashSet; import java.util.Map; import java.util.Map.Entry; import java.util.Set; +import java.util.concurrent.CompletionStage; +import java.util.function.Consumer; import org.jboss.jandex.AnnotationTarget; import org.jboss.jandex.ClassInfo; import org.jboss.jandex.DotName; import org.jboss.jandex.FieldInfo; +import org.jboss.jandex.IndexView; import org.jboss.jandex.MethodInfo; import org.jboss.jandex.Type.Kind; +import io.quarkus.gizmo.BytecodeCreator; import io.quarkus.gizmo.ClassCreator; import io.quarkus.gizmo.ClassOutput; import io.quarkus.gizmo.FieldDescriptor; import io.quarkus.gizmo.MethodCreator; import io.quarkus.gizmo.MethodDescriptor; import io.quarkus.gizmo.ResultHandle; +import io.quarkus.gizmo.Switch.StringSwitch; +import io.quarkus.qute.EvalContext; import io.quarkus.qute.TemplateGlobal; +import io.quarkus.qute.TemplateGlobalProvider; import io.quarkus.qute.TemplateInstance; /** * Generates {@link TemplateInstance.Initializer}s for {@link TemplateGlobal} annotations. */ -public class TemplateGlobalGenerator { +public class TemplateGlobalGenerator extends AbstractGenerator { public static final DotName TEMPLATE_GLOBAL = DotName.createSimple(TemplateGlobal.class.getName()); public static final String NAME = "name"; public static final String SUFFIX = "_Globals"; - private final Set generatedTypes; - private final ClassOutput classOutput; + private final String namespace; + private int priority; - public TemplateGlobalGenerator(ClassOutput classOutput) { - this.generatedTypes = new HashSet<>(); - this.classOutput = classOutput; + public TemplateGlobalGenerator(ClassOutput classOutput, String namespace, int priority, IndexView index) { + super(index, classOutput); + this.namespace = namespace; + this.priority = priority; } public void generate(ClassInfo declaringClass, Map targets) { @@ -58,10 +65,11 @@ public void generate(ClassInfo declaringClass, Map tar String generatedName = generatedNameFromTarget(targetPackage, baseName, SUFFIX); generatedTypes.add(generatedName.replace('/', '.')); - ClassCreator initializer = ClassCreator.builder().classOutput(classOutput).className(generatedName) - .interfaces(TemplateInstance.Initializer.class).build(); + ClassCreator provider = ClassCreator.builder().classOutput(classOutput).className(generatedName) + .interfaces(TemplateGlobalProvider.class).build(); - MethodCreator accept = initializer.getMethodCreator("accept", void.class, Object.class) + // TemplateInstance.Initializer#accept() + MethodCreator accept = provider.getMethodCreator("accept", void.class, Object.class) .setModifiers(ACC_PUBLIC); for (Entry entry : targets.entrySet()) { @@ -84,7 +92,48 @@ public void generate(ClassInfo declaringClass, Map tar accept.invokeInterfaceMethod(Descriptors.TEMPLATE_INSTANCE_DATA, accept.getMethodParam(0), name, global); } accept.returnValue(null); - initializer.close(); + + // NamespaceResolver#getNamespace() + MethodCreator getNamespace = provider.getMethodCreator("getNamespace", String.class); + getNamespace.returnValue(getNamespace.load(namespace)); + + // WithPriority#getPriority() + MethodCreator getPriority = provider.getMethodCreator("getPriority", int.class); + // Namespace resolvers for the same namespace may not share the same priority + // So we increase the initial priority for each provider + getPriority.returnValue(getPriority.load(priority++)); + + // Resolver#resolve() + MethodCreator resolve = provider.getMethodCreator("resolve", CompletionStage.class, EvalContext.class) + .setModifiers(ACC_PUBLIC); + ResultHandle evalContext = resolve.getMethodParam(0); + ResultHandle name = resolve.invokeInterfaceMethod(Descriptors.GET_NAME, evalContext); + StringSwitch nameSwitch = resolve.stringSwitch(name); + for (Entry e : targets.entrySet()) { + Consumer readGlobal = new Consumer() { + @Override + public void accept(BytecodeCreator bc) { + switch (e.getValue().kind()) { + case FIELD: + FieldInfo field = e.getValue().asField(); + processReturnVal(bc, field.type(), bc.readStaticField(FieldDescriptor.of(field)), provider); + break; + case METHOD: + MethodInfo method = e.getValue().asMethod(); + processReturnVal(bc, method.returnType(), bc.invokeStaticMethod(MethodDescriptor.of(method)), + provider); + break; + default: + throw new IllegalStateException("Unsupported target: " + e.getValue()); + } + + } + }; + nameSwitch.caseOf(e.getKey(), readGlobal); + } + resolve.returnValue(resolve.invokeStaticMethod(Descriptors.RESULTS_NOT_FOUND_EC, evalContext)); + + provider.close(); } public Set getGeneratedTypes() { diff --git a/independent-projects/qute/generator/src/main/java/io/quarkus/qute/generator/ValueResolverGenerator.java b/independent-projects/qute/generator/src/main/java/io/quarkus/qute/generator/ValueResolverGenerator.java index ff869c8a15e7a..f6d13de82b267 100644 --- a/independent-projects/qute/generator/src/main/java/io/quarkus/qute/generator/ValueResolverGenerator.java +++ b/independent-projects/qute/generator/src/main/java/io/quarkus/qute/generator/ValueResolverGenerator.java @@ -1,7 +1,6 @@ package io.quarkus.qute.generator; import static java.util.function.Predicate.not; -import static org.objectweb.asm.Opcodes.ACC_PRIVATE; import static org.objectweb.asm.Opcodes.ACC_PUBLIC; import java.lang.reflect.Modifier; @@ -34,7 +33,6 @@ import org.jboss.jandex.IndexView; import org.jboss.jandex.MethodInfo; import org.jboss.jandex.PrimitiveType; -import org.jboss.jandex.PrimitiveType.Primitive; import org.jboss.jandex.Type; import org.jboss.logging.Logger; @@ -48,13 +46,11 @@ import io.quarkus.gizmo.FieldDescriptor; import io.quarkus.gizmo.FunctionCreator; import io.quarkus.gizmo.Gizmo; -import io.quarkus.gizmo.IfThenElse; import io.quarkus.gizmo.MethodCreator; import io.quarkus.gizmo.MethodDescriptor; import io.quarkus.gizmo.ResultHandle; import io.quarkus.gizmo.Switch; import io.quarkus.gizmo.TryBlock; -import io.quarkus.qute.CompletedStage; import io.quarkus.qute.EvalContext; import io.quarkus.qute.EvaluatedParams; import io.quarkus.qute.NamespaceResolver; @@ -66,7 +62,7 @@ * * @see ValueResolver */ -public class ValueResolverGenerator { +public class ValueResolverGenerator extends AbstractGenerator { public static Builder builder() { return new Builder(); @@ -92,9 +88,6 @@ public static Builder builder() { public static final int DEFAULT_PRIORITY = 10; - private final Set generatedTypes; - private final IndexView index; - private final ClassOutput classOutput; private final Map nameToClass; private final Map nameToTemplateData; @@ -103,18 +96,12 @@ public static Builder builder() { ValueResolverGenerator(IndexView index, ClassOutput classOutput, Map nameToClass, Map nameToTemplateData, Function> forceGettersFunction) { - this.generatedTypes = new HashSet<>(); - this.classOutput = classOutput; - this.index = index; + super(index, classOutput); this.nameToClass = new HashMap<>(nameToClass); this.nameToTemplateData = new HashMap<>(nameToTemplateData); this.forceGettersFunction = forceGettersFunction; } - public Set getGeneratedTypes() { - return generatedTypes; - } - /** * Generate value resolvers for all classes added via {@link Builder#addClass(ClassInfo, AnnotationInstance)}. */ @@ -377,33 +364,13 @@ private boolean implementResolve(ClassCreator valueResolver, String clazzName, C @Override public void accept(BytecodeCreator bc) { Type returnType = method.returnType(); - boolean hasCompletionStage = !skipMemberType(returnType) - && hasCompletionStageInTypeClosure(index.getClassByName(method.returnType().name()), index); ResultHandle invokeRet; if (Modifier.isInterface(clazz.flags())) { invokeRet = bc.invokeInterfaceMethod(MethodDescriptor.of(method), base); } else { invokeRet = bc.invokeVirtualMethod(MethodDescriptor.of(method), base); } - if (hasCompletionStage) { - bc.returnValue(invokeRet); - } else { - // Try to use some shared CompletedStage constants - if (returnType.kind() == org.jboss.jandex.Type.Kind.PRIMITIVE - && returnType.asPrimitiveType().primitive() == Primitive.BOOLEAN) { - completeBoolean(bc, invokeRet); - } else if (method.returnType().name().equals(DotNames.BOOLEAN)) { - BytecodeCreator isNull = bc.ifNull(invokeRet).trueBranch(); - isNull.returnValue(isNull.readStaticField(Descriptors.RESULTS_NULL)); - completeBoolean(bc, bc.invokeVirtualMethod(Descriptors.BOOLEAN_VALUE, invokeRet)); - } else if (isEnum(returnType)) { - BytecodeCreator isNull = bc.ifNull(invokeRet).trueBranch(); - isNull.returnValue(isNull.readStaticField(Descriptors.RESULTS_NULL)); - completeEnum(index.getClassByName(returnType.name()), valueResolver, invokeRet, bc); - } else { - bc.returnValue(bc.invokeStaticMethod(Descriptors.COMPLETED_STAGE_OF, invokeRet)); - } - } + processReturnVal(bc, returnType, invokeRet, valueResolver); } }; nameSwitch.caseOf(matchingNames, invokeMethod); @@ -487,71 +454,6 @@ public void accept(BytecodeCreator bc) { return true; } - private void completeBoolean(BytecodeCreator bc, ResultHandle result) { - BranchResult isTrue = bc.ifTrue(result); - BytecodeCreator trueBranch = isTrue.trueBranch(); - trueBranch.returnValue(trueBranch.readStaticField(Descriptors.RESULTS_TRUE)); - BytecodeCreator falseBranch = isTrue.falseBranch(); - falseBranch.returnValue(falseBranch.readStaticField(Descriptors.RESULTS_FALSE)); - } - - private boolean isEnum(Type returnType) { - if (returnType.kind() != org.jboss.jandex.Type.Kind.CLASS) { - return false; - } - ClassInfo maybeEnum = index.getClassByName(returnType.name()); - return maybeEnum != null && maybeEnum.isEnum(); - } - - private boolean completeEnum(ClassInfo enumClass, ClassCreator valueResolver, ResultHandle result, BytecodeCreator bc) { - IfThenElse ifThenElse = null; - for (FieldInfo enumConstant : enumClass.enumConstants()) { - String name = enumClass.name().toString().replace(".", "_") + "$$" - + enumConstant.name(); - FieldDescriptor enumConstantField = FieldDescriptor.of(enumClass.name().toString(), - enumConstant.name(), enumClass.name().toString()); - - // Additional methods and fields are generated for enums that are part of the index - // We don't care about visibility and atomicity here - // private CompletedStage org_acme_MyEnum$$CONSTANT; - FieldDescriptor csField = valueResolver - .getFieldCreator(name, CompletedStage.class).setModifiers(ACC_PRIVATE) - .getFieldDescriptor(); - // private CompletedStage org_acme_MyEnum$$CONSTANT() { - // if (org_acme_MyEnum$$CONSTANT == null) { - // org_acme_MyEnum$$CONSTANT = CompletedStage.of(MyEnum.CONSTANT); - // } - // return org_acme_MyEnum$$CONSTANT; - // } - MethodCreator enumConstantMethod = valueResolver.getMethodCreator(name, - CompletedStage.class).setModifiers(ACC_PRIVATE); - BytecodeCreator isNull = enumConstantMethod.ifNull(enumConstantMethod - .readInstanceField(csField, enumConstantMethod.getThis())) - .trueBranch(); - ResultHandle val = isNull.readStaticField(enumConstantField); - isNull.writeInstanceField(csField, enumConstantMethod.getThis(), - isNull.invokeStaticMethod(Descriptors.COMPLETED_STAGE_OF, val)); - enumConstantMethod.returnValue(enumConstantMethod - .readInstanceField(csField, enumConstantMethod.getThis())); - - // Unfortunately, we can't use the BytecodeCreator#enumSwitch() here because the enum class is not loaded - // if(val.equals(MyEnum.CONSTANT)) - // return org_acme_MyEnum$$CONSTANT(); - BytecodeCreator match; - if (ifThenElse == null) { - ifThenElse = bc.ifThenElse( - Gizmo.equals(bc, bc.readStaticField(enumConstantField), result)); - match = ifThenElse.then(); - } else { - match = ifThenElse.elseIf( - b -> Gizmo.equals(b, b.readStaticField(enumConstantField), result)); - } - match.returnValue(match.invokeVirtualMethod( - enumConstantMethod.getMethodDescriptor(), match.getThis())); - } - return true; - } - private boolean implementNamespaceResolve(ClassCreator valueResolver, String clazzName, ClassInfo clazz, Predicate filter) { MethodCreator resolve = valueResolver.getMethodCreator("resolve", CompletionStage.class, EvalContext.class) @@ -611,15 +513,13 @@ private boolean implementNamespaceResolve(ClassCreator valueResolver, String cla try (BytecodeCreator matchScope = createMatchScope(resolve, method.name(), 0, method.returnType(), name, params, paramsCount)) { ResultHandle ret; - boolean hasCompletionStage = !skipMemberType(method.returnType()) - && hasCompletionStageInTypeClosure(index.getClassByName(method.returnType().name()), index); ResultHandle invokeRet; if (Modifier.isInterface(clazz.flags())) { invokeRet = matchScope.invokeStaticInterfaceMethod(MethodDescriptor.of(method)); } else { invokeRet = matchScope.invokeStaticMethod(MethodDescriptor.of(method)); } - if (hasCompletionStage) { + if (hasCompletionStage(method.returnType())) { ret = invokeRet; } else { ret = matchScope.invokeStaticMethod(Descriptors.COMPLETED_STAGE_OF, invokeRet); @@ -702,11 +602,8 @@ private void matchMethod(MethodInfo method, ClassInfo clazz, MethodCreator resol paramsCount); // Invoke the method - ResultHandle ret; - boolean hasCompletionStage = !skipMemberType(method.returnType()) - && hasCompletionStageInTypeClosure(index.getClassByName(method.returnType().name()), index); // Evaluate the params first - ret = matchScope + ResultHandle ret = matchScope .newInstance(MethodDescriptor.ofConstructor(CompletableFuture.class)); // The CompletionStage upon which we invoke whenComplete() @@ -809,7 +706,7 @@ private void matchMethod(MethodInfo method, ClassInfo clazz, MethodCreator resol } } - if (hasCompletionStage) { + if (hasCompletionStage(method.returnType())) { FunctionCreator invokeWhenCompleteFun = tryCatch.createFunction(BiConsumer.class); tryCatch.invokeInterfaceMethod(Descriptors.CF_WHEN_COMPLETE, invokeRet, invokeWhenCompleteFun.getInstance()); @@ -908,9 +805,6 @@ private void matchMethods(String matchName, int matchParamsCount, Collection initFilters(AnnotationInstance templateData) { Predicate filter = ValueResolverGenerator::defaultFilter; if (templateData != null) { @@ -1332,38 +1211,6 @@ private static boolean noneMethodMatches(List methods, String name) { return true; } - public static boolean hasCompletionStageInTypeClosure(ClassInfo classInfo, - IndexView index) { - return hasClassInTypeClosure(classInfo, DotNames.COMPLETION_STAGE, index); - } - - public static boolean hasClassInTypeClosure(ClassInfo classInfo, DotName className, - IndexView index) { - - if (classInfo == null) { - // TODO cannot perform analysis - return false; - } - if (classInfo.name().equals(className)) { - return true; - } - // Interfaces - for (Type interfaceType : classInfo.interfaceTypes()) { - ClassInfo interfaceClassInfo = index.getClassByName(interfaceType.name()); - if (interfaceClassInfo != null && hasCompletionStageInTypeClosure(interfaceClassInfo, index)) { - return true; - } - } - // Superclass - if (classInfo.superClassType() != null) { - ClassInfo superClassInfo = index.getClassByName(classInfo.superName()); - if (superClassInfo != null && hasClassInTypeClosure(superClassInfo, className, index)) { - return true; - } - } - return false; - } - public static boolean isVarArgs(MethodInfo method) { return (method.flags() & 0x00000080) != 0; } diff --git a/independent-projects/qute/pom.xml b/independent-projects/qute/pom.xml index 4a03e88808107..3e7a74567000d 100644 --- a/independent-projects/qute/pom.xml +++ b/independent-projects/qute/pom.xml @@ -38,12 +38,9 @@ UTF-8 - 11 - 11 - 11 - 5.10.0 + 5.10.1 3.24.2 - 3.1.5 + 3.1.6 1.7.0 3.5.3.Final 3.11.0 diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/api/QuarkusRestClientProperties.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/api/QuarkusRestClientProperties.java index 8783a86417535..e4e16d0682b1c 100644 --- a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/api/QuarkusRestClientProperties.java +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/api/QuarkusRestClientProperties.java @@ -27,7 +27,7 @@ public class QuarkusRestClientProperties { public static final String READ_TIMEOUT = "io.quarkus.rest.client.read-timeout"; /** - * See {@link io.netty.handler.codec.http.multipart.HttpPostRequestEncoder.EncoderMode}, RFC1738 by default + * See {@link EncoderMode}, RFC1738 by default */ public static final String MULTIPART_ENCODER_MODE = "io.quarkus.rest.client.multipart-post-encoder-mode"; diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientReaderInterceptorContextImpl.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientReaderInterceptorContextImpl.java index 200f0c5bcc8eb..99fa3837fd647 100644 --- a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientReaderInterceptorContextImpl.java +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientReaderInterceptorContextImpl.java @@ -91,7 +91,8 @@ public Object proceed() throws IOException, WebApplicationException { } } - StringBuilder errorMessage = new StringBuilder("Response could not be mapped to type " + entityType); + StringBuilder errorMessage = new StringBuilder( + "Response could not be mapped to type " + entityType + " for response with media type " + mediaType); if (!contextualizers.isEmpty()) { var input = new MissingMessageBodyReaderErrorMessageContextualizer.Input() { @Override diff --git a/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/EndpointIndexer.java b/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/EndpointIndexer.java index 0f4dca20e286b..e3c51521ec99a 100644 --- a/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/EndpointIndexer.java +++ b/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/EndpointIndexer.java @@ -19,6 +19,8 @@ import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.ENCODED; import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.FLOAT; import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.FORM_PARAM; +import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.GET; +import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.HEAD; import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.HEADER_PARAM; import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.HTTP_HEADERS; import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.INSTANT; @@ -37,6 +39,7 @@ import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.OFFSET_DATE_TIME; import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.OFFSET_TIME; import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.OPTIONAL; +import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.OPTIONS; import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.PATH; import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.PATH_PARAM; import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.PATH_SEGMENT; @@ -626,6 +629,10 @@ private ResourceMethod createResourceMethod(ClassInfo currentClassInfo, ClassInf "Resource method " + currentMethodInfo + " can only have a single body parameter: " + currentMethodInfo.parameterName(i)); bodyParamType = paramType; + if (GET.equals(httpMethod) || HEAD.equals(httpMethod) || OPTIONS.equals(httpMethod)) { + log.warn("Using a body parameter with " + httpMethod + " is strongly discouraged. Offending method is '" + + currentMethodInfo.declaringClass().name() + "#" + currentMethodInfo + "'"); + } } String elementType = parameterResult.getElementType(); boolean single = parameterResult.isSingle(); diff --git a/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/core/Serialisers.java b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/core/Serialisers.java index dc5224b5ba00e..c4bb99212a723 100644 --- a/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/core/Serialisers.java +++ b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/core/Serialisers.java @@ -4,7 +4,6 @@ import java.util.ArrayList; import java.util.Collections; import java.util.Deque; -import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; @@ -31,7 +30,6 @@ public abstract class Serialisers { public static final Annotation[] NO_ANNOTATION = new Annotation[0]; public static final ReaderInterceptor[] NO_READER_INTERCEPTOR = new ReaderInterceptor[0]; public static final WriterInterceptor[] NO_WRITER_INTERCEPTOR = new WriterInterceptor[0]; - protected static final Map, Class> primitivesToWrappers = new HashMap<>(); // FIXME: spec says we should use generic type, but not sure how to pass that type from Jandex to reflection protected final QuarkusMultivaluedMap, ResourceWriter> writers = new QuarkusMultivaluedHashMap<>(); protected final QuarkusMultivaluedMap, ResourceReader> readers = new QuarkusMultivaluedHashMap<>(); @@ -46,9 +44,7 @@ public List> findReaders(ConfigurationImpl configuration, C List desired = MediaTypeHelper.getUngroupedMediaTypes(mediaType); List> ret = new ArrayList<>(); Deque> toProcess = new LinkedList<>(); - Class klass = entityType; - if (primitivesToWrappers.containsKey(klass)) - klass = primitivesToWrappers.get(klass); + Class klass = lookupPrimitiveWrapper(entityType); QuarkusMultivaluedMap, ResourceReader> readers; if (configuration != null && !configuration.getResourceReaders().isEmpty()) { readers = new QuarkusMultivaluedHashMap<>(); @@ -124,9 +120,7 @@ public List> findBuildTimeWriters(Class entityType, Runt if (Response.class.isAssignableFrom(entityType)) { return Collections.emptyList(); } - Class klass = entityType; - if (primitivesToWrappers.containsKey(klass)) - klass = primitivesToWrappers.get(klass); + Class klass = primitiveWrapperOf(entityType); //first we check to make sure that the return type is build time selectable //this fails when there are eligible writers for a sub type of the entity type //e.g. if the entity type is Object and there are mappers for String then we @@ -241,14 +235,17 @@ public List> findWriters(ConfigurationImpl configuration, C return findWriters(configuration, entityType, resolvedMediaType, null); } + protected Class lookupPrimitiveWrapper(Class entityType) { + return entityType; + } + public List> findWriters(ConfigurationImpl configuration, Class entityType, MediaType resolvedMediaType, RuntimeType runtimeType) { // FIXME: invocation is very different between client and server, where the server doesn't treat GenericEntity specially // it's probably missing from there, while the client handles it upstack List mt = Collections.singletonList(resolvedMediaType); Class klass = entityType; - if (primitivesToWrappers.containsKey(klass)) - klass = primitivesToWrappers.get(klass); + klass = primitiveWrapperOf(entityType); QuarkusMultivaluedMap, ResourceWriter> writers; if (configuration != null && !configuration.getResourceWriters().isEmpty()) { writers = new QuarkusMultivaluedHashMap<>(); @@ -263,6 +260,10 @@ public List> findWriters(ConfigurationImpl configuration, C return toMessageBodyWriters(findResourceWriters(writers, klass, mt, runtimeType)); } + private Class primitiveWrapperOf(Class entityType) { + return entityType; + } + public static class Builtin { public final Class entityClass; public final String mediaType; diff --git a/independent-projects/resteasy-reactive/pom.xml b/independent-projects/resteasy-reactive/pom.xml index 0b5bf88744fa0..2533ad87f2d33 100644 --- a/independent-projects/resteasy-reactive/pom.xml +++ b/independent-projects/resteasy-reactive/pom.xml @@ -43,15 +43,12 @@ 4.0.1 1.1.5 UTF-8 - 11 - 11 - 11 4.0.1 - 3.1.5 + 3.1.6 1.12.12 - 5.10.0 - 3.9.5 + 5.10.1 + 3.9.6 3.24.2 3.5.3.Final 2.1.1 @@ -64,9 +61,9 @@ 2.5.1 2.1.2 4.4.6 - 5.3.2 + 5.4.0 1.0.0.Final - 2.15.3 + 2.16.0 2.4.0 3.0.2 3.0.3 diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/ResteasyReactiveRequestContext.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/ResteasyReactiveRequestContext.java index dedaf7dd127cd..2821582f8cd01 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/ResteasyReactiveRequestContext.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/ResteasyReactiveRequestContext.java @@ -135,6 +135,7 @@ public abstract class ResteasyReactiveRequestContext private OutputStream outputStream; private OutputStream underlyingOutputStream; private FormData formData; + private boolean producesChecked; public ResteasyReactiveRequestContext(Deployment deployment, ThreadSetupAction requestContext, ServerRestHandler[] handlerChain, ServerRestHandler[] abortHandlerChain) { @@ -226,18 +227,20 @@ public void setMaxPathParams(int maxPathParams) { } } - public String getPathParam(int index) { - return doGetPathParam(index, pathParamValues); + public String getPathParam(int index, boolean encoded) { + return doGetPathParam(index, pathParamValues, encoded); } - private String doGetPathParam(int index, Object pathParamValues) { + private String doGetPathParam(int index, Object pathParamValues, boolean encoded) { if (pathParamValues instanceof String[]) { - return ((String[]) pathParamValues)[index]; + String pathParam = ((String[]) pathParamValues)[index]; + return encoded ? pathParam : Encode.decodePath(pathParam); } if (index > 1) { throw new IndexOutOfBoundsException(); } - return (String) pathParamValues; + String pathParam = (String) pathParamValues; + return encoded ? pathParam : Encode.decodePath(pathParam); } public ResteasyReactiveRequestContext setPathParamValue(int index, String value) { @@ -794,6 +797,14 @@ public void initPathSegments() { } } + public void setProducesChecked(boolean checked) { + producesChecked = checked; + } + + public boolean isProducesChecked() { + return producesChecked; + } + @Override public Object getHeader(String name, boolean single) { if (httpHeaders == null) { @@ -926,18 +937,11 @@ public String getPathParameter(String name, boolean encoded) { Integer index = target.getPathParameterIndexes().get(name); String value; if (index != null) { - value = getPathParam(index); - } else { - // Check previous resources if the path is not defined in the current target - value = getResourceLocatorPathParam(name); - } - - // It's possible to inject a path param that's not defined, return null in this case - if (encoded && value != null) { - return Encode.encodeQueryParam(value); + return getPathParam(index, encoded); } - return value; + // Check previous resources if the path is not defined in the current target + return getResourceLocatorPathParam(name, encoded); } @Override @@ -996,8 +1000,8 @@ public ResteasyReactiveResourceInfo getResteasyReactiveResourceInfo() { public abstract Runnable registerTimer(long millis, Runnable task); - public String getResourceLocatorPathParam(String name) { - return getResourceLocatorPathParam(name, (PreviousResource) getProperty(PreviousResource.PROPERTY_KEY)); + public String getResourceLocatorPathParam(String name, boolean encoded) { + return getResourceLocatorPathParam(name, (PreviousResource) getProperty(PreviousResource.PROPERTY_KEY), encoded); } public FormData getFormData() { @@ -1009,7 +1013,7 @@ public ResteasyReactiveRequestContext setFormData(FormData formData) { return this; } - private String getResourceLocatorPathParam(String name, PreviousResource previousResource) { + private String getResourceLocatorPathParam(String name, PreviousResource previousResource, boolean encoded) { if (previousResource == null) { return null; } @@ -1020,13 +1024,13 @@ private String getResourceLocatorPathParam(String name, PreviousResource previou for (URITemplate.TemplateComponent component : classPath.components) { if (component.name != null) { if (component.name.equals(name)) { - return doGetPathParam(index, previousResource.locatorPathParamValues); + return doGetPathParam(index, previousResource.locatorPathParamValues, encoded); } index++; } else if (component.names != null) { for (String nm : component.names) { if (nm.equals(name)) { - return doGetPathParam(index, previousResource.locatorPathParamValues); + return doGetPathParam(index, previousResource.locatorPathParamValues, encoded); } } index++; @@ -1036,19 +1040,19 @@ private String getResourceLocatorPathParam(String name, PreviousResource previou for (URITemplate.TemplateComponent component : previousResource.locatorTarget.getPath().components) { if (component.name != null) { if (component.name.equals(name)) { - return doGetPathParam(index, previousResource.locatorPathParamValues); + return doGetPathParam(index, previousResource.locatorPathParamValues, encoded); } index++; } else if (component.names != null) { for (String nm : component.names) { if (nm.equals(name)) { - return doGetPathParam(index, previousResource.locatorPathParamValues); + return doGetPathParam(index, previousResource.locatorPathParamValues, encoded); } } index++; } } - return getResourceLocatorPathParam(name, previousResource.prev); + return getResourceLocatorPathParam(name, previousResource.prev, encoded); } public abstract boolean resumeExternalProcessing(); diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/ServerSerialisers.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/ServerSerialisers.java index 60eeac37665a2..59fc88f67d98d 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/ServerSerialisers.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/ServerSerialisers.java @@ -86,17 +86,6 @@ public void accept(ResteasyReactiveRequestContext context) { private static final String LENGTH_LOWER = "length"; private static final String CONTENT_TYPE = CONTENT + "-" + TYPE; // use this instead of the Vert.x constant because the TCK expects upper case - static { - primitivesToWrappers.put(boolean.class, Boolean.class); - primitivesToWrappers.put(char.class, Character.class); - primitivesToWrappers.put(byte.class, Byte.class); - primitivesToWrappers.put(short.class, Short.class); - primitivesToWrappers.put(int.class, Integer.class); - primitivesToWrappers.put(long.class, Long.class); - primitivesToWrappers.put(float.class, Float.class); - primitivesToWrappers.put(double.class, Double.class); - } - public final static List BUILTIN_READERS = List.of( new Serialisers.BuiltinReader(String.class, ServerStringMessageBodyHandler.class, MediaType.WILDCARD), @@ -189,6 +178,32 @@ public List apply(Class aClass) { } }; + @Override + protected final Class lookupPrimitiveWrapper(final Class entityType) { + if (!entityType.isPrimitive()) { + return entityType; + } + if (entityType == boolean.class) { + return Boolean.class; + } else if (entityType == char.class) { + return Character.class; + } else if (entityType == byte.class) { + return Byte.class; + } else if (entityType == short.class) { + return Short.class; + } else if (entityType == int.class) { + return Integer.class; + } else if (entityType == long.class) { + return Long.class; + } else if (entityType == float.class) { + return Float.class; + } else if (entityType == double.class) { + return Double.class; + } + // this shouldn't really happen, but better be safe than sorry + return entityType; + } + public static boolean invokeWriter(ResteasyReactiveRequestContext context, Object entity, MessageBodyWriter writer, ServerSerialisers serialisers) throws IOException { diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/parameters/LocatableResourcePathParamExtractor.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/parameters/LocatableResourcePathParamExtractor.java index 835be0b515340..e478141b7c45c 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/parameters/LocatableResourcePathParamExtractor.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/parameters/LocatableResourcePathParamExtractor.java @@ -12,7 +12,7 @@ public LocatableResourcePathParamExtractor(String name) { @Override public Object extractParameter(ResteasyReactiveRequestContext context) { - return context.getResourceLocatorPathParam(name); + return context.getResourceLocatorPathParam(name, false); } } diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/parameters/PathParamExtractor.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/parameters/PathParamExtractor.java index ccfa99edca5c3..d56f72b856bf5 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/parameters/PathParamExtractor.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/parameters/PathParamExtractor.java @@ -1,6 +1,8 @@ package org.jboss.resteasy.reactive.server.core.parameters; +import java.util.Arrays; import java.util.List; +import java.util.stream.Collectors; import org.jboss.resteasy.reactive.common.util.Encode; import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext; @@ -19,14 +21,14 @@ public PathParamExtractor(int index, boolean encoded, boolean single) { @Override public Object extractParameter(ResteasyReactiveRequestContext context) { - String pathParam = context.getPathParam(index); - if (encoded) { - pathParam = Encode.encodeQueryParam(pathParam); - } + String pathParam = context.getPathParam(index, true); if (single) { - return pathParam; + return encoded ? pathParam : Encode.decodePath(pathParam); } else { - return List.of(pathParam.split("/")); + return encoded + ? List.of(pathParam.split("/")) + : Arrays.stream(pathParam.split("/")).map(Encode::decodePath) + .collect(Collectors.toList()); } } } diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/handlers/ClassRoutingHandler.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/handlers/ClassRoutingHandler.java index df1d6de283c35..1b8fdc9b901d7 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/handlers/ClassRoutingHandler.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/handlers/ClassRoutingHandler.java @@ -141,6 +141,8 @@ public void handle(ResteasyReactiveRequestContext requestContext) throws Excepti throw new NotAcceptableException(INVALID_ACCEPT_HEADER_MESSAGE); } } + + requestContext.setProducesChecked(true); } requestContext.restart(target.value); diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/handlers/FixedProducesHandler.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/handlers/FixedProducesHandler.java index 8029481fc125c..16d30f65fd0c5 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/handlers/FixedProducesHandler.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/handlers/FixedProducesHandler.java @@ -35,8 +35,9 @@ public FixedProducesHandler(MediaType mediaType, EntityWriter writer) { @Override public void handle(ResteasyReactiveRequestContext requestContext) throws Exception { - List acceptValues = (List) requestContext.getHeader(HttpHeaders.ACCEPT, false); - if (acceptValues.isEmpty()) { + List acceptValues; + if (requestContext.isProducesChecked() || + (acceptValues = (List) requestContext.getHeader(HttpHeaders.ACCEPT, false)).isEmpty()) { requestContext.setResponseContentType(mediaType); requestContext.setEntityWriter(writer); } else { diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/jaxrs/UriInfoImpl.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/jaxrs/UriInfoImpl.java index 7cbd13cb272a6..987bbf2838113 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/jaxrs/UriInfoImpl.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/jaxrs/UriInfoImpl.java @@ -152,7 +152,7 @@ public MultivaluedMap getPathParameters(boolean decode) { RuntimeResource target = currentRequest.getTarget(); if (target != null) { // a target can be null if this happens in a filter that runs before the target is set for (Entry pathParam : target.getPathParameterIndexes().entrySet()) { - pathParams.add(pathParam.getKey(), currentRequest.getPathParam(pathParam.getValue())); + pathParams.add(pathParam.getKey(), currentRequest.getPathParam(pathParam.getValue(), false)); } } } diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/mapping/RequestMapper.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/mapping/RequestMapper.java index 5d8d5fc931191..7fd7cb535f518 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/mapping/RequestMapper.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/mapping/RequestMapper.java @@ -8,8 +8,6 @@ import java.util.Map; import java.util.regex.Matcher; -import org.jboss.resteasy.reactive.common.util.URIDecoder; - public class RequestMapper { private static final String[] EMPTY_STRING_ARRAY = new String[0]; @@ -111,7 +109,7 @@ private RequestMatch mapFromPathMatcher(String path, PathMatcher.PathMatch= 0) && (fileRange.getStart() <= fileRange.getEnd())) { + String contentRange = "bytes " + fileRange.getStart() + "-" + fileRange.getEnd() + "/" + fileLength; + long length = fileRange.getEnd() - fileRange.getStart() + 1; + context.serverResponse() + .setStatusCode(Response.Status.PARTIAL_CONTENT.getStatusCode()) + .setResponseHeader("Content-Range", contentRange) + .sendFile(file.getAbsolutePath(), fileRange.getStart(), length); + return; + } } + context.serverResponse().sendFile(file.getAbsolutePath(), 0, fileLength); } /** @@ -138,7 +147,7 @@ public static ByteRange parse(String rangeHeader) { if (index + 1 < part.length()) { end = Long.parseLong(part.substring(index + 1)); } else { - end = -1; + end = Long.MAX_VALUE; } ranges.add(new Range(start, end)); } diff --git a/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/matching/RegexMatchTest.java b/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/matching/RegexMatchTest.java index 8a2b6f529eff3..d3cf07d6bf142 100644 --- a/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/matching/RegexMatchTest.java +++ b/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/matching/RegexMatchTest.java @@ -41,6 +41,10 @@ public void testLiteralInRegex() { .then() .statusCode(200) .body(equalTo("plain:abb/foo/alongpathtotriggerbug")); + RestAssured.get("/regex/first space/foo/second space") + .then() + .statusCode(200) + .body(equalTo("plain:first space/foo/second space")); RestAssured.get("/regex/abb/literal/ddc") .then() .statusCode(200) diff --git a/independent-projects/revapi/pom.xml b/independent-projects/revapi/pom.xml index 88d0a4eff7910..1913f434d0c3f 100644 --- a/independent-projects/revapi/pom.xml +++ b/independent-projects/revapi/pom.xml @@ -8,7 +8,7 @@ 999-SNAPSHOT ../parent/pom.xml - + quarkus-revapi-config Quarkus - Revapi Configuration 999-SNAPSHOT @@ -30,15 +30,12 @@ 3.2.1 3.1.2 - 11 - 11 - 11 true - diff --git a/independent-projects/tools/analytics-common/pom.xml b/independent-projects/tools/analytics-common/pom.xml index c9bafe823db1d..2f50312201a80 100644 --- a/independent-projects/tools/analytics-common/pom.xml +++ b/independent-projects/tools/analytics-common/pom.xml @@ -16,7 +16,7 @@ 3.3.1 4.5.14 - 3.2.0 + 3.3.1 1.0.0.Final diff --git a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-extension/code/extension-base/java/pom.tpl.qute.xml b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-extension/code/extension-base/java/pom.tpl.qute.xml index 495ab9aeb16b8..e69db96309167 100644 --- a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-extension/code/extension-base/java/pom.tpl.qute.xml +++ b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-extension/code/extension-base/java/pom.tpl.qute.xml @@ -32,7 +32,7 @@ UTF-8 UTF-8 - 11 + {java.version} {#if quarkus.version}{quarkus.version}{/if} {#if maven.compiler-plugin-version}{maven.compiler-plugin-version}{/if} {#if maven.surefire-plugin.version} diff --git a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-extension/code/quarkiverse/java/.github/workflows/build.yml b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-extension/code/quarkiverse/java/.github/workflows/build.tpl.qute.yml similarity index 93% rename from independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-extension/code/quarkiverse/java/.github/workflows/build.yml rename to independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-extension/code/quarkiverse/java/.github/workflows/build.tpl.qute.yml index 5da57b27e4dba..7fb0447cfde84 100644 --- a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-extension/code/quarkiverse/java/.github/workflows/build.yml +++ b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-extension/code/quarkiverse/java/.github/workflows/build.tpl.qute.yml @@ -45,11 +45,11 @@ jobs: if: startsWith(matrix.os, 'windows') - uses: actions/checkout@v3 - - name: Set up JDK 11 + - name: Set up JDK {java.version} uses: actions/setup-java@v3 with: distribution: temurin - java-version: 11 + java-version: {java.version} cache: 'maven' - name: Build with Maven diff --git a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-extension/code/quarkiverse/java/.github/workflows/quarkus-snapshot.tpl.qute.yaml b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-extension/code/quarkiverse/java/.github/workflows/quarkus-snapshot.tpl.qute.yaml index ed334a3b39e75..2f2022972f3e5 100644 --- a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-extension/code/quarkiverse/java/.github/workflows/quarkus-snapshot.tpl.qute.yaml +++ b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-extension/code/quarkiverse/java/.github/workflows/quarkus-snapshot.tpl.qute.yaml @@ -10,7 +10,7 @@ on: env: ECOSYSTEM_CI_REPO: quarkusio/quarkus-ecosystem-ci ECOSYSTEM_CI_REPO_FILE: context.yaml - JAVA_VERSION: 11 + JAVA_VERSION: {java.version} ######################### # Repo specific setting # diff --git a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-extension/code/quarkiverse/java/.github/workflows/release.yml b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-extension/code/quarkiverse/java/.github/workflows/release.tpl.qute.yml similarity index 92% rename from independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-extension/code/quarkiverse/java/.github/workflows/release.yml rename to independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-extension/code/quarkiverse/java/.github/workflows/release.tpl.qute.yml index 0a3894f1bdd33..075d011604ab5 100644 --- a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-extension/code/quarkiverse/java/.github/workflows/release.yml +++ b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-extension/code/quarkiverse/java/.github/workflows/release.tpl.qute.yml @@ -37,11 +37,11 @@ jobs: gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} passphrase: ${{ secrets.GPG_PASSPHRASE }} - - name: Set up JDK 11 + - name: Set up JDK {java.version} uses: actions/setup-java@v3 with: distribution: temurin - java-version: 11 + java-version: {java.version} cache: 'maven' server-id: ossrh server-username: MAVEN_USERNAME @@ -54,7 +54,7 @@ jobs: - name: Update latest release version in docs run: | - mvn -B -ntp -pl docs -am generate-resources -Denforcer.skip -Dformatter.skip -Dimpsort.skip + mvn -B -ntp -pl docs -am package -DskipTests -DskipITs -Denforcer.skip -Dformatter.skip -Dimpsort.skip if ! git diff --quiet docs/modules/ROOT/pages/includes/attributes.adoc; then git add docs/modules/ROOT/pages/includes/attributes.adoc git commit -m "Update the latest release version ${{steps.metadata.outputs.current-version}} in documentation" diff --git a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-extension/code/quarkiverse/java/README.tpl.qute.md b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-extension/code/quarkiverse/java/README.tpl.qute.md index 1184c7f8f67eb..0e5dbe880f4e5 100644 --- a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-extension/code/quarkiverse/java/README.tpl.qute.md +++ b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-extension/code/quarkiverse/java/README.tpl.qute.md @@ -1,6 +1,6 @@ # {extension.full-name} -[![Version](https://img.shields.io/maven-central/v/{group-id}/{namespace.id}{extension.id}?logo=apache-maven&style=flat-square)](https://search.maven.org/artifact/{group-id}/{namespace.id}{extension.id}) +[![Version](https://img.shields.io/maven-central/v/{group-id}/{namespace.id}{extension.id}?logo=apache-maven&style=flat-square)](https://central.sonatype.com/artifact/{group-id}/{namespace.id}{extension.id}-parent) ## Welcome to Quarkiverse! @@ -22,4 +22,6 @@ The documentation for this extension should be maintained as part of this reposi The layout should follow the [Antora's Standard File and Directory Set](https://docs.antora.org/antora/2.3/standard-directories/). -Once the docs are ready to be published, please open a PR including this repository in the [Quarkiverse Docs Antora playbook](https://github.com/quarkiverse/quarkiverse-docs/blob/main/antora-playbook.yml#L7). See an example [here](https://github.com/quarkiverse/quarkiverse-docs/pull/1). +Once the docs are ready to be published, please open a PR including this repository in the [Quarkiverse Docs Antora playbook](https://github.com/quarkiverse/quarkiverse-docs/blob/main/antora-playbook.yml#L7). See an example [here](https://github.com/quarkiverse/quarkiverse-docs/pull/1) + +Your documentation will then be published to the https://docs.quarkiverse.io/ website. diff --git a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-jbang/tooling/jbang-wrapper/base/jbang b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-jbang/tooling/jbang-wrapper/base/jbang index 27d5f8344f5ab..8651f46005bd1 100755 --- a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-jbang/tooling/jbang-wrapper/base/jbang +++ b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-jbang/tooling/jbang-wrapper/base/jbang @@ -7,7 +7,7 @@ # # The Java version to install when it's not installed on the system yet -javaVersion=${JBANG_DEFAULT_JAVA_VERSION:-11} +javaVersion=${JBANG_DEFAULT_JAVA_VERSION:-17} absolute_path() { # if the given path to the jbang launcher is absolute (i.e. it is either starting with / or a @@ -126,7 +126,7 @@ if [[ $os == windows && -f "$abs_jbang_dir/jbang.cmd" && "$(uname -s)" == CYGWIN fi if [[ -z "$JBANG_JDK_VENDOR" ]]; then - if [[ "$javaVersion" -eq 8 || "$javaVersion" -eq 11 || "$javaVersion" -ge 17 ]]; then + if [[ "$javaVersion" -eq 8 || "$javaVersion" -eq 11 || "$javaVersion" -eq 17 || "$javaVersion" -eq 21 ]]; then distro="temurin"; else distro="aoj"; fi diff --git a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/buildtool/maven/scala/pom.tpl.qute.xml b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/buildtool/maven/scala/pom.tpl.qute.xml index 94f4093a1e466..d6bbe9a156ab7 100644 --- a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/buildtool/maven/scala/pom.tpl.qute.xml +++ b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/buildtool/maven/scala/pom.tpl.qute.xml @@ -52,12 +52,7 @@ -deprecation -feature -explaintypes - {#if scala.version.startsWith("2.12.")} - -target:jvm-1.8 - -Ypartial-unification - {#else} - -target:jvm-11 - {/if} + -release:17 diff --git a/independent-projects/tools/devtools-common/pom.xml b/independent-projects/tools/devtools-common/pom.xml index a8dfd837041f7..2e5832fddb6af 100644 --- a/independent-projects/tools/devtools-common/pom.xml +++ b/independent-projects/tools/devtools-common/pom.xml @@ -50,6 +50,10 @@ io.smallrye.common smallrye-common-version + + io.smallrye.common + smallrye-common-os + io.fabric8 maven-model-helper diff --git a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/codestarts/extension/QuarkusExtensionCodestartCatalog.java b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/codestarts/extension/QuarkusExtensionCodestartCatalog.java index 9575a522ba100..c3cedb8417823 100644 --- a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/codestarts/extension/QuarkusExtensionCodestartCatalog.java +++ b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/codestarts/extension/QuarkusExtensionCodestartCatalog.java @@ -38,6 +38,7 @@ public enum QuarkusExtensionData implements DataKey { QUARKUS_BOM_GROUP_ID("quarkus.bom.group-id"), QUARKUS_BOM_ARTIFACT_ID("quarkus.bom.artifact-id"), QUARKUS_BOM_VERSION("quarkus.bom.version"), + JAVA_VERSION("java.version"), PROPERTIES_FROM_PARENT("properties.from-parent"), PARENT_GROUP_ID("parent.group-id"), PARENT_ARTIFACT_ID("parent.artifact-id"), diff --git a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/commands/CreateExtension.java b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/commands/CreateExtension.java index ced0076c1d651..9ea5fb45a326a 100644 --- a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/commands/CreateExtension.java +++ b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/commands/CreateExtension.java @@ -1,7 +1,36 @@ package io.quarkus.devtools.commands; -import static io.quarkus.devtools.codestarts.extension.QuarkusExtensionCodestartCatalog.QuarkusExtensionData.*; +import static io.quarkus.devtools.codestarts.extension.QuarkusExtensionCodestartCatalog.QuarkusExtensionData.CLASS_NAME_BASE; +import static io.quarkus.devtools.codestarts.extension.QuarkusExtensionCodestartCatalog.QuarkusExtensionData.EXTENSION_DESCRIPTION; +import static io.quarkus.devtools.codestarts.extension.QuarkusExtensionCodestartCatalog.QuarkusExtensionData.EXTENSION_FULL_NAME; +import static io.quarkus.devtools.codestarts.extension.QuarkusExtensionCodestartCatalog.QuarkusExtensionData.EXTENSION_GUIDE; +import static io.quarkus.devtools.codestarts.extension.QuarkusExtensionCodestartCatalog.QuarkusExtensionData.EXTENSION_ID; +import static io.quarkus.devtools.codestarts.extension.QuarkusExtensionCodestartCatalog.QuarkusExtensionData.EXTENSION_NAME; +import static io.quarkus.devtools.codestarts.extension.QuarkusExtensionCodestartCatalog.QuarkusExtensionData.GROUP_ID; +import static io.quarkus.devtools.codestarts.extension.QuarkusExtensionCodestartCatalog.QuarkusExtensionData.HAS_DOCS_MODULE; +import static io.quarkus.devtools.codestarts.extension.QuarkusExtensionCodestartCatalog.QuarkusExtensionData.IT_PARENT_ARTIFACT_ID; +import static io.quarkus.devtools.codestarts.extension.QuarkusExtensionCodestartCatalog.QuarkusExtensionData.IT_PARENT_GROUP_ID; +import static io.quarkus.devtools.codestarts.extension.QuarkusExtensionCodestartCatalog.QuarkusExtensionData.IT_PARENT_RELATIVE_PATH; +import static io.quarkus.devtools.codestarts.extension.QuarkusExtensionCodestartCatalog.QuarkusExtensionData.IT_PARENT_VERSION; +import static io.quarkus.devtools.codestarts.extension.QuarkusExtensionCodestartCatalog.QuarkusExtensionData.JAVA_VERSION; +import static io.quarkus.devtools.codestarts.extension.QuarkusExtensionCodestartCatalog.QuarkusExtensionData.MAVEN_COMPILER_PLUGIN_VERSION; +import static io.quarkus.devtools.codestarts.extension.QuarkusExtensionCodestartCatalog.QuarkusExtensionData.MAVEN_QUARKUS_EXTENSION_PLUGIN; +import static io.quarkus.devtools.codestarts.extension.QuarkusExtensionCodestartCatalog.QuarkusExtensionData.MAVEN_SUREFIRE_PLUGIN_VERSION; +import static io.quarkus.devtools.codestarts.extension.QuarkusExtensionCodestartCatalog.QuarkusExtensionData.NAMESPACE_ID; +import static io.quarkus.devtools.codestarts.extension.QuarkusExtensionCodestartCatalog.QuarkusExtensionData.NAMESPACE_NAME; +import static io.quarkus.devtools.codestarts.extension.QuarkusExtensionCodestartCatalog.QuarkusExtensionData.PACKAGE_NAME; +import static io.quarkus.devtools.codestarts.extension.QuarkusExtensionCodestartCatalog.QuarkusExtensionData.PARENT_ARTIFACT_ID; +import static io.quarkus.devtools.codestarts.extension.QuarkusExtensionCodestartCatalog.QuarkusExtensionData.PARENT_GROUP_ID; +import static io.quarkus.devtools.codestarts.extension.QuarkusExtensionCodestartCatalog.QuarkusExtensionData.PARENT_RELATIVE_PATH; +import static io.quarkus.devtools.codestarts.extension.QuarkusExtensionCodestartCatalog.QuarkusExtensionData.PARENT_VERSION; +import static io.quarkus.devtools.codestarts.extension.QuarkusExtensionCodestartCatalog.QuarkusExtensionData.PROPERTIES_FROM_PARENT; +import static io.quarkus.devtools.codestarts.extension.QuarkusExtensionCodestartCatalog.QuarkusExtensionData.QUARKUS_BOM_ARTIFACT_ID; +import static io.quarkus.devtools.codestarts.extension.QuarkusExtensionCodestartCatalog.QuarkusExtensionData.QUARKUS_BOM_GROUP_ID; +import static io.quarkus.devtools.codestarts.extension.QuarkusExtensionCodestartCatalog.QuarkusExtensionData.QUARKUS_BOM_VERSION; +import static io.quarkus.devtools.codestarts.extension.QuarkusExtensionCodestartCatalog.QuarkusExtensionData.QUARKUS_VERSION; +import static io.quarkus.devtools.codestarts.extension.QuarkusExtensionCodestartCatalog.QuarkusExtensionData.VERSION; import static io.quarkus.devtools.commands.handlers.CreateExtensionCommandHandler.readPom; +import static io.quarkus.devtools.project.JavaVersion.computeJavaVersion; import static java.util.Objects.requireNonNull; import static org.apache.commons.lang3.StringUtils.isEmpty; @@ -29,6 +58,8 @@ import io.quarkus.devtools.commands.data.QuarkusCommandOutcome; import io.quarkus.devtools.commands.handlers.CreateExtensionCommandHandler; import io.quarkus.devtools.messagewriter.MessageWriter; +import io.quarkus.devtools.project.JavaVersion; +import io.quarkus.devtools.project.SourceType; import io.quarkus.maven.utilities.MojoUtils; /** @@ -73,6 +104,7 @@ public enum LayoutType { private String bomRelativeDir = "bom/application"; private String extensionsRelativeDir = "extensions"; private boolean withCodestart; + private String javaVersion; public CreateExtension(final Path baseDir) { this.baseDir = requireNonNull(baseDir, "extensionDirPath is required"); @@ -165,6 +197,11 @@ public CreateExtension quarkusBomVersion(String quarkusBomVersion) { return this; } + public CreateExtension javaVersion(String javaVersion) { + this.javaVersion = javaVersion; + return this; + } + public CreateExtension withCodestart(boolean withCodestart) { this.withCodestart = withCodestart; return this; @@ -227,6 +264,10 @@ public CreateExtensionCommandHandler prepare() throws QuarkusCommandException { data.put(EXTENSION_FULL_NAME, data.getRequiredStringValue(NAMESPACE_NAME) + data.getRequiredStringValue(EXTENSION_NAME)); + // for now, we only support Java extensions + data.put(JAVA_VERSION, javaVersion == null ? JavaVersion.DEFAULT_JAVA_VERSION_FOR_EXTENSION + : computeJavaVersion(SourceType.JAVA, javaVersion)); + final String runtimeArtifactId = getRuntimeArtifactIdFromData(); ensureRequiredStringData(GROUP_ID, resolveGroupId(baseModel)); diff --git a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/project/JavaVersion.java b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/project/JavaVersion.java index ffa49e92acafb..ea5149c058c41 100644 --- a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/project/JavaVersion.java +++ b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/project/JavaVersion.java @@ -62,9 +62,11 @@ public String toString() { } // ordering is important here, so let's keep them ordered - public static final SortedSet JAVA_VERSIONS_LTS = new TreeSet<>(List.of(11, 17, 21)); - public static final int DEFAULT_JAVA_VERSION = 11; - public static final int MAX_LTS_SUPPORTED_BY_KOTLIN = 17; + public static final SortedSet JAVA_VERSIONS_LTS = new TreeSet<>(List.of(17, 21)); + public static final int DEFAULT_JAVA_VERSION = 17; + // we want to maximize the compatibility of extensions with the Quarkus ecosystem so let's stick to 17 by default + public static final String DEFAULT_JAVA_VERSION_FOR_EXTENSION = "17"; + public static final int MAX_LTS_SUPPORTED_BY_KOTLIN = 21; public static final String DETECT_JAVA_RUNTIME_VERSION = "<>"; public static final Pattern JAVA_VERSION_PATTERN = Pattern.compile("(\\d+)(?:\\..*)?"); diff --git a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/project/update/rewrite/QuarkusUpdateCommand.java b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/project/update/rewrite/QuarkusUpdateCommand.java index b7ea74069b464..f53c3def02dd4 100644 --- a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/project/update/rewrite/QuarkusUpdateCommand.java +++ b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/project/update/rewrite/QuarkusUpdateCommand.java @@ -24,6 +24,7 @@ import io.quarkus.devtools.messagewriter.MessageWriter; import io.quarkus.devtools.project.BuildTool; import io.quarkus.qute.Qute; +import io.smallrye.common.os.OS; public class QuarkusUpdateCommand { @@ -57,6 +58,16 @@ public static void handle(MessageWriter log, BuildTool buildTool, Path baseDir, } } + private static void runMavenUpdate(MessageWriter log, Path baseDir, String rewritePluginVersion, String recipesGAV, + Path recipe, + boolean dryRun) { + final String mvnBinary = findMvnBinary(baseDir); + executeCommand(baseDir, getMavenUpdateCommand(mvnBinary, rewritePluginVersion, recipesGAV, recipe, dryRun), log); + + // format the sources + executeCommand(baseDir, getMavenProcessSourcesCommand(mvnBinary), log); + } + private static void runGradleUpdate(MessageWriter log, Path baseDir, String rewritePluginVersion, String recipesGAV, Path recipe, boolean dryRun) { Path tempInit = null; @@ -119,19 +130,11 @@ private static void propagateSystemPropertyIfSet(String name, List comma } } - private static void runMavenUpdate(MessageWriter log, Path baseDir, String rewritePluginVersion, String recipesGAV, - Path recipe, - boolean dryRun) { - final String mvnBinary = findMvnBinary(baseDir); - executeCommand(baseDir, getMavenUpdateCommand(mvnBinary, rewritePluginVersion, recipesGAV, recipe, dryRun), log); - - // format the sources - executeCommand(baseDir, getMavenProcessSourcesCommand(mvnBinary), log); - } - private static List getMavenProcessSourcesCommand(String mvnBinary) { List command = new ArrayList<>(); command.add(mvnBinary); + command.add("-B"); + command.add("clean"); command.add("process-sources"); final String mavenSettings = getMavenSettingsArg(); if (mavenSettings != null) { @@ -146,6 +149,7 @@ private static List getMavenUpdateCommand(String mvnBinary, String rewri boolean dryRun) { final List command = new ArrayList<>(); command.add(mvnBinary); + command.add("-B"); command.add("-e"); command.add( String.format("%s:%s:%s:%s", MAVEN_REWRITE_PLUGIN_GROUP, MAVEN_REWRITE_PLUGIN_ARTIFACT, rewritePluginVersion, @@ -305,18 +309,8 @@ private static boolean isExecutable(Path file) { return false; } - private static String OS = System.getProperty("os.name").toLowerCase(); - public static boolean isWindows() { - return OS.contains("win"); - } - - static boolean hasGradle(Path dir) { - return Files.exists(dir.resolve("build.gradle")); - } - - private static boolean hasMaven(Path dir) { - return Files.exists(dir.resolve("pom.xml")); + return OS.WINDOWS.isCurrent(); } private enum LogLevel { @@ -351,13 +345,13 @@ private String clean(String line) { return line; } - String pattern = "[" + name() + "]"; + String pattern = "[" + name() + "] "; - if (line.length() < pattern.length()) { + if (!line.startsWith(pattern)) { return line; } - return line.substring(pattern.length()).trim(); + return line.substring(pattern.length()); } private boolean matches(String line) { diff --git a/independent-projects/tools/devtools-common/src/test/java/io/quarkus/devtools/project/JavaVersionTest.java b/independent-projects/tools/devtools-common/src/test/java/io/quarkus/devtools/project/JavaVersionTest.java index 3e0d82dd75241..45d3ca585bcdf 100644 --- a/independent-projects/tools/devtools-common/src/test/java/io/quarkus/devtools/project/JavaVersionTest.java +++ b/independent-projects/tools/devtools-common/src/test/java/io/quarkus/devtools/project/JavaVersionTest.java @@ -20,24 +20,19 @@ public void givenJavaVersion17ShouldReturn17() { } @Test - public void givenJavaVersion16ShouldReturn11() { - assertEquals("11", computeJavaVersion(JAVA, "16.0.1")); + public void givenJavaVersion22ShouldReturn21() { + assertEquals("21", computeJavaVersion(JAVA, "22.0.1")); } @Test - public void givenJavaVersion11ShouldReturn11() { - assertEquals("11", computeJavaVersion(JAVA, "11")); - } - - @Test - public void givenJavaVersion18ShouldReturn17() { - assertEquals("17", computeJavaVersion(JAVA, "18")); + public void givenJavaVersion21ShouldReturn21() { + assertEquals("21", computeJavaVersion(JAVA, "21")); } @Test void shouldProperlyUseMinJavaVersion() { - assertThat(getCompatibleLTSVersions(new JavaVersion("11"))).isEqualTo(JAVA_VERSIONS_LTS); - assertThat(getCompatibleLTSVersions(new JavaVersion("17"))).containsExactly(17, 21); + assertThat(getCompatibleLTSVersions(new JavaVersion("17"))).isEqualTo(JAVA_VERSIONS_LTS); + assertThat(getCompatibleLTSVersions(new JavaVersion("21"))).containsExactly(21); assertThat(getCompatibleLTSVersions(new JavaVersion("100"))).isEmpty(); assertThat(getCompatibleLTSVersions(JavaVersion.NA)).isEqualTo(JAVA_VERSIONS_LTS); } @@ -50,11 +45,12 @@ public void givenAutoDetectShouldReturnAppropriateVersion() { @Test public void testDetermineBestLtsVersion() { - assertEquals(11, determineBestJavaLtsVersion(8)); - assertEquals(11, determineBestJavaLtsVersion(11)); - assertEquals(11, determineBestJavaLtsVersion(12)); + assertEquals(17, determineBestJavaLtsVersion(8)); + assertEquals(17, determineBestJavaLtsVersion(11)); assertEquals(17, determineBestJavaLtsVersion(17)); assertEquals(17, determineBestJavaLtsVersion(18)); + assertEquals(21, determineBestJavaLtsVersion(21)); + assertEquals(21, determineBestJavaLtsVersion(22)); } @Test diff --git a/independent-projects/tools/devtools-testing/src/main/java/io/quarkus/devtools/testing/codestarts/QuarkusCodestartTesting.java b/independent-projects/tools/devtools-testing/src/main/java/io/quarkus/devtools/testing/codestarts/QuarkusCodestartTesting.java index 4f86ced09d98a..a97233c8b1a26 100644 --- a/independent-projects/tools/devtools-testing/src/main/java/io/quarkus/devtools/testing/codestarts/QuarkusCodestartTesting.java +++ b/independent-projects/tools/devtools-testing/src/main/java/io/quarkus/devtools/testing/codestarts/QuarkusCodestartTesting.java @@ -34,7 +34,7 @@ public static Map getMockedTestInputData(final Map getRealTestInputData(ExtensionCatalog catalog, data.put(SCALA_MAVEN_PLUGIN_VERSION.key(), quarkusProp.getProperty(ToolsConstants.PROP_SCALA_PLUGIN_VERSION)); data.put(MAVEN_COMPILER_PLUGIN_VERSION.key(), quarkusProp.getProperty(ToolsConstants.PROP_COMPILER_PLUGIN_VERSION)); data.put(MAVEN_SUREFIRE_PLUGIN_VERSION.key(), quarkusProp.getProperty(ToolsConstants.PROP_SUREFIRE_PLUGIN_VERSION)); - data.put(JAVA_VERSION.key(), "11"); + data.put(JAVA_VERSION.key(), "17"); if (override != null) data.putAll(override); return data; diff --git a/independent-projects/tools/devtools-testing/src/main/resources/fake-catalog.json b/independent-projects/tools/devtools-testing/src/main/resources/fake-catalog.json index 3b970eaec4e5a..89aa29ee4265a 100644 --- a/independent-projects/tools/devtools-testing/src/main/resources/fake-catalog.json +++ b/independent-projects/tools/devtools-testing/src/main/resources/fake-catalog.json @@ -75,6 +75,62 @@ "io.quarkus:quarkus-fake-bom:999-FAKE:json:999-FAKE" ] }, + { + "name" : "RESTEasy Reactive Jackson", + "description" : "Jackson serialization support for RESTEasy Reactive. This extension is not compatible with the quarkus-resteasy extension, or any of the extensions that depend on it", + "metadata" : { + "codestart" : { + "name" : "resteasy-reactive", + "kind" : "core", + "languages" : [ "java", "kotlin", "scala" ], + "artifact" : "io.quarkus:quarkus-project-core-extension-codestarts::jar:999-FAKE" + }, + "minimum-java-version" : "11", + "status" : "stable", + "config" : [ "quarkus.resteasy-reactive.", "quarkus.jackson." ], + "built-with-quarkus-core" : "999-FAKE", + "scm-url" : "https://github.com/quarkus-release/release", + "short-name" : "resteasy-reactive-jackson", + "capabilities" : { + "provides" : [ "io.quarkus.rest.jackson", "io.quarkus.resteasy.reactive.json.jackson" ] + }, + "categories" : [ "web", "reactive" ], + "extension-dependencies" : [ "io.quarkus:quarkus-resteasy-reactive", "io.quarkus:quarkus-resteasy-reactive-common", "io.quarkus:quarkus-mutiny", "io.quarkus:quarkus-smallrye-context-propagation", "io.quarkus:quarkus-vertx", "io.quarkus:quarkus-netty", "io.quarkus:quarkus-vertx-http", "io.quarkus:quarkus-core", "io.quarkus:quarkus-jsonp", "io.quarkus:quarkus-virtual-threads", "io.quarkus:quarkus-arc", "io.quarkus:quarkus-resteasy-reactive-jackson-common", "io.quarkus:quarkus-jackson" ], + "keywords" : [ "rest-jackson", "quarkus-resteasy-reactive-json", "jaxrs-json", "rest", "jaxrs", "json", "jackson", "jakarta-rest" ] + }, + "artifact" : "io.quarkus:quarkus-resteasy-reactive-jackson::jar:999-FAKE", + "origins": [ + "io.quarkus:quarkus-fake-bom:999-FAKE:json:999-FAKE" + ] + }, + { + "name" : "RESTEasy Reactive JSON-B", + "description" : "JSON-B serialization support for RESTEasy Reactive. This extension is not compatible with the quarkus-resteasy extension, or any of the extensions that depend on it.", + "metadata" : { + "codestart" : { + "name" : "resteasy-reactive", + "kind" : "core", + "languages" : [ "java", "kotlin", "scala" ], + "artifact" : "io.quarkus:quarkus-project-core-extension-codestarts::jar:999-FAKE" + }, + "minimum-java-version" : "11", + "status" : "stable", + "config" : [ "quarkus.resteasy-reactive." ], + "built-with-quarkus-core" : "999-FAKE", + "scm-url" : "https://github.com/quarkus-release/release", + "short-name" : "resteasy-reactive-jsonb", + "capabilities" : { + "provides" : [ "io.quarkus.rest.jsonb", "io.quarkus.resteasy.reactive.json.jsonb" ] + }, + "categories" : [ "web", "reactive" ], + "extension-dependencies" : [ "io.quarkus:quarkus-resteasy-reactive", "io.quarkus:quarkus-resteasy-reactive-common", "io.quarkus:quarkus-mutiny", "io.quarkus:quarkus-smallrye-context-propagation", "io.quarkus:quarkus-vertx", "io.quarkus:quarkus-netty", "io.quarkus:quarkus-vertx-http", "io.quarkus:quarkus-core", "io.quarkus:quarkus-jsonp", "io.quarkus:quarkus-virtual-threads", "io.quarkus:quarkus-arc", "io.quarkus:quarkus-resteasy-reactive-jsonb-common", "io.quarkus:quarkus-jsonb" ], + "keywords" : [ "rest-jsonb", "resteasy-reactive-json", "jaxrs-json", "rest", "jaxrs", "json", "jsonb", "jakarta-rest" ] + }, + "artifact" : "io.quarkus:quarkus-resteasy-reactive-jsonb::jar:999-FAKE", + "origins": [ + "io.quarkus:quarkus-fake-bom:999-FAKE:json:999-FAKE" + ] + }, { "name": "YAML Configuration", "description": "Use YAML to configure your Quarkus application", @@ -388,9 +444,9 @@ "supported-maven-versions": "[3.6.2,)", "minimum-java-version": "11", "recommended-java-version": "17", - "proposed-maven-version": "3.9.5", + "proposed-maven-version": "3.9.6", "maven-wrapper-version": "3.2.0", - "gradle-wrapper-version": "8.4" + "gradle-wrapper-version": "8.5" } }, "codestarts-artifacts": [ diff --git a/independent-projects/tools/devtools-testing/src/test/java/io/quarkus/devtools/codestarts/extension/QuarkusExtensionCodestartGenerationTest.java b/independent-projects/tools/devtools-testing/src/test/java/io/quarkus/devtools/codestarts/extension/QuarkusExtensionCodestartGenerationTest.java index 0d32e3e9dcba5..fae3f7d36961d 100644 --- a/independent-projects/tools/devtools-testing/src/test/java/io/quarkus/devtools/codestarts/extension/QuarkusExtensionCodestartGenerationTest.java +++ b/independent-projects/tools/devtools-testing/src/test/java/io/quarkus/devtools/codestarts/extension/QuarkusExtensionCodestartGenerationTest.java @@ -31,7 +31,8 @@ private QuarkusExtensionCodestartProjectInputBuilder prepareInput() { .putData(QuarkusExtensionData.EXTENSION_NAME, "My Extension") .putData(QuarkusExtensionData.VERSION, "1.0.0-SNAPSHOT") .putData(QuarkusExtensionData.PACKAGE_NAME, "org.extension") - .putData(QuarkusExtensionData.CLASS_NAME_BASE, "MyExtension"); + .putData(QuarkusExtensionData.CLASS_NAME_BASE, "MyExtension") + .putData(QuarkusExtensionData.JAVA_VERSION, "11"); } @Test diff --git a/independent-projects/tools/devtools-testing/src/test/java/io/quarkus/devtools/codestarts/quarkus/QuarkusCodestartGenerationTest.java b/independent-projects/tools/devtools-testing/src/test/java/io/quarkus/devtools/codestarts/quarkus/QuarkusCodestartGenerationTest.java index e822bd176c4ea..3df62f156b9b5 100644 --- a/independent-projects/tools/devtools-testing/src/test/java/io/quarkus/devtools/codestarts/quarkus/QuarkusCodestartGenerationTest.java +++ b/independent-projects/tools/devtools-testing/src/test/java/io/quarkus/devtools/codestarts/quarkus/QuarkusCodestartGenerationTest.java @@ -315,13 +315,13 @@ private void checkDockerfilesWithMaven(Path projectDir) { assertThat(projectDir.resolve("src/main/docker/Dockerfile.jvm")).exists() .satisfies(checkContains("./mvnw package")) .satisfies(checkContains("docker build -f src/main/docker/Dockerfile.jvm")) - .satisfies(checkContains("registry.access.redhat.com/ubi8/openjdk-11:1.18"))//TODO: make a test for java17 + .satisfies(checkContains("registry.access.redhat.com/ubi8/openjdk-17:1.18")) .satisfies(checkContains("ENV JAVA_APP_JAR=\"/deployments/quarkus-run.jar\"")) .satisfies(checkContains("ENTRYPOINT [ \"/opt/jboss/container/java/run/run-java.sh\" ]")); assertThat(projectDir.resolve("src/main/docker/Dockerfile.legacy-jar")).exists() .satisfies(checkContains("./mvnw package -Dquarkus.package.type=legacy-jar")) .satisfies(checkContains("docker build -f src/main/docker/Dockerfile.legacy-jar")) - .satisfies(checkContains("registry.access.redhat.com/ubi8/openjdk-11:1.18")) + .satisfies(checkContains("registry.access.redhat.com/ubi8/openjdk-17:1.18")) .satisfies(checkContains("EXPOSE 8080")) .satisfies(checkContains("USER 185")) .satisfies(checkContains("ENV JAVA_APP_JAR=\"/deployments/quarkus-run.jar\"")) @@ -341,13 +341,13 @@ private void checkDockerfilesWithGradle(Path projectDir) { assertThat(projectDir.resolve("src/main/docker/Dockerfile.jvm")).exists() .satisfies(checkContains("./gradlew build")) .satisfies(checkContains("docker build -f src/main/docker/Dockerfile.jvm")) - .satisfies(checkContains("registry.access.redhat.com/ubi8/openjdk-11:1.18"))//TODO: make a test for java17 + .satisfies(checkContains("registry.access.redhat.com/ubi8/openjdk-17:1.18")) .satisfies(checkContains("ENV JAVA_APP_JAR=\"/deployments/quarkus-run.jar\"")) .satisfies(checkContains("ENTRYPOINT [ \"/opt/jboss/container/java/run/run-java.sh\" ]")); assertThat(projectDir.resolve("src/main/docker/Dockerfile.legacy-jar")).exists() .satisfies(checkContains("./gradlew build -Dquarkus.package.type=legacy-jar")) .satisfies(checkContains("docker build -f src/main/docker/Dockerfile.legacy-jar")) - .satisfies(checkContains("registry.access.redhat.com/ubi8/openjdk-11:1.18")) + .satisfies(checkContains("registry.access.redhat.com/ubi8/openjdk-17:1.18")) .satisfies(checkContains("EXPOSE 8080")) .satisfies(checkContains("USER 185")) .satisfies(checkContains("ENV JAVA_APP_JAR=\"/deployments/quarkus-run.jar\"")) diff --git a/independent-projects/tools/devtools-testing/src/test/resources/__snapshots__/QuarkusCodestartGenerationTest/generateDefault/pom.xml b/independent-projects/tools/devtools-testing/src/test/resources/__snapshots__/QuarkusCodestartGenerationTest/generateDefault/pom.xml index 51ef9c95e7f4e..28937572322ef 100644 --- a/independent-projects/tools/devtools-testing/src/test/resources/__snapshots__/QuarkusCodestartGenerationTest/generateDefault/pom.xml +++ b/independent-projects/tools/devtools-testing/src/test/resources/__snapshots__/QuarkusCodestartGenerationTest/generateDefault/pom.xml @@ -7,7 +7,7 @@ 1.0.0-codestart 3.8.1-MOCK - 11 + 17 UTF-8 UTF-8 quarkus-mock-bom diff --git a/independent-projects/tools/devtools-testing/src/test/resources/__snapshots__/QuarkusCodestartGenerationTest/generateGradleNoWrapperGithubAction/.github_workflows_ci.yml b/independent-projects/tools/devtools-testing/src/test/resources/__snapshots__/QuarkusCodestartGenerationTest/generateGradleNoWrapperGithubAction/.github_workflows_ci.yml index b91dae4af7558..5126ed7fe1b37 100644 --- a/independent-projects/tools/devtools-testing/src/test/resources/__snapshots__/QuarkusCodestartGenerationTest/generateGradleNoWrapperGithubAction/.github_workflows_ci.yml +++ b/independent-projects/tools/devtools-testing/src/test/resources/__snapshots__/QuarkusCodestartGenerationTest/generateGradleNoWrapperGithubAction/.github_workflows_ci.yml @@ -13,10 +13,10 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - name: Set up JDK 11 + - name: Set up JDK 17 uses: actions/setup-java@v3 with: - java-version: 11 + java-version: 17 distribution: temurin cache: gradle - name: Build diff --git a/independent-projects/tools/devtools-testing/src/test/resources/__snapshots__/QuarkusCodestartGenerationTest/generateGradleWrapperGithubAction/.github_workflows_ci.yml b/independent-projects/tools/devtools-testing/src/test/resources/__snapshots__/QuarkusCodestartGenerationTest/generateGradleWrapperGithubAction/.github_workflows_ci.yml index 214af798d2fbe..2b00bec6d5160 100644 --- a/independent-projects/tools/devtools-testing/src/test/resources/__snapshots__/QuarkusCodestartGenerationTest/generateGradleWrapperGithubAction/.github_workflows_ci.yml +++ b/independent-projects/tools/devtools-testing/src/test/resources/__snapshots__/QuarkusCodestartGenerationTest/generateGradleWrapperGithubAction/.github_workflows_ci.yml @@ -13,10 +13,10 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - name: Set up JDK 11 + - name: Set up JDK 17 uses: actions/setup-java@v3 with: - java-version: 11 + java-version: 17 distribution: temurin cache: maven - name: Build diff --git a/independent-projects/tools/devtools-testing/src/test/resources/__snapshots__/QuarkusCodestartGenerationTest/generateMavenWithCustomDep/pom.xml b/independent-projects/tools/devtools-testing/src/test/resources/__snapshots__/QuarkusCodestartGenerationTest/generateMavenWithCustomDep/pom.xml index 9871c74a9df19..6abb5fb856874 100644 --- a/independent-projects/tools/devtools-testing/src/test/resources/__snapshots__/QuarkusCodestartGenerationTest/generateMavenWithCustomDep/pom.xml +++ b/independent-projects/tools/devtools-testing/src/test/resources/__snapshots__/QuarkusCodestartGenerationTest/generateMavenWithCustomDep/pom.xml @@ -7,7 +7,7 @@ 1.0.0-codestart 3.8.1-MOCK - 11 + 17 UTF-8 UTF-8 quarkus-mock-bom diff --git a/independent-projects/tools/pom.xml b/independent-projects/tools/pom.xml index b2ad75132b552..20e306446b8b7 100644 --- a/independent-projects/tools/pom.xml +++ b/independent-projects/tools/pom.xml @@ -35,34 +35,31 @@ 1.3.2 1 UTF-8 - 11 - 11 - 11 - 3.9.5 + 3.9.6 3.2.0 - 8.4 + 8.5 3.11.0 1.6.0 - 2.12.13 + 2.13.12 4.4.0 3.24.2 - 2.15.3 + 2.16.0 4.0.1 - 5.10.0 + 5.10.1 1.25.0 3.5.3.Final - 5.3.1 + 5.8.0 3.2.1 3.1.2 ${project.version} 25 - 3.1.5 + 3.1.6 2.0.2 4.2.0 diff --git a/independent-projects/tools/registry-client/src/main/java/io/quarkus/registry/config/RegistriesConfigMapperHelper.java b/independent-projects/tools/registry-client/src/main/java/io/quarkus/registry/config/RegistriesConfigMapperHelper.java index 1d794a200ae72..41ae53657be3a 100644 --- a/independent-projects/tools/registry-client/src/main/java/io/quarkus/registry/config/RegistriesConfigMapperHelper.java +++ b/independent-projects/tools/registry-client/src/main/java/io/quarkus/registry/config/RegistriesConfigMapperHelper.java @@ -36,10 +36,11 @@ private static ObjectMapper mapper(Path p) { } public static ObjectMapper initMapper(ObjectMapper mapper) { - mapper.addMixIn(ArtifactCoords.class, JsonArtifactCoordsMixin.class); - mapper.enable(SerializationFeature.INDENT_OUTPUT); - mapper.setPropertyNamingStrategy(PropertyNamingStrategies.KEBAB_CASE); - mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + mapper.addMixIn(ArtifactCoords.class, JsonArtifactCoordsMixin.class) + .enable(SerializationFeature.INDENT_OUTPUT) + .enable(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS) + .setPropertyNamingStrategy(PropertyNamingStrategies.KEBAB_CASE) + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); return mapper; } diff --git a/integration-tests/container-image/maven-invoker-way/src/it/container-build-docker/pom.xml b/integration-tests/container-image/maven-invoker-way/src/it/container-build-docker/pom.xml index 4107e1c2f5be0..49369c0468cae 100644 --- a/integration-tests/container-image/maven-invoker-way/src/it/container-build-docker/pom.xml +++ b/integration-tests/container-image/maven-invoker-way/src/it/container-build-docker/pom.xml @@ -8,9 +8,9 @@ UTF-8 3.1.2 - 11 + 17 UTF-8 - 11 + 17 diff --git a/integration-tests/container-image/maven-invoker-way/src/it/container-build-docker/src/main/docker/Dockerfile.jvm b/integration-tests/container-image/maven-invoker-way/src/it/container-build-docker/src/main/docker/Dockerfile.jvm index 566ba7b5a17bf..6de170527f1ed 100644 --- a/integration-tests/container-image/maven-invoker-way/src/it/container-build-docker/src/main/docker/Dockerfile.jvm +++ b/integration-tests/container-image/maven-invoker-way/src/it/container-build-docker/src/main/docker/Dockerfile.jvm @@ -23,7 +23,7 @@ ### FROM registry.access.redhat.com/ubi8/ubi-minimal:8.9 -ARG JAVA_PACKAGE=java-11-openjdk-headless +ARG JAVA_PACKAGE=java-17-openjdk-headless ARG RUN_JAVA_VERSION=1.3.8 ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en' # Install java and the run-java script diff --git a/integration-tests/container-image/maven-invoker-way/src/it/container-build-jib-appcds/pom.xml b/integration-tests/container-image/maven-invoker-way/src/it/container-build-jib-appcds/pom.xml index 51b11f2950e77..f6b93fec7d58a 100644 --- a/integration-tests/container-image/maven-invoker-way/src/it/container-build-jib-appcds/pom.xml +++ b/integration-tests/container-image/maven-invoker-way/src/it/container-build-jib-appcds/pom.xml @@ -8,9 +8,9 @@ UTF-8 3.1.2 - 11 + 17 UTF-8 - 11 + 17 diff --git a/integration-tests/container-image/maven-invoker-way/src/it/container-build-jib-inherit/pom.xml b/integration-tests/container-image/maven-invoker-way/src/it/container-build-jib-inherit/pom.xml index 6f9cbef0fe4c3..902e7e36b521d 100644 --- a/integration-tests/container-image/maven-invoker-way/src/it/container-build-jib-inherit/pom.xml +++ b/integration-tests/container-image/maven-invoker-way/src/it/container-build-jib-inherit/pom.xml @@ -8,9 +8,9 @@ UTF-8 3.1.2 - 11 + 17 UTF-8 - 11 + 17 diff --git a/integration-tests/container-image/maven-invoker-way/src/it/container-build-jib/pom.xml b/integration-tests/container-image/maven-invoker-way/src/it/container-build-jib/pom.xml index 6c048387d387f..6c34e5253acbc 100644 --- a/integration-tests/container-image/maven-invoker-way/src/it/container-build-jib/pom.xml +++ b/integration-tests/container-image/maven-invoker-way/src/it/container-build-jib/pom.xml @@ -8,9 +8,9 @@ UTF-8 3.1.2 - 11 + 17 UTF-8 - 11 + 17 diff --git a/integration-tests/container-image/maven-invoker-way/src/it/container-build-multiple-tags-docker/pom.xml b/integration-tests/container-image/maven-invoker-way/src/it/container-build-multiple-tags-docker/pom.xml index 398a54e232883..439ea5e27a654 100644 --- a/integration-tests/container-image/maven-invoker-way/src/it/container-build-multiple-tags-docker/pom.xml +++ b/integration-tests/container-image/maven-invoker-way/src/it/container-build-multiple-tags-docker/pom.xml @@ -8,9 +8,9 @@ UTF-8 3.1.2 - 11 + 17 UTF-8 - 11 + 17 diff --git a/integration-tests/container-image/maven-invoker-way/src/it/container-build-multiple-tags-docker/src/main/docker/Dockerfile.jvm b/integration-tests/container-image/maven-invoker-way/src/it/container-build-multiple-tags-docker/src/main/docker/Dockerfile.jvm index 566ba7b5a17bf..6de170527f1ed 100644 --- a/integration-tests/container-image/maven-invoker-way/src/it/container-build-multiple-tags-docker/src/main/docker/Dockerfile.jvm +++ b/integration-tests/container-image/maven-invoker-way/src/it/container-build-multiple-tags-docker/src/main/docker/Dockerfile.jvm @@ -23,7 +23,7 @@ ### FROM registry.access.redhat.com/ubi8/ubi-minimal:8.9 -ARG JAVA_PACKAGE=java-11-openjdk-headless +ARG JAVA_PACKAGE=java-17-openjdk-headless ARG RUN_JAVA_VERSION=1.3.8 ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en' # Install java and the run-java script diff --git a/integration-tests/container-image/maven-invoker-way/src/it/container-build-multiple-tags-jib/pom.xml b/integration-tests/container-image/maven-invoker-way/src/it/container-build-multiple-tags-jib/pom.xml index 6db37cb78d2d9..0f8604d355bad 100644 --- a/integration-tests/container-image/maven-invoker-way/src/it/container-build-multiple-tags-jib/pom.xml +++ b/integration-tests/container-image/maven-invoker-way/src/it/container-build-multiple-tags-jib/pom.xml @@ -8,9 +8,9 @@ UTF-8 3.1.2 - 11 + 17 UTF-8 - 11 + 17 diff --git a/integration-tests/container-image/maven-invoker-way/src/it/container-image-push/pom.xml b/integration-tests/container-image/maven-invoker-way/src/it/container-image-push/pom.xml index 1c70b1664f0d0..5da922f81f7e8 100644 --- a/integration-tests/container-image/maven-invoker-way/src/it/container-image-push/pom.xml +++ b/integration-tests/container-image/maven-invoker-way/src/it/container-image-push/pom.xml @@ -8,9 +8,9 @@ UTF-8 3.1.2 - 11 + 17 UTF-8 - 11 + 17 diff --git a/integration-tests/devmode/src/test/java/io/quarkus/test/devui/DevUIGrpcSmokeTest.java b/integration-tests/devmode/src/test/java/io/quarkus/test/devui/DevUIGrpcSmokeTest.java index 3fc764660561a..a37b5a8fab5b1 100644 --- a/integration-tests/devmode/src/test/java/io/quarkus/test/devui/DevUIGrpcSmokeTest.java +++ b/integration-tests/devmode/src/test/java/io/quarkus/test/devui/DevUIGrpcSmokeTest.java @@ -64,7 +64,7 @@ public void testServices() throws Exception { @Test public void testTestService() throws Exception { - Map params = Map.of( + Map params = Map.of( "serviceName", "helloworld.Greeter", "methodName", "SayHello", "methodType", "UNARY", diff --git a/integration-tests/devtools/src/test/java/io/quarkus/devtools/codestarts/quarkus/QuarkusCodestartBuildIT.java b/integration-tests/devtools/src/test/java/io/quarkus/devtools/codestarts/quarkus/QuarkusCodestartBuildIT.java index 32b90968cf96e..9cccca1c54fbe 100644 --- a/integration-tests/devtools/src/test/java/io/quarkus/devtools/codestarts/quarkus/QuarkusCodestartBuildIT.java +++ b/integration-tests/devtools/src/test/java/io/quarkus/devtools/codestarts/quarkus/QuarkusCodestartBuildIT.java @@ -120,7 +120,6 @@ private void generateProjectRunTests(String buildToolName, String language, List final BuildTool buildTool = BuildTool.findTool(buildToolName); final Map data = getTestInputData(Collections.singletonMap("artifact-id", name)); - // for JVM 8 and 14 this will generate project with java 1.8, for JVM 11 project with java 11 final QuarkusCodestartProjectInput input = QuarkusCodestartProjectInput.builder() .addData(data) .buildTool(buildTool) diff --git a/integration-tests/gradle/gradle/wrapper/gradle-wrapper.properties b/integration-tests/gradle/gradle/wrapper/gradle-wrapper.properties index 82b3bd91387cb..80f3d5675f491 100644 --- a/integration-tests/gradle/gradle/wrapper/gradle-wrapper.properties +++ b/integration-tests/gradle/gradle/wrapper/gradle-wrapper.properties @@ -1,8 +1,8 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists # https://gradle.org/release-checksums/ -distributionSha256Sum=f2b9ed0faf8472cbe469255ae6c86eddb77076c75191741b4a462f33128dd419 -distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-all.zip +distributionSha256Sum=c16d517b50dd28b3f5838f0e844b7520b8f1eb610f2f29de7e4e04a1b7c9c79b +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-all.zip networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/integration-tests/gradle/src/main/resources/add-extension-multi-module-kts/build.gradle.kts b/integration-tests/gradle/src/main/resources/add-extension-multi-module-kts/build.gradle.kts index e4862ab443303..3ece50bb7b142 100644 --- a/integration-tests/gradle/src/main/resources/add-extension-multi-module-kts/build.gradle.kts +++ b/integration-tests/gradle/src/main/resources/add-extension-multi-module-kts/build.gradle.kts @@ -26,8 +26,8 @@ subprojects { apply(plugin = "java") java { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } tasks { diff --git a/integration-tests/gradle/src/main/resources/add-extension-multi-module/build.gradle b/integration-tests/gradle/src/main/resources/add-extension-multi-module/build.gradle index e8fe347d47c79..be2336ebb1cf3 100644 --- a/integration-tests/gradle/src/main/resources/add-extension-multi-module/build.gradle +++ b/integration-tests/gradle/src/main/resources/add-extension-multi-module/build.gradle @@ -24,8 +24,8 @@ subprojects { apply plugin: 'java' java { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } test { diff --git a/integration-tests/gradle/src/main/resources/conditional-dependencies-kotlin/build.gradle.kts b/integration-tests/gradle/src/main/resources/conditional-dependencies-kotlin/build.gradle.kts index e5d23bacc50de..c2b7e6d1cc051 100644 --- a/integration-tests/gradle/src/main/resources/conditional-dependencies-kotlin/build.gradle.kts +++ b/integration-tests/gradle/src/main/resources/conditional-dependencies-kotlin/build.gradle.kts @@ -1,6 +1,6 @@ plugins { - kotlin("jvm") version "1.9.10" - kotlin("plugin.allopen") version "1.9.10" + kotlin("jvm") version "1.9.21" + kotlin("plugin.allopen") version "1.9.21" id("io.quarkus") } @@ -34,8 +34,8 @@ group = "org.acme" version = "1.0.0-SNAPSHOT" java { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } allOpen { @@ -46,6 +46,6 @@ allOpen { } tasks.withType { - kotlinOptions.jvmTarget = JavaVersion.VERSION_11.toString() + kotlinOptions.jvmTarget = JavaVersion.VERSION_17.toString() kotlinOptions.javaParameters = true } 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 index 57535418f2507..01ed5ced48ab4 100644 --- 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 @@ -24,8 +24,8 @@ group 'org.acme' version '1.0.0-SNAPSHOT' java { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } test { 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 index 57535418f2507..01ed5ced48ab4 100644 --- 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 @@ -24,8 +24,8 @@ group 'org.acme' version '1.0.0-SNAPSHOT' java { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } test { 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 index 57535418f2507..01ed5ced48ab4 100644 --- a/integration-tests/gradle/src/main/resources/grpc-descriptor-set/build.gradle +++ b/integration-tests/gradle/src/main/resources/grpc-descriptor-set/build.gradle @@ -24,8 +24,8 @@ group 'org.acme' version '1.0.0-SNAPSHOT' java { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } test { diff --git a/integration-tests/gradle/src/main/resources/grpc-include-project/build.gradle b/integration-tests/gradle/src/main/resources/grpc-include-project/build.gradle index 57535418f2507..01ed5ced48ab4 100644 --- a/integration-tests/gradle/src/main/resources/grpc-include-project/build.gradle +++ b/integration-tests/gradle/src/main/resources/grpc-include-project/build.gradle @@ -24,8 +24,8 @@ group 'org.acme' version '1.0.0-SNAPSHOT' java { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } test { diff --git a/integration-tests/gradle/src/main/resources/multi-module-with-empty-module/build.gradle.kts b/integration-tests/gradle/src/main/resources/multi-module-with-empty-module/build.gradle.kts index 22c0d009fb62c..87f48bdc76cdd 100644 --- a/integration-tests/gradle/src/main/resources/multi-module-with-empty-module/build.gradle.kts +++ b/integration-tests/gradle/src/main/resources/multi-module-with-empty-module/build.gradle.kts @@ -30,7 +30,7 @@ subprojects { val quarkusPlatformArtifactId: String by project val quarkusPlatformVersion: String by project - val javaVersion = "11" + val javaVersion = "17" dependencies { "implementation"(enforcedPlatform("$quarkusPlatformGroupId:$quarkusPlatformArtifactId:$quarkusPlatformVersion")) diff --git a/integration-tests/hibernate-orm-panache/src/main/java/io/quarkus/it/panache/TestEndpoint.java b/integration-tests/hibernate-orm-panache/src/main/java/io/quarkus/it/panache/TestEndpoint.java index 8643dda0b99cb..a292f449c14ea 100644 --- a/integration-tests/hibernate-orm-panache/src/main/java/io/quarkus/it/panache/TestEndpoint.java +++ b/integration-tests/hibernate-orm-panache/src/main/java/io/quarkus/it/panache/TestEndpoint.java @@ -193,6 +193,9 @@ public String testModel() { Assertions.assertThrows(PanacheQueryException.class, () -> Person.update("Person.updateAllNames", Parameters.with("name", "stef2").map())); + Assertions.assertThrows(PanacheQueryException.class, + () -> Person.update("Person.updateAllNames")); + Assertions.assertEquals(1, Person.update("#Person.updateAllNames", Parameters.with("name", "stef3"))); persons = Person.find("#Person.getByName", Parameters.with("name", "stef3")).list(); Assertions.assertEquals(1, persons.size()); diff --git a/integration-tests/hibernate-search-orm-elasticsearch-coordination-outbox-polling/README.md b/integration-tests/hibernate-search-orm-elasticsearch-outbox-polling/README.md similarity index 100% rename from integration-tests/hibernate-search-orm-elasticsearch-coordination-outbox-polling/README.md rename to integration-tests/hibernate-search-orm-elasticsearch-outbox-polling/README.md diff --git a/integration-tests/hibernate-search-orm-elasticsearch-coordination-outbox-polling/pom.xml b/integration-tests/hibernate-search-orm-elasticsearch-outbox-polling/pom.xml similarity index 97% rename from integration-tests/hibernate-search-orm-elasticsearch-coordination-outbox-polling/pom.xml rename to integration-tests/hibernate-search-orm-elasticsearch-outbox-polling/pom.xml index 1c29e00db422b..f144ae7fec4e5 100644 --- a/integration-tests/hibernate-search-orm-elasticsearch-coordination-outbox-polling/pom.xml +++ b/integration-tests/hibernate-search-orm-elasticsearch-outbox-polling/pom.xml @@ -9,8 +9,8 @@ 4.0.0 - quarkus-integration-test-hibernate-search-orm-elasticsearch-coordination-outbox-polling - Quarkus - Integration Tests - Hibernate Search ORM + Elasticsearch - Coordination - Outbox-polling + quarkus-integration-test-hibernate-search-orm-elasticsearch-outbox-polling + Quarkus - Integration Tests - Hibernate Search ORM + Elasticsearch - Outbox-polling io.quarkus @@ -22,7 +22,7 @@ io.quarkus - quarkus-hibernate-search-orm-coordination-outbox-polling + quarkus-hibernate-search-orm-outbox-polling io.quarkus @@ -83,7 +83,7 @@ io.quarkus - quarkus-hibernate-search-orm-coordination-outbox-polling-deployment + quarkus-hibernate-search-orm-outbox-polling-deployment ${project.version} pom test diff --git a/integration-tests/hibernate-search-orm-elasticsearch-coordination-outbox-polling/src/main/java/io/quarkus/it/hibernate/search/orm/elasticsearch/coordination/outboxpolling/HibernateSearchOutboxPollingTestResource.java b/integration-tests/hibernate-search-orm-elasticsearch-outbox-polling/src/main/java/io/quarkus/it/hibernate/search/orm/elasticsearch/coordination/outboxpolling/HibernateSearchOutboxPollingTestResource.java similarity index 100% rename from integration-tests/hibernate-search-orm-elasticsearch-coordination-outbox-polling/src/main/java/io/quarkus/it/hibernate/search/orm/elasticsearch/coordination/outboxpolling/HibernateSearchOutboxPollingTestResource.java rename to integration-tests/hibernate-search-orm-elasticsearch-outbox-polling/src/main/java/io/quarkus/it/hibernate/search/orm/elasticsearch/coordination/outboxpolling/HibernateSearchOutboxPollingTestResource.java diff --git a/integration-tests/hibernate-search-orm-elasticsearch-coordination-outbox-polling/src/main/java/io/quarkus/it/hibernate/search/orm/elasticsearch/coordination/outboxpolling/OutboxPollingTestUtils.java b/integration-tests/hibernate-search-orm-elasticsearch-outbox-polling/src/main/java/io/quarkus/it/hibernate/search/orm/elasticsearch/coordination/outboxpolling/OutboxPollingTestUtils.java similarity index 75% rename from integration-tests/hibernate-search-orm-elasticsearch-coordination-outbox-polling/src/main/java/io/quarkus/it/hibernate/search/orm/elasticsearch/coordination/outboxpolling/OutboxPollingTestUtils.java rename to integration-tests/hibernate-search-orm-elasticsearch-outbox-polling/src/main/java/io/quarkus/it/hibernate/search/orm/elasticsearch/coordination/outboxpolling/OutboxPollingTestUtils.java index 3391237bf34bc..9791044473929 100644 --- a/integration-tests/hibernate-search-orm-elasticsearch-coordination-outbox-polling/src/main/java/io/quarkus/it/hibernate/search/orm/elasticsearch/coordination/outboxpolling/OutboxPollingTestUtils.java +++ b/integration-tests/hibernate-search-orm-elasticsearch-outbox-polling/src/main/java/io/quarkus/it/hibernate/search/orm/elasticsearch/coordination/outboxpolling/OutboxPollingTestUtils.java @@ -14,9 +14,11 @@ import jakarta.transaction.SystemException; import jakarta.transaction.UserTransaction; -import org.hibernate.search.mapper.orm.coordination.outboxpolling.cluster.impl.Agent; -import org.hibernate.search.mapper.orm.coordination.outboxpolling.cluster.impl.AgentState; -import org.hibernate.search.mapper.orm.coordination.outboxpolling.event.impl.OutboxEvent; +import org.hibernate.search.mapper.orm.outboxpolling.cluster.impl.Agent; +import org.hibernate.search.mapper.orm.outboxpolling.cluster.impl.AgentState; +import org.hibernate.search.mapper.orm.outboxpolling.cluster.impl.OutboxPollingAgentAdditionalJaxbMappingProducer; +import org.hibernate.search.mapper.orm.outboxpolling.event.impl.OutboxEvent; +import org.hibernate.search.mapper.orm.outboxpolling.event.impl.OutboxPollingOutboxEventAdditionalJaxbMappingProducer; public class OutboxPollingTestUtils { @@ -47,7 +49,9 @@ public static void awaitAgentsRunning(EntityManager entityManager, UserTransacti .pollInterval(Duration.ofMillis(5)) .atMost(Duration.ofSeconds(10)) // CI can be rather slow... .untilAsserted(() -> inTransaction(userTransaction, () -> { - List agents = entityManager.createQuery("select a from Agent a order by a.id", Agent.class) + List agents = entityManager + .createQuery("select a from " + OutboxPollingAgentAdditionalJaxbMappingProducer.ENTITY_NAME + + " a order by a.id", Agent.class) .getResultList(); assertThat(agents) .hasSize(expectedAgentCount) @@ -65,7 +69,8 @@ public static void awaitNoMoreOutboxEvents(EntityManager entityManager, UserTran .atMost(Duration.ofSeconds(20)) // CI can be rather slow... .untilAsserted(() -> inTransaction(userTransaction, () -> { List events = entityManager - .createQuery("select e from OutboxEvent e order by e.id", OutboxEvent.class) + .createQuery("select e from " + OutboxPollingOutboxEventAdditionalJaxbMappingProducer.ENTITY_NAME + + " e order by e.id", OutboxEvent.class) .getResultList(); assertThat(events).isEmpty(); })); diff --git a/integration-tests/hibernate-search-orm-elasticsearch-coordination-outbox-polling/src/main/java/io/quarkus/it/hibernate/search/orm/elasticsearch/coordination/outboxpolling/Person.java b/integration-tests/hibernate-search-orm-elasticsearch-outbox-polling/src/main/java/io/quarkus/it/hibernate/search/orm/elasticsearch/coordination/outboxpolling/Person.java similarity index 100% rename from integration-tests/hibernate-search-orm-elasticsearch-coordination-outbox-polling/src/main/java/io/quarkus/it/hibernate/search/orm/elasticsearch/coordination/outboxpolling/Person.java rename to integration-tests/hibernate-search-orm-elasticsearch-outbox-polling/src/main/java/io/quarkus/it/hibernate/search/orm/elasticsearch/coordination/outboxpolling/Person.java diff --git a/integration-tests/hibernate-search-orm-elasticsearch-coordination-outbox-polling/src/main/resources/META-INF/beans.xml b/integration-tests/hibernate-search-orm-elasticsearch-outbox-polling/src/main/resources/META-INF/beans.xml similarity index 100% rename from integration-tests/hibernate-search-orm-elasticsearch-coordination-outbox-polling/src/main/resources/META-INF/beans.xml rename to integration-tests/hibernate-search-orm-elasticsearch-outbox-polling/src/main/resources/META-INF/beans.xml diff --git a/integration-tests/hibernate-search-orm-elasticsearch-coordination-outbox-polling/src/main/resources/application.properties b/integration-tests/hibernate-search-orm-elasticsearch-outbox-polling/src/main/resources/application.properties similarity index 100% rename from integration-tests/hibernate-search-orm-elasticsearch-coordination-outbox-polling/src/main/resources/application.properties rename to integration-tests/hibernate-search-orm-elasticsearch-outbox-polling/src/main/resources/application.properties diff --git a/integration-tests/hibernate-search-orm-elasticsearch-coordination-outbox-polling/src/test/java/io/quarkus/it/hibernate/search/orm/elasticsearch/coordination/outboxpolling/HibernateSearchOutboxPollingInGraalIT.java b/integration-tests/hibernate-search-orm-elasticsearch-outbox-polling/src/test/java/io/quarkus/it/hibernate/search/orm/elasticsearch/coordination/outboxpolling/HibernateSearchOutboxPollingInGraalIT.java similarity index 100% rename from integration-tests/hibernate-search-orm-elasticsearch-coordination-outbox-polling/src/test/java/io/quarkus/it/hibernate/search/orm/elasticsearch/coordination/outboxpolling/HibernateSearchOutboxPollingInGraalIT.java rename to integration-tests/hibernate-search-orm-elasticsearch-outbox-polling/src/test/java/io/quarkus/it/hibernate/search/orm/elasticsearch/coordination/outboxpolling/HibernateSearchOutboxPollingInGraalIT.java diff --git a/integration-tests/hibernate-search-orm-elasticsearch-coordination-outbox-polling/src/test/java/io/quarkus/it/hibernate/search/orm/elasticsearch/coordination/outboxpolling/HibernateSearchOutboxPollingTest.java b/integration-tests/hibernate-search-orm-elasticsearch-outbox-polling/src/test/java/io/quarkus/it/hibernate/search/orm/elasticsearch/coordination/outboxpolling/HibernateSearchOutboxPollingTest.java similarity index 100% rename from integration-tests/hibernate-search-orm-elasticsearch-coordination-outbox-polling/src/test/java/io/quarkus/it/hibernate/search/orm/elasticsearch/coordination/outboxpolling/HibernateSearchOutboxPollingTest.java rename to integration-tests/hibernate-search-orm-elasticsearch-outbox-polling/src/test/java/io/quarkus/it/hibernate/search/orm/elasticsearch/coordination/outboxpolling/HibernateSearchOutboxPollingTest.java diff --git a/integration-tests/hibernate-search-orm-elasticsearch-coordination-outbox-polling/src/test/java/io/quarkus/it/hibernate/search/orm/elasticsearch/coordination/outboxpolling/TestResources.java b/integration-tests/hibernate-search-orm-elasticsearch-outbox-polling/src/test/java/io/quarkus/it/hibernate/search/orm/elasticsearch/coordination/outboxpolling/TestResources.java similarity index 100% rename from integration-tests/hibernate-search-orm-elasticsearch-coordination-outbox-polling/src/test/java/io/quarkus/it/hibernate/search/orm/elasticsearch/coordination/outboxpolling/TestResources.java rename to integration-tests/hibernate-search-orm-elasticsearch-outbox-polling/src/test/java/io/quarkus/it/hibernate/search/orm/elasticsearch/coordination/outboxpolling/TestResources.java diff --git a/integration-tests/istio/maven-invoker-way/src/it/xds-grpc/pom.xml b/integration-tests/istio/maven-invoker-way/src/it/xds-grpc/pom.xml index 30481f7b37455..24c16c370fec2 100644 --- a/integration-tests/istio/maven-invoker-way/src/it/xds-grpc/pom.xml +++ b/integration-tests/istio/maven-invoker-way/src/it/xds-grpc/pom.xml @@ -8,9 +8,9 @@ UTF-8 3.1.2 - 11 + 17 UTF-8 - 11 + 17 diff --git a/integration-tests/jackson/src/test/java/io/quarkus/it/jackson/DateDeserializerPojoResourceTest.java b/integration-tests/jackson/src/test/java/io/quarkus/it/jackson/DateDeserializerPojoResourceTest.java index 5ead6ca1f17dd..9ada23b6f6f0a 100644 --- a/integration-tests/jackson/src/test/java/io/quarkus/it/jackson/DateDeserializerPojoResourceTest.java +++ b/integration-tests/jackson/src/test/java/io/quarkus/it/jackson/DateDeserializerPojoResourceTest.java @@ -3,11 +3,11 @@ import static io.quarkus.it.jackson.TestUtil.getObjectMapperForTest; import static io.restassured.RestAssured.given; import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.startsWith; import java.io.IOException; import java.sql.Date; import java.sql.Timestamp; +import java.time.ZoneId; import org.junit.jupiter.api.Test; @@ -32,13 +32,16 @@ public void testSqlTimestamp() throws IOException { @Test public void testSqlDate() throws IOException { SqlDatePojo pojo = new SqlDatePojo(); - pojo.date = new Date(0); + Date sqlDate = new Date(0); + pojo.date = sqlDate; + // the date will pass through Jackson's incorrect conversion; here is our equivalent: + sqlDate = new Date(sqlDate.toLocalDate().atStartOfDay(ZoneId.of("UTC")).toEpochSecond() * 1000L); given() .body(getObjectMapperForTest().writeValueAsString(pojo)) .when().post("/datedeserializers/sql/date") .then() .statusCode(200) - .body("date", startsWith("1970-")); + .body("date", equalTo(sqlDate.toString())); } } diff --git a/integration-tests/java-17/src/main/java/io/quarkus/it/hibernate/panache/person/PersonResource.java b/integration-tests/java-17/src/main/java/io/quarkus/it/hibernate/panache/person/PersonResource.java deleted file mode 100644 index 7935235a7c18b..0000000000000 --- a/integration-tests/java-17/src/main/java/io/quarkus/it/hibernate/panache/person/PersonResource.java +++ /dev/null @@ -1,62 +0,0 @@ -package io.quarkus.it.hibernate.panache.person; - -import java.net.URI; -import java.util.Arrays; -import java.util.List; - -import jakarta.transaction.Transactional; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.POST; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.core.Response; - -@Path("/hibernate/persons") -@Transactional -public class PersonResource { - @GET - public List getPersons() { - return Person.findAll().project(PersonName.class).list(); - } - - @POST - public Response addPerson(Person person) { - person.persist(); - String id = person.id.toString(); - return Response.created(URI.create("/persons/entity/" + id)).build(); - } - - @GET - @Path("hql-project") - @Transactional - public Response testPanacheHqlProject() { - var mark = new Person(); - mark.firstname = "Mark"; - mark.lastname = "Mark"; - mark.persistAndFlush(); - - var hqlWithoutSpace = """ - select - firstname, - lastname - from - io.quarkus.it.hibernate.panache.person.Person - where - firstname = ?1 - """; - var persistedWithoutSpace = Person.find(hqlWithoutSpace, "Mark").project(PersonName.class).firstResult(); - - // We need to escape the whitespace in Java otherwise the compiler removes it. - var hqlWithSpace = """ - select\s - firstname, - lastname - from - io.quarkus.it.hibernate.panache.person.Person - where - firstname = ?1 - """; - var persistedWithSpace = Person.find(hqlWithSpace, "Mark").project(PersonName.class).firstResult(); - - return Response.ok(Arrays.asList(persistedWithoutSpace, persistedWithSpace)).build(); - } -} diff --git a/integration-tests/java-17/src/main/java/io/quarkus/it/mongodb/panache/person/Person.java b/integration-tests/java-17/src/main/java/io/quarkus/it/mongodb/panache/person/Person.java deleted file mode 100644 index 80479c3dab703..0000000000000 --- a/integration-tests/java-17/src/main/java/io/quarkus/it/mongodb/panache/person/Person.java +++ /dev/null @@ -1,9 +0,0 @@ -package io.quarkus.it.mongodb.panache.person; - -import io.quarkus.mongodb.panache.PanacheMongoEntity; - -public class Person extends PanacheMongoEntity { - public String firstname; - public String lastname; - public Status status = Status.ALIVE; -} diff --git a/integration-tests/java-17/src/main/java/io/quarkus/it/mongodb/panache/person/PersonResource.java b/integration-tests/java-17/src/main/java/io/quarkus/it/mongodb/panache/person/PersonResource.java deleted file mode 100644 index f719d5800ecf1..0000000000000 --- a/integration-tests/java-17/src/main/java/io/quarkus/it/mongodb/panache/person/PersonResource.java +++ /dev/null @@ -1,24 +0,0 @@ -package io.quarkus.it.mongodb.panache.person; - -import java.net.URI; -import java.util.List; - -import jakarta.ws.rs.GET; -import jakarta.ws.rs.POST; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.core.Response; - -@Path("/mongo/persons") -public class PersonResource { - @GET - public List getPersons() { - return Person.findAll().project(PersonName.class).list(); - } - - @POST - public Response addPerson(Person person) { - person.persist(); - String id = person.id.toString(); - return Response.created(URI.create("/persons/entity/" + id)).build(); - } -} diff --git a/integration-tests/java-17/src/main/resources/application.properties b/integration-tests/java-17/src/main/resources/application.properties deleted file mode 100644 index b54b42cd1b42f..0000000000000 --- a/integration-tests/java-17/src/main/resources/application.properties +++ /dev/null @@ -1,7 +0,0 @@ -quarkus.mongodb.connection-string=mongodb://localhost:27017 -quarkus.mongodb.database=persons - -quarkus.datasource.db-kind=h2 -quarkus.datasource.jdbc.url=jdbc:h2:tcp://localhost/mem:test -quarkus.datasource.jdbc.max-size=8 -quarkus.hibernate-orm.database.generation=drop-and-create \ No newline at end of file diff --git a/integration-tests/java-17/src/test/java/io/quarkus/it/hibernate/panache/person/PersonResourceTest.java b/integration-tests/java-17/src/test/java/io/quarkus/it/hibernate/panache/person/PersonResourceTest.java deleted file mode 100644 index 601241f450872..0000000000000 --- a/integration-tests/java-17/src/test/java/io/quarkus/it/hibernate/panache/person/PersonResourceTest.java +++ /dev/null @@ -1,54 +0,0 @@ -package io.quarkus.it.hibernate.panache.person; - -import static io.restassured.RestAssured.given; -import static io.restassured.RestAssured.when; -import static org.hamcrest.CoreMatchers.is; - -import org.junit.jupiter.api.Test; - -import io.quarkus.it.mongodb.panache.person.Person; -import io.quarkus.it.mongodb.panache.person.Status; -import io.quarkus.test.TestTransaction; -import io.quarkus.test.common.QuarkusTestResource; -import io.quarkus.test.h2.H2DatabaseTestResource; -import io.quarkus.test.junit.QuarkusTest; -import io.restassured.http.ContentType; - -@QuarkusTest -@QuarkusTestResource(H2DatabaseTestResource.class) -class PersonResourceTest { - private static final String ROOT_URL = "/hibernate/persons"; - - @Test - @TestTransaction - void testRecordInPanache() { - var person1 = new Person(); - person1.firstname = "Loïc"; - person1.lastname = "Mathieu"; - person1.status = Status.ALIVE; - var person2 = new Person(); - person1.firstname = "Zombie"; - person2.lastname = "Zombie"; - person2.status = Status.DEAD; - - given().body(person1).contentType(ContentType.JSON) - .when().post(ROOT_URL) - .then().statusCode(201); - given().body(person2).contentType(ContentType.JSON) - .when().post(ROOT_URL) - .then().statusCode(201); - - when().get(ROOT_URL) - .then() - .statusCode(200) - .body("size()", is(2)); - } - - @Test - @TestTransaction - void testHqlPanacheProject() { - when().get(ROOT_URL + "/hql-project") - .then().statusCode(200) - .body("size()", is(2)); - } -} diff --git a/integration-tests/jpa-db2/pom.xml b/integration-tests/jpa-db2/pom.xml index 53c4faae983b6..28d1dbb1da613 100644 --- a/integration-tests/jpa-db2/pom.xml +++ b/integration-tests/jpa-db2/pom.xml @@ -20,15 +20,15 @@ io.quarkus - quarkus-undertow + quarkus-hibernate-orm io.quarkus - quarkus-hibernate-orm + quarkus-jdbc-db2 io.quarkus - quarkus-jdbc-db2 + quarkus-resteasy-reactive @@ -81,7 +81,7 @@ io.quarkus - quarkus-undertow-deployment + quarkus-resteasy-reactive-deployment ${project.version} pom test diff --git a/integration-tests/jpa-db2/src/main/java/io/quarkus/it/jpa/db2/JPAFunctionalityTestEndpoint.java b/integration-tests/jpa-db2/src/main/java/io/quarkus/it/jpa/db2/JPAFunctionalityTestEndpoint.java index a87e5568de2f5..530eeefe367f9 100644 --- a/integration-tests/jpa-db2/src/main/java/io/quarkus/it/jpa/db2/JPAFunctionalityTestEndpoint.java +++ b/integration-tests/jpa-db2/src/main/java/io/quarkus/it/jpa/db2/JPAFunctionalityTestEndpoint.java @@ -1,158 +1,102 @@ package io.quarkus.it.jpa.db2; import java.io.IOException; -import java.io.PrintWriter; import java.util.List; import java.util.UUID; import jakarta.inject.Inject; import jakarta.persistence.EntityManager; -import jakarta.persistence.EntityManagerFactory; -import jakarta.persistence.EntityTransaction; import jakarta.persistence.TypedQuery; import jakarta.persistence.criteria.CriteriaBuilder; import jakarta.persistence.criteria.CriteriaQuery; import jakarta.persistence.criteria.Root; -import jakarta.servlet.annotation.WebServlet; -import jakarta.servlet.http.HttpServlet; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +import io.quarkus.narayana.jta.QuarkusTransaction; /** * Various tests covering JPA functionality. All tests should work in both standard JVM and native mode. */ -@SuppressWarnings("serial") -@WebServlet(name = "JPATestBootstrapEndpoint", urlPatterns = "/jpa/testfunctionality") -public class JPAFunctionalityTestEndpoint extends HttpServlet { +@Path("/jpa/testfunctionality") +@Produces(MediaType.TEXT_PLAIN) +public class JPAFunctionalityTestEndpoint { @Inject - EntityManagerFactory entityManagerFactory; - - @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { - try { - doStuffWithHibernate(entityManagerFactory); - } catch (Exception e) { - reportException("An error occurred while performing Hibernate operations", e, resp); - } - resp.getWriter().write("OK"); - } - - /** - * Lists the various operations we want to test for: - */ - private static void doStuffWithHibernate(EntityManagerFactory entityManagerFactory) { + EntityManager em; - //Cleanup any existing data: - deleteAllPerson(entityManagerFactory); + @GET + public String test() throws IOException { + cleanUpData(); //Store some well known Person instances we can then test on: - storeTestPersons(entityManagerFactory); + QuarkusTransaction.requiringNew().run(() -> { + persistNewPerson("Gizmo"); + persistNewPerson("Quarkus"); + persistNewPerson("Hibernate ORM"); + }); //Load all persons and run some checks on the query results: - verifyListOfExistingPersons(entityManagerFactory); + QuarkusTransaction.requiringNew().run(() -> { + CriteriaBuilder cb = em.getCriteriaBuilder(); + + CriteriaQuery cq = cb.createQuery(Person.class); + Root from = cq.from(Person.class); + cq.select(from).orderBy(cb.asc(from.get("name"))); + TypedQuery q = em.createQuery(cq); + List allpersons = q.getResultList(); + if (allpersons.size() != 3) { + throw new RuntimeException("Incorrect number of results"); + } + if (!allpersons.get(0).getName().equals("Gizmo")) { + throw new RuntimeException("Incorrect order of results"); + } + StringBuilder sb = new StringBuilder("list of stored Person names:\n\t"); + for (Person p : allpersons) { + p.describeFully(sb); + sb.append("\n\t"); + if (p.getStatus() != Status.LIVING) { + throw new RuntimeException("Incorrect status " + p); + } + } + sb.append("\nList complete.\n"); + System.out.print(sb); + }); //Try a JPA named query: - verifyJPANamedQuery(entityManagerFactory); - - deleteAllPerson(entityManagerFactory); - - } + QuarkusTransaction.requiringNew().run(() -> { + TypedQuery typedQuery = em.createNamedQuery( + "get_person_by_name", Person.class); + typedQuery.setParameter("name", "Quarkus"); + final Person singleResult = typedQuery.getSingleResult(); + + if (!singleResult.getName().equals("Quarkus")) { + throw new RuntimeException("Wrong result from named JPA query"); + } + }); - private static void verifyJPANamedQuery(final EntityManagerFactory emf) { - EntityManager em = emf.createEntityManager(); - EntityTransaction transaction = em.getTransaction(); - transaction.begin(); - TypedQuery typedQuery = em.createNamedQuery( - "get_person_by_name", Person.class); - typedQuery.setParameter("name", "Quarkus"); - final Person singleResult = typedQuery.getSingleResult(); - - if (!singleResult.getName().equals("Quarkus")) { - throw new RuntimeException("Wrong result from named JPA query"); - } - - transaction.commit(); - em.close(); - } + cleanUpData(); - private static void verifyListOfExistingPersons(final EntityManagerFactory emf) { - EntityManager em = emf.createEntityManager(); - EntityTransaction transaction = em.getTransaction(); - transaction.begin(); - listExistingPersons(em); - transaction.commit(); - em.close(); + return "OK"; } - private static void storeTestPersons(final EntityManagerFactory emf) { - EntityManager em = emf.createEntityManager(); - EntityTransaction transaction = em.getTransaction(); - transaction.begin(); - persistNewPerson(em, "Gizmo"); - persistNewPerson(em, "Quarkus"); - persistNewPerson(em, "Hibernate ORM"); - transaction.commit(); - em.close(); + private void cleanUpData() { + QuarkusTransaction.requiringNew() + .run(() -> em.createNativeQuery("Delete from Person").executeUpdate()); } - private static void deleteAllPerson(final EntityManagerFactory emf) { - EntityManager em = emf.createEntityManager(); - EntityTransaction transaction = em.getTransaction(); - transaction.begin(); - em.createNativeQuery("Delete from Person").executeUpdate(); - transaction.commit(); - em.close(); - } - - private static void listExistingPersons(EntityManager em) { - CriteriaBuilder cb = em.getCriteriaBuilder(); - - CriteriaQuery cq = cb.createQuery(Person.class); - Root from = cq.from(Person.class); - cq.select(from).orderBy(cb.asc(from.get("name"))); - TypedQuery q = em.createQuery(cq); - List allpersons = q.getResultList(); - if (allpersons.size() != 3) { - throw new RuntimeException("Incorrect number of results"); - } - if (!allpersons.get(0).getName().equals("Gizmo")) { - throw new RuntimeException("Incorrect order of results"); - } - StringBuilder sb = new StringBuilder("list of stored Person names:\n\t"); - for (Person p : allpersons) { - p.describeFully(sb); - sb.append("\n\t"); - if (p.getStatus() != Status.LIVING) { - throw new RuntimeException("Incorrect status " + p); - } - } - sb.append("\nList complete.\n"); - System.out.print(sb); - } - - private static void persistNewPerson(EntityManager entityManager, String name) { + private void persistNewPerson(String name) { Person person = new Person(); person.setName(name); person.setStatus(Status.LIVING); person.setAddress(new SequencedAddress("Street " + randomName())); - entityManager.persist(person); + em.persist(person); } private static String randomName() { return UUID.randomUUID().toString(); } - private void reportException(String errorMessage, final Exception e, final HttpServletResponse resp) throws IOException { - final PrintWriter writer = resp.getWriter(); - if (errorMessage != null) { - writer.write(errorMessage); - writer.write(" "); - } - writer.write(e.toString()); - writer.append("\n\t"); - e.printStackTrace(writer); - writer.append("\n\t"); - } - } diff --git a/integration-tests/jpa-derby/pom.xml b/integration-tests/jpa-derby/pom.xml index 6a13b889ab6bf..37ec78874274d 100644 --- a/integration-tests/jpa-derby/pom.xml +++ b/integration-tests/jpa-derby/pom.xml @@ -26,7 +26,7 @@ io.quarkus - quarkus-undertow + quarkus-resteasy-reactive @@ -85,7 +85,7 @@ io.quarkus - quarkus-undertow-deployment + quarkus-resteasy-reactive-deployment ${project.version} pom test diff --git a/integration-tests/jpa-derby/src/main/java/io/quarkus/it/jpa/derby/JPAFunctionalityTestEndpoint.java b/integration-tests/jpa-derby/src/main/java/io/quarkus/it/jpa/derby/JPAFunctionalityTestEndpoint.java index 406f4bcf8eaf5..e996e51eb641b 100644 --- a/integration-tests/jpa-derby/src/main/java/io/quarkus/it/jpa/derby/JPAFunctionalityTestEndpoint.java +++ b/integration-tests/jpa-derby/src/main/java/io/quarkus/it/jpa/derby/JPAFunctionalityTestEndpoint.java @@ -1,176 +1,101 @@ package io.quarkus.it.jpa.derby; -import java.io.IOException; -import java.io.PrintWriter; import java.util.List; import java.util.UUID; import jakarta.inject.Inject; import jakarta.persistence.EntityManager; -import jakarta.persistence.EntityManagerFactory; -import jakarta.persistence.EntityTransaction; import jakarta.persistence.TypedQuery; import jakarta.persistence.criteria.CriteriaBuilder; import jakarta.persistence.criteria.CriteriaQuery; import jakarta.persistence.criteria.Root; -import jakarta.servlet.annotation.WebServlet; -import jakarta.servlet.http.HttpServlet; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +import io.quarkus.narayana.jta.QuarkusTransaction; /** * Basic test running JPA with the Derby database. - * The application can work in either standard JVM or in native mode, while we run H2 as a separate JVM process. + * The application can work in either standard JVM or in native mode, while we run Derby as a separate JVM process. */ -@WebServlet(name = "JPATestBootstrapEndpoint", urlPatterns = "/jpa-derby/testfunctionality") -public class JPAFunctionalityTestEndpoint extends HttpServlet { +@Path("/jpa-derby/testfunctionality") +@Produces(MediaType.TEXT_PLAIN) +public class JPAFunctionalityTestEndpoint { @Inject - EntityManagerFactory entityManagerFactory; - - @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { - try { - doStuffWithHibernate(entityManagerFactory); - } catch (Exception e) { - reportException("An error occurred while performing Hibernate operations", e, resp); - } - resp.getWriter().write("OK"); - } - - /** - * Lists the various operations we want to test for: - */ - private static void doStuffWithHibernate(EntityManagerFactory entityManagerFactory) { + EntityManager em; - //Cleanup any existing data: - deleteAllPerson(entityManagerFactory); + @GET + public String test() { + cleanUpData(); //Store some well known Person instances we can then test on: - storeTestPersons(entityManagerFactory); + QuarkusTransaction.requiringNew().run(() -> { + persistNewPerson("Gizmo"); + persistNewPerson("Quarkus"); + persistNewPerson("Hibernate ORM"); + }); //Load all persons and run some checks on the query results: - verifyListOfExistingPersons(entityManagerFactory); + QuarkusTransaction.requiringNew().run(() -> { + CriteriaBuilder cb = em.getCriteriaBuilder(); + + CriteriaQuery cq = cb.createQuery(Person.class); + Root from = cq.from(Person.class); + cq.select(from).orderBy(cb.asc(from.get("name"))); + TypedQuery q = em.createQuery(cq); + List allpersons = q.getResultList(); + if (allpersons.size() != 3) { + throw new RuntimeException("Incorrect number of results"); + } + if (!allpersons.get(0).getName().equals("Gizmo")) { + throw new RuntimeException("Incorrect order of results"); + } + StringBuilder sb = new StringBuilder("list of stored Person names:\n\t"); + for (Person p : allpersons) { + p.describeFully(sb); + } + sb.append("\nList complete.\n"); + System.out.print(sb); + }); //Try a JPA named query: - verifyJPANamedQuery(entityManagerFactory); - - verifyHqlFetch(entityManagerFactory); - - deleteAllPerson(entityManagerFactory); - - } - - private static void verifyHqlFetch(EntityManagerFactory emf) { - EntityManager em = emf.createEntityManager(); - try { - EntityTransaction transaction = em.getTransaction(); - try { - transaction.begin(); - - em.createQuery("from Person p left join fetch p.address a").getResultList(); - - transaction.commit(); - } catch (Exception e) { - if (transaction.isActive()) { - transaction.rollback(); - } - throw e; + QuarkusTransaction.requiringNew().run(() -> { + TypedQuery typedQuery = em.createNamedQuery( + "get_person_by_name", Person.class); + typedQuery.setParameter("name", "Quarkus"); + final Person singleResult = typedQuery.getSingleResult(); + + if (!singleResult.getName().equals("Quarkus")) { + throw new RuntimeException("Wrong result from named JPA query"); } - } finally { - em.close(); - } - } + }); - private static void verifyJPANamedQuery(final EntityManagerFactory emf) { - EntityManager em = emf.createEntityManager(); - EntityTransaction transaction = em.getTransaction(); - transaction.begin(); - TypedQuery typedQuery = em.createNamedQuery( - "get_person_by_name", Person.class); - typedQuery.setParameter("name", "Quarkus"); - final Person singleResult = typedQuery.getSingleResult(); - - if (!singleResult.getName().equals("Quarkus")) { - throw new RuntimeException("Wrong result from named JPA query"); - } - - transaction.commit(); - em.close(); - } + //Check that HQL fetch does not fail + QuarkusTransaction.requiringNew() + .run(() -> em.createQuery("from Person p left join fetch p.address a").getResultList()); - private static void verifyListOfExistingPersons(final EntityManagerFactory emf) { - EntityManager em = emf.createEntityManager(); - EntityTransaction transaction = em.getTransaction(); - transaction.begin(); - listExistingPersons(em); - transaction.commit(); - em.close(); - } + cleanUpData(); - private static void storeTestPersons(final EntityManagerFactory emf) { - EntityManager em = emf.createEntityManager(); - EntityTransaction transaction = em.getTransaction(); - transaction.begin(); - persistNewPerson(em, "Gizmo"); - persistNewPerson(em, "Quarkus"); - persistNewPerson(em, "Hibernate ORM"); - transaction.commit(); - em.close(); + return "OK"; } - private static void deleteAllPerson(final EntityManagerFactory emf) { - EntityManager em = emf.createEntityManager(); - EntityTransaction transaction = em.getTransaction(); - transaction.begin(); - em.createNativeQuery("Delete from Person").executeUpdate(); - transaction.commit(); - em.close(); + private void cleanUpData() { + QuarkusTransaction.requiringNew() + .run(() -> em.createNativeQuery("Delete from Person").executeUpdate()); } - private static void listExistingPersons(EntityManager em) { - CriteriaBuilder cb = em.getCriteriaBuilder(); - - CriteriaQuery cq = cb.createQuery(Person.class); - Root from = cq.from(Person.class); - cq.select(from).orderBy(cb.asc(from.get("name"))); - TypedQuery q = em.createQuery(cq); - List allpersons = q.getResultList(); - if (allpersons.size() != 3) { - throw new RuntimeException("Incorrect number of results"); - } - if (!allpersons.get(0).getName().equals("Gizmo")) { - throw new RuntimeException("Incorrect order of results"); - } - StringBuilder sb = new StringBuilder("list of stored Person names:\n\t"); - for (Person p : allpersons) { - p.describeFully(sb); - } - sb.append("\nList complete.\n"); - System.out.print(sb); - } - - private static void persistNewPerson(EntityManager entityManager, String name) { + private void persistNewPerson(String name) { Person person = new Person(); person.setName(name); person.setAddress(new SequencedAddress("Street " + randomName())); - entityManager.persist(person); + em.persist(person); } private static String randomName() { return UUID.randomUUID().toString(); } - private void reportException(String errorMessage, final Exception e, final HttpServletResponse resp) throws IOException { - final PrintWriter writer = resp.getWriter(); - if (errorMessage != null) { - writer.write(errorMessage); - writer.write(" "); - } - writer.write(e.toString()); - writer.append("\n\t"); - e.printStackTrace(writer); - writer.append("\n\t"); - } - } diff --git a/integration-tests/jpa-h2-embedded/pom.xml b/integration-tests/jpa-h2-embedded/pom.xml index 759784b2de62f..c0e4c31552543 100644 --- a/integration-tests/jpa-h2-embedded/pom.xml +++ b/integration-tests/jpa-h2-embedded/pom.xml @@ -26,7 +26,7 @@ io.quarkus - quarkus-undertow + quarkus-resteasy-reactive @@ -80,7 +80,7 @@ io.quarkus - quarkus-undertow-deployment + quarkus-resteasy-reactive-deployment ${project.version} pom test diff --git a/integration-tests/jpa-h2-embedded/src/main/java/io/quarkus/it/jpa/h2/DialectEndpoint.java b/integration-tests/jpa-h2-embedded/src/main/java/io/quarkus/it/jpa/h2/DialectEndpoint.java index 0a863c5f2f88a..5c74ab76c647e 100644 --- a/integration-tests/jpa-h2-embedded/src/main/java/io/quarkus/it/jpa/h2/DialectEndpoint.java +++ b/integration-tests/jpa-h2-embedded/src/main/java/io/quarkus/it/jpa/h2/DialectEndpoint.java @@ -1,44 +1,28 @@ package io.quarkus.it.jpa.h2; import java.io.IOException; -import java.io.PrintWriter; import jakarta.inject.Inject; -import jakarta.servlet.annotation.WebServlet; -import jakarta.servlet.http.HttpServlet; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; import org.hibernate.SessionFactory; import org.hibernate.engine.spi.SessionFactoryImplementor; import io.quarkus.hibernate.orm.runtime.config.DialectVersions; -@WebServlet(name = "DialectEndpoint", urlPatterns = "/dialect/version") -public class DialectEndpoint extends HttpServlet { +@Path("/dialect/version") +@Produces(MediaType.TEXT_PLAIN) +public class DialectEndpoint { @Inject SessionFactory sessionFactory; - @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { - try { - var version = sessionFactory.unwrap(SessionFactoryImplementor.class).getJdbcServices().getDialect().getVersion(); - resp.getWriter().write(DialectVersions.toString(version)); - } catch (Exception e) { - reportException("Failed to retrieve dialect version", e, resp); - } - } - - private void reportException(String errorMessage, final Exception e, final HttpServletResponse resp) throws IOException { - final PrintWriter writer = resp.getWriter(); - if (errorMessage != null) { - writer.write(errorMessage); - writer.write(" "); - } - writer.write(e.toString()); - writer.append("\n\t"); - e.printStackTrace(writer); - writer.append("\n\t"); + @GET + public String test() throws IOException { + var version = sessionFactory.unwrap(SessionFactoryImplementor.class).getJdbcServices().getDialect().getVersion(); + return DialectVersions.toString(version); } } diff --git a/integration-tests/jpa-h2-embedded/src/main/java/io/quarkus/it/jpa/h2/JPAFunctionalityTestEndpoint.java b/integration-tests/jpa-h2-embedded/src/main/java/io/quarkus/it/jpa/h2/JPAFunctionalityTestEndpoint.java index ae88d6721c91b..1286d555e877d 100644 --- a/integration-tests/jpa-h2-embedded/src/main/java/io/quarkus/it/jpa/h2/JPAFunctionalityTestEndpoint.java +++ b/integration-tests/jpa-h2-embedded/src/main/java/io/quarkus/it/jpa/h2/JPAFunctionalityTestEndpoint.java @@ -1,196 +1,102 @@ package io.quarkus.it.jpa.h2; import java.io.IOException; -import java.io.PrintWriter; import java.util.List; import java.util.UUID; -import java.util.function.Consumer; +import jakarta.inject.Inject; import jakarta.persistence.EntityManager; -import jakarta.persistence.EntityManagerFactory; -import jakarta.persistence.EntityTransaction; -import jakarta.persistence.PersistenceUnit; import jakarta.persistence.TypedQuery; import jakarta.persistence.criteria.CriteriaBuilder; import jakarta.persistence.criteria.CriteriaQuery; import jakarta.persistence.criteria.Root; -import jakarta.servlet.annotation.WebServlet; -import jakarta.servlet.http.HttpServlet; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +import io.quarkus.narayana.jta.QuarkusTransaction; /** * Basic test running JPA with the H2 database. * The application can work in either standard JVM or in native mode, embedding H2 within the application. */ -@WebServlet(name = "JPATestBootstrapEndpoint", urlPatterns = "/jpa-h2-embedded/testfunctionality") -public class JPAFunctionalityTestEndpoint extends HttpServlet { - - @PersistenceUnit - EntityManagerFactory entityManagerFactory; - - @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { - try { - doStuffWithHibernate(entityManagerFactory); - } catch (Exception e) { - reportException("An error occurred while performing Hibernate operations", e, resp); - } - resp.getWriter().write("OK"); - } +@Path("/jpa-h2-embedded/testfunctionality") +@Produces(MediaType.TEXT_PLAIN) +public class JPAFunctionalityTestEndpoint { - /** - * Lists the various operations we want to test for: - */ - private static void doStuffWithHibernate(EntityManagerFactory entityManagerFactory) { + @Inject + EntityManager em; - //Cleanup any existing data: - deleteAllPerson(entityManagerFactory); + @GET + public String test() throws IOException { + cleanUpData(); //Store some well known Person instances we can then test on: - storeTestPersons(entityManagerFactory); + QuarkusTransaction.requiringNew().run(() -> { + persistNewPerson("Gizmo"); + persistNewPerson("Quarkus"); + persistNewPerson("Hibernate ORM"); + }); //Load all persons and run some checks on the query results: - verifyListOfExistingPersons(entityManagerFactory); + QuarkusTransaction.requiringNew().run(() -> { + CriteriaBuilder cb = em.getCriteriaBuilder(); + + CriteriaQuery cq = cb.createQuery(Person.class); + Root from = cq.from(Person.class); + cq.select(from).orderBy(cb.asc(from.get("name"))); + TypedQuery q = em.createQuery(cq); + List allpersons = q.getResultList(); + if (allpersons.size() != 3) { + throw new RuntimeException("Incorrect number of results"); + } + if (!allpersons.get(0).getName().equals("Gizmo")) { + throw new RuntimeException("Incorrect order of results"); + } + StringBuilder sb = new StringBuilder("list of stored Person names:\n\t"); + for (Person p : allpersons) { + p.describeFully(sb); + } + sb.append("\nList complete.\n"); + System.out.print(sb); + }); //Try a JPA named query: - verifyJPANamedQuery(entityManagerFactory); - - verifyHqlFetch(entityManagerFactory); - - deleteAllPerson(entityManagerFactory); - - } - - private static void verifyHqlFetch(EntityManagerFactory emf) { - EntityManager em = emf.createEntityManager(); - try { - EntityTransaction transaction = em.getTransaction(); - try { - transaction.begin(); - - em.createQuery("from Person p left join fetch p.address a").getResultList(); - - transaction.commit(); - } catch (Exception e) { - if (transaction.isActive()) { - transaction.rollback(); - } - throw e; + QuarkusTransaction.requiringNew().run(() -> { + TypedQuery typedQuery = em.createNamedQuery( + "get_person_by_name", Person.class); + typedQuery.setParameter("name", "Quarkus"); + final Person singleResult = typedQuery.getSingleResult(); + + if (!singleResult.getName().equals("Quarkus")) { + throw new RuntimeException("Wrong result from named JPA query"); } - } finally { - em.close(); - } - } + }); - private static void verifyJPANamedQuery(final EntityManagerFactory emf) { - EntityManager em = emf.createEntityManager(); - EntityTransaction transaction = em.getTransaction(); - transaction.begin(); - TypedQuery typedQuery = em.createNamedQuery( - "get_person_by_name", Person.class); - typedQuery.setParameter("name", "Quarkus"); - final Person singleResult = typedQuery.getSingleResult(); - - if (!singleResult.getName().equals("Quarkus")) { - throw new RuntimeException("Wrong result from named JPA query"); - } - - transaction.commit(); - em.close(); - } + //Check that HQL fetch does not throw an exception + QuarkusTransaction.requiringNew() + .run(() -> em.createQuery("from Person p left join fetch p.address a").getResultList()); - private static void verifyListOfExistingPersons(final EntityManagerFactory emf) { - EntityManager em = emf.createEntityManager(); - EntityTransaction transaction = em.getTransaction(); - transaction.begin(); - listExistingPersons(em); - transaction.commit(); - em.close(); - } - - private static void storeTestPersons(final EntityManagerFactory emf) { - EntityManager em = emf.createEntityManager(); - EntityTransaction transaction = em.getTransaction(); - transaction.begin(); - persistNewPerson(em, "Gizmo"); - persistNewPerson(em, "Quarkus"); - persistNewPerson(em, "Hibernate ORM"); - transaction.commit(); - em.close(); - } + cleanUpData(); - private static void deleteAllPerson(final EntityManagerFactory emf) { - EntityManager em = emf.createEntityManager(); - EntityTransaction transaction = em.getTransaction(); - transaction.begin(); - em.createNativeQuery("Delete from Person").executeUpdate(); - transaction.commit(); - em.close(); + return "OK"; } - private static void listExistingPersons(EntityManager em) { - CriteriaBuilder cb = em.getCriteriaBuilder(); - - CriteriaQuery cq = cb.createQuery(Person.class); - Root from = cq.from(Person.class); - cq.select(from).orderBy(cb.asc(from.get("name"))); - TypedQuery q = em.createQuery(cq); - List allpersons = q.getResultList(); - if (allpersons.size() != 3) { - throw new RuntimeException("Incorrect number of results"); - } - if (!allpersons.get(0).getName().equals("Gizmo")) { - throw new RuntimeException("Incorrect order of results"); - } - StringBuilder sb = new StringBuilder("list of stored Person names:\n\t"); - for (Person p : allpersons) { - p.describeFully(sb); - } - sb.append("\nList complete.\n"); - System.out.print(sb); + private void cleanUpData() { + QuarkusTransaction.requiringNew() + .run(() -> em.createNativeQuery("Delete from Person").executeUpdate()); } - private static void persistNewPerson(EntityManager entityManager, String name) { + private void persistNewPerson(String name) { Person person = new Person(); person.setName(name); person.setAddress(new SequencedAddress("Street " + randomName())); - entityManager.persist(person); + em.persist(person); } private static String randomName() { return UUID.randomUUID().toString(); } - private void reportException(String errorMessage, final Exception e, final HttpServletResponse resp) throws IOException { - final PrintWriter writer = resp.getWriter(); - if (errorMessage != null) { - writer.write(errorMessage); - writer.write(" "); - } - writer.write(e.toString()); - writer.append("\n\t"); - e.printStackTrace(writer); - writer.append("\n\t"); - } - - private static void doAsUnit(EntityManagerFactory emf, Consumer f) { - final EntityManager em = emf.createEntityManager(); - try { - EntityTransaction transaction = em.getTransaction(); - try { - transaction.begin(); - f.accept(em); - transaction.commit(); - } catch (Exception e) { - if (transaction.isActive()) { - transaction.rollback(); - } - throw e; - } - } finally { - em.close(); - } - } - } diff --git a/integration-tests/jpa-h2/pom.xml b/integration-tests/jpa-h2/pom.xml index f42c3a87201f2..6964791967d95 100644 --- a/integration-tests/jpa-h2/pom.xml +++ b/integration-tests/jpa-h2/pom.xml @@ -26,7 +26,7 @@ io.quarkus - quarkus-undertow + quarkus-resteasy-reactive @@ -85,7 +85,7 @@ io.quarkus - quarkus-undertow-deployment + quarkus-resteasy-reactive-deployment ${project.version} pom test diff --git a/integration-tests/jpa-h2/src/main/java/io/quarkus/it/jpa/h2/DialectEndpoint.java b/integration-tests/jpa-h2/src/main/java/io/quarkus/it/jpa/h2/DialectEndpoint.java index 0a863c5f2f88a..5c74ab76c647e 100644 --- a/integration-tests/jpa-h2/src/main/java/io/quarkus/it/jpa/h2/DialectEndpoint.java +++ b/integration-tests/jpa-h2/src/main/java/io/quarkus/it/jpa/h2/DialectEndpoint.java @@ -1,44 +1,28 @@ package io.quarkus.it.jpa.h2; import java.io.IOException; -import java.io.PrintWriter; import jakarta.inject.Inject; -import jakarta.servlet.annotation.WebServlet; -import jakarta.servlet.http.HttpServlet; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; import org.hibernate.SessionFactory; import org.hibernate.engine.spi.SessionFactoryImplementor; import io.quarkus.hibernate.orm.runtime.config.DialectVersions; -@WebServlet(name = "DialectEndpoint", urlPatterns = "/dialect/version") -public class DialectEndpoint extends HttpServlet { +@Path("/dialect/version") +@Produces(MediaType.TEXT_PLAIN) +public class DialectEndpoint { @Inject SessionFactory sessionFactory; - @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { - try { - var version = sessionFactory.unwrap(SessionFactoryImplementor.class).getJdbcServices().getDialect().getVersion(); - resp.getWriter().write(DialectVersions.toString(version)); - } catch (Exception e) { - reportException("Failed to retrieve dialect version", e, resp); - } - } - - private void reportException(String errorMessage, final Exception e, final HttpServletResponse resp) throws IOException { - final PrintWriter writer = resp.getWriter(); - if (errorMessage != null) { - writer.write(errorMessage); - writer.write(" "); - } - writer.write(e.toString()); - writer.append("\n\t"); - e.printStackTrace(writer); - writer.append("\n\t"); + @GET + public String test() throws IOException { + var version = sessionFactory.unwrap(SessionFactoryImplementor.class).getJdbcServices().getDialect().getVersion(); + return DialectVersions.toString(version); } } diff --git a/integration-tests/jpa-h2/src/main/java/io/quarkus/it/jpa/h2/JPAFunctionalityTestEndpoint.java b/integration-tests/jpa-h2/src/main/java/io/quarkus/it/jpa/h2/JPAFunctionalityTestEndpoint.java index 2cda46b09649e..dc9eca79984ae 100644 --- a/integration-tests/jpa-h2/src/main/java/io/quarkus/it/jpa/h2/JPAFunctionalityTestEndpoint.java +++ b/integration-tests/jpa-h2/src/main/java/io/quarkus/it/jpa/h2/JPAFunctionalityTestEndpoint.java @@ -1,218 +1,102 @@ package io.quarkus.it.jpa.h2; import java.io.IOException; -import java.io.PrintWriter; import java.util.List; import java.util.UUID; -import java.util.function.Consumer; import jakarta.inject.Inject; import jakarta.persistence.EntityManager; -import jakarta.persistence.EntityManagerFactory; -import jakarta.persistence.EntityTransaction; import jakarta.persistence.TypedQuery; import jakarta.persistence.criteria.CriteriaBuilder; import jakarta.persistence.criteria.CriteriaQuery; import jakarta.persistence.criteria.Root; -import jakarta.servlet.annotation.WebServlet; -import jakarta.servlet.http.HttpServlet; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; -import org.wildfly.common.Assert; +import io.quarkus.narayana.jta.QuarkusTransaction; /** * Basic test running JPA with the H2 database. * The application can work in either standard JVM or in native mode, while we run H2 as a separate JVM process. */ -@WebServlet(name = "JPATestBootstrapEndpoint", urlPatterns = "/jpa-h2/testfunctionality") -public class JPAFunctionalityTestEndpoint extends HttpServlet { +@Path("/jpa-h2/testfunctionality") +@Produces(MediaType.TEXT_PLAIN) +public class JPAFunctionalityTestEndpoint { @Inject - EntityManagerFactory entityManagerFactory; - - @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { - try { - doStuffWithHibernate(entityManagerFactory); - } catch (Exception e) { - reportException("An error occurred while performing Hibernate operations", e, resp); - } - resp.getWriter().write("OK"); - } - - /** - * Lists the various operations we want to test for: - */ - private static void doStuffWithHibernate(EntityManagerFactory entityManagerFactory) { + EntityManager em; - //Cleanup any existing data: - deleteAllPerson(entityManagerFactory); + @GET + public String test() throws IOException { + cleanUpData(); //Store some well known Person instances we can then test on: - storeTestPersons(entityManagerFactory); + QuarkusTransaction.requiringNew().run(() -> { + persistNewPerson("Gizmo"); + persistNewPerson("Quarkus"); + persistNewPerson("Hibernate ORM"); + }); //Load all persons and run some checks on the query results: - verifyListOfExistingPersons(entityManagerFactory); + QuarkusTransaction.requiringNew().run(() -> { + CriteriaBuilder cb = em.getCriteriaBuilder(); + + CriteriaQuery cq = cb.createQuery(Person.class); + Root from = cq.from(Person.class); + cq.select(from).orderBy(cb.asc(from.get("name"))); + TypedQuery q = em.createQuery(cq); + List allpersons = q.getResultList(); + if (allpersons.size() != 3) { + throw new RuntimeException("Incorrect number of results"); + } + if (!allpersons.get(0).getName().equals("Gizmo")) { + throw new RuntimeException("Incorrect order of results"); + } + StringBuilder sb = new StringBuilder("list of stored Person names:\n\t"); + for (Person p : allpersons) { + p.describeFully(sb); + } + sb.append("\nList complete.\n"); + System.out.print(sb); + }); //Try a JPA named query: - verifyJPANamedQuery(entityManagerFactory); - - verifyHqlFetch(entityManagerFactory); - - deleteAllPerson(entityManagerFactory); - - //Test capability to load enhanced proxies: - verifyEnhancedProxies(entityManagerFactory); - - } - - private static void verifyEnhancedProxies(EntityManagerFactory emf) { - //Define the test data: - CompanyCustomer company = new CompanyCustomer(); - company.companyname = "Quarked consulting, inc."; - Project project = new Project(); - project.name = "Hibernate RX"; - project.customer = company; - - //Store the test model: - doAsUnit(emf, em -> em.persist(project)); - final Integer testId = project.id; - Assert.assertNotNull(testId); - - //Now try to load it, should trigger the use of enhanced proxies: - doAsUnit(emf, em -> em.find(Project.class, testId)); - } - - private static void verifyHqlFetch(EntityManagerFactory emf) { - EntityManager em = emf.createEntityManager(); - try { - EntityTransaction transaction = em.getTransaction(); - try { - transaction.begin(); - - em.createQuery("from Person p left join fetch p.address a").getResultList(); - - transaction.commit(); - } catch (Exception e) { - if (transaction.isActive()) { - transaction.rollback(); - } - throw e; + QuarkusTransaction.requiringNew().run(() -> { + TypedQuery typedQuery = em.createNamedQuery( + "get_person_by_name", Person.class); + typedQuery.setParameter("name", "Quarkus"); + final Person singleResult = typedQuery.getSingleResult(); + + if (!singleResult.getName().equals("Quarkus")) { + throw new RuntimeException("Wrong result from named JPA query"); } - } finally { - em.close(); - } - } - - private static void verifyJPANamedQuery(final EntityManagerFactory emf) { - EntityManager em = emf.createEntityManager(); - EntityTransaction transaction = em.getTransaction(); - transaction.begin(); - TypedQuery typedQuery = em.createNamedQuery( - "get_person_by_name", Person.class); - typedQuery.setParameter("name", "Quarkus"); - final Person singleResult = typedQuery.getSingleResult(); - - if (!singleResult.getName().equals("Quarkus")) { - throw new RuntimeException("Wrong result from named JPA query"); - } - - transaction.commit(); - em.close(); - } + }); - private static void verifyListOfExistingPersons(final EntityManagerFactory emf) { - EntityManager em = emf.createEntityManager(); - EntityTransaction transaction = em.getTransaction(); - transaction.begin(); - listExistingPersons(em); - transaction.commit(); - em.close(); - } + //Check that HQL fetch does not throw an exception + QuarkusTransaction.requiringNew() + .run(() -> em.createQuery("from Person p left join fetch p.address a").getResultList()); - private static void storeTestPersons(final EntityManagerFactory emf) { - EntityManager em = emf.createEntityManager(); - EntityTransaction transaction = em.getTransaction(); - transaction.begin(); - persistNewPerson(em, "Gizmo"); - persistNewPerson(em, "Quarkus"); - persistNewPerson(em, "Hibernate ORM"); - transaction.commit(); - em.close(); - } + cleanUpData(); - private static void deleteAllPerson(final EntityManagerFactory emf) { - EntityManager em = emf.createEntityManager(); - EntityTransaction transaction = em.getTransaction(); - transaction.begin(); - em.createNativeQuery("Delete from Person").executeUpdate(); - transaction.commit(); - em.close(); + return "OK"; } - private static void listExistingPersons(EntityManager em) { - CriteriaBuilder cb = em.getCriteriaBuilder(); - - CriteriaQuery cq = cb.createQuery(Person.class); - Root from = cq.from(Person.class); - cq.select(from).orderBy(cb.asc(from.get("name"))); - TypedQuery q = em.createQuery(cq); - List allpersons = q.getResultList(); - if (allpersons.size() != 3) { - throw new RuntimeException("Incorrect number of results"); - } - if (!allpersons.get(0).getName().equals("Gizmo")) { - throw new RuntimeException("Incorrect order of results"); - } - StringBuilder sb = new StringBuilder("list of stored Person names:\n\t"); - for (Person p : allpersons) { - p.describeFully(sb); - } - sb.append("\nList complete.\n"); - System.out.print(sb); + private void cleanUpData() { + QuarkusTransaction.requiringNew() + .run(() -> em.createNativeQuery("Delete from Person").executeUpdate()); } - private static void persistNewPerson(EntityManager entityManager, String name) { + private void persistNewPerson(String name) { Person person = new Person(); person.setName(name); person.setAddress(new SequencedAddress("Street " + randomName())); - entityManager.persist(person); + em.persist(person); } private static String randomName() { return UUID.randomUUID().toString(); } - private void reportException(String errorMessage, final Exception e, final HttpServletResponse resp) throws IOException { - final PrintWriter writer = resp.getWriter(); - if (errorMessage != null) { - writer.write(errorMessage); - writer.write(" "); - } - writer.write(e.toString()); - writer.append("\n\t"); - e.printStackTrace(writer); - writer.append("\n\t"); - } - - private static void doAsUnit(EntityManagerFactory emf, Consumer f) { - final EntityManager em = emf.createEntityManager(); - try { - EntityTransaction transaction = em.getTransaction(); - try { - transaction.begin(); - f.accept(em); - transaction.commit(); - } catch (Exception e) { - if (transaction.isActive()) { - transaction.rollback(); - } - throw e; - } - } finally { - em.close(); - } - } - } diff --git a/integration-tests/jpa-h2/src/main/java/io/quarkus/it/jpa/h2/basicproxy/BasicProxyTestEndpoint.java b/integration-tests/jpa-h2/src/main/java/io/quarkus/it/jpa/h2/basicproxy/BasicProxyTestEndpoint.java deleted file mode 100644 index 47a866369e6d5..0000000000000 --- a/integration-tests/jpa-h2/src/main/java/io/quarkus/it/jpa/h2/basicproxy/BasicProxyTestEndpoint.java +++ /dev/null @@ -1,41 +0,0 @@ -package io.quarkus.it.jpa.h2.basicproxy; - -import java.io.IOException; -import java.util.List; - -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.enterprise.event.Observes; -import jakarta.inject.Inject; -import jakarta.persistence.EntityManager; -import jakarta.servlet.annotation.WebServlet; -import jakarta.servlet.http.HttpServlet; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import jakarta.transaction.Transactional; - -import io.quarkus.runtime.StartupEvent; - -@ApplicationScoped -@WebServlet(urlPatterns = "/jpa-h2/testbasicproxy") -public class BasicProxyTestEndpoint extends HttpServlet { - - @Inject - EntityManager entityManager; - - @Transactional - public void setup(@Observes StartupEvent startupEvent) { - ConcreteEntity entity = new ConcreteEntity(); - entity.id = "1"; - entity.type = "Concrete"; - entityManager.persist(entity); - } - - @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { - final List list = entityManager.createQuery("from ConcreteEntity").getResultList(); - if (list.size() != 1) { - throw new RuntimeException("Expected 1 result, got " + list.size()); - } - resp.getWriter().write("OK"); - } -} diff --git a/integration-tests/jpa-h2/src/main/resources/application.properties b/integration-tests/jpa-h2/src/main/resources/application.properties index 889612ceae6cd..45d910294f7ed 100644 --- a/integration-tests/jpa-h2/src/main/resources/application.properties +++ b/integration-tests/jpa-h2/src/main/resources/application.properties @@ -1,5 +1,4 @@ quarkus.datasource.jdbc.max-size=8 quarkus.hibernate-orm.database.generation=drop-and-create -dummy.transaction.timeout=30 transaction.timeout.1s=1 diff --git a/integration-tests/jpa-h2/src/test/java/io/quarkus/it/jpa/h2/JPAFunctionalityTest.java b/integration-tests/jpa-h2/src/test/java/io/quarkus/it/jpa/h2/JPAFunctionalityTest.java index 986926d4ae2a1..a13d79e2f7024 100644 --- a/integration-tests/jpa-h2/src/test/java/io/quarkus/it/jpa/h2/JPAFunctionalityTest.java +++ b/integration-tests/jpa-h2/src/test/java/io/quarkus/it/jpa/h2/JPAFunctionalityTest.java @@ -20,14 +20,4 @@ public void testJPAFunctionalityFromServlet() throws Exception { RestAssured.when().get("/jpa-h2/testfunctionality").then().body(is("OK")); } - @Test - public void testHibernateEnhancedProxies() throws Exception { - RestAssured.when().get("/jpa-h2/testproxy").then().body(is("OK")); - } - - @Test - public void testHibernateEnhancedBasicProxies() throws Exception { - RestAssured.when().get("/jpa-h2/testbasicproxy").then().body(is("OK")); - } - } diff --git a/integration-tests/jpa-mapping-xml/legacy-app/pom.xml b/integration-tests/jpa-mapping-xml/legacy-app/pom.xml index 265293dbdb9ce..aa410b3d79cc4 100644 --- a/integration-tests/jpa-mapping-xml/legacy-app/pom.xml +++ b/integration-tests/jpa-mapping-xml/legacy-app/pom.xml @@ -16,7 +16,7 @@ io.quarkus - quarkus-resteasy + quarkus-resteasy-reactive io.quarkus @@ -93,7 +93,7 @@ io.quarkus - quarkus-resteasy-deployment + quarkus-resteasy-reactive-deployment ${project.version} pom test diff --git a/integration-tests/jpa-mapping-xml/modern-app/pom.xml b/integration-tests/jpa-mapping-xml/modern-app/pom.xml index 5c0efa36ec3c6..2882e803599fa 100644 --- a/integration-tests/jpa-mapping-xml/modern-app/pom.xml +++ b/integration-tests/jpa-mapping-xml/modern-app/pom.xml @@ -17,7 +17,7 @@ io.quarkus - quarkus-resteasy + quarkus-resteasy-reactive io.quarkus @@ -94,7 +94,7 @@ io.quarkus - quarkus-resteasy-deployment + quarkus-resteasy-reactive-deployment ${project.version} pom test diff --git a/integration-tests/jpa-mariadb/pom.xml b/integration-tests/jpa-mariadb/pom.xml index 90637bb628b28..47d0357dff2c7 100644 --- a/integration-tests/jpa-mariadb/pom.xml +++ b/integration-tests/jpa-mariadb/pom.xml @@ -20,15 +20,15 @@ io.quarkus - quarkus-undertow + quarkus-hibernate-orm io.quarkus - quarkus-hibernate-orm + quarkus-jdbc-mariadb io.quarkus - quarkus-jdbc-mariadb + quarkus-resteasy-reactive @@ -82,7 +82,7 @@ io.quarkus - quarkus-undertow-deployment + quarkus-resteasy-reactive-deployment ${project.version} pom test diff --git a/integration-tests/jpa-mariadb/src/main/java/io/quarkus/it/jpa/mariadb/DialectEndpoint.java b/integration-tests/jpa-mariadb/src/main/java/io/quarkus/it/jpa/mariadb/DialectEndpoint.java index 1fec613561c17..32ddb042c862f 100644 --- a/integration-tests/jpa-mariadb/src/main/java/io/quarkus/it/jpa/mariadb/DialectEndpoint.java +++ b/integration-tests/jpa-mariadb/src/main/java/io/quarkus/it/jpa/mariadb/DialectEndpoint.java @@ -1,44 +1,28 @@ package io.quarkus.it.jpa.mariadb; import java.io.IOException; -import java.io.PrintWriter; import jakarta.inject.Inject; -import jakarta.servlet.annotation.WebServlet; -import jakarta.servlet.http.HttpServlet; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; import org.hibernate.SessionFactory; import org.hibernate.engine.spi.SessionFactoryImplementor; import io.quarkus.hibernate.orm.runtime.config.DialectVersions; -@WebServlet(name = "DialectEndpoint", urlPatterns = "/dialect/version") -public class DialectEndpoint extends HttpServlet { +@Path("/dialect/version") +@Produces(MediaType.TEXT_PLAIN) +public class DialectEndpoint { @Inject SessionFactory sessionFactory; - @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { - try { - var version = sessionFactory.unwrap(SessionFactoryImplementor.class).getJdbcServices().getDialect().getVersion(); - resp.getWriter().write(DialectVersions.toString(version)); - } catch (Exception e) { - reportException("Failed to retrieve dialect version", e, resp); - } - } - - private void reportException(String errorMessage, final Exception e, final HttpServletResponse resp) throws IOException { - final PrintWriter writer = resp.getWriter(); - if (errorMessage != null) { - writer.write(errorMessage); - writer.write(" "); - } - writer.write(e.toString()); - writer.append("\n\t"); - e.printStackTrace(writer); - writer.append("\n\t"); + @GET + public String test() throws IOException { + var version = sessionFactory.unwrap(SessionFactoryImplementor.class).getJdbcServices().getDialect().getVersion(); + return DialectVersions.toString(version); } } diff --git a/integration-tests/jpa-mariadb/src/main/java/io/quarkus/it/jpa/mariadb/JPAFunctionalityTestEndpoint.java b/integration-tests/jpa-mariadb/src/main/java/io/quarkus/it/jpa/mariadb/JPAFunctionalityTestEndpoint.java index acbaef9bc9455..07a33f33fe9c3 100644 --- a/integration-tests/jpa-mariadb/src/main/java/io/quarkus/it/jpa/mariadb/JPAFunctionalityTestEndpoint.java +++ b/integration-tests/jpa-mariadb/src/main/java/io/quarkus/it/jpa/mariadb/JPAFunctionalityTestEndpoint.java @@ -1,7 +1,6 @@ package io.quarkus.it.jpa.mariadb; import java.io.IOException; -import java.io.PrintWriter; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; @@ -12,167 +11,109 @@ import jakarta.inject.Inject; import jakarta.persistence.EntityManager; -import jakarta.persistence.EntityManagerFactory; -import jakarta.persistence.EntityTransaction; import jakarta.persistence.TypedQuery; import jakarta.persistence.criteria.CriteriaBuilder; import jakarta.persistence.criteria.CriteriaQuery; import jakarta.persistence.criteria.Root; -import jakarta.servlet.annotation.WebServlet; -import jakarta.servlet.http.HttpServlet; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +import io.quarkus.narayana.jta.QuarkusTransaction; /** * Basic test running JPA with the MariaDB database. * The application can work in either standard JVM or in native mode. */ -@WebServlet(name = "JPATestBootstrapEndpoint", urlPatterns = "/jpa-mariadb/testfunctionality") -public class JPAFunctionalityTestEndpoint extends HttpServlet { +@Path("/jpa-mariadb/testfunctionality") +@Produces(MediaType.TEXT_PLAIN) +public class JPAFunctionalityTestEndpoint { @Inject - EntityManagerFactory entityManagerFactory; + EntityManager em; @Inject DataSource ds; - @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { - try { - doStuffWithHibernate(entityManagerFactory, ds); - } catch (Exception e) { - reportException("An error occurred while performing Hibernate operations", e, resp); - } - resp.getWriter().write("OK"); - } - - /** - * Lists the various operations we want to test for: - */ - private static void doStuffWithHibernate(EntityManagerFactory entityManagerFactory, DataSource ds) throws SQLException { - - //Cleanup any existing data: - deleteAllPerson(entityManagerFactory); + @GET + public String test() throws IOException, SQLException { + cleanUpData(); //Store some well known Person instances we can then test on: - storeTestPersons(entityManagerFactory); + QuarkusTransaction.requiringNew().run(() -> { + persistNewPerson("Gizmo"); + persistNewPerson("Quarkus"); + persistNewPerson("Hibernate ORM"); + }); //Load all persons and run some checks on the query results: - verifyListOfExistingPersons(entityManagerFactory); + QuarkusTransaction.requiringNew().run(() -> { + CriteriaBuilder cb = em.getCriteriaBuilder(); + + CriteriaQuery cq = cb.createQuery(Person.class); + Root from = cq.from(Person.class); + cq.select(from).orderBy(cb.asc(from.get("name"))); + TypedQuery q = em.createQuery(cq); + List allpersons = q.getResultList(); + if (allpersons.size() != 3) { + throw new RuntimeException("Incorrect number of results"); + } + if (!allpersons.get(0).getName().equals("Gizmo")) { + throw new RuntimeException("Incorrect order of results"); + } + StringBuilder sb = new StringBuilder("list of stored Person names:\n\t"); + for (Person p : allpersons) { + p.describeFully(sb); + } + sb.append("\nList complete.\n"); + System.out.print(sb); + }); //Try to use prepared statement with setObject: - verifyGetPersonUsingSetObject(ds); - - //Try a JPA named query: - verifyJPANamedQuery(entityManagerFactory); - - deleteAllPerson(entityManagerFactory); - - } + try (PreparedStatement ps = ds.getConnection().prepareStatement("select * from Person as p where p.name = ?")) { + ps.setObject(1, "Quarkus"); + final ResultSet resultSet = ps.executeQuery(); - private static void verifyJPANamedQuery(final EntityManagerFactory emf) { - EntityManager em = emf.createEntityManager(); - EntityTransaction transaction = em.getTransaction(); - transaction.begin(); - TypedQuery typedQuery = em.createNamedQuery( - "get_person_by_name", Person.class); - typedQuery.setParameter("name", "Quarkus"); - final Person singleResult = typedQuery.getSingleResult(); - - if (!singleResult.getName().equals("Quarkus")) { - throw new RuntimeException("Wrong result from named JPA query"); + if (!resultSet.next()) { + throw new RuntimeException("Person Quarkus doesn't exist when it should"); + } } - transaction.commit(); - em.close(); - } + //Try a JPA named query: + QuarkusTransaction.requiringNew().run(() -> { + TypedQuery typedQuery = em.createNamedQuery( + "get_person_by_name", Person.class); + typedQuery.setParameter("name", "Quarkus"); + final Person singleResult = typedQuery.getSingleResult(); + + if (!singleResult.getName().equals("Quarkus")) { + throw new RuntimeException("Wrong result from named JPA query"); + } + }); - private static void verifyListOfExistingPersons(final EntityManagerFactory emf) { - EntityManager em = emf.createEntityManager(); - EntityTransaction transaction = em.getTransaction(); - transaction.begin(); - listExistingPersons(em); - transaction.commit(); - em.close(); - } + //Check that HQL fetch does not throw an exception + QuarkusTransaction.requiringNew() + .run(() -> em.createQuery("from Person p left join fetch p.address a").getResultList()); - private static void storeTestPersons(final EntityManagerFactory emf) { - EntityManager em = emf.createEntityManager(); - EntityTransaction transaction = em.getTransaction(); - transaction.begin(); - persistNewPerson(em, "Gizmo"); - persistNewPerson(em, "Quarkus"); - persistNewPerson(em, "Hibernate ORM"); - transaction.commit(); - em.close(); - } + cleanUpData(); - private static void deleteAllPerson(final EntityManagerFactory emf) { - EntityManager em = emf.createEntityManager(); - EntityTransaction transaction = em.getTransaction(); - transaction.begin(); - em.createNativeQuery("Delete from Person").executeUpdate(); - transaction.commit(); - em.close(); + return "OK"; } - private static void listExistingPersons(EntityManager em) { - CriteriaBuilder cb = em.getCriteriaBuilder(); - - CriteriaQuery cq = cb.createQuery(Person.class); - Root from = cq.from(Person.class); - cq.select(from).orderBy(cb.asc(from.get("name"))); - TypedQuery q = em.createQuery(cq); - List allpersons = q.getResultList(); - if (allpersons.size() != 3) { - throw new RuntimeException("Incorrect number of results"); - } - if (!allpersons.get(0).getName().equals("Gizmo")) { - throw new RuntimeException("Incorrect order of results"); - } - StringBuilder sb = new StringBuilder("list of stored Person names:\n\t"); - for (Person p : allpersons) { - p.describeFully(sb); - } - sb.append("\nList complete.\n"); - System.out.print(sb); + private void cleanUpData() { + QuarkusTransaction.requiringNew() + .run(() -> em.createNativeQuery("Delete from Person").executeUpdate()); } - private static void persistNewPerson(EntityManager entityManager, String name) { + private void persistNewPerson(String name) { Person person = new Person(); person.setName(name); person.setAddress(new Address("Street " + randomName())); - entityManager.persist(person); + em.persist(person); } private static String randomName() { return UUID.randomUUID().toString(); } - - private void reportException(String errorMessage, final Exception e, final HttpServletResponse resp) throws IOException { - final PrintWriter writer = resp.getWriter(); - if (errorMessage != null) { - writer.write(errorMessage); - writer.write(" "); - } - writer.write(e.toString()); - writer.append("\n\t"); - e.printStackTrace(writer); - writer.append("\n\t"); - } - - private static void verifyGetPersonUsingSetObject(final DataSource ds) throws SQLException { - getExistingPersonUsingSetObject(ds); - } - - private static void getExistingPersonUsingSetObject(DataSource ds) throws SQLException { - try (PreparedStatement ps = ds.getConnection().prepareStatement("select * from Person as p where p.name = ?")) { - ps.setObject(1, "Quarkus"); - final ResultSet resultSet = ps.executeQuery(); - - if (!resultSet.next()) { - throw new RuntimeException("Person Quarkus doesn't exist when it should"); - } - } - } } diff --git a/integration-tests/jpa-mssql/pom.xml b/integration-tests/jpa-mssql/pom.xml index 6fd97cf2bc75c..69fe76e3b95c9 100644 --- a/integration-tests/jpa-mssql/pom.xml +++ b/integration-tests/jpa-mssql/pom.xml @@ -33,7 +33,7 @@ io.quarkus - quarkus-undertow + quarkus-resteasy-reactive @@ -87,7 +87,7 @@ io.quarkus - quarkus-undertow-deployment + quarkus-resteasy-reactive-deployment ${project.version} pom test diff --git a/integration-tests/jpa-mssql/src/main/java/io/quarkus/it/jpa/mssql/DialectEndpoint.java b/integration-tests/jpa-mssql/src/main/java/io/quarkus/it/jpa/mssql/DialectEndpoint.java index 984757375f66b..b1c0029ca61f4 100644 --- a/integration-tests/jpa-mssql/src/main/java/io/quarkus/it/jpa/mssql/DialectEndpoint.java +++ b/integration-tests/jpa-mssql/src/main/java/io/quarkus/it/jpa/mssql/DialectEndpoint.java @@ -1,44 +1,28 @@ package io.quarkus.it.jpa.mssql; import java.io.IOException; -import java.io.PrintWriter; import jakarta.inject.Inject; -import jakarta.servlet.annotation.WebServlet; -import jakarta.servlet.http.HttpServlet; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; import org.hibernate.SessionFactory; import org.hibernate.engine.spi.SessionFactoryImplementor; import io.quarkus.hibernate.orm.runtime.config.DialectVersions; -@WebServlet(name = "DialectEndpoint", urlPatterns = "/dialect/version") -public class DialectEndpoint extends HttpServlet { +@Path("/dialect/version") +@Produces(MediaType.TEXT_PLAIN) +public class DialectEndpoint { @Inject SessionFactory sessionFactory; - @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { - try { - var version = sessionFactory.unwrap(SessionFactoryImplementor.class).getJdbcServices().getDialect().getVersion(); - resp.getWriter().write(DialectVersions.toString(version)); - } catch (Exception e) { - reportException("Failed to retrieve dialect version", e, resp); - } - } - - private void reportException(String errorMessage, final Exception e, final HttpServletResponse resp) throws IOException { - final PrintWriter writer = resp.getWriter(); - if (errorMessage != null) { - writer.write(errorMessage); - writer.write(" "); - } - writer.write(e.toString()); - writer.append("\n\t"); - e.printStackTrace(writer); - writer.append("\n\t"); + @GET + public String test() throws IOException { + var version = sessionFactory.unwrap(SessionFactoryImplementor.class).getJdbcServices().getDialect().getVersion(); + return DialectVersions.toString(version); } } diff --git a/integration-tests/jpa-mssql/src/main/java/io/quarkus/it/jpa/mssql/JPAFunctionalityTestEndpoint.java b/integration-tests/jpa-mssql/src/main/java/io/quarkus/it/jpa/mssql/JPAFunctionalityTestEndpoint.java index 97742a9c07f6d..dd6cdcc5040d0 100644 --- a/integration-tests/jpa-mssql/src/main/java/io/quarkus/it/jpa/mssql/JPAFunctionalityTestEndpoint.java +++ b/integration-tests/jpa-mssql/src/main/java/io/quarkus/it/jpa/mssql/JPAFunctionalityTestEndpoint.java @@ -1,183 +1,106 @@ package io.quarkus.it.jpa.mssql; import java.io.IOException; -import java.io.PrintWriter; import java.nio.charset.Charset; import java.util.List; import java.util.UUID; import jakarta.inject.Inject; import jakarta.persistence.EntityManager; -import jakarta.persistence.EntityManagerFactory; -import jakarta.persistence.EntityTransaction; import jakarta.persistence.TypedQuery; import jakarta.persistence.criteria.CriteriaBuilder; import jakarta.persistence.criteria.CriteriaQuery; import jakarta.persistence.criteria.Root; -import jakarta.servlet.annotation.WebServlet; -import jakarta.servlet.http.HttpServlet; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +import io.quarkus.narayana.jta.QuarkusTransaction; /** * Basic test running JPA with the MS SQL database. * The application can work in either standard JVM or in native mode. */ -@WebServlet(name = "JPATestBootstrapEndpoint", urlPatterns = "/jpa-mssql/testfunctionality") -public class JPAFunctionalityTestEndpoint extends HttpServlet { +@Path("/jpa-mssql/testfunctionality") +@Produces(MediaType.TEXT_PLAIN) +public class JPAFunctionalityTestEndpoint { @Inject - EntityManagerFactory entityManagerFactory; - - @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { - try { - checkCharacterSets(); - doStuffWithHibernate(entityManagerFactory); - } catch (Exception e) { - reportException("An error occurred while performing Hibernate operations", e, resp); - } - resp.getWriter().write("OK"); - } + EntityManager em; - private void checkCharacterSets() { + @GET + public String test() throws IOException { if (!Charset.isSupported("Cp1252")) throw new IllegalStateException("You will very likely need support for Codepage Cp1252 to connect to SQL Server"); - } - /** - * Lists the various operations we want to test for: - */ - private static void doStuffWithHibernate(EntityManagerFactory entityManagerFactory) { - - //Cleanup any existing data: - deleteAllPerson(entityManagerFactory); + cleanUpData(); //Store some well known Person instances we can then test on: - storeTestPersons(entityManagerFactory); + QuarkusTransaction.requiringNew().run(() -> { + persistNewPerson("Gizmo"); + persistNewPerson("Quarkus"); + persistNewPerson("Hibernate ORM"); + }); //Load all persons and run some checks on the query results: - verifyListOfExistingPersons(entityManagerFactory); + QuarkusTransaction.requiringNew().run(() -> { + CriteriaBuilder cb = em.getCriteriaBuilder(); + + CriteriaQuery cq = cb.createQuery(Person.class); + Root from = cq.from(Person.class); + cq.select(from).orderBy(cb.asc(from.get("name"))); + TypedQuery q = em.createQuery(cq); + List allpersons = q.getResultList(); + if (allpersons.size() != 3) { + throw new RuntimeException("Incorrect number of results"); + } + if (!allpersons.get(0).getName().equals("Gizmo")) { + throw new RuntimeException("Incorrect order of results"); + } + StringBuilder sb = new StringBuilder("list of stored Person names:\n\t"); + for (Person p : allpersons) { + p.describeFully(sb); + } + sb.append("\nList complete.\n"); + System.out.print(sb); + }); //Try a JPA named query: - verifyJPANamedQuery(entityManagerFactory); - - verifyHqlFetch(entityManagerFactory); - - deleteAllPerson(entityManagerFactory); - - } - - private static void verifyHqlFetch(EntityManagerFactory emf) { - EntityManager em = emf.createEntityManager(); - try { - EntityTransaction transaction = em.getTransaction(); - try { - transaction.begin(); - - em.createQuery("from Person p left join fetch p.address a").getResultList(); - - transaction.commit(); - } catch (Exception e) { - if (transaction.isActive()) { - transaction.rollback(); - } - throw e; + QuarkusTransaction.requiringNew().run(() -> { + TypedQuery typedQuery = em.createNamedQuery( + "get_person_by_name", Person.class); + typedQuery.setParameter("name", "Quarkus"); + final Person singleResult = typedQuery.getSingleResult(); + + if (!singleResult.getName().equals("Quarkus")) { + throw new RuntimeException("Wrong result from named JPA query"); } - } finally { - em.close(); - } - } + }); - private static void verifyJPANamedQuery(final EntityManagerFactory emf) { - EntityManager em = emf.createEntityManager(); - EntityTransaction transaction = em.getTransaction(); - transaction.begin(); - TypedQuery typedQuery = em.createNamedQuery( - "get_person_by_name", Person.class); - typedQuery.setParameter("name", "Quarkus"); - final Person singleResult = typedQuery.getSingleResult(); - - if (!singleResult.getName().equals("Quarkus")) { - throw new RuntimeException("Wrong result from named JPA query"); - } - - transaction.commit(); - em.close(); - } + //Check that HQL fetch does not throw an exception + QuarkusTransaction.requiringNew() + .run(() -> em.createQuery("from Person p left join fetch p.address a").getResultList()); - private static void verifyListOfExistingPersons(final EntityManagerFactory emf) { - EntityManager em = emf.createEntityManager(); - EntityTransaction transaction = em.getTransaction(); - transaction.begin(); - listExistingPersons(em); - transaction.commit(); - em.close(); - } + cleanUpData(); - private static void storeTestPersons(final EntityManagerFactory emf) { - EntityManager em = emf.createEntityManager(); - EntityTransaction transaction = em.getTransaction(); - transaction.begin(); - persistNewPerson(em, "Gizmo"); - persistNewPerson(em, "Quarkus"); - persistNewPerson(em, "Hibernate ORM"); - transaction.commit(); - em.close(); + return "OK"; } - private static void deleteAllPerson(final EntityManagerFactory emf) { - EntityManager em = emf.createEntityManager(); - EntityTransaction transaction = em.getTransaction(); - transaction.begin(); - em.createNativeQuery("Delete from Person").executeUpdate(); - transaction.commit(); - em.close(); + private void cleanUpData() { + QuarkusTransaction.requiringNew() + .run(() -> em.createNativeQuery("Delete from Person").executeUpdate()); } - private static void listExistingPersons(EntityManager em) { - CriteriaBuilder cb = em.getCriteriaBuilder(); - - CriteriaQuery cq = cb.createQuery(Person.class); - Root from = cq.from(Person.class); - cq.select(from).orderBy(cb.asc(from.get("name"))); - TypedQuery q = em.createQuery(cq); - List allpersons = q.getResultList(); - if (allpersons.size() != 3) { - throw new RuntimeException("Incorrect number of results"); - } - if (!allpersons.get(0).getName().equals("Gizmo")) { - throw new RuntimeException("Incorrect order of results"); - } - StringBuilder sb = new StringBuilder("list of stored Person names:\n\t"); - for (Person p : allpersons) { - p.describeFully(sb); - } - sb.append("\nList complete.\n"); - System.out.print(sb); - } - - private static void persistNewPerson(EntityManager entityManager, String name) { + private void persistNewPerson(String name) { Person person = new Person(); person.setName(name); person.setAddress(new SequencedAddress("Street " + randomName())); - entityManager.persist(person); + em.persist(person); } private static String randomName() { return UUID.randomUUID().toString(); } - private void reportException(String errorMessage, final Exception e, final HttpServletResponse resp) throws IOException { - final PrintWriter writer = resp.getWriter(); - if (errorMessage != null) { - writer.write(errorMessage); - writer.write(" "); - } - writer.write(e.toString()); - writer.append("\n\t"); - e.printStackTrace(writer); - writer.append("\n\t"); - } - } diff --git a/integration-tests/jpa-mysql/pom.xml b/integration-tests/jpa-mysql/pom.xml index af53db1d55b3a..b53b57ce3f8d9 100644 --- a/integration-tests/jpa-mysql/pom.xml +++ b/integration-tests/jpa-mysql/pom.xml @@ -20,15 +20,15 @@ io.quarkus - quarkus-undertow + quarkus-hibernate-orm io.quarkus - quarkus-hibernate-orm + quarkus-jdbc-mysql io.quarkus - quarkus-jdbc-mysql + quarkus-resteasy-reactive @@ -82,7 +82,7 @@ io.quarkus - quarkus-undertow-deployment + quarkus-resteasy-reactive-deployment ${project.version} pom test diff --git a/integration-tests/jpa-mysql/src/main/java/io/quarkus/it/jpa/mysql/DialectEndpoint.java b/integration-tests/jpa-mysql/src/main/java/io/quarkus/it/jpa/mysql/DialectEndpoint.java deleted file mode 100644 index 9ea5556394181..0000000000000 --- a/integration-tests/jpa-mysql/src/main/java/io/quarkus/it/jpa/mysql/DialectEndpoint.java +++ /dev/null @@ -1,44 +0,0 @@ -package io.quarkus.it.jpa.mysql; - -import java.io.IOException; -import java.io.PrintWriter; - -import jakarta.inject.Inject; -import jakarta.servlet.annotation.WebServlet; -import jakarta.servlet.http.HttpServlet; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; - -import org.hibernate.SessionFactory; -import org.hibernate.engine.spi.SessionFactoryImplementor; - -import io.quarkus.hibernate.orm.runtime.config.DialectVersions; - -@WebServlet(name = "DialectEndpoint", urlPatterns = "/dialect/version") -public class DialectEndpoint extends HttpServlet { - @Inject - SessionFactory sessionFactory; - - @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { - try { - var version = sessionFactory.unwrap(SessionFactoryImplementor.class).getJdbcServices().getDialect().getVersion(); - resp.getWriter().write(DialectVersions.toString(version)); - } catch (Exception e) { - reportException("Failed to retrieve dialect version", e, resp); - } - } - - private void reportException(String errorMessage, final Exception e, final HttpServletResponse resp) throws IOException { - final PrintWriter writer = resp.getWriter(); - if (errorMessage != null) { - writer.write(errorMessage); - writer.write(" "); - } - writer.write(e.toString()); - writer.append("\n\t"); - e.printStackTrace(writer); - writer.append("\n\t"); - } - -} diff --git a/integration-tests/jpa-mysql/src/main/java/io/quarkus/it/jpa/mysql/JPAFunctionalityTestEndpoint.java b/integration-tests/jpa-mysql/src/main/java/io/quarkus/it/jpa/mysql/JPAFunctionalityTestEndpoint.java index 6fa74b7cd7a6c..0c1b74321aca1 100644 --- a/integration-tests/jpa-mysql/src/main/java/io/quarkus/it/jpa/mysql/JPAFunctionalityTestEndpoint.java +++ b/integration-tests/jpa-mysql/src/main/java/io/quarkus/it/jpa/mysql/JPAFunctionalityTestEndpoint.java @@ -1,153 +1,98 @@ package io.quarkus.it.jpa.mysql; import java.io.IOException; -import java.io.PrintWriter; import java.util.List; import java.util.UUID; import jakarta.inject.Inject; import jakarta.persistence.EntityManager; -import jakarta.persistence.EntityManagerFactory; -import jakarta.persistence.EntityTransaction; import jakarta.persistence.TypedQuery; import jakarta.persistence.criteria.CriteriaBuilder; import jakarta.persistence.criteria.CriteriaQuery; import jakarta.persistence.criteria.Root; -import jakarta.servlet.annotation.WebServlet; -import jakarta.servlet.http.HttpServlet; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +import io.quarkus.narayana.jta.QuarkusTransaction; /** * Basic test running JPA with the MySQL database. * The application can work in either standard JVM or in native mode. */ -@WebServlet(name = "JPATestBootstrapEndpoint", urlPatterns = "/jpa-mysql/testfunctionality") -public class JPAFunctionalityTestEndpoint extends HttpServlet { +@Path("/jpa-mysql/testfunctionality") +@Produces(MediaType.TEXT_PLAIN) +public class JPAFunctionalityTestEndpoint { @Inject - EntityManagerFactory entityManagerFactory; - - @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { - try { - doStuffWithHibernate(entityManagerFactory); - } catch (Exception e) { - reportException("An error occurred while performing Hibernate operations", e, resp); - } - resp.getWriter().write("OK"); - } - - /** - * Lists the various operations we want to test for: - */ - private static void doStuffWithHibernate(EntityManagerFactory entityManagerFactory) { + EntityManager em; - //Cleanup any existing data: - deleteAllPerson(entityManagerFactory); + @GET + public String test() throws IOException { + cleanUpData(); //Store some well known Person instances we can then test on: - storeTestPersons(entityManagerFactory); + QuarkusTransaction.requiringNew().run(() -> { + persistNewPerson("Gizmo"); + persistNewPerson("Quarkus"); + persistNewPerson("Hibernate ORM"); + }); //Load all persons and run some checks on the query results: - verifyListOfExistingPersons(entityManagerFactory); + QuarkusTransaction.requiringNew().run(() -> { + CriteriaBuilder cb = em.getCriteriaBuilder(); + + CriteriaQuery cq = cb.createQuery(Person.class); + Root from = cq.from(Person.class); + cq.select(from).orderBy(cb.asc(from.get("name"))); + TypedQuery q = em.createQuery(cq); + List allpersons = q.getResultList(); + if (allpersons.size() != 3) { + throw new RuntimeException("Incorrect number of results"); + } + if (!allpersons.get(0).getName().equals("Gizmo")) { + throw new RuntimeException("Incorrect order of results"); + } + StringBuilder sb = new StringBuilder("list of stored Person names:\n\t"); + for (Person p : allpersons) { + p.describeFully(sb); + } + sb.append("\nList complete.\n"); + System.out.print(sb); + }); //Try a JPA named query: - verifyJPANamedQuery(entityManagerFactory); - - deleteAllPerson(entityManagerFactory); - - } + QuarkusTransaction.requiringNew().run(() -> { + TypedQuery typedQuery = em.createNamedQuery( + "get_person_by_name", Person.class); + typedQuery.setParameter("name", "Quarkus"); + final Person singleResult = typedQuery.getSingleResult(); - private static void verifyJPANamedQuery(final EntityManagerFactory emf) { - EntityManager em = emf.createEntityManager(); - EntityTransaction transaction = em.getTransaction(); - transaction.begin(); - TypedQuery typedQuery = em.createNamedQuery( - "get_person_by_name", Person.class); - typedQuery.setParameter("name", "Quarkus"); - final Person singleResult = typedQuery.getSingleResult(); - - if (!singleResult.getName().equals("Quarkus")) { - throw new RuntimeException("Wrong result from named JPA query"); - } - - transaction.commit(); - em.close(); - } - - private static void verifyListOfExistingPersons(final EntityManagerFactory emf) { - EntityManager em = emf.createEntityManager(); - EntityTransaction transaction = em.getTransaction(); - transaction.begin(); - listExistingPersons(em); - transaction.commit(); - em.close(); - } + if (!singleResult.getName().equals("Quarkus")) { + throw new RuntimeException("Wrong result from named JPA query"); + } + }); - private static void storeTestPersons(final EntityManagerFactory emf) { - EntityManager em = emf.createEntityManager(); - EntityTransaction transaction = em.getTransaction(); - transaction.begin(); - persistNewPerson(em, "Gizmo"); - persistNewPerson(em, "Quarkus"); - persistNewPerson(em, "Hibernate ORM"); - transaction.commit(); - em.close(); - } + cleanUpData(); - private static void deleteAllPerson(final EntityManagerFactory emf) { - EntityManager em = emf.createEntityManager(); - EntityTransaction transaction = em.getTransaction(); - transaction.begin(); - em.createNativeQuery("Delete from Person").executeUpdate(); - transaction.commit(); - em.close(); + return "OK"; } - private static void listExistingPersons(EntityManager em) { - CriteriaBuilder cb = em.getCriteriaBuilder(); - - CriteriaQuery cq = cb.createQuery(Person.class); - Root from = cq.from(Person.class); - cq.select(from).orderBy(cb.asc(from.get("name"))); - TypedQuery q = em.createQuery(cq); - List allpersons = q.getResultList(); - if (allpersons.size() != 3) { - throw new RuntimeException("Incorrect number of results"); - } - if (!allpersons.get(0).getName().equals("Gizmo")) { - throw new RuntimeException("Incorrect order of results"); - } - StringBuilder sb = new StringBuilder("list of stored Person names:\n\t"); - for (Person p : allpersons) { - p.describeFully(sb); - } - sb.append("\nList complete.\n"); - System.out.print(sb); + private void cleanUpData() { + QuarkusTransaction.requiringNew() + .run(() -> em.createNativeQuery("Delete from Person").executeUpdate()); } - private static void persistNewPerson(EntityManager entityManager, String name) { + private void persistNewPerson(String name) { Person person = new Person(); person.setName(name); person.setAddress(new Address("Street " + randomName())); - entityManager.persist(person); + em.persist(person); } private static String randomName() { return UUID.randomUUID().toString(); } - private void reportException(String errorMessage, final Exception e, final HttpServletResponse resp) throws IOException { - final PrintWriter writer = resp.getWriter(); - if (errorMessage != null) { - writer.write(errorMessage); - writer.write(" "); - } - writer.write(e.toString()); - writer.append("\n\t"); - e.printStackTrace(writer); - writer.append("\n\t"); - } - } diff --git a/integration-tests/jpa-mysql/src/main/java/io/quarkus/it/jpa/mysql/XaConnectionsEndpoint.java b/integration-tests/jpa-mysql/src/main/java/io/quarkus/it/jpa/mysql/XaConnectionsEndpoint.java index d0598a29513f6..21f4a54a85e0c 100644 --- a/integration-tests/jpa-mysql/src/main/java/io/quarkus/it/jpa/mysql/XaConnectionsEndpoint.java +++ b/integration-tests/jpa-mysql/src/main/java/io/quarkus/it/jpa/mysql/XaConnectionsEndpoint.java @@ -5,24 +5,25 @@ import java.sql.SQLException; import jakarta.inject.Inject; -import jakarta.servlet.annotation.WebServlet; -import jakarta.servlet.http.HttpServlet; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; import io.agroal.api.AgroalDataSource; import io.agroal.api.configuration.AgroalConnectionFactoryConfiguration; import io.quarkus.agroal.DataSource; -@WebServlet(name = "XaConnectionEndpoint", urlPatterns = "/jpa-mysql/testxaconnection") -public class XaConnectionsEndpoint extends HttpServlet { +@Path("/jpa-mysql/testxaconnection") +@Produces(MediaType.TEXT_PLAIN) +public class XaConnectionsEndpoint { @Inject @DataSource("samebutxa") AgroalDataSource xaDatasource; - @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { + @GET + public String test() throws IOException { // Test 1# // Verify that the connection can be obtained @@ -38,9 +39,9 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO .connectionFactoryConfiguration(); Class connectionProviderClass = cfg.connectionProviderClass(); if (connectionProviderClass.equals(com.mysql.cj.jdbc.MysqlXADataSource.class)) { - resp.getWriter().write("OK"); + return "OK"; } else { - resp.getWriter().write("Unexpected Driver class: " + connectionProviderClass.getName()); + return "Unexpected Driver class: " + connectionProviderClass.getName(); } } diff --git a/integration-tests/jpa-mysql/src/test/java/io/quarkus/it/jpa/mysql/DialectInGraalITCase.java b/integration-tests/jpa-mysql/src/test/java/io/quarkus/it/jpa/mysql/DialectInGraalITCase.java deleted file mode 100644 index 09e6455d311be..0000000000000 --- a/integration-tests/jpa-mysql/src/test/java/io/quarkus/it/jpa/mysql/DialectInGraalITCase.java +++ /dev/null @@ -1,11 +0,0 @@ -package io.quarkus.it.jpa.mysql; - -import io.quarkus.test.junit.QuarkusIntegrationTest; - -/** - * DialectTest, but in native mode. - */ -@QuarkusIntegrationTest -public class DialectInGraalITCase extends DialectTest { - -} diff --git a/integration-tests/jpa-mysql/src/test/java/io/quarkus/it/jpa/mysql/DialectTest.java b/integration-tests/jpa-mysql/src/test/java/io/quarkus/it/jpa/mysql/DialectTest.java deleted file mode 100644 index e83c58a2676b8..0000000000000 --- a/integration-tests/jpa-mysql/src/test/java/io/quarkus/it/jpa/mysql/DialectTest.java +++ /dev/null @@ -1,27 +0,0 @@ -package io.quarkus.it.jpa.mysql; - -import static org.assertj.core.api.Assertions.assertThat; - -import org.junit.jupiter.api.Test; - -import io.quarkus.hibernate.orm.runtime.config.DialectVersions; -import io.quarkus.test.junit.QuarkusTest; -import io.restassured.RestAssured; - -/** - * Test the dialect used by default in Quarkus. - */ -@QuarkusTest -public class DialectTest { - - /** - * This is important for backwards compatibility reasons: - * we want to keep using at least the same version as before by default. - */ - @Test - public void version() { - String version = RestAssured.when().get("/dialect/version").then().extract().body().asString(); - assertThat(version).startsWith(DialectVersions.Defaults.MYSQL); - } - -} diff --git a/integration-tests/jpa-oracle/pom.xml b/integration-tests/jpa-oracle/pom.xml index 68ddee13237d0..20d193bf8f191 100644 --- a/integration-tests/jpa-oracle/pom.xml +++ b/integration-tests/jpa-oracle/pom.xml @@ -39,7 +39,7 @@ io.quarkus - quarkus-undertow + quarkus-resteasy-reactive @@ -106,7 +106,7 @@ io.quarkus - quarkus-undertow-deployment + quarkus-resteasy-reactive-deployment ${project.version} pom test diff --git a/integration-tests/jpa-oracle/src/main/java/io/quarkus/example/jpaoracle/DialectEndpoint.java b/integration-tests/jpa-oracle/src/main/java/io/quarkus/example/jpaoracle/DialectEndpoint.java deleted file mode 100644 index 432aa7387552d..0000000000000 --- a/integration-tests/jpa-oracle/src/main/java/io/quarkus/example/jpaoracle/DialectEndpoint.java +++ /dev/null @@ -1,44 +0,0 @@ -package io.quarkus.example.jpaoracle; - -import java.io.IOException; -import java.io.PrintWriter; - -import jakarta.inject.Inject; -import jakarta.servlet.annotation.WebServlet; -import jakarta.servlet.http.HttpServlet; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; - -import org.hibernate.SessionFactory; -import org.hibernate.engine.spi.SessionFactoryImplementor; - -import io.quarkus.hibernate.orm.runtime.config.DialectVersions; - -@WebServlet(name = "DialectEndpoint", urlPatterns = "/dialect/version") -public class DialectEndpoint extends HttpServlet { - @Inject - SessionFactory sessionFactory; - - @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { - try { - var version = sessionFactory.unwrap(SessionFactoryImplementor.class).getJdbcServices().getDialect().getVersion(); - resp.getWriter().write(DialectVersions.toString(version)); - } catch (Exception e) { - reportException("Failed to retrieve dialect version", e, resp); - } - } - - private void reportException(String errorMessage, final Exception e, final HttpServletResponse resp) throws IOException { - final PrintWriter writer = resp.getWriter(); - if (errorMessage != null) { - writer.write(errorMessage); - writer.write(" "); - } - writer.write(e.toString()); - writer.append("\n\t"); - e.printStackTrace(writer); - writer.append("\n\t"); - } - -} diff --git a/integration-tests/jpa-oracle/src/main/java/io/quarkus/example/jpaoracle/JPAFunctionalityTestEndpoint.java b/integration-tests/jpa-oracle/src/main/java/io/quarkus/example/jpaoracle/JPAFunctionalityTestEndpoint.java index df7d6663dfa97..99b1080c7d08c 100644 --- a/integration-tests/jpa-oracle/src/main/java/io/quarkus/example/jpaoracle/JPAFunctionalityTestEndpoint.java +++ b/integration-tests/jpa-oracle/src/main/java/io/quarkus/example/jpaoracle/JPAFunctionalityTestEndpoint.java @@ -1,176 +1,102 @@ package io.quarkus.example.jpaoracle; import java.io.IOException; -import java.io.PrintWriter; import java.util.List; import java.util.UUID; import jakarta.inject.Inject; import jakarta.persistence.EntityManager; -import jakarta.persistence.EntityManagerFactory; -import jakarta.persistence.EntityTransaction; import jakarta.persistence.TypedQuery; import jakarta.persistence.criteria.CriteriaBuilder; import jakarta.persistence.criteria.CriteriaQuery; import jakarta.persistence.criteria.Root; -import jakarta.servlet.annotation.WebServlet; -import jakarta.servlet.http.HttpServlet; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +import io.quarkus.narayana.jta.QuarkusTransaction; /** * Basic test running JPA with the Oracle database. * The application can work in either standard JVM or SubstrateVM. */ -@WebServlet(name = "JPATestBootstrapEndpoint", urlPatterns = "/jpa-oracle/testfunctionality") -public class JPAFunctionalityTestEndpoint extends HttpServlet { +@Path("/jpa-oracle/testfunctionality") +@Produces(MediaType.TEXT_PLAIN) +public class JPAFunctionalityTestEndpoint { @Inject - EntityManagerFactory entityManagerFactory; - - @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { - try { - doStuffWithHibernate(entityManagerFactory); - } catch (Exception e) { - reportException("An error occurred while performing Hibernate operations", e, resp); - } - resp.getWriter().write("OK"); - } - - /** - * Lists the various operations we want to test for: - */ - private static void doStuffWithHibernate(EntityManagerFactory entityManagerFactory) { + EntityManager em; - //Cleanup any existing data: - deleteAllPerson(entityManagerFactory); + @GET + public String test() throws IOException { + cleanUpData(); //Store some well known Person instances we can then test on: - storeTestPersons(entityManagerFactory); + QuarkusTransaction.requiringNew().run(() -> { + persistNewPerson("Gizmo"); + persistNewPerson("Quarkus"); + persistNewPerson("Hibernate ORM"); + }); //Load all persons and run some checks on the query results: - verifyListOfExistingPersons(entityManagerFactory); + QuarkusTransaction.requiringNew().run(() -> { + CriteriaBuilder cb = em.getCriteriaBuilder(); + + CriteriaQuery cq = cb.createQuery(Person.class); + Root from = cq.from(Person.class); + cq.select(from).orderBy(cb.asc(from.get("name"))); + TypedQuery q = em.createQuery(cq); + List allpersons = q.getResultList(); + if (allpersons.size() != 3) { + throw new RuntimeException("Incorrect number of results"); + } + if (!allpersons.get(0).getName().equals("Gizmo")) { + throw new RuntimeException("Incorrect order of results"); + } + StringBuilder sb = new StringBuilder("list of stored Person names:\n\t"); + for (Person p : allpersons) { + p.describeFully(sb); + } + sb.append("\nList complete.\n"); + System.out.print(sb); + }); //Try a JPA named query: - verifyJPANamedQuery(entityManagerFactory); - - verifyHqlFetch(entityManagerFactory); - - deleteAllPerson(entityManagerFactory); - - } - - private static void verifyHqlFetch(EntityManagerFactory emf) { - EntityManager em = emf.createEntityManager(); - try { - EntityTransaction transaction = em.getTransaction(); - try { - transaction.begin(); - - em.createQuery("from Person p left join fetch p.address a").getResultList(); - - transaction.commit(); - } catch (Exception e) { - if (transaction.isActive()) { - transaction.rollback(); - } - throw e; + QuarkusTransaction.requiringNew().run(() -> { + TypedQuery typedQuery = em.createNamedQuery( + "get_person_by_name", Person.class); + typedQuery.setParameter("name", "Quarkus"); + final Person singleResult = typedQuery.getSingleResult(); + + if (!singleResult.getName().equals("Quarkus")) { + throw new RuntimeException("Wrong result from named JPA query"); } - } finally { - em.close(); - } - } + }); - private static void verifyJPANamedQuery(final EntityManagerFactory emf) { - EntityManager em = emf.createEntityManager(); - EntityTransaction transaction = em.getTransaction(); - transaction.begin(); - TypedQuery typedQuery = em.createNamedQuery( - "get_person_by_name", Person.class); - typedQuery.setParameter("name", "Quarkus"); - final Person singleResult = typedQuery.getSingleResult(); - - if (!singleResult.getName().equals("Quarkus")) { - throw new RuntimeException("Wrong result from named JPA query"); - } - - transaction.commit(); - em.close(); - } + //Check that HQL fetch does not throw an exception + QuarkusTransaction.requiringNew() + .run(() -> em.createQuery("from Person p left join fetch p.address a").getResultList()); - private static void verifyListOfExistingPersons(final EntityManagerFactory emf) { - EntityManager em = emf.createEntityManager(); - EntityTransaction transaction = em.getTransaction(); - transaction.begin(); - listExistingPersons(em); - transaction.commit(); - em.close(); - } + cleanUpData(); - private static void storeTestPersons(final EntityManagerFactory emf) { - EntityManager em = emf.createEntityManager(); - EntityTransaction transaction = em.getTransaction(); - transaction.begin(); - persistNewPerson(em, "Gizmo"); - persistNewPerson(em, "Quarkus"); - persistNewPerson(em, "Hibernate ORM"); - transaction.commit(); - em.close(); + return "OK"; } - private static void deleteAllPerson(final EntityManagerFactory emf) { - EntityManager em = emf.createEntityManager(); - EntityTransaction transaction = em.getTransaction(); - transaction.begin(); - em.createNativeQuery("Delete from Person").executeUpdate(); - transaction.commit(); - em.close(); + private void cleanUpData() { + QuarkusTransaction.requiringNew() + .run(() -> em.createNativeQuery("Delete from Person").executeUpdate()); } - private static void listExistingPersons(EntityManager em) { - CriteriaBuilder cb = em.getCriteriaBuilder(); - - CriteriaQuery cq = cb.createQuery(Person.class); - Root from = cq.from(Person.class); - cq.select(from).orderBy(cb.asc(from.get("name"))); - TypedQuery q = em.createQuery(cq); - List allpersons = q.getResultList(); - if (allpersons.size() != 3) { - throw new RuntimeException("Incorrect number of results"); - } - if (!allpersons.get(0).getName().equals("Gizmo")) { - throw new RuntimeException("Incorrect order of results"); - } - StringBuilder sb = new StringBuilder("list of stored Person names:\n\t"); - for (Person p : allpersons) { - p.describeFully(sb); - } - sb.append("\nList complete.\n"); - System.out.print(sb); - } - - private static void persistNewPerson(EntityManager entityManager, String name) { + private void persistNewPerson(String name) { Person person = new Person(); person.setName(name); person.setAddress(new SequencedAddress("Street " + randomName())); - entityManager.persist(person); + em.persist(person); } private static String randomName() { return UUID.randomUUID().toString(); } - private void reportException(String errorMessage, final Exception e, final HttpServletResponse resp) throws IOException { - final PrintWriter writer = resp.getWriter(); - if (errorMessage != null) { - writer.write(errorMessage); - writer.write(" "); - } - writer.write(e.toString()); - writer.append("\n\t"); - e.printStackTrace(writer); - writer.append("\n\t"); - } - } diff --git a/integration-tests/jpa-oracle/src/main/java/io/quarkus/example/jpaoracle/LdapUrlTestEndpoint.java b/integration-tests/jpa-oracle/src/main/java/io/quarkus/example/jpaoracle/LdapUrlTestEndpoint.java index bf1bc072350b9..b94af308d1b4c 100644 --- a/integration-tests/jpa-oracle/src/main/java/io/quarkus/example/jpaoracle/LdapUrlTestEndpoint.java +++ b/integration-tests/jpa-oracle/src/main/java/io/quarkus/example/jpaoracle/LdapUrlTestEndpoint.java @@ -1,21 +1,21 @@ package io.quarkus.example.jpaoracle; -import java.io.IOException; import java.sql.SQLException; import jakarta.inject.Inject; -import jakarta.servlet.annotation.WebServlet; -import jakarta.servlet.http.HttpServlet; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; import org.jboss.logging.Logger; import io.agroal.api.AgroalDataSource; import io.quarkus.agroal.DataSource; -@WebServlet(name = "JPATestOracleLdap", urlPatterns = "/jpa-oracle/testldap") -public class LdapUrlTestEndpoint extends HttpServlet { +@Path("/jpa-oracle/testldap") +@Produces(MediaType.TEXT_PLAIN) +public class LdapUrlTestEndpoint { private final Logger LOG = Logger.getLogger(LdapUrlTestEndpoint.class.getName()); @@ -23,19 +23,8 @@ public class LdapUrlTestEndpoint extends HttpServlet { @DataSource("ldap") AgroalDataSource ds; - @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { - try { - final String output = ldap(); - resp.getWriter().write(output); - - } catch (Exception e) { - e.printStackTrace(resp.getWriter()); - } - } - - private String ldap() throws SQLException { - + @GET + public String test() throws SQLException { try { ds.getConnection().close(); } catch (SQLException e) { diff --git a/integration-tests/jpa-oracle/src/main/java/io/quarkus/example/jpaoracle/SerializationTestEndpoint.java b/integration-tests/jpa-oracle/src/main/java/io/quarkus/example/jpaoracle/SerializationTestEndpoint.java index 4b5f8c23ea238..ff7132a9c93c5 100644 --- a/integration-tests/jpa-oracle/src/main/java/io/quarkus/example/jpaoracle/SerializationTestEndpoint.java +++ b/integration-tests/jpa-oracle/src/main/java/io/quarkus/example/jpaoracle/SerializationTestEndpoint.java @@ -6,26 +6,17 @@ import java.io.ObjectInputStream; import java.io.ObjectOutputStream; -import jakarta.servlet.annotation.WebServlet; -import jakarta.servlet.http.HttpServlet; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; -@WebServlet(name = "JPATestOracleSerialization", urlPatterns = "/jpa-oracle/testserialization") -public class SerializationTestEndpoint extends HttpServlet { +@Path("/jpa-oracle/testserialization") +@Produces(MediaType.TEXT_PLAIN) +public class SerializationTestEndpoint { - @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { - try { - final String output = serializedstring(); - resp.getWriter().write(output); - - } catch (Exception e) { - resp.getWriter().write("An error occurred while attempting serialization operations"); - } - } - - private String serializedstring() throws IOException, ClassNotFoundException { + @GET + public String test() throws IOException, ClassNotFoundException { byte[] bytes = null; try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos)) { diff --git a/integration-tests/jpa-oracle/src/main/java/io/quarkus/example/jpaoracle/procedurecall/ProcedureCallEndpoint.java b/integration-tests/jpa-oracle/src/main/java/io/quarkus/example/jpaoracle/procedurecall/ProcedureCallEndpoint.java index fd0d171782ad2..3efe9dfe199c0 100644 --- a/integration-tests/jpa-oracle/src/main/java/io/quarkus/example/jpaoracle/procedurecall/ProcedureCallEndpoint.java +++ b/integration-tests/jpa-oracle/src/main/java/io/quarkus/example/jpaoracle/procedurecall/ProcedureCallEndpoint.java @@ -13,18 +13,18 @@ import jakarta.persistence.EntityManager; import jakarta.persistence.ParameterMode; import jakarta.persistence.StoredProcedureQuery; -import jakarta.servlet.annotation.WebServlet; -import jakarta.servlet.http.HttpServlet; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; import jakarta.transaction.Transactional; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +import org.jboss.resteasy.reactive.RestQuery; import io.agroal.api.AgroalDataSource; import io.quarkus.arc.Arc; import io.quarkus.runtime.StartupEvent; -@WebServlet("/jpa-oracle/procedure-call/") -public class ProcedureCallEndpoint extends HttpServlet { +@Path("/jpa-oracle/procedure-call/") +public class ProcedureCallEndpoint { private static final String PROCEDURE_NAME = "myproc"; @@ -59,10 +59,9 @@ private void persistEntity(String name) { em.persist(entity); } - @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { + @GET + public String test(@RestQuery String pattern) throws IOException { try { - String pattern = req.getParameter("pattern"); StoredProcedureQuery storedProcedure = em.createStoredProcedureQuery(PROCEDURE_NAME); storedProcedure.registerStoredProcedureParameter("p_pattern", String.class, ParameterMode.IN); storedProcedure.registerStoredProcedureParameter("p_cur", Object.class, ParameterMode.REF_CURSOR); @@ -74,7 +73,7 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO while (resultSet.next()) { result.add(resultSet.getString(1)); } - resp.getWriter().write(String.join("\n", result)); + return String.join("\n", result); } catch (SQLException e) { throw new RuntimeException(e); } diff --git a/integration-tests/jpa-oracle/src/test/java/io/quarkus/it/jpa/oracle/DialectInGraalITCase.java b/integration-tests/jpa-oracle/src/test/java/io/quarkus/it/jpa/oracle/DialectInGraalITCase.java deleted file mode 100644 index b1975d7f0d969..0000000000000 --- a/integration-tests/jpa-oracle/src/test/java/io/quarkus/it/jpa/oracle/DialectInGraalITCase.java +++ /dev/null @@ -1,11 +0,0 @@ -package io.quarkus.it.jpa.oracle; - -import io.quarkus.test.junit.QuarkusIntegrationTest; - -/** - * DialectTest, but in native mode. - */ -@QuarkusIntegrationTest -public class DialectInGraalITCase extends DialectTest { - -} diff --git a/integration-tests/jpa-oracle/src/test/java/io/quarkus/it/jpa/oracle/DialectTest.java b/integration-tests/jpa-oracle/src/test/java/io/quarkus/it/jpa/oracle/DialectTest.java deleted file mode 100644 index 130102d228006..0000000000000 --- a/integration-tests/jpa-oracle/src/test/java/io/quarkus/it/jpa/oracle/DialectTest.java +++ /dev/null @@ -1,27 +0,0 @@ -package io.quarkus.it.jpa.oracle; - -import static org.assertj.core.api.Assertions.assertThat; - -import org.junit.jupiter.api.Test; - -import io.quarkus.hibernate.orm.runtime.config.DialectVersions; -import io.quarkus.test.junit.QuarkusTest; -import io.restassured.RestAssured; - -/** - * Test the dialect used by default in Quarkus. - */ -@QuarkusTest -public class DialectTest { - - /** - * This is important for backwards compatibility reasons: - * we want to keep using at least the same version as before by default. - */ - @Test - public void version() { - String version = RestAssured.when().get("/dialect/version").then().extract().body().asString(); - assertThat(version).startsWith(DialectVersions.Defaults.ORACLE); - } - -} diff --git a/integration-tests/jpa-oracle/src/test/java/io/quarkus/it/jpa/oracle/procedurecall/ProcedureCallTest.java b/integration-tests/jpa-oracle/src/test/java/io/quarkus/it/jpa/oracle/procedurecall/ProcedureCallTest.java index fbd8bd011752d..3056456385474 100644 --- a/integration-tests/jpa-oracle/src/test/java/io/quarkus/it/jpa/oracle/procedurecall/ProcedureCallTest.java +++ b/integration-tests/jpa-oracle/src/test/java/io/quarkus/it/jpa/oracle/procedurecall/ProcedureCallTest.java @@ -19,7 +19,7 @@ public class ProcedureCallTest { @Test public void test() { - String response = given().param("pattern", "prefix%") + String response = given().queryParam("pattern", "prefix%") .when().get("/jpa-oracle/procedure-call/").then() .statusCode(200) .extract().body().asString(); diff --git a/integration-tests/jpa-postgresql-withxml/pom.xml b/integration-tests/jpa-postgresql-withxml/pom.xml index b506ff5f45108..7d8d2690730a3 100644 --- a/integration-tests/jpa-postgresql-withxml/pom.xml +++ b/integration-tests/jpa-postgresql-withxml/pom.xml @@ -10,7 +10,7 @@ 4.0.0 quarkus-integration-test-jpa-postgresql-withxml - Quarkus - Integration Tests - JPA - PostgreSQL + Quarkus - Integration Tests - JPA - PostgreSQL with XML Module that contains JPA related tests running with the PostgreSQL database @@ -20,15 +20,19 @@ io.quarkus - quarkus-undertow + quarkus-hibernate-orm io.quarkus - quarkus-hibernate-orm + quarkus-jdbc-postgresql io.quarkus - quarkus-jdbc-postgresql + quarkus-resteasy-reactive + + + io.quarkus + quarkus-jaxb @@ -72,7 +76,20 @@ io.quarkus - quarkus-undertow-deployment + quarkus-resteasy-reactive-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-jaxb-deployment ${project.version} pom test diff --git a/integration-tests/jpa-postgresql-withxml/src/main/java/io/quarkus/it/jpa/postgresql/EntityWithXml.java b/integration-tests/jpa-postgresql-withxml/src/main/java/io/quarkus/it/jpa/postgresql/EntityWithXml.java new file mode 100644 index 0000000000000..470ce79c8f365 --- /dev/null +++ b/integration-tests/jpa-postgresql-withxml/src/main/java/io/quarkus/it/jpa/postgresql/EntityWithXml.java @@ -0,0 +1,74 @@ +package io.quarkus.it.jpa.postgresql; + +import java.time.LocalDate; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.xml.bind.annotation.XmlElement; +import jakarta.xml.bind.annotation.XmlRootElement; +import jakarta.xml.bind.annotation.adapters.XmlAdapter; +import jakarta.xml.bind.annotation.adapters.XmlJavaTypeAdapter; + +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.type.SqlTypes; + +import io.quarkus.runtime.annotations.RegisterForReflection; + +@Entity +public class EntityWithXml { + @Id + @GeneratedValue + Long id; + + @JdbcTypeCode(SqlTypes.SQLXML) + ToBeSerializedWithDateTime xml; + + public EntityWithXml() { + } + + public EntityWithXml(ToBeSerializedWithDateTime data) { + this.xml = data; + } + + @Override + public String toString() { + return "EntityWithXml{" + + "id=" + id + + ", xml=" + xml + + '}'; + } + + @RegisterForReflection + @XmlRootElement + public static class ToBeSerializedWithDateTime { + @XmlElement + @XmlJavaTypeAdapter(value = LocalDateXmlAdapter.class) + LocalDate date; + + public ToBeSerializedWithDateTime() { + } + + public ToBeSerializedWithDateTime(LocalDate date) { + this.date = date; + } + + @Override + public String toString() { + return "ToBeSerializedWithDateTime{" + + "date=" + date + + '}'; + } + } + + @RegisterForReflection + public static class LocalDateXmlAdapter extends XmlAdapter { + public LocalDate unmarshal(String string) { + return string == null ? null : LocalDate.parse(string); + } + + public String marshal(LocalDate localDate) { + return localDate == null ? null : localDate.toString(); + } + } +} diff --git a/integration-tests/jpa-postgresql-withxml/src/main/java/io/quarkus/it/jpa/postgresql/JPAFunctionalityTestEndpoint.java b/integration-tests/jpa-postgresql-withxml/src/main/java/io/quarkus/it/jpa/postgresql/JPAFunctionalityTestEndpoint.java index e8f1a786615cc..c489c84120f30 100644 --- a/integration-tests/jpa-postgresql-withxml/src/main/java/io/quarkus/it/jpa/postgresql/JPAFunctionalityTestEndpoint.java +++ b/integration-tests/jpa-postgresql-withxml/src/main/java/io/quarkus/it/jpa/postgresql/JPAFunctionalityTestEndpoint.java @@ -1,7 +1,5 @@ package io.quarkus.it.jpa.postgresql; -import java.io.IOException; -import java.io.PrintWriter; import java.io.StringReader; import java.sql.Connection; import java.sql.PreparedStatement; @@ -9,6 +7,7 @@ import java.sql.SQLException; import java.sql.SQLXML; import java.sql.Statement; +import java.time.LocalDate; import java.util.List; import java.util.UUID; @@ -23,49 +22,119 @@ import jakarta.inject.Inject; import jakarta.persistence.EntityManager; -import jakarta.persistence.EntityManagerFactory; -import jakarta.persistence.EntityTransaction; import jakarta.persistence.TypedQuery; import jakarta.persistence.criteria.CriteriaBuilder; import jakarta.persistence.criteria.CriteriaQuery; import jakarta.persistence.criteria.Root; -import jakarta.servlet.annotation.WebServlet; -import jakarta.servlet.http.HttpServlet; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +import io.quarkus.hibernate.orm.PersistenceUnit; +import io.quarkus.it.jpa.postgresql.otherpu.EntityWithXmlOtherPU; +import io.quarkus.narayana.jta.QuarkusTransaction; /** * First we run a smoke test for basic Hibernate ORM functionality, * then we specifically focus on supporting the PgSQLXML mapping abilities for XML types: * both need to work. */ -@WebServlet(name = "JPATestBootstrapEndpoint", urlPatterns = "/jpa-withxml/testfunctionality") -public class JPAFunctionalityTestEndpoint extends HttpServlet { +@Path("/jpa-withxml/testfunctionality") +@Produces(MediaType.TEXT_PLAIN) +public class JPAFunctionalityTestEndpoint { @Inject - EntityManagerFactory entityManagerFactory; + EntityManager em; + @Inject + @PersistenceUnit("other") + EntityManager otherEm; @Inject DataSource ds; - @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { - try { - doStuffWithHibernate(entityManagerFactory); - doStuffWithDatasource(); - } catch (Exception e) { - reportException("An error occurred while performing Hibernate operations", e, resp); - } - resp.getWriter().write("OK"); + @GET + @Path("base") + public String base() { + cleanUpData(); + + //Store some well known Person instances we can then test on: + QuarkusTransaction.requiringNew().run(() -> { + persistNewPerson("Gizmo"); + persistNewPerson("Quarkus"); + persistNewPerson("Hibernate ORM"); + }); + + //Load all persons and run some checks on the query results: + QuarkusTransaction.requiringNew().run(() -> { + CriteriaBuilder cb = em.getCriteriaBuilder(); + + CriteriaQuery cq = cb.createQuery(Person.class); + Root from = cq.from(Person.class); + cq.select(from).orderBy(cb.asc(from.get("name"))); + TypedQuery q = em.createQuery(cq); + List allpersons = q.getResultList(); + if (allpersons.size() != 3) { + throw new RuntimeException("Incorrect number of results"); + } + if (!allpersons.get(0).getName().equals("Gizmo")) { + throw new RuntimeException("Incorrect order of results"); + } + StringBuilder sb = new StringBuilder("list of stored Person names:\n\t"); + for (Person p : allpersons) { + p.describeFully(sb); + } + sb.append("\nList complete.\n"); + System.out.print(sb); + }); + + //Try a JPA named query: + QuarkusTransaction.requiringNew().run(() -> { + TypedQuery typedQuery = em.createNamedQuery( + "get_person_by_name", Person.class); + typedQuery.setParameter("name", "Quarkus"); + final Person singleResult = typedQuery.getSingleResult(); + + if (!singleResult.getName().equals("Quarkus")) { + throw new RuntimeException("Wrong result from named JPA query"); + } + }); + + //Check that HQL fetch does not throw an exception + QuarkusTransaction.requiringNew() + .run(() -> em.createQuery("from Person p left join fetch p.address a").getResultList()); + + cleanUpData(); + + return "OK"; + } + + private void cleanUpData() { + QuarkusTransaction.requiringNew() + .run(() -> em.createNativeQuery("Delete from Person").executeUpdate()); + } + + private void persistNewPerson(String name) { + Person person = new Person(); + person.setName(name); + person.setAddress(new SequencedAddress("Street " + randomName())); + em.persist(person); + } + + private static String randomName() { + return UUID.randomUUID().toString(); } - private void doStuffWithDatasource() throws SQLException, TransformerException { + @GET + @Path("datasource-xml") + public String datasourceXml() throws SQLException, TransformerException { try (final Connection con = ds.getConnection()) { deleteXmlSchema(con); createXmlSchema(con); writeXmlObject(con); checkWrittenXmlObject(con); } + return "OK"; } private void checkWrittenXmlObject(Connection con) throws SQLException { @@ -120,106 +189,49 @@ private void deleteXmlSchema(Connection con) { } } - /** - * Lists the various operations we want to test for: - */ - private static void doStuffWithHibernate(EntityManagerFactory entityManagerFactory) { - - //Store some well known Person instances we can then test on: - storeTestPersons(entityManagerFactory); - - //Load all persons and run some checks on the query results: - verifyListOfExistingPersons(entityManagerFactory); + @GET + @Path("hibernate-xml") + public String hibernateXml() { + QuarkusTransaction.requiringNew().run(() -> { + EntityWithXml entity = new EntityWithXml( + new EntityWithXml.ToBeSerializedWithDateTime(LocalDate.of(2023, 7, 28))); + em.persist(entity); + }); + + QuarkusTransaction.requiringNew().run(() -> { + List entities = em + .createQuery("select e from EntityWithXml e", EntityWithXml.class) + .getResultList(); + if (entities.isEmpty()) { + throw new AssertionError("No entities with XML were found"); + } + }); - //Try a JPA named query: - verifyJPANamedQuery(entityManagerFactory); - } + QuarkusTransaction.requiringNew().run(() -> { + em.createQuery("delete from EntityWithXml").executeUpdate(); + }); - private static void verifyJPANamedQuery(final EntityManagerFactory emf) { - EntityManager em = emf.createEntityManager(); - EntityTransaction transaction = em.getTransaction(); - transaction.begin(); - TypedQuery typedQuery = em.createNamedQuery( - "get_person_by_name", Person.class); - typedQuery.setParameter("name", "Quarkus"); - final Person singleResult = typedQuery.getSingleResult(); - - if (!singleResult.getName().equals("Quarkus")) { - throw new RuntimeException("Wrong result from named JPA query"); + Exception exception = null; + try { + QuarkusTransaction.requiringNew().run(() -> { + EntityWithXmlOtherPU otherPU = new EntityWithXmlOtherPU( + new EntityWithXmlOtherPU.ToBeSerializedWithDateTime(LocalDate.of(2023, 7, 28))); + otherEm.persist(otherPU); + }); + } catch (Exception e) { + exception = e; } - transaction.commit(); - em.close(); - } - - private static void verifyListOfExistingPersons(final EntityManagerFactory emf) { - EntityManager em = emf.createEntityManager(); - EntityTransaction transaction = em.getTransaction(); - transaction.begin(); - listExistingPersons(em); - transaction.commit(); - em.close(); - } - - private static void storeTestPersons(final EntityManagerFactory emf) { - EntityManager em = emf.createEntityManager(); - EntityTransaction transaction = em.getTransaction(); - transaction.begin(); - persistNewPerson(em, "Gizmo"); - persistNewPerson(em, "Quarkus"); - persistNewPerson(em, "Hibernate ORM"); - transaction.commit(); - em.close(); - } - - private static void listExistingPersons(EntityManager em) { - CriteriaBuilder cb = em.getCriteriaBuilder(); - - CriteriaQuery cq = cb.createQuery(Person.class); - Root from = cq.from(Person.class); - cq.select(from).orderBy(cb.asc(from.get("name"))); - TypedQuery q = em.createQuery(cq); - List allpersons = q.getResultList(); - if (allpersons.size() != 3) { - throw new RuntimeException("Incorrect number of results"); - } - if (!allpersons.get(0).getName().equals("Gizmo")) { - throw new RuntimeException("Incorrect order of results"); + if (exception == null) { + throw new AssertionError( + "Our custom XML format mapper throws exceptions. So we were expecting transaction to fail, but it did not!"); } - StringBuilder sb = new StringBuilder("list of stored Person names:\n\t"); - for (Person p : allpersons) { - p.describeFully(sb); - sb.append("\n\t"); - if (p.getStatus() != Status.LIVING) { - throw new RuntimeException("Incorrect status " + p); - } + if (!(exception instanceof UnsupportedOperationException) + || !exception.getMessage().contains("I cannot convert anything to XML")) { + throw new AssertionError("flush failed for a different reason than expected.", exception); } - sb.append("\nList complete.\n"); - System.out.print(sb); - } - - private static void persistNewPerson(EntityManager entityManager, String name) { - Person person = new Person(); - person.setName(name); - person.setStatus(Status.LIVING); - person.setAddress(new SequencedAddress("Street " + randomName())); - entityManager.persist(person); - } - private static String randomName() { - return UUID.randomUUID().toString(); - } - - private void reportException(String errorMessage, final Exception e, final HttpServletResponse resp) throws IOException { - final PrintWriter writer = resp.getWriter(); - if (errorMessage != null) { - writer.write(errorMessage); - writer.write(" "); - } - writer.write(e.toString()); - writer.append("\n\t"); - e.printStackTrace(writer); - writer.append("\n\t"); + return "OK"; } } diff --git a/integration-tests/jpa-postgresql-withxml/src/main/java/io/quarkus/it/jpa/postgresql/otherpu/EntityWithXmlOtherPU.java b/integration-tests/jpa-postgresql-withxml/src/main/java/io/quarkus/it/jpa/postgresql/otherpu/EntityWithXmlOtherPU.java new file mode 100644 index 0000000000000..c8f16813eca44 --- /dev/null +++ b/integration-tests/jpa-postgresql-withxml/src/main/java/io/quarkus/it/jpa/postgresql/otherpu/EntityWithXmlOtherPU.java @@ -0,0 +1,57 @@ + +package io.quarkus.it.jpa.postgresql.otherpu; + +import java.time.LocalDate; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; + +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.type.SqlTypes; + +import io.quarkus.runtime.annotations.RegisterForReflection; + +@Entity +public class EntityWithXmlOtherPU { + @Id + @GeneratedValue + Long id; + + @JdbcTypeCode(SqlTypes.SQLXML) + ToBeSerializedWithDateTime xml; + + public EntityWithXmlOtherPU() { + } + + public EntityWithXmlOtherPU(ToBeSerializedWithDateTime data) { + this.xml = data; + } + + @Override + public String toString() { + return "EntityWithXmlOtherPU{" + + "id=" + id + + ", xml" + xml + + '}'; + } + + @RegisterForReflection + public static class ToBeSerializedWithDateTime { + LocalDate date; + + public ToBeSerializedWithDateTime() { + } + + public ToBeSerializedWithDateTime(LocalDate date) { + this.date = date; + } + + @Override + public String toString() { + return "ToBeSerializedWithDateTime{" + + "date=" + date + + '}'; + } + } +} diff --git a/integration-tests/jpa-postgresql-withxml/src/main/java/io/quarkus/it/jpa/postgresql/otherpu/XmlFormatMapper.java b/integration-tests/jpa-postgresql-withxml/src/main/java/io/quarkus/it/jpa/postgresql/otherpu/XmlFormatMapper.java new file mode 100644 index 0000000000000..8c9c57bc003ed --- /dev/null +++ b/integration-tests/jpa-postgresql-withxml/src/main/java/io/quarkus/it/jpa/postgresql/otherpu/XmlFormatMapper.java @@ -0,0 +1,23 @@ +package io.quarkus.it.jpa.postgresql.otherpu; + +import org.hibernate.type.descriptor.WrapperOptions; +import org.hibernate.type.descriptor.java.JavaType; +import org.hibernate.type.format.FormatMapper; + +import io.quarkus.hibernate.orm.PersistenceUnitExtension; +import io.quarkus.hibernate.orm.XmlFormat; + +@XmlFormat +@PersistenceUnitExtension("other") +public class XmlFormatMapper implements FormatMapper { + + @Override + public T fromString(CharSequence charSequence, JavaType javaType, WrapperOptions wrapperOptions) { + throw new UnsupportedOperationException("I cannot convert anything from XML."); + } + + @Override + public String toString(T value, JavaType javaType, WrapperOptions wrapperOptions) { + throw new UnsupportedOperationException("I cannot convert anything to XML."); + } +} diff --git a/integration-tests/jpa-postgresql-withxml/src/main/resources/application.properties b/integration-tests/jpa-postgresql-withxml/src/main/resources/application.properties index b93311019b049..c0ff6325328d0 100644 --- a/integration-tests/jpa-postgresql-withxml/src/main/resources/application.properties +++ b/integration-tests/jpa-postgresql-withxml/src/main/resources/application.properties @@ -3,6 +3,7 @@ quarkus.datasource.password=hibernate_orm_test quarkus.datasource.jdbc.url=${postgres.url} quarkus.datasource.jdbc.max-size=8 +quarkus.hibernate-orm.packages=io.quarkus.it.jpa.postgresql quarkus.hibernate-orm.database.generation=drop-and-create #Necessary for assertions in JPAFunctionalityInGraalITCase: @@ -10,3 +11,7 @@ quarkus.native.enable-reports=true #Useful to get some more insight in the trigger: quarkus.native.additional-build-args=-J-Dio.quarkus.jdbc.postgresql.graalvm.diagnostics=true + +# Define non-default PU so that we can configure a custom XML format mapper. The default PU is using the default mapper. +quarkus.hibernate-orm."other".datasource= +quarkus.hibernate-orm."other".packages=io.quarkus.it.jpa.postgresql.otherpu \ No newline at end of file diff --git a/integration-tests/jpa-postgresql-withxml/src/test/java/io/quarkus/it/jpa/postgresql/JPAFunctionalityTest.java b/integration-tests/jpa-postgresql-withxml/src/test/java/io/quarkus/it/jpa/postgresql/JPAFunctionalityTest.java index d7ade11f80c3c..75f9943714d45 100644 --- a/integration-tests/jpa-postgresql-withxml/src/test/java/io/quarkus/it/jpa/postgresql/JPAFunctionalityTest.java +++ b/integration-tests/jpa-postgresql-withxml/src/test/java/io/quarkus/it/jpa/postgresql/JPAFunctionalityTest.java @@ -14,8 +14,18 @@ public class JPAFunctionalityTest { @Test - public void testJPAFunctionalityFromServlet() throws Exception { - RestAssured.when().get("/jpa-withxml/testfunctionality").then().body(is("OK")); + public void testBase() throws Exception { + RestAssured.when().get("/jpa-withxml/testfunctionality/base").then().body(is("OK")); + } + + @Test + public void testDatasourceXml() throws Exception { + RestAssured.when().get("/jpa-withxml/testfunctionality/datasource-xml").then().body(is("OK")); + } + + @Test + public void testHibernateXml() throws Exception { + RestAssured.when().get("/jpa-withxml/testfunctionality/hibernate-xml").then().body(is("OK")); } } diff --git a/integration-tests/jpa-postgresql/pom.xml b/integration-tests/jpa-postgresql/pom.xml index d4d9ad27b2f6a..236e61604bbe9 100644 --- a/integration-tests/jpa-postgresql/pom.xml +++ b/integration-tests/jpa-postgresql/pom.xml @@ -20,15 +20,19 @@ io.quarkus - quarkus-undertow + quarkus-hibernate-orm io.quarkus - quarkus-hibernate-orm + quarkus-jdbc-postgresql io.quarkus - quarkus-jdbc-postgresql + quarkus-resteasy-reactive + + + io.quarkus + quarkus-jackson @@ -82,7 +86,20 @@ io.quarkus - quarkus-undertow-deployment + quarkus-resteasy-reactive-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-jackson-deployment ${project.version} pom test diff --git a/integration-tests/jpa-postgresql/src/main/java/io/quarkus/it/jpa/postgresql/EntityWithJson.java b/integration-tests/jpa-postgresql/src/main/java/io/quarkus/it/jpa/postgresql/EntityWithJson.java new file mode 100644 index 0000000000000..e513a7d8b1d23 --- /dev/null +++ b/integration-tests/jpa-postgresql/src/main/java/io/quarkus/it/jpa/postgresql/EntityWithJson.java @@ -0,0 +1,59 @@ +package io.quarkus.it.jpa.postgresql; + +import java.time.LocalDate; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; + +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.type.SqlTypes; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import io.quarkus.runtime.annotations.RegisterForReflection; + +@Entity +public class EntityWithJson { + @Id + @GeneratedValue + Long id; + + @JdbcTypeCode(SqlTypes.JSON) + ToBeSerializedWithDateTime json; + + public EntityWithJson() { + } + + public EntityWithJson(ToBeSerializedWithDateTime data) { + this.json = data; + } + + @Override + public String toString() { + return "EntityWithJson{" + + "id=" + id + + ", json=" + json + + '}'; + } + + @RegisterForReflection + public static class ToBeSerializedWithDateTime { + @JsonProperty + LocalDate date; + + public ToBeSerializedWithDateTime() { + } + + public ToBeSerializedWithDateTime(LocalDate date) { + this.date = date; + } + + @Override + public String toString() { + return "ToBeSerializedWithDateTime{" + + "date=" + date + + '}'; + } + } +} diff --git a/integration-tests/jpa-postgresql/src/main/java/io/quarkus/it/jpa/postgresql/JPAFunctionalityTestEndpoint.java b/integration-tests/jpa-postgresql/src/main/java/io/quarkus/it/jpa/postgresql/JPAFunctionalityTestEndpoint.java index ac23341e763f4..6b9b3df9d6433 100644 --- a/integration-tests/jpa-postgresql/src/main/java/io/quarkus/it/jpa/postgresql/JPAFunctionalityTestEndpoint.java +++ b/integration-tests/jpa-postgresql/src/main/java/io/quarkus/it/jpa/postgresql/JPAFunctionalityTestEndpoint.java @@ -1,182 +1,172 @@ package io.quarkus.it.jpa.postgresql; -import java.io.IOException; -import java.io.PrintWriter; -import java.time.Duration; +import java.time.LocalDate; import java.util.List; import java.util.UUID; import jakarta.inject.Inject; import jakarta.persistence.EntityManager; -import jakarta.persistence.EntityManagerFactory; -import jakarta.persistence.EntityTransaction; import jakarta.persistence.TypedQuery; import jakarta.persistence.criteria.CriteriaBuilder; import jakarta.persistence.criteria.CriteriaQuery; import jakarta.persistence.criteria.Root; -import jakarta.servlet.annotation.WebServlet; -import jakarta.servlet.http.HttpServlet; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +import io.quarkus.hibernate.orm.PersistenceUnit; +import io.quarkus.it.jpa.postgresql.otherpu.EntityWithJsonOtherPU; +import io.quarkus.narayana.jta.QuarkusTransaction; /** * Various tests covering JPA functionality. All tests should work in both standard JVM and in native mode. */ -@WebServlet(name = "JPATestBootstrapEndpoint", urlPatterns = "/jpa/testfunctionality") -public class JPAFunctionalityTestEndpoint extends HttpServlet { +@Path("/jpa/testfunctionality") +@Produces(MediaType.TEXT_PLAIN) +public class JPAFunctionalityTestEndpoint { @Inject - EntityManagerFactory entityManagerFactory; + EntityManager em; - @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { - try { - doStuffWithHibernate(entityManagerFactory); - } catch (Exception e) { - reportException("An error occurred while performing Hibernate operations", e, resp); - } - resp.getWriter().write("OK"); - } - - /** - * Lists the various operations we want to test for: - */ - private static void doStuffWithHibernate(EntityManagerFactory entityManagerFactory) { + @Inject + @PersistenceUnit("other") + EntityManager otherEm; - //Cleanup any existing data: - deleteAllPerson(entityManagerFactory); + @GET + @Path("base") + public String base() { + cleanUpData(); //Store some well known Person instances we can then test on: - storeTestPersons(entityManagerFactory); + QuarkusTransaction.requiringNew().run(() -> { + persistNewPerson("Gizmo"); + persistNewPerson("Quarkus"); + persistNewPerson("Hibernate ORM"); + }); //Load all persons and run some checks on the query results: - verifyListOfExistingPersons(entityManagerFactory); + QuarkusTransaction.requiringNew().run(() -> { + CriteriaBuilder cb = em.getCriteriaBuilder(); + + CriteriaQuery cq = cb.createQuery(Person.class); + Root from = cq.from(Person.class); + cq.select(from).orderBy(cb.asc(from.get("name"))); + TypedQuery q = em.createQuery(cq); + List allpersons = q.getResultList(); + if (allpersons.size() != 3) { + throw new RuntimeException("Incorrect number of results"); + } + if (!allpersons.get(0).getName().equals("Gizmo")) { + throw new RuntimeException("Incorrect order of results"); + } + StringBuilder sb = new StringBuilder("list of stored Person names:\n\t"); + for (Person p : allpersons) { + p.describeFully(sb); + } + sb.append("\nList complete.\n"); + System.out.print(sb); + }); //Try a JPA named query: - verifyJPANamedQuery(entityManagerFactory); - - deleteAllPerson(entityManagerFactory); - - // Try an entity using a UUID - verifyUUIDEntity(entityManagerFactory); - } - - private static void verifyJPANamedQuery(final EntityManagerFactory emf) { - EntityManager em = emf.createEntityManager(); - EntityTransaction transaction = em.getTransaction(); - transaction.begin(); - TypedQuery typedQuery = em.createNamedQuery( - "get_person_by_name", Person.class); - typedQuery.setParameter("name", "Quarkus"); - final Person singleResult = typedQuery.getSingleResult(); - - if (!singleResult.getName().equals("Quarkus")) { - throw new RuntimeException("Wrong result from named JPA query"); - } - - transaction.commit(); - em.close(); - } + QuarkusTransaction.requiringNew().run(() -> { + TypedQuery typedQuery = em.createNamedQuery( + "get_person_by_name", Person.class); + typedQuery.setParameter("name", "Quarkus"); + final Person singleResult = typedQuery.getSingleResult(); + + if (!singleResult.getName().equals("Quarkus")) { + throw new RuntimeException("Wrong result from named JPA query"); + } + }); - private static void verifyListOfExistingPersons(final EntityManagerFactory emf) { - EntityManager em = emf.createEntityManager(); - EntityTransaction transaction = em.getTransaction(); - transaction.begin(); - listExistingPersons(em); - transaction.commit(); - em.close(); - } + //Check that HQL fetch does not throw an exception + QuarkusTransaction.requiringNew() + .run(() -> em.createQuery("from Person p left join fetch p.address a").getResultList()); - private static void storeTestPersons(final EntityManagerFactory emf) { - EntityManager em = emf.createEntityManager(); - EntityTransaction transaction = em.getTransaction(); - transaction.begin(); - persistNewPerson(em, "Gizmo"); - persistNewPerson(em, "Quarkus"); - persistNewPerson(em, "Hibernate ORM"); - transaction.commit(); - em.close(); - } + cleanUpData(); - private static void deleteAllPerson(final EntityManagerFactory emf) { - EntityManager em = emf.createEntityManager(); - EntityTransaction transaction = em.getTransaction(); - transaction.begin(); - em.createNativeQuery("Delete from myschema.Person").executeUpdate(); - transaction.commit(); - em.close(); + return "OK"; } - private static void listExistingPersons(EntityManager em) { - CriteriaBuilder cb = em.getCriteriaBuilder(); - - CriteriaQuery cq = cb.createQuery(Person.class); - Root from = cq.from(Person.class); - cq.select(from).orderBy(cb.asc(from.get("name"))); - TypedQuery q = em.createQuery(cq); - List allpersons = q.getResultList(); - if (allpersons.size() != 3) { - throw new RuntimeException("Incorrect number of results"); - } - if (!allpersons.get(0).getName().equals("Gizmo")) { - throw new RuntimeException("Incorrect order of results"); - } - StringBuilder sb = new StringBuilder("list of stored Person names:\n\t"); - for (Person p : allpersons) { - p.describeFully(sb); - sb.append("\n\t"); - if (p.getStatus() != Status.LIVING) { - throw new RuntimeException("Incorrect status " + p); - } - } - sb.append("\nList complete.\n"); - System.out.print(sb); + private void cleanUpData() { + QuarkusTransaction.requiringNew() + .run(() -> em.createNativeQuery("Delete from myschema.Person").executeUpdate()); } - private static void persistNewPerson(EntityManager entityManager, String name) { + private void persistNewPerson(String name) { Person person = new Person(); person.setName(name); - person.setStatus(Status.LIVING); person.setAddress(new SequencedAddress("Street " + randomName())); - person.setLatestLunchBreakDuration(Duration.ofMinutes(30)); - entityManager.persist(person); + em.persist(person); } private static String randomName() { return UUID.randomUUID().toString(); } - private static void verifyUUIDEntity(final EntityManagerFactory emf) { - EntityManager em = emf.createEntityManager(); - EntityTransaction transaction = em.getTransaction(); - transaction.begin(); - MyUUIDEntity myEntity = new MyUUIDEntity(); - myEntity.setName("George"); - em.persist(myEntity); - transaction.commit(); - em.close(); - - em = emf.createEntityManager(); - transaction = em.getTransaction(); - transaction.begin(); - myEntity = em.find(MyUUIDEntity.class, myEntity.getId()); - if (myEntity == null || !"George".equals(myEntity.getName())) { - throw new RuntimeException("Incorrect loaded MyUUIDEntity " + myEntity); - } - transaction.commit(); - em.close(); + @GET + @Path("uuid") + public String uuid() { + var id = QuarkusTransaction.requiringNew().call(() -> { + MyUUIDEntity myEntity = new MyUUIDEntity(); + myEntity.setName("George"); + em.persist(myEntity); + return myEntity.getId(); + }); + + QuarkusTransaction.requiringNew().run(() -> { + var myEntity = em.find(MyUUIDEntity.class, id); + if (myEntity == null || !"George".equals(myEntity.getName())) { + throw new RuntimeException("Incorrect loaded MyUUIDEntity " + myEntity); + } + }); + return "OK"; } - private void reportException(String errorMessage, final Exception e, final HttpServletResponse resp) throws IOException { - final PrintWriter writer = resp.getWriter(); - if (errorMessage != null) { - writer.write(errorMessage); - writer.write(" "); + @GET + @Path("json") + public String json() { + QuarkusTransaction.requiringNew().run(() -> { + EntityWithJson entity = new EntityWithJson( + new EntityWithJson.ToBeSerializedWithDateTime(LocalDate.of(2023, 7, 28))); + em.persist(entity); + }); + + QuarkusTransaction.requiringNew().run(() -> { + List entities = em + .createQuery("select e from EntityWithJson e", EntityWithJson.class) + .getResultList(); + if (entities.isEmpty()) { + throw new AssertionError("No entities with json were found"); + } + }); + + QuarkusTransaction.requiringNew().run(() -> { + em.createQuery("delete from EntityWithJson").executeUpdate(); + }); + + Exception exception = null; + try { + QuarkusTransaction.requiringNew().run(() -> { + EntityWithJsonOtherPU otherPU = new EntityWithJsonOtherPU( + new EntityWithJsonOtherPU.ToBeSerializedWithDateTime(LocalDate.of(2023, 7, 28))); + otherEm.persist(otherPU); + }); + } catch (Exception e) { + exception = e; } - writer.write(e.toString()); - writer.append("\n\t"); - e.printStackTrace(writer); - writer.append("\n\t"); + + if (exception == null) { + throw new AssertionError( + "Default mapper cannot process date/time properties. So we were expecting transaction to fail, but it did not!"); + } + if (!(exception instanceof UnsupportedOperationException) + || !exception.getMessage().contains("I cannot convert anything to JSON")) { + throw new AssertionError("flush failed for a different reason than expected.", exception); + } + + return "OK"; } } diff --git a/integration-tests/jpa-postgresql/src/main/java/io/quarkus/it/jpa/postgresql/JPATestReflectionEndpoint.java b/integration-tests/jpa-postgresql/src/main/java/io/quarkus/it/jpa/postgresql/JPATestReflectionEndpoint.java index 705eae8071dea..fb8f672784738 100644 --- a/integration-tests/jpa-postgresql/src/main/java/io/quarkus/it/jpa/postgresql/JPATestReflectionEndpoint.java +++ b/integration-tests/jpa-postgresql/src/main/java/io/quarkus/it/jpa/postgresql/JPATestReflectionEndpoint.java @@ -2,34 +2,46 @@ import java.io.IOException; import java.io.PrintWriter; +import java.io.StringWriter; import java.lang.reflect.Field; import java.lang.reflect.Method; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; -import jakarta.servlet.annotation.WebServlet; -import jakarta.servlet.http.HttpServlet; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; +import javax.xml.transform.TransformerException; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; /** * Various tests for the JPA integration. * WARNING: these tests will ONLY pass in native mode, as it also verifies reflection non-functionality. */ -@WebServlet(name = "JPATestReflectionEndpoint", urlPatterns = "/jpa/testreflection") -public class JPATestReflectionEndpoint extends HttpServlet { - - @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { - makeSureNonEntityAreDCE(resp); - makeSureEntitiesAreAccessibleViaReflection(resp); - makeSureNonAnnotatedEmbeddableAreAccessibleViaReflection(resp); - makeSureAnnotatedEmbeddableAreAccessibleViaReflection(resp); +@Path("/jpa/testreflection") +@Produces(MediaType.TEXT_PLAIN) +public class JPATestReflectionEndpoint { + + @GET + public String test() throws SQLException, TransformerException, IOException { + List errors = new ArrayList<>(); + makeSureNonEntityAreDCE(errors); + makeSureEntitiesAreAccessibleViaReflection(errors); + makeSureNonAnnotatedEmbeddableAreAccessibleViaReflection(errors); + makeSureAnnotatedEmbeddableAreAccessibleViaReflection(errors); String packageName = this.getClass().getPackage().getName(); - makeSureClassAreAccessibleViaReflection(packageName + ".Human", "Unable to enlist @MappedSuperclass", resp); - makeSureClassAreAccessibleViaReflection(packageName + ".Animal", "Unable to enlist entity superclass", resp); - resp.getWriter().write("OK"); + makeSureClassAreAccessibleViaReflection(packageName + ".Human", "Unable to enlist @MappedSuperclass", errors); + makeSureClassAreAccessibleViaReflection(packageName + ".Animal", "Unable to enlist entity superclass", errors); + if (errors.isEmpty()) { + return "OK"; + } else { + return String.join("\n", errors); + } } - private void makeSureClassAreAccessibleViaReflection(String className, String errorMessage, HttpServletResponse resp) + private void makeSureClassAreAccessibleViaReflection(String className, String errorMessage, List errors) throws IOException { try { className = getTrickedClassName(className); @@ -37,11 +49,11 @@ private void makeSureClassAreAccessibleViaReflection(String className, String er Class custClass = Class.forName(className); Object instance = custClass.getDeclaredConstructor().newInstance(); } catch (Exception e) { - reportException(errorMessage, e, resp); + reportException(errorMessage, e, errors); } } - private void makeSureEntitiesAreAccessibleViaReflection(HttpServletResponse resp) throws IOException { + private void makeSureEntitiesAreAccessibleViaReflection(List errors) throws IOException { try { String className = getTrickedClassName(Customer.class.getName()); @@ -50,20 +62,20 @@ private void makeSureEntitiesAreAccessibleViaReflection(HttpServletResponse resp Field id = custClass.getDeclaredField("id"); id.setAccessible(true); if (id.get(instance) != null) { - resp.getWriter().write("id should be reachable and null"); + errors.add("id should be reachable and null"); } Method setter = custClass.getDeclaredMethod("setName", String.class); Method getter = custClass.getDeclaredMethod("getName"); setter.invoke(instance, "Emmanuel"); if (!"Emmanuel".equals(getter.invoke(instance))) { - resp.getWriter().write("getter / setter should be reachable and usable"); + errors.add("getter / setter should be reachable and usable"); } } catch (Exception e) { - reportException(e, resp); + reportException(e, errors); } } - private void makeSureAnnotatedEmbeddableAreAccessibleViaReflection(HttpServletResponse resp) throws IOException { + private void makeSureAnnotatedEmbeddableAreAccessibleViaReflection(List errors) throws IOException { try { String className = getTrickedClassName(WorkAddress.class.getName()); @@ -73,14 +85,14 @@ private void makeSureAnnotatedEmbeddableAreAccessibleViaReflection(HttpServletRe Method getter = custClass.getDeclaredMethod("getCompany"); setter.invoke(instance, "Red Hat"); if (!"Red Hat".equals(getter.invoke(instance))) { - resp.getWriter().write("@Embeddable embeddable should be reachable and usable"); + errors.add("@Embeddable embeddable should be reachable and usable"); } } catch (Exception e) { - reportException(e, resp); + reportException(e, errors); } } - private void makeSureNonAnnotatedEmbeddableAreAccessibleViaReflection(HttpServletResponse resp) throws IOException { + private void makeSureNonAnnotatedEmbeddableAreAccessibleViaReflection(List errors) throws IOException { try { String className = getTrickedClassName(Address.class.getName()); @@ -90,19 +102,19 @@ private void makeSureNonAnnotatedEmbeddableAreAccessibleViaReflection(HttpServle Method getter = custClass.getDeclaredMethod("getStreet1"); setter.invoke(instance, "1 rue du General Leclerc"); if (!"1 rue du General Leclerc".equals(getter.invoke(instance))) { - resp.getWriter().write("Non @Embeddable embeddable getter / setter should be reachable and usable"); + errors.add("Non @Embeddable embeddable getter / setter should be reachable and usable"); } } catch (Exception e) { - reportException(e, resp); + reportException(e, errors); } } - private void makeSureNonEntityAreDCE(HttpServletResponse resp) { + private void makeSureNonEntityAreDCE(List errors) { try { String className = getTrickedClassName(NotAnEntityNotReferenced.class.getName()); Class custClass = Class.forName(className); - resp.getWriter().write("Should not be able to find a non referenced non entity class"); + errors.add("Should not be able to find a non referenced non entity class"); Object instance = custClass.getDeclaredConstructor().newInstance(); } catch (Exception e) { // Expected outcome @@ -118,20 +130,24 @@ private String getTrickedClassName(String className) { return className; } - private void reportException(final Exception e, final HttpServletResponse resp) throws IOException { - reportException(null, e, resp); + private void reportException(final Exception e, final List errors) throws IOException { + reportException(null, e, errors); } - private void reportException(String errorMessage, final Exception e, final HttpServletResponse resp) throws IOException { - final PrintWriter writer = resp.getWriter(); + private void reportException(String errorMessage, final Exception e, final List errors) throws IOException { + StringWriter stringWriter = new StringWriter(); + final PrintWriter writer = new PrintWriter(stringWriter); if (errorMessage != null) { writer.write(errorMessage); writer.write(" "); } - writer.write(e.toString()); + if (e.getMessage() != null) { + writer.write(e.getMessage()); + } writer.append("\n\t"); e.printStackTrace(writer); writer.append("\n\t"); + errors.add(stringWriter.toString()); } } diff --git a/integration-tests/jpa-postgresql/src/main/java/io/quarkus/it/jpa/postgresql/otherpu/EntityWithJsonOtherPU.java b/integration-tests/jpa-postgresql/src/main/java/io/quarkus/it/jpa/postgresql/otherpu/EntityWithJsonOtherPU.java new file mode 100644 index 0000000000000..0793b93f3fcf6 --- /dev/null +++ b/integration-tests/jpa-postgresql/src/main/java/io/quarkus/it/jpa/postgresql/otherpu/EntityWithJsonOtherPU.java @@ -0,0 +1,60 @@ + +package io.quarkus.it.jpa.postgresql.otherpu; + +import java.time.LocalDate; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; + +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.type.SqlTypes; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import io.quarkus.runtime.annotations.RegisterForReflection; + +@Entity +public class EntityWithJsonOtherPU { + @Id + @GeneratedValue + Long id; + + @JdbcTypeCode(SqlTypes.JSON) + ToBeSerializedWithDateTime json; + + public EntityWithJsonOtherPU() { + } + + public EntityWithJsonOtherPU(ToBeSerializedWithDateTime data) { + this.json = data; + } + + @Override + public String toString() { + return "EntityWithJsonOtherPU{" + + "id=" + id + + ", json=" + json + + '}'; + } + + @RegisterForReflection + public static class ToBeSerializedWithDateTime { + @JsonProperty + LocalDate date; + + public ToBeSerializedWithDateTime() { + } + + public ToBeSerializedWithDateTime(LocalDate date) { + this.date = date; + } + + @Override + public String toString() { + return "ToBeSerializedWithDateTime{" + + "date=" + date + + '}'; + } + } +} diff --git a/integration-tests/jpa-postgresql/src/main/java/io/quarkus/it/jpa/postgresql/otherpu/JsonFormatMapper.java b/integration-tests/jpa-postgresql/src/main/java/io/quarkus/it/jpa/postgresql/otherpu/JsonFormatMapper.java new file mode 100644 index 0000000000000..70925dc93bf48 --- /dev/null +++ b/integration-tests/jpa-postgresql/src/main/java/io/quarkus/it/jpa/postgresql/otherpu/JsonFormatMapper.java @@ -0,0 +1,23 @@ +package io.quarkus.it.jpa.postgresql.otherpu; + +import org.hibernate.type.descriptor.WrapperOptions; +import org.hibernate.type.descriptor.java.JavaType; +import org.hibernate.type.format.FormatMapper; + +import io.quarkus.hibernate.orm.JsonFormat; +import io.quarkus.hibernate.orm.PersistenceUnitExtension; + +@JsonFormat +@PersistenceUnitExtension("other") +public class JsonFormatMapper implements FormatMapper { + + @Override + public T fromString(CharSequence charSequence, JavaType javaType, WrapperOptions wrapperOptions) { + throw new UnsupportedOperationException("I cannot convert anything from JSON."); + } + + @Override + public String toString(T value, JavaType javaType, WrapperOptions wrapperOptions) { + throw new UnsupportedOperationException("I cannot convert anything to JSON."); + } +} diff --git a/integration-tests/jpa-postgresql/src/main/resources/application.properties b/integration-tests/jpa-postgresql/src/main/resources/application.properties index c3f7971387997..90370fd628984 100644 --- a/integration-tests/jpa-postgresql/src/main/resources/application.properties +++ b/integration-tests/jpa-postgresql/src/main/resources/application.properties @@ -3,8 +3,12 @@ quarkus.datasource.password=hibernate_orm_test quarkus.datasource.jdbc.url=${postgres.url} quarkus.datasource.jdbc.max-size=8 +quarkus.hibernate-orm.packages=io.quarkus.it.jpa.postgresql quarkus.hibernate-orm.database.generation=drop-and-create quarkus.hibernate-orm.database.generation.create-schemas=true +# Define non-default PU so that we can configure a custom JSON format mapper. The default PU is using the default mapper. +quarkus.hibernate-orm."other".datasource= +quarkus.hibernate-orm."other".packages=io.quarkus.it.jpa.postgresql.otherpu #Necessary for assertions in JPAFunctionalityInGraalITCase: quarkus.native.enable-reports=true \ No newline at end of file diff --git a/integration-tests/jpa-postgresql/src/test/java/io/quarkus/it/jpa/postgresql/JPAFunctionalityTest.java b/integration-tests/jpa-postgresql/src/test/java/io/quarkus/it/jpa/postgresql/JPAFunctionalityTest.java index 559b6451726cc..0ccf51d52d76a 100644 --- a/integration-tests/jpa-postgresql/src/test/java/io/quarkus/it/jpa/postgresql/JPAFunctionalityTest.java +++ b/integration-tests/jpa-postgresql/src/test/java/io/quarkus/it/jpa/postgresql/JPAFunctionalityTest.java @@ -14,8 +14,18 @@ public class JPAFunctionalityTest { @Test - public void testJPAFunctionalityFromServlet() throws Exception { - RestAssured.when().get("/jpa/testfunctionality").then().body(is("OK")); + public void base() { + RestAssured.when().get("/jpa/testfunctionality/base").then().body(is("OK")); + } + + @Test + public void uuid() { + RestAssured.when().get("/jpa/testfunctionality/uuid").then().body(is("OK")); + } + + @Test + public void json() { + RestAssured.when().get("/jpa/testfunctionality/json").then().body(is("OK")); } } diff --git a/integration-tests/jpa-without-entity/pom.xml b/integration-tests/jpa-without-entity/pom.xml index 11fdfcc39aaab..c710184b9bd70 100644 --- a/integration-tests/jpa-without-entity/pom.xml +++ b/integration-tests/jpa-without-entity/pom.xml @@ -22,28 +22,29 @@ quarkus-hibernate-orm - io.quarkus - quarkus-resteasy + quarkus-jdbc-postgresql io.quarkus - quarkus-junit5 - test + quarkus-resteasy-reactive - io.rest-assured - rest-assured - test + io.quarkus + quarkus-resteasy-reactive-jackson + + io.quarkus - quarkus-jdbc-postgresql + quarkus-junit5 + test - io.quarkus - quarkus-resteasy-jackson + io.rest-assured + rest-assured + test @@ -75,7 +76,7 @@ io.quarkus - quarkus-resteasy-deployment + quarkus-resteasy-reactive-deployment ${project.version} pom test @@ -88,7 +89,7 @@ io.quarkus - quarkus-resteasy-jackson-deployment + quarkus-resteasy-reactive-jackson-deployment ${project.version} pom test diff --git a/integration-tests/jpa/pom.xml b/integration-tests/jpa/pom.xml index 25e7369b80cde..da832c2141f6d 100644 --- a/integration-tests/jpa/pom.xml +++ b/integration-tests/jpa/pom.xml @@ -13,10 +13,6 @@ Quarkus - Integration Tests - JPA - - io.quarkus - quarkus-resteasy - io.quarkus quarkus-hibernate-orm @@ -30,6 +26,10 @@ assertj-core compile + + io.quarkus + quarkus-resteasy-reactive + @@ -82,7 +82,7 @@ io.quarkus - quarkus-resteasy-deployment + quarkus-resteasy-reactive-deployment ${project.version} pom test diff --git a/integration-tests/jpa/src/main/java/io/quarkus/it/jpa/attributeconverter/AttributeConverterResource.java b/integration-tests/jpa/src/main/java/io/quarkus/it/jpa/attributeconverter/AttributeConverterResource.java index bcf2fdab0c75b..3d6d9e17ae11a 100644 --- a/integration-tests/jpa/src/main/java/io/quarkus/it/jpa/attributeconverter/AttributeConverterResource.java +++ b/integration-tests/jpa/src/main/java/io/quarkus/it/jpa/attributeconverter/AttributeConverterResource.java @@ -9,7 +9,7 @@ import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.MediaType; -import org.jboss.resteasy.annotations.jaxrs.QueryParam; +import org.jboss.resteasy.reactive.RestQuery; @Path("/attribute-converter") @ApplicationScoped @@ -22,7 +22,7 @@ public class AttributeConverterResource { @Path("/with-cdi") @Produces(MediaType.TEXT_PLAIN) @Transactional - public String withCdi(@QueryParam String theData) { + public String withCdi(@RestQuery String theData) { EntityWithAttributeConverters entity = new EntityWithAttributeConverters(); entity.setMyDataRequiringCDI(new MyDataRequiringCDI(theData)); em.persist(entity); @@ -39,7 +39,7 @@ public String withCdi(@QueryParam String theData) { @Path("/without-cdi") @Produces(MediaType.TEXT_PLAIN) @Transactional - public String withoutCdi(@QueryParam String theData) { + public String withoutCdi(@RestQuery String theData) { EntityWithAttributeConverters entity = new EntityWithAttributeConverters(); entity.setMyDataNotRequiringCDI(new MyDataNotRequiringCDI(theData)); em.persist(entity); diff --git a/integration-tests/jpa/src/main/java/io/quarkus/it/jpa/defaultcatalogandschema/DefaultCatalogAndSchemaResource.java b/integration-tests/jpa/src/main/java/io/quarkus/it/jpa/defaultcatalogandschema/DefaultCatalogAndSchemaResource.java index 6e6fd83ed8575..dee3fc27abd3f 100644 --- a/integration-tests/jpa/src/main/java/io/quarkus/it/jpa/defaultcatalogandschema/DefaultCatalogAndSchemaResource.java +++ b/integration-tests/jpa/src/main/java/io/quarkus/it/jpa/defaultcatalogandschema/DefaultCatalogAndSchemaResource.java @@ -14,7 +14,7 @@ import org.hibernate.Session; import org.hibernate.type.StandardBasicTypes; -import org.jboss.resteasy.annotations.jaxrs.QueryParam; +import org.jboss.resteasy.reactive.RestQuery; @Path("/default-catalog-and-schema") @ApplicationScoped @@ -27,7 +27,7 @@ public class DefaultCatalogAndSchemaResource { @Path("/test") @Produces(MediaType.TEXT_PLAIN) @Transactional - public String test(@QueryParam String expectedSchema) { + public String test(@RestQuery String expectedSchema) { assertThat(findUsingNativeQuery(expectedSchema, "foo")).isEmpty(); EntityWithDefaultCatalogAndSchema entity = new EntityWithDefaultCatalogAndSchema(); diff --git a/integration-tests/jpa/src/main/java/io/quarkus/it/jpa/elementcollection/OpeningTimes.java b/integration-tests/jpa/src/main/java/io/quarkus/it/jpa/elementcollection/OpeningTimes.java index e4351539b54c2..ac2bc3edb223a 100644 --- a/integration-tests/jpa/src/main/java/io/quarkus/it/jpa/elementcollection/OpeningTimes.java +++ b/integration-tests/jpa/src/main/java/io/quarkus/it/jpa/elementcollection/OpeningTimes.java @@ -16,7 +16,6 @@ import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.Id; -import jakarta.validation.constraints.NotNull; import org.hibernate.annotations.Cascade; import org.hibernate.annotations.GenericGenerator; @@ -28,7 +27,6 @@ public class OpeningTimes { @GenericGenerator(name = "UUID", strategy = "org.hibernate.id.UUIDGenerator") private UUID id; - @NotNull private String name; private String description; private LocalTime timeFrom; diff --git a/integration-tests/jpa-h2/src/main/java/io/quarkus/it/jpa/h2/generics/AbstractGenericMappedSuperType.java b/integration-tests/jpa/src/main/java/io/quarkus/it/jpa/generics/AbstractGenericMappedSuperType.java similarity index 86% rename from integration-tests/jpa-h2/src/main/java/io/quarkus/it/jpa/h2/generics/AbstractGenericMappedSuperType.java rename to integration-tests/jpa/src/main/java/io/quarkus/it/jpa/generics/AbstractGenericMappedSuperType.java index d93b8d28d0a74..853f37d9f7946 100644 --- a/integration-tests/jpa-h2/src/main/java/io/quarkus/it/jpa/h2/generics/AbstractGenericMappedSuperType.java +++ b/integration-tests/jpa/src/main/java/io/quarkus/it/jpa/generics/AbstractGenericMappedSuperType.java @@ -1,4 +1,4 @@ -package io.quarkus.it.jpa.h2.generics; +package io.quarkus.it.jpa.generics; import jakarta.persistence.Basic; import jakarta.persistence.Lob; diff --git a/integration-tests/jpa-h2/src/main/java/io/quarkus/it/jpa/h2/generics/IntermediateAbstractMapped.java b/integration-tests/jpa/src/main/java/io/quarkus/it/jpa/generics/IntermediateAbstractMapped.java similarity index 96% rename from integration-tests/jpa-h2/src/main/java/io/quarkus/it/jpa/h2/generics/IntermediateAbstractMapped.java rename to integration-tests/jpa/src/main/java/io/quarkus/it/jpa/generics/IntermediateAbstractMapped.java index fb1ca748d3671..4ccbe37ee7ad3 100644 --- a/integration-tests/jpa-h2/src/main/java/io/quarkus/it/jpa/h2/generics/IntermediateAbstractMapped.java +++ b/integration-tests/jpa/src/main/java/io/quarkus/it/jpa/generics/IntermediateAbstractMapped.java @@ -1,4 +1,4 @@ -package io.quarkus.it.jpa.h2.generics; +package io.quarkus.it.jpa.generics; import java.io.Serializable; import java.util.Objects; diff --git a/integration-tests/jpa-h2/src/main/java/io/quarkus/it/jpa/h2/generics/SnapshotEventEntry.java b/integration-tests/jpa/src/main/java/io/quarkus/it/jpa/generics/SnapshotEventEntry.java similarity index 95% rename from integration-tests/jpa-h2/src/main/java/io/quarkus/it/jpa/h2/generics/SnapshotEventEntry.java rename to integration-tests/jpa/src/main/java/io/quarkus/it/jpa/generics/SnapshotEventEntry.java index f44a4f87dc7b2..a2ef3e11440f3 100644 --- a/integration-tests/jpa-h2/src/main/java/io/quarkus/it/jpa/h2/generics/SnapshotEventEntry.java +++ b/integration-tests/jpa/src/main/java/io/quarkus/it/jpa/generics/SnapshotEventEntry.java @@ -1,4 +1,4 @@ -package io.quarkus.it.jpa.h2.generics; +package io.quarkus.it.jpa.generics; import jakarta.persistence.Entity; diff --git a/integration-tests/jpa/src/main/java/io/quarkus/it/jpa/nonquarkus/NonQuarkusApiResource.java b/integration-tests/jpa/src/main/java/io/quarkus/it/jpa/nonquarkus/NonQuarkusApiResource.java new file mode 100644 index 0000000000000..1c61195643a59 --- /dev/null +++ b/integration-tests/jpa/src/main/java/io/quarkus/it/jpa/nonquarkus/NonQuarkusApiResource.java @@ -0,0 +1,130 @@ +package io.quarkus.it.jpa.nonquarkus; + +import java.io.IOException; +import java.util.List; +import java.util.UUID; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.persistence.EntityManager; +import jakarta.persistence.EntityManagerFactory; +import jakarta.persistence.EntityTransaction; +import jakarta.persistence.PersistenceUnit; +import jakarta.persistence.TypedQuery; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Root; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +@Path("/non-quarkus") +@ApplicationScoped +public class NonQuarkusApiResource { + + @PersistenceUnit + EntityManagerFactory entityManagerFactory; + + @GET + @Path("test") + public String test() throws IOException { + //Cleanup any existing data: + deleteAllPerson(entityManagerFactory); + + //Store some well known Person instances we can then test on: + storeTestPersons(entityManagerFactory); + + //Load all persons and run some checks on the query results: + verifyListOfExistingPersons(entityManagerFactory); + + //Try a JPA named query: + verifyJPANamedQuery(entityManagerFactory); + + deleteAllPerson(entityManagerFactory); + + return "OK"; + } + + private static void verifyJPANamedQuery(final EntityManagerFactory emf) { + EntityManager em = emf.createEntityManager(); + EntityTransaction transaction = em.getTransaction(); + transaction.begin(); + TypedQuery typedQuery = em.createNamedQuery( + "get_person_by_name", Person.class); + typedQuery.setParameter("name", "Quarkus"); + final Person singleResult = typedQuery.getSingleResult(); + + if (!singleResult.getName().equals("Quarkus")) { + throw new RuntimeException("Wrong result from named JPA query"); + } + + transaction.commit(); + em.close(); + } + + private static void verifyListOfExistingPersons(final EntityManagerFactory emf) { + EntityManager em = emf.createEntityManager(); + EntityTransaction transaction = em.getTransaction(); + transaction.begin(); + listExistingPersons(em); + transaction.commit(); + em.close(); + } + + private static void storeTestPersons(final EntityManagerFactory emf) { + EntityManager em = emf.createEntityManager(); + EntityTransaction transaction = em.getTransaction(); + transaction.begin(); + persistNewPerson(em, "Gizmo"); + persistNewPerson(em, "Quarkus"); + persistNewPerson(em, "Hibernate ORM"); + transaction.commit(); + em.close(); + } + + private static void deleteAllPerson(final EntityManagerFactory emf) { + EntityManager em = emf.createEntityManager(); + EntityTransaction transaction = em.getTransaction(); + transaction.begin(); + em.createNativeQuery("Delete from Person").executeUpdate(); + transaction.commit(); + em.close(); + } + + private static void listExistingPersons(EntityManager em) { + CriteriaBuilder cb = em.getCriteriaBuilder(); + + CriteriaQuery cq = cb.createQuery(Person.class); + Root from = cq.from(Person.class); + cq.select(from).orderBy(cb.asc(from.get("name"))); + TypedQuery q = em.createQuery(cq); + List allpersons = q.getResultList(); + if (allpersons.size() != 3) { + throw new RuntimeException("Incorrect number of results"); + } + if (!allpersons.get(0).getName().equals("Gizmo")) { + throw new RuntimeException("Incorrect order of results"); + } + StringBuilder sb = new StringBuilder("list of stored Person names:\n\t"); + for (Person p : allpersons) { + p.describeFully(sb); + sb.append("\n\t"); + if (p.getStatus() != Status.LIVING) { + throw new RuntimeException("Incorrect status " + p); + } + } + sb.append("\nList complete.\n"); + System.out.print(sb); + } + + private static void persistNewPerson(EntityManager entityManager, String name) { + Person person = new Person(); + person.setName(name); + person.setStatus(Status.LIVING); + person.setAddress(new SequencedAddress("Street " + randomName())); + entityManager.persist(person); + } + + private static String randomName() { + return UUID.randomUUID().toString(); + } + +} diff --git a/integration-tests/jpa/src/main/java/io/quarkus/it/jpa/nonquarkus/Person.java b/integration-tests/jpa/src/main/java/io/quarkus/it/jpa/nonquarkus/Person.java new file mode 100644 index 0000000000000..2a00014b5854c --- /dev/null +++ b/integration-tests/jpa/src/main/java/io/quarkus/it/jpa/nonquarkus/Person.java @@ -0,0 +1,71 @@ +package io.quarkus.it.jpa.nonquarkus; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.NamedQuery; + +@Entity +@NamedQuery(name = "get_person_by_name", query = "select p from Person p where name = :name") +public class Person { + + private long id; + private String name; + private SequencedAddress address; + private Status status; + + public Person() { + } + + public Person(long id, String name, SequencedAddress address) { + this.id = id; + this.name = name; + this.address = address; + } + + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "personSeq") + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @ManyToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY) + public SequencedAddress getAddress() { + return address; + } + + public void setAddress(SequencedAddress address) { + this.address = address; + } + + public Status getStatus() { + return status; + } + + public void setStatus(Status status) { + this.status = status; + } + + public void describeFully(StringBuilder sb) { + sb.append("Person with id=").append(id).append(", name='").append(name).append("', status='").append(status) + .append("', address { "); + getAddress().describeFully(sb); + sb.append(" }"); + } +} diff --git a/integration-tests/jpa/src/main/java/io/quarkus/it/jpa/nonquarkus/SequencedAddress.java b/integration-tests/jpa/src/main/java/io/quarkus/it/jpa/nonquarkus/SequencedAddress.java new file mode 100644 index 0000000000000..bc469e79ed786 --- /dev/null +++ b/integration-tests/jpa/src/main/java/io/quarkus/it/jpa/nonquarkus/SequencedAddress.java @@ -0,0 +1,42 @@ +package io.quarkus.it.jpa.nonquarkus; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; + +@Entity +public class SequencedAddress { + + private long id; + private String street; + + public SequencedAddress() { + } + + public SequencedAddress(String street) { + this.street = street; + } + + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "addressSeq") + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public String getStreet() { + return street; + } + + public void setStreet(String name) { + this.street = name; + } + + public void describeFully(StringBuilder sb) { + sb.append("Address with id=").append(id).append(", street='").append(street).append("'"); + } +} diff --git a/integration-tests/jpa/src/main/java/io/quarkus/it/jpa/nonquarkus/Status.java b/integration-tests/jpa/src/main/java/io/quarkus/it/jpa/nonquarkus/Status.java new file mode 100644 index 0000000000000..9cdcc0e0445fb --- /dev/null +++ b/integration-tests/jpa/src/main/java/io/quarkus/it/jpa/nonquarkus/Status.java @@ -0,0 +1,6 @@ +package io.quarkus.it.jpa.nonquarkus; + +public enum Status { + LIVING, + DECEASED +} diff --git a/integration-tests/jpa-h2/src/main/java/io/quarkus/it/jpa/h2/basicproxy/AbstractEntity.java b/integration-tests/jpa/src/main/java/io/quarkus/it/jpa/proxy/AbstractEntity.java similarity index 95% rename from integration-tests/jpa-h2/src/main/java/io/quarkus/it/jpa/h2/basicproxy/AbstractEntity.java rename to integration-tests/jpa/src/main/java/io/quarkus/it/jpa/proxy/AbstractEntity.java index eac2f69234373..7adac2ceb7381 100644 --- a/integration-tests/jpa-h2/src/main/java/io/quarkus/it/jpa/h2/basicproxy/AbstractEntity.java +++ b/integration-tests/jpa/src/main/java/io/quarkus/it/jpa/proxy/AbstractEntity.java @@ -1,4 +1,4 @@ -package io.quarkus.it.jpa.h2.basicproxy; +package io.quarkus.it.jpa.proxy; import java.io.Serializable; import java.util.Objects; diff --git a/integration-tests/jpa-h2/src/main/java/io/quarkus/it/jpa/h2/proxy/Cat.java b/integration-tests/jpa/src/main/java/io/quarkus/it/jpa/proxy/Cat.java similarity index 86% rename from integration-tests/jpa-h2/src/main/java/io/quarkus/it/jpa/h2/proxy/Cat.java rename to integration-tests/jpa/src/main/java/io/quarkus/it/jpa/proxy/Cat.java index b1ff52532b1e4..260bc11a73f20 100644 --- a/integration-tests/jpa-h2/src/main/java/io/quarkus/it/jpa/h2/proxy/Cat.java +++ b/integration-tests/jpa/src/main/java/io/quarkus/it/jpa/proxy/Cat.java @@ -1,4 +1,4 @@ -package io.quarkus.it.jpa.h2.proxy; +package io.quarkus.it.jpa.proxy; import jakarta.persistence.DiscriminatorValue; import jakarta.persistence.Entity; diff --git a/integration-tests/jpa-h2/src/main/java/io/quarkus/it/jpa/h2/CompanyCustomer.java b/integration-tests/jpa/src/main/java/io/quarkus/it/jpa/proxy/CompanyCustomer.java similarity index 91% rename from integration-tests/jpa-h2/src/main/java/io/quarkus/it/jpa/h2/CompanyCustomer.java rename to integration-tests/jpa/src/main/java/io/quarkus/it/jpa/proxy/CompanyCustomer.java index 7e03220c7a8e2..4bc86c874a943 100644 --- a/integration-tests/jpa-h2/src/main/java/io/quarkus/it/jpa/h2/CompanyCustomer.java +++ b/integration-tests/jpa/src/main/java/io/quarkus/it/jpa/proxy/CompanyCustomer.java @@ -1,4 +1,4 @@ -package io.quarkus.it.jpa.h2; +package io.quarkus.it.jpa.proxy; import jakarta.persistence.Entity; diff --git a/integration-tests/jpa-h2/src/main/java/io/quarkus/it/jpa/h2/basicproxy/ConcreteEntity.java b/integration-tests/jpa/src/main/java/io/quarkus/it/jpa/proxy/ConcreteEntity.java similarity index 85% rename from integration-tests/jpa-h2/src/main/java/io/quarkus/it/jpa/h2/basicproxy/ConcreteEntity.java rename to integration-tests/jpa/src/main/java/io/quarkus/it/jpa/proxy/ConcreteEntity.java index ba1b07d4bac83..48a58872a4903 100644 --- a/integration-tests/jpa-h2/src/main/java/io/quarkus/it/jpa/h2/basicproxy/ConcreteEntity.java +++ b/integration-tests/jpa/src/main/java/io/quarkus/it/jpa/proxy/ConcreteEntity.java @@ -1,4 +1,4 @@ -package io.quarkus.it.jpa.h2.basicproxy; +package io.quarkus.it.jpa.proxy; import jakarta.persistence.DiscriminatorValue; import jakarta.persistence.Entity; diff --git a/integration-tests/jpa-h2/src/main/java/io/quarkus/it/jpa/h2/Customer.java b/integration-tests/jpa/src/main/java/io/quarkus/it/jpa/proxy/Customer.java similarity index 87% rename from integration-tests/jpa-h2/src/main/java/io/quarkus/it/jpa/h2/Customer.java rename to integration-tests/jpa/src/main/java/io/quarkus/it/jpa/proxy/Customer.java index 960a417b6bff1..458973c6554f4 100644 --- a/integration-tests/jpa-h2/src/main/java/io/quarkus/it/jpa/h2/Customer.java +++ b/integration-tests/jpa/src/main/java/io/quarkus/it/jpa/proxy/Customer.java @@ -1,4 +1,4 @@ -package io.quarkus.it.jpa.h2; +package io.quarkus.it.jpa.proxy; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; diff --git a/integration-tests/jpa-h2/src/main/java/io/quarkus/it/jpa/h2/proxy/Dog.java b/integration-tests/jpa/src/main/java/io/quarkus/it/jpa/proxy/Dog.java similarity index 94% rename from integration-tests/jpa-h2/src/main/java/io/quarkus/it/jpa/h2/proxy/Dog.java rename to integration-tests/jpa/src/main/java/io/quarkus/it/jpa/proxy/Dog.java index 6be1e4e5ac330..30e16e8356b2a 100644 --- a/integration-tests/jpa-h2/src/main/java/io/quarkus/it/jpa/h2/proxy/Dog.java +++ b/integration-tests/jpa/src/main/java/io/quarkus/it/jpa/proxy/Dog.java @@ -1,4 +1,4 @@ -package io.quarkus.it.jpa.h2.proxy; +package io.quarkus.it.jpa.proxy; import jakarta.persistence.DiscriminatorValue; import jakarta.persistence.Entity; diff --git a/integration-tests/jpa-h2/src/main/java/io/quarkus/it/jpa/h2/proxy/DogProxy.java b/integration-tests/jpa/src/main/java/io/quarkus/it/jpa/proxy/DogProxy.java similarity index 77% rename from integration-tests/jpa-h2/src/main/java/io/quarkus/it/jpa/h2/proxy/DogProxy.java rename to integration-tests/jpa/src/main/java/io/quarkus/it/jpa/proxy/DogProxy.java index 55bf0a66902f2..d128fee16fe11 100644 --- a/integration-tests/jpa-h2/src/main/java/io/quarkus/it/jpa/h2/proxy/DogProxy.java +++ b/integration-tests/jpa/src/main/java/io/quarkus/it/jpa/proxy/DogProxy.java @@ -1,4 +1,4 @@ -package io.quarkus.it.jpa.h2.proxy; +package io.quarkus.it.jpa.proxy; public interface DogProxy { diff --git a/integration-tests/jpa-h2/src/main/java/io/quarkus/it/jpa/h2/IndividualCustomer.java b/integration-tests/jpa/src/main/java/io/quarkus/it/jpa/proxy/IndividualCustomer.java similarity index 80% rename from integration-tests/jpa-h2/src/main/java/io/quarkus/it/jpa/h2/IndividualCustomer.java rename to integration-tests/jpa/src/main/java/io/quarkus/it/jpa/proxy/IndividualCustomer.java index e76267b2e731b..48c5fc23166a1 100644 --- a/integration-tests/jpa-h2/src/main/java/io/quarkus/it/jpa/h2/IndividualCustomer.java +++ b/integration-tests/jpa/src/main/java/io/quarkus/it/jpa/proxy/IndividualCustomer.java @@ -1,4 +1,4 @@ -package io.quarkus.it.jpa.h2; +package io.quarkus.it.jpa.proxy; import jakarta.persistence.Entity; diff --git a/integration-tests/jpa-h2/src/main/java/io/quarkus/it/jpa/h2/proxy/Pet.java b/integration-tests/jpa/src/main/java/io/quarkus/it/jpa/proxy/Pet.java similarity index 95% rename from integration-tests/jpa-h2/src/main/java/io/quarkus/it/jpa/h2/proxy/Pet.java rename to integration-tests/jpa/src/main/java/io/quarkus/it/jpa/proxy/Pet.java index fad96348116ea..8da2622a49b81 100644 --- a/integration-tests/jpa-h2/src/main/java/io/quarkus/it/jpa/h2/proxy/Pet.java +++ b/integration-tests/jpa/src/main/java/io/quarkus/it/jpa/proxy/Pet.java @@ -1,4 +1,4 @@ -package io.quarkus.it.jpa.h2.proxy; +package io.quarkus.it.jpa.proxy; import jakarta.persistence.DiscriminatorColumn; import jakarta.persistence.DiscriminatorValue; diff --git a/integration-tests/jpa-h2/src/main/java/io/quarkus/it/jpa/h2/proxy/PetOwner.java b/integration-tests/jpa/src/main/java/io/quarkus/it/jpa/proxy/PetOwner.java similarity index 95% rename from integration-tests/jpa-h2/src/main/java/io/quarkus/it/jpa/h2/proxy/PetOwner.java rename to integration-tests/jpa/src/main/java/io/quarkus/it/jpa/proxy/PetOwner.java index 93fe8efa7c6fb..8c430db0c82bb 100644 --- a/integration-tests/jpa-h2/src/main/java/io/quarkus/it/jpa/h2/proxy/PetOwner.java +++ b/integration-tests/jpa/src/main/java/io/quarkus/it/jpa/proxy/PetOwner.java @@ -1,4 +1,4 @@ -package io.quarkus.it.jpa.h2.proxy; +package io.quarkus.it.jpa.proxy; import jakarta.persistence.CascadeType; import jakarta.persistence.Entity; diff --git a/integration-tests/jpa-h2/src/main/java/io/quarkus/it/jpa/h2/proxy/PetProxy.java b/integration-tests/jpa/src/main/java/io/quarkus/it/jpa/proxy/PetProxy.java similarity index 60% rename from integration-tests/jpa-h2/src/main/java/io/quarkus/it/jpa/h2/proxy/PetProxy.java rename to integration-tests/jpa/src/main/java/io/quarkus/it/jpa/proxy/PetProxy.java index 225f4b56a64ac..cac54dad6f5d5 100644 --- a/integration-tests/jpa-h2/src/main/java/io/quarkus/it/jpa/h2/proxy/PetProxy.java +++ b/integration-tests/jpa/src/main/java/io/quarkus/it/jpa/proxy/PetProxy.java @@ -1,4 +1,4 @@ -package io.quarkus.it.jpa.h2.proxy; +package io.quarkus.it.jpa.proxy; public interface PetProxy { diff --git a/integration-tests/jpa-h2/src/main/java/io/quarkus/it/jpa/h2/Project.java b/integration-tests/jpa/src/main/java/io/quarkus/it/jpa/proxy/Project.java similarity index 92% rename from integration-tests/jpa-h2/src/main/java/io/quarkus/it/jpa/h2/Project.java rename to integration-tests/jpa/src/main/java/io/quarkus/it/jpa/proxy/Project.java index b7ad744063c37..3e6f81258dc84 100644 --- a/integration-tests/jpa-h2/src/main/java/io/quarkus/it/jpa/h2/Project.java +++ b/integration-tests/jpa/src/main/java/io/quarkus/it/jpa/proxy/Project.java @@ -1,4 +1,4 @@ -package io.quarkus.it.jpa.h2; +package io.quarkus.it.jpa.proxy; import jakarta.persistence.CascadeType; import jakarta.persistence.Entity; diff --git a/integration-tests/jpa-h2/src/main/java/io/quarkus/it/jpa/h2/proxy/ProxyTestEndpoint.java b/integration-tests/jpa/src/main/java/io/quarkus/it/jpa/proxy/ProxyTestEndpoint.java similarity index 66% rename from integration-tests/jpa-h2/src/main/java/io/quarkus/it/jpa/h2/proxy/ProxyTestEndpoint.java rename to integration-tests/jpa/src/main/java/io/quarkus/it/jpa/proxy/ProxyTestEndpoint.java index 9e592ac9e98c8..be2a1564d561f 100644 --- a/integration-tests/jpa-h2/src/main/java/io/quarkus/it/jpa/h2/proxy/ProxyTestEndpoint.java +++ b/integration-tests/jpa/src/main/java/io/quarkus/it/jpa/proxy/ProxyTestEndpoint.java @@ -1,24 +1,25 @@ -package io.quarkus.it.jpa.h2.proxy; +package io.quarkus.it.jpa.proxy; -import java.io.IOException; +import java.util.List; import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.event.Observes; import jakarta.inject.Inject; import jakarta.persistence.EntityManager; -import jakarta.servlet.ServletException; -import jakarta.servlet.annotation.WebServlet; -import jakarta.servlet.http.HttpServlet; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; import jakarta.transaction.Transactional; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import io.quarkus.narayana.jta.QuarkusTransaction; import io.quarkus.narayana.jta.runtime.TransactionConfiguration; import io.quarkus.runtime.StartupEvent; -@WebServlet(urlPatterns = "/jpa-h2/testproxy") @ApplicationScoped -public class ProxyTestEndpoint extends HttpServlet { +@Path("/proxy/") +@Produces(MediaType.TEXT_PLAIN) +public class ProxyTestEndpoint { @Inject EntityManager entityManager; @@ -26,6 +27,11 @@ public class ProxyTestEndpoint extends HttpServlet { @Transactional @TransactionConfiguration(timeoutFromConfigProperty = "dummy.transaction.timeout") public void setup(@Observes StartupEvent startupEvent) { + ConcreteEntity entity = new ConcreteEntity(); + entity.id = "1"; + entity.type = "Concrete"; + entityManager.persist(entity); + Pet pet = new Pet(); pet.setId(1); pet.setName("Goose"); @@ -59,7 +65,17 @@ public void setup(@Observes StartupEvent startupEvent) { petOwner.setPet(pet); entityManager.persist(petOwner); + } + @GET + @Path("basic") + @Transactional + public String testBasic() { + final List list = entityManager.createQuery("from ConcreteEntity").getResultList(); + if (list.size() != 1) { + throw new RuntimeException("Expected 1 result, got " + list.size()); + } + return "OK"; } /** @@ -67,9 +83,10 @@ public void setup(@Observes StartupEvent startupEvent) { * * We need to do our own proxy generation at build time, so this tests that the logic matches what hibernate expects */ - @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { - + @GET + @Path("inheritance") + @Transactional + public String inheritance() { PetOwner owner = entityManager.find(PetOwner.class, 1); expectEquals("Stuart", owner.getName()); expectEquals("Generic pet noises", owner.getPet().makeNoise()); @@ -97,8 +114,30 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws Se expectFalse(owner.getPet() instanceof Pet); expectTrue(owner.getPet() instanceof DogProxy); - resp.getWriter().write("OK"); + return "OK"; + } + @GET + @Path("enhanced") + public String testEnhanced() { + //Define the test data: + CompanyCustomer company = new CompanyCustomer(); + company.companyname = "Quarked consulting, inc."; + Project project = new Project(); + project.name = "Hibernate RX"; + project.customer = company; + + //Store the test model: + QuarkusTransaction.requiringNew() + .run(() -> entityManager.persist(project)); + final Integer testId = project.id; + expectTrue(testId != null); + + //Now try to load it, should trigger the use of enhanced proxies: + QuarkusTransaction.requiringNew() + .run(() -> entityManager.find(Project.class, testId)); + + return "OK"; } void expectEquals(Object expected, Object actual) { diff --git a/integration-tests/jpa/src/main/resources/application.properties b/integration-tests/jpa/src/main/resources/application.properties index d0642c9cb3b42..ce0333451d50d 100644 --- a/integration-tests/jpa/src/main/resources/application.properties +++ b/integration-tests/jpa/src/main/resources/application.properties @@ -5,4 +5,6 @@ quarkus.hibernate-orm.database.generation=drop-and-create quarkus.hibernate-orm.metadata-builder-contributor=io.quarkus.it.jpa.defaultcatalogandschema.Schema1MetadataBuilderContributor -quarkus.hibernate-orm.multitenant=DISCRIMINATOR \ No newline at end of file +quarkus.hibernate-orm.multitenant=DISCRIMINATOR + +dummy.transaction.timeout=30 \ No newline at end of file diff --git a/integration-tests/jpa/src/test/java/io/quarkus/it/jpa/nonquarkus/NonQuarkusApiInGraalITCase.java b/integration-tests/jpa/src/test/java/io/quarkus/it/jpa/nonquarkus/NonQuarkusApiInGraalITCase.java new file mode 100644 index 0000000000000..1fa655987df8a --- /dev/null +++ b/integration-tests/jpa/src/test/java/io/quarkus/it/jpa/nonquarkus/NonQuarkusApiInGraalITCase.java @@ -0,0 +1,7 @@ +package io.quarkus.it.jpa.nonquarkus; + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +public class NonQuarkusApiInGraalITCase extends NonQuarkusApiTest { +} diff --git a/integration-tests/jpa/src/test/java/io/quarkus/it/jpa/nonquarkus/NonQuarkusApiTest.java b/integration-tests/jpa/src/test/java/io/quarkus/it/jpa/nonquarkus/NonQuarkusApiTest.java new file mode 100644 index 0000000000000..490af87e39f48 --- /dev/null +++ b/integration-tests/jpa/src/test/java/io/quarkus/it/jpa/nonquarkus/NonQuarkusApiTest.java @@ -0,0 +1,29 @@ +package io.quarkus.it.jpa.nonquarkus; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.core.Is.is; + +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.QuarkusTest; + +/** + * Test usage of JPA in a way that is not recommended with Quarkus: + * creating the entity manager manually, using `em.getTransaction()`, ... + *

    + * This is mainly tested to ensure migration is relatively easy, like we test `persistence.xml` support: + * we expect developers to move to `@Inject EntityManager em;` + * and `QuarkusTransaction`/`UserTransaction` for best results. + */ +@QuarkusTest +public class NonQuarkusApiTest { + + @Test + public void test() { + given().queryParam("expectedSchema", "SCHEMA1") + .when().get("/jpa-test/non-quarkus/test").then() + .body(is("OK")) + .statusCode(200); + } + +} diff --git a/integration-tests/jpa/src/test/java/io/quarkus/it/jpa/proxy/ProxyInGraalITCase.java b/integration-tests/jpa/src/test/java/io/quarkus/it/jpa/proxy/ProxyInGraalITCase.java new file mode 100644 index 0000000000000..3d067e929eeb0 --- /dev/null +++ b/integration-tests/jpa/src/test/java/io/quarkus/it/jpa/proxy/ProxyInGraalITCase.java @@ -0,0 +1,11 @@ +package io.quarkus.it.jpa.proxy; + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +/** + * Test various JPA operations running in native mode + */ +@QuarkusIntegrationTest +public class ProxyInGraalITCase extends ProxyTest { + +} diff --git a/integration-tests/jpa/src/test/java/io/quarkus/it/jpa/proxy/ProxyTest.java b/integration-tests/jpa/src/test/java/io/quarkus/it/jpa/proxy/ProxyTest.java new file mode 100644 index 0000000000000..b0e6f6b5ff47f --- /dev/null +++ b/integration-tests/jpa/src/test/java/io/quarkus/it/jpa/proxy/ProxyTest.java @@ -0,0 +1,28 @@ +package io.quarkus.it.jpa.proxy; + +import static org.hamcrest.Matchers.is; + +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.RestAssured; + +@QuarkusTest +public class ProxyTest { + + @Test + public void testBasicProxies() { + RestAssured.when().get("/jpa-test/proxy/basic").then().body(is("OK")); + } + + @Test + public void testProxyInheritance() { + RestAssured.when().get("/jpa-test/proxy/inheritance").then().body(is("OK")); + } + + @Test + public void testEnhancedProxies() { + RestAssured.when().get("/jpa-test/proxy/enhanced").then().body(is("OK")); + } + +} diff --git a/integration-tests/kafka-json-schema-apicurio2/src/main/java/io/quarkus/it/kafka/jsonschema/JsonSchemaKafkaCreator.java b/integration-tests/kafka-json-schema-apicurio2/src/main/java/io/quarkus/it/kafka/jsonschema/JsonSchemaKafkaCreator.java index 20b3933c8a3ef..edfc63a904df1 100644 --- a/integration-tests/kafka-json-schema-apicurio2/src/main/java/io/quarkus/it/kafka/jsonschema/JsonSchemaKafkaCreator.java +++ b/integration-tests/kafka-json-schema-apicurio2/src/main/java/io/quarkus/it/kafka/jsonschema/JsonSchemaKafkaCreator.java @@ -1,9 +1,11 @@ package io.quarkus.it.kafka.jsonschema; -import io.apicurio.registry.serde.SerdeConfig; -import io.apicurio.registry.serde.jsonschema.JsonSchemaKafkaDeserializer; -import io.apicurio.registry.serde.jsonschema.JsonSchemaKafkaSerializer; +import java.util.Collections; +import java.util.Properties; +import java.util.UUID; + import jakarta.enterprise.context.ApplicationScoped; + import org.apache.kafka.clients.consumer.ConsumerConfig; import org.apache.kafka.clients.consumer.KafkaConsumer; import org.apache.kafka.clients.producer.KafkaProducer; @@ -12,9 +14,9 @@ import org.apache.kafka.common.serialization.IntegerSerializer; import org.eclipse.microprofile.config.inject.ConfigProperty; -import java.util.Collections; -import java.util.Properties; -import java.util.UUID; +import io.apicurio.registry.serde.SerdeConfig; +import io.apicurio.registry.serde.jsonschema.JsonSchemaKafkaDeserializer; +import io.apicurio.registry.serde.jsonschema.JsonSchemaKafkaSerializer; /** * Create Json Schema Kafka Consumers and Producers @@ -49,13 +51,13 @@ public KafkaProducer createApicurioProducer(String clientId) { } public static KafkaConsumer createApicurioConsumer(String bootstrap, String apicurio, - String groupdIdConfig, String subscribtionName) { + String groupdIdConfig, String subscribtionName) { Properties p = getApicurioConsumerProperties(bootstrap, apicurio, groupdIdConfig); return createConsumer(p, subscribtionName); } public static KafkaProducer createApicurioProducer(String bootstrap, String apicurio, - String clientId) { + String clientId) { Properties p = getApicurioProducerProperties(bootstrap, apicurio, clientId); return createProducer(p); } diff --git a/integration-tests/kafka-json-schema-apicurio2/src/test/java/io/quarkus/it/kafka/KafkaJsonSchemaIT.java b/integration-tests/kafka-json-schema-apicurio2/src/test/java/io/quarkus/it/kafka/KafkaJsonSchemaIT.java index 3e51516254338..31ddb23296938 100644 --- a/integration-tests/kafka-json-schema-apicurio2/src/test/java/io/quarkus/it/kafka/KafkaJsonSchemaIT.java +++ b/integration-tests/kafka-json-schema-apicurio2/src/test/java/io/quarkus/it/kafka/KafkaJsonSchemaIT.java @@ -1,12 +1,13 @@ package io.quarkus.it.kafka; +import org.junit.jupiter.api.BeforeAll; + import io.apicurio.registry.rest.client.RegistryClientFactory; import io.apicurio.rest.client.VertxHttpClientProvider; import io.quarkus.it.kafka.jsonschema.JsonSchemaKafkaCreator; import io.quarkus.test.common.QuarkusTestResource; import io.quarkus.test.junit.QuarkusIntegrationTest; import io.vertx.core.Vertx; -import org.junit.jupiter.api.BeforeAll; @QuarkusIntegrationTest @QuarkusTestResource(value = KafkaResource.class, restrictToAnnotatedClass = true) diff --git a/integration-tests/kafka-json-schema-apicurio2/src/test/java/io/quarkus/it/kafka/KafkaJsonSchemaTest.java b/integration-tests/kafka-json-schema-apicurio2/src/test/java/io/quarkus/it/kafka/KafkaJsonSchemaTest.java index 4bcfe8c6982b2..606ded95dadfb 100644 --- a/integration-tests/kafka-json-schema-apicurio2/src/test/java/io/quarkus/it/kafka/KafkaJsonSchemaTest.java +++ b/integration-tests/kafka-json-schema-apicurio2/src/test/java/io/quarkus/it/kafka/KafkaJsonSchemaTest.java @@ -1,8 +1,9 @@ package io.quarkus.it.kafka; +import jakarta.inject.Inject; + import io.quarkus.it.kafka.jsonschema.JsonSchemaKafkaCreator; import io.quarkus.test.junit.QuarkusTest; -import jakarta.inject.Inject; @QuarkusTest public class KafkaJsonSchemaTest extends KafkaJsonSchemaTestBase { diff --git a/integration-tests/kafka-json-schema-apicurio2/src/test/java/io/quarkus/it/kafka/KafkaJsonSchemaTestBase.java b/integration-tests/kafka-json-schema-apicurio2/src/test/java/io/quarkus/it/kafka/KafkaJsonSchemaTestBase.java index 71444631ba03e..1c0fb9d44a9aa 100644 --- a/integration-tests/kafka-json-schema-apicurio2/src/test/java/io/quarkus/it/kafka/KafkaJsonSchemaTestBase.java +++ b/integration-tests/kafka-json-schema-apicurio2/src/test/java/io/quarkus/it/kafka/KafkaJsonSchemaTestBase.java @@ -1,10 +1,8 @@ package io.quarkus.it.kafka; -import io.apicurio.registry.rest.client.RegistryClientFactory; -import io.apicurio.registry.types.ArtifactType; -import io.quarkus.it.kafka.jsonschema.JsonSchemaKafkaCreator; -import io.quarkus.it.kafka.jsonschema.Pet; -import io.restassured.RestAssured; +import java.io.InputStream; +import java.time.Duration; + import org.apache.kafka.clients.consumer.ConsumerRecord; import org.apache.kafka.clients.consumer.KafkaConsumer; import org.apache.kafka.clients.producer.KafkaProducer; @@ -12,8 +10,11 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; -import java.io.InputStream; -import java.time.Duration; +import io.apicurio.registry.rest.client.RegistryClientFactory; +import io.apicurio.registry.types.ArtifactType; +import io.quarkus.it.kafka.jsonschema.JsonSchemaKafkaCreator; +import io.quarkus.it.kafka.jsonschema.Pet; +import io.restassured.RestAssured; public abstract class KafkaJsonSchemaTestBase { @@ -32,7 +33,8 @@ public void testApicurioJsonSchemaProducer() { //Since autoregister is not supported for json schema, we must register the schema beforehand. InputStream jsonSchema = getClass().getResourceAsStream("/io/quarkus/it/kafka/json-schema.json"); - RegistryClientFactory.create(creator().getApicurioRegistryUrl()).createArtifact(null, subscriptionName + "-value", ArtifactType.JSON, jsonSchema); + RegistryClientFactory.create(creator().getApicurioRegistryUrl()).createArtifact(null, subscriptionName + "-value", + ArtifactType.JSON, jsonSchema); KafkaConsumer consumer = creator().createApicurioConsumer( "test-json-schema-apicurio", @@ -46,7 +48,8 @@ public void testApicurioJsonSchemaConsumer() { //Since autoregister is not supported for json schema, we must register the schema beforehand. InputStream jsonSchema = getClass().getResourceAsStream("/io/quarkus/it/kafka/json-schema.json"); - RegistryClientFactory.create(creator().getApicurioRegistryUrl()).createArtifact(null, topic + "-value", ArtifactType.JSON, jsonSchema); + RegistryClientFactory.create(creator().getApicurioRegistryUrl()).createArtifact(null, topic + "-value", + ArtifactType.JSON, jsonSchema); KafkaProducer producer = creator().createApicurioProducer("test-json-schema-apicurio-test"); testJsonSchemaConsumer(producer, APICURIO_PATH, topic); diff --git a/integration-tests/kafka-json-schema-apicurio2/src/test/java/io/quarkus/it/kafka/KafkaResource.java b/integration-tests/kafka-json-schema-apicurio2/src/test/java/io/quarkus/it/kafka/KafkaResource.java index 3ea4a51d86b6b..dabe27a7715ed 100644 --- a/integration-tests/kafka-json-schema-apicurio2/src/test/java/io/quarkus/it/kafka/KafkaResource.java +++ b/integration-tests/kafka-json-schema-apicurio2/src/test/java/io/quarkus/it/kafka/KafkaResource.java @@ -1,12 +1,12 @@ package io.quarkus.it.kafka; +import java.util.Collections; +import java.util.Map; + import io.quarkus.it.kafka.jsonschema.JsonSchemaKafkaCreator; import io.quarkus.test.common.DevServicesContext; import io.quarkus.test.common.QuarkusTestResourceLifecycleManager; -import java.util.Collections; -import java.util.Map; - public class KafkaResource implements QuarkusTestResourceLifecycleManager, DevServicesContext.ContextAware { JsonSchemaKafkaCreator creator; diff --git a/integration-tests/kotlin/src/test/resources/projects/classic-kotlin/pom.xml b/integration-tests/kotlin/src/test/resources/projects/classic-kotlin/pom.xml index 11e69627d833a..8be32af85948a 100644 --- a/integration-tests/kotlin/src/test/resources/projects/classic-kotlin/pom.xml +++ b/integration-tests/kotlin/src/test/resources/projects/classic-kotlin/pom.xml @@ -9,9 +9,9 @@ 1.0-SNAPSHOT @project.version@ - 11 + 17 UTF-8 - 11 + 17 @kotlin.version@ diff --git a/integration-tests/kotlin/src/test/resources/projects/external-reloadable-artifacts/app/pom.xml b/integration-tests/kotlin/src/test/resources/projects/external-reloadable-artifacts/app/pom.xml index 6038adf509dfc..3dda73071c6a6 100644 --- a/integration-tests/kotlin/src/test/resources/projects/external-reloadable-artifacts/app/pom.xml +++ b/integration-tests/kotlin/src/test/resources/projects/external-reloadable-artifacts/app/pom.xml @@ -9,9 +9,9 @@ @project.version@ - 11 + 17 UTF-8 - 11 + 17 @kotlin.version@ diff --git a/integration-tests/kotlin/src/test/resources/projects/external-reloadable-artifacts/external-lib/pom.xml b/integration-tests/kotlin/src/test/resources/projects/external-reloadable-artifacts/external-lib/pom.xml index c3e3b414f7eb7..a477ef7274fe5 100644 --- a/integration-tests/kotlin/src/test/resources/projects/external-reloadable-artifacts/external-lib/pom.xml +++ b/integration-tests/kotlin/src/test/resources/projects/external-reloadable-artifacts/external-lib/pom.xml @@ -8,9 +8,9 @@ 1.0-SNAPSHOT - 11 + 17 UTF-8 - 11 + 17 3.2.2 @kotlin.version@ diff --git a/integration-tests/kotlin/src/test/resources/projects/kotlin-compiler-args/pom.xml b/integration-tests/kotlin/src/test/resources/projects/kotlin-compiler-args/pom.xml index c264411637c04..c28c5fbdc223f 100644 --- a/integration-tests/kotlin/src/test/resources/projects/kotlin-compiler-args/pom.xml +++ b/integration-tests/kotlin/src/test/resources/projects/kotlin-compiler-args/pom.xml @@ -9,9 +9,9 @@ 1.0-SNAPSHOT @project.version@ - 11 + 17 UTF-8 - 11 + 17 @kotlin.version@ diff --git a/integration-tests/kubernetes-client-hack-extension/deployment/pom.xml b/integration-tests/kubernetes-client-hack-extension/deployment/pom.xml deleted file mode 100644 index 913462707b124..0000000000000 --- a/integration-tests/kubernetes-client-hack-extension/deployment/pom.xml +++ /dev/null @@ -1,58 +0,0 @@ - - - 4.0.0 - - - io.quarkus - quarkus-integration-test-kubernetes-client-hack-extension-parent - 999-SNAPSHOT - - - quarkus-integration-test-kubernetes-client-hack-extension-deployment - Quarkus - Integration Tests - Kubernetes Client Hack Extension - Deployment - - - - io.quarkus - quarkus-core-deployment - ${project.version} - - - io.quarkus - quarkus-integration-test-kubernetes-client-hack-extension - ${project.version} - - - - - - - maven-compiler-plugin - - - - io.quarkus - quarkus-extension-processor - ${project.version} - - - - - - org.codehaus.mojo - templating-maven-plugin - 1.0.0 - - - filtering-java-templates - - filter-sources - - - - - - - diff --git a/integration-tests/kubernetes-client-hack-extension/deployment/src/main/java/io/quarkus/kubernetes/client/deployment/NativeOverrides.java b/integration-tests/kubernetes-client-hack-extension/deployment/src/main/java/io/quarkus/kubernetes/client/deployment/NativeOverrides.java deleted file mode 100644 index 91372ea46589e..0000000000000 --- a/integration-tests/kubernetes-client-hack-extension/deployment/src/main/java/io/quarkus/kubernetes/client/deployment/NativeOverrides.java +++ /dev/null @@ -1,12 +0,0 @@ -package io.quarkus.kubernetes.client.deployment; - -import io.quarkus.deployment.annotations.BuildStep; -import io.quarkus.deployment.builditem.nativeimage.NativeImageAllowIncompleteClasspathBuildItem; - -public class NativeOverrides { - - @BuildStep - NativeImageAllowIncompleteClasspathBuildItem incompleteModel() { - return new NativeImageAllowIncompleteClasspathBuildItem("quarkus-kubernetes-client"); - } -} diff --git a/integration-tests/kubernetes-client-hack-extension/pom.xml b/integration-tests/kubernetes-client-hack-extension/pom.xml deleted file mode 100644 index c29e01f59239c..0000000000000 --- a/integration-tests/kubernetes-client-hack-extension/pom.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - 4.0.0 - - - quarkus-extensions-parent - io.quarkus - 999-SNAPSHOT - ../../extensions/pom.xml - - - quarkus-integration-test-kubernetes-client-hack-extension-parent - Quarkus - Integration Tests - Kubernetes Client Hack Extension - pom - - deployment - runtime - - diff --git a/integration-tests/kubernetes-client-hack-extension/runtime/pom.xml b/integration-tests/kubernetes-client-hack-extension/runtime/pom.xml deleted file mode 100644 index a0278c43dafaa..0000000000000 --- a/integration-tests/kubernetes-client-hack-extension/runtime/pom.xml +++ /dev/null @@ -1,48 +0,0 @@ - - - 4.0.0 - - - io.quarkus - quarkus-integration-test-kubernetes-client-hack-extension-parent - 999-SNAPSHOT - - - quarkus-integration-test-kubernetes-client-hack-extension - Quarkus - Integration Tests - Kubernetes Client Hack Extension - Runtime - - - - io.quarkus - quarkus-core - ${project.version} - - - - - - - io.quarkus - quarkus-extension-maven-plugin - ${project.version} - - ${project.groupId}:${project.artifactId}-deployment:${project.version} - - - - maven-compiler-plugin - - - - io.quarkus - quarkus-extension-processor - ${project.version} - - - - - - - diff --git a/integration-tests/kubernetes-client-hack-extension/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/integration-tests/kubernetes-client-hack-extension/runtime/src/main/resources/META-INF/quarkus-extension.yaml deleted file mode 100644 index 42eda1b64d7c5..0000000000000 --- a/integration-tests/kubernetes-client-hack-extension/runtime/src/main/resources/META-INF/quarkus-extension.yaml +++ /dev/null @@ -1,11 +0,0 @@ ---- -artifact: ${project.groupId}:${project.artifactId}:${project.version} -name: "Kubernetes Client Hack Extension" -metadata: - keywords: - - "kubernetes-client" - guide: "https://quarkus.io/guides/kubernetes-client" - categories: - - "cloud" - status: "test" - config: diff --git a/integration-tests/kubernetes-client/pom.xml b/integration-tests/kubernetes-client/pom.xml index a08bd4306d397..74540dca95406 100644 --- a/integration-tests/kubernetes-client/pom.xml +++ b/integration-tests/kubernetes-client/pom.xml @@ -22,22 +22,21 @@ io.quarkus quarkus-kubernetes-config - - io.quarkus - quarkus-integration-test-kubernetes-client-hack-extension - ${project.version} - io.quarkus quarkus-openshift-client io.fabric8 - openshift-model-operator + openshift-model-hive io.fabric8 - openshift-model-operator-hub + openshift-model-miscellaneous + + + io.fabric8 + openshift-model-operator @@ -90,19 +89,6 @@ - - io.quarkus - quarkus-integration-test-kubernetes-client-hack-extension-deployment - ${project.version} - pom - test - - - * - * - - - io.quarkus quarkus-openshift-client-deployment diff --git a/integration-tests/kubernetes/maven-invoker-way/src/it/knative-jib-build-and-deploy/pom.xml b/integration-tests/kubernetes/maven-invoker-way/src/it/knative-jib-build-and-deploy/pom.xml index 32c73c981697b..caad51d1650d4 100644 --- a/integration-tests/kubernetes/maven-invoker-way/src/it/knative-jib-build-and-deploy/pom.xml +++ b/integration-tests/kubernetes/maven-invoker-way/src/it/knative-jib-build-and-deploy/pom.xml @@ -8,9 +8,9 @@ UTF-8 3.1.2 - 11 + 17 UTF-8 - 11 + 17 diff --git a/integration-tests/kubernetes/maven-invoker-way/src/it/kubernetes-docker-build-and-deploy-deployment/pom.xml b/integration-tests/kubernetes/maven-invoker-way/src/it/kubernetes-docker-build-and-deploy-deployment/pom.xml index 0e24d8374a39d..fa1e0fe8f0d1e 100644 --- a/integration-tests/kubernetes/maven-invoker-way/src/it/kubernetes-docker-build-and-deploy-deployment/pom.xml +++ b/integration-tests/kubernetes/maven-invoker-way/src/it/kubernetes-docker-build-and-deploy-deployment/pom.xml @@ -8,9 +8,9 @@ UTF-8 3.1.2 - 11 + 17 UTF-8 - 11 + 17 diff --git a/integration-tests/kubernetes/maven-invoker-way/src/it/kubernetes-docker-build-and-deploy-statefulset/pom.xml b/integration-tests/kubernetes/maven-invoker-way/src/it/kubernetes-docker-build-and-deploy-statefulset/pom.xml index fe6451e7fa880..f22eed57d8ef9 100644 --- a/integration-tests/kubernetes/maven-invoker-way/src/it/kubernetes-docker-build-and-deploy-statefulset/pom.xml +++ b/integration-tests/kubernetes/maven-invoker-way/src/it/kubernetes-docker-build-and-deploy-statefulset/pom.xml @@ -8,9 +8,9 @@ UTF-8 3.1.2 - 11 + 17 UTF-8 - 11 + 17 diff --git a/integration-tests/kubernetes/maven-invoker-way/src/it/kubernetes-jib-build-and-deploy/pom.xml b/integration-tests/kubernetes/maven-invoker-way/src/it/kubernetes-jib-build-and-deploy/pom.xml index cd221c0458b65..de6c5713afd4c 100644 --- a/integration-tests/kubernetes/maven-invoker-way/src/it/kubernetes-jib-build-and-deploy/pom.xml +++ b/integration-tests/kubernetes/maven-invoker-way/src/it/kubernetes-jib-build-and-deploy/pom.xml @@ -8,9 +8,9 @@ UTF-8 3.1.2 - 11 + 17 UTF-8 - 11 + 17 diff --git a/integration-tests/kubernetes/maven-invoker-way/src/it/kubernetes-with-existing-selectorless-manifest/pom.xml b/integration-tests/kubernetes/maven-invoker-way/src/it/kubernetes-with-existing-selectorless-manifest/pom.xml index ebc57fe7f5931..31074411b816b 100644 --- a/integration-tests/kubernetes/maven-invoker-way/src/it/kubernetes-with-existing-selectorless-manifest/pom.xml +++ b/integration-tests/kubernetes/maven-invoker-way/src/it/kubernetes-with-existing-selectorless-manifest/pom.xml @@ -9,9 +9,9 @@ UTF-8 3.1.2 - 11 + 17 UTF-8 - 11 + 17 diff --git a/integration-tests/kubernetes/maven-invoker-way/src/it/kubernetes-with-grpc-same-server/pom.xml b/integration-tests/kubernetes/maven-invoker-way/src/it/kubernetes-with-grpc-same-server/pom.xml index a61b38cec58b0..a992773921f87 100644 --- a/integration-tests/kubernetes/maven-invoker-way/src/it/kubernetes-with-grpc-same-server/pom.xml +++ b/integration-tests/kubernetes/maven-invoker-way/src/it/kubernetes-with-grpc-same-server/pom.xml @@ -8,9 +8,9 @@ UTF-8 3.1.2 - 11 + 17 UTF-8 - 11 + 17 diff --git a/integration-tests/kubernetes/maven-invoker-way/src/it/kubernetes-with-grpc/pom.xml b/integration-tests/kubernetes/maven-invoker-way/src/it/kubernetes-with-grpc/pom.xml index f6a3236a785ae..8aac50cdfedeb 100644 --- a/integration-tests/kubernetes/maven-invoker-way/src/it/kubernetes-with-grpc/pom.xml +++ b/integration-tests/kubernetes/maven-invoker-way/src/it/kubernetes-with-grpc/pom.xml @@ -8,9 +8,9 @@ UTF-8 3.1.2 - 11 + 17 UTF-8 - 11 + 17 diff --git a/integration-tests/kubernetes/maven-invoker-way/src/it/minikube-with-existing-manifest/pom.xml b/integration-tests/kubernetes/maven-invoker-way/src/it/minikube-with-existing-manifest/pom.xml index f04333673c6f0..d78d280567409 100644 --- a/integration-tests/kubernetes/maven-invoker-way/src/it/minikube-with-existing-manifest/pom.xml +++ b/integration-tests/kubernetes/maven-invoker-way/src/it/minikube-with-existing-manifest/pom.xml @@ -8,9 +8,9 @@ UTF-8 3.1.2 - 11 + 17 UTF-8 - 11 + 17 diff --git a/integration-tests/kubernetes/maven-invoker-way/src/it/openshift-docker-build-and-deploy/pom.xml b/integration-tests/kubernetes/maven-invoker-way/src/it/openshift-docker-build-and-deploy/pom.xml index 9835e419f434e..667a46e0355c1 100644 --- a/integration-tests/kubernetes/maven-invoker-way/src/it/openshift-docker-build-and-deploy/pom.xml +++ b/integration-tests/kubernetes/maven-invoker-way/src/it/openshift-docker-build-and-deploy/pom.xml @@ -8,9 +8,9 @@ UTF-8 3.1.2 - 11 + 17 UTF-8 - 11 + 17 diff --git a/integration-tests/kubernetes/maven-invoker-way/src/it/openshift-s2i-build-and-deploy/pom.xml b/integration-tests/kubernetes/maven-invoker-way/src/it/openshift-s2i-build-and-deploy/pom.xml index c32baabdc8130..7343f26e67d79 100644 --- a/integration-tests/kubernetes/maven-invoker-way/src/it/openshift-s2i-build-and-deploy/pom.xml +++ b/integration-tests/kubernetes/maven-invoker-way/src/it/openshift-s2i-build-and-deploy/pom.xml @@ -8,9 +8,9 @@ UTF-8 3.1.2 - 11 + 17 UTF-8 - 11 + 17 diff --git a/integration-tests/kubernetes/maven-invoker-way/src/it/openshift-with-output-directory-build-and-deploy/pom.xml b/integration-tests/kubernetes/maven-invoker-way/src/it/openshift-with-output-directory-build-and-deploy/pom.xml index 369de652a1e99..c191bcdacf8dc 100644 --- a/integration-tests/kubernetes/maven-invoker-way/src/it/openshift-with-output-directory-build-and-deploy/pom.xml +++ b/integration-tests/kubernetes/maven-invoker-way/src/it/openshift-with-output-directory-build-and-deploy/pom.xml @@ -8,9 +8,9 @@ UTF-8 3.0.0 - 11 + 17 UTF-8 - 11 + 17 diff --git a/integration-tests/kubernetes/quarkus-standard-way-kafka/pom.xml b/integration-tests/kubernetes/quarkus-standard-way-kafka/pom.xml index 0d755826914b1..5ae7b23635e79 100644 --- a/integration-tests/kubernetes/quarkus-standard-way-kafka/pom.xml +++ b/integration-tests/kubernetes/quarkus-standard-way-kafka/pom.xml @@ -22,8 +22,7 @@ quarkus-container-image-docker-deployment, quarkus-container-image-jib-deployment, - quarkus-container-image-openshift-deployment, - quarkus-container-image-s2i-deployment + quarkus-container-image-openshift-deployment @@ -195,19 +194,6 @@ - - io.quarkus - quarkus-container-image-s2i-deployment - ${project.version} - pom - test - - - * - * - - - diff --git a/integration-tests/kubernetes/quarkus-standard-way/pom.xml b/integration-tests/kubernetes/quarkus-standard-way/pom.xml index af55a47197ebb..99979dfdda80d 100644 --- a/integration-tests/kubernetes/quarkus-standard-way/pom.xml +++ b/integration-tests/kubernetes/quarkus-standard-way/pom.xml @@ -22,8 +22,7 @@ quarkus-container-image-docker-deployment, quarkus-container-image-jib-deployment, - quarkus-container-image-openshift-deployment, - quarkus-container-image-s2i-deployment + quarkus-container-image-openshift-deployment @@ -235,19 +234,6 @@ - - io.quarkus - quarkus-container-image-s2i-deployment - ${project.version} - pom - test - - - * - * - - - io.quarkus quarkus-smallrye-health-deployment diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithMetricsCustomAbsoluteTest.java b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithMetricsCustomAbsoluteTest.java index 65287b49e0d01..38d59234d3081 100644 --- a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithMetricsCustomAbsoluteTest.java +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithMetricsCustomAbsoluteTest.java @@ -92,7 +92,8 @@ public void assertGeneratedResources() throws IOException { assertThat(spec.getEndpoints()).hasSize(1); assertThat(spec.getEndpoints().get(0)).isInstanceOfSatisfying(Endpoint.class, e -> { assertThat(e.getScheme()).isEqualTo("http"); - assertThat(e.getTargetPort().getStrVal()).isEqualTo("9090"); + assertThat(e.getTargetPort().getStrVal()).isNull(); + assertThat(e.getTargetPort().getIntVal()).isEqualTo(9090); assertThat(e.getPath()).isEqualTo("/absolute-metrics"); }); }); diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithMetricsCustomRelativeTest.java b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithMetricsCustomRelativeTest.java index b9b9a113331ac..e12348bea4266 100644 --- a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithMetricsCustomRelativeTest.java +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithMetricsCustomRelativeTest.java @@ -93,7 +93,8 @@ public void assertGeneratedResources() throws IOException { assertThat(spec.getEndpoints()).hasSize(1); assertThat(spec.getEndpoints().get(0)).isInstanceOfSatisfying(Endpoint.class, e -> { assertThat(e.getScheme()).isEqualTo("http"); - assertThat(e.getTargetPort().getStrVal()).isEqualTo("9090"); + assertThat(e.getTargetPort().getStrVal()).isNull(); + assertThat(e.getTargetPort().getIntVal()).isEqualTo(9090); assertThat(e.getPath()).isEqualTo("/q/met"); }); }); diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithMetricsNoAnnotationsTest.java b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithMetricsNoAnnotationsTest.java index aa2d0878db11e..f33973cc55fcb 100644 --- a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithMetricsNoAnnotationsTest.java +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithMetricsNoAnnotationsTest.java @@ -92,7 +92,8 @@ public void assertGeneratedResources() throws IOException { assertThat(spec.getEndpoints()).hasSize(1); assertThat(spec.getEndpoints().get(0)).isInstanceOfSatisfying(Endpoint.class, e -> { assertThat(e.getScheme()).isEqualTo("http"); - assertThat(e.getTargetPort().getStrVal()).isEqualTo("9090"); + assertThat(e.getTargetPort().getStrVal()).isNull(); + assertThat(e.getTargetPort().getIntVal()).isEqualTo(9090); assertThat(e.getPath()).isEqualTo("/q/metrics"); }); }); diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithMetricsTest.java b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithMetricsTest.java index 37c51a12afcdc..f5b8d9bba4067 100644 --- a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithMetricsTest.java +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithMetricsTest.java @@ -95,7 +95,8 @@ public void assertGeneratedResources() throws IOException { assertThat(spec.getEndpoints()).hasSize(1); assertThat(spec.getEndpoints().get(0)).isInstanceOfSatisfying(Endpoint.class, e -> { assertThat(e.getScheme()).isEqualTo("http"); - assertThat(e.getTargetPort().getStrVal()).isEqualTo("9090"); + assertThat(e.getTargetPort().getStrVal()).isNull(); + assertThat(e.getTargetPort().getIntVal()).isEqualTo(9090); assertThat(e.getPath()).isEqualTo("/q/metrics"); }); }); diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithMicrometerTest.java b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithMicrometerTest.java index b41e32d046c14..96b0ca8037269 100644 --- a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithMicrometerTest.java +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithMicrometerTest.java @@ -72,7 +72,8 @@ public void assertGeneratedResources() throws IOException { assertThat(spec.getEndpoints()).hasSize(1); assertThat(spec.getEndpoints().get(0)).isInstanceOfSatisfying(Endpoint.class, e -> { assertThat(e.getScheme()).isEqualTo("http"); - assertThat(e.getTargetPort().getStrVal()).isEqualTo("8080"); + assertThat(e.getTargetPort().getStrVal()).isNull(); + assertThat(e.getTargetPort().getIntVal()).isEqualTo(8080); assertThat(e.getPath()).isEqualTo("/q/metrics"); }); }); diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithQuarkusAppNameTest.java b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithQuarkusAppNameTest.java index 71f0bc4a5e6d7..3d405714b7ceb 100644 --- a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithQuarkusAppNameTest.java +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithQuarkusAppNameTest.java @@ -28,7 +28,7 @@ public class KubernetesWithQuarkusAppNameTest { .withConfigurationResource("kubernetes-with-quarkus-app-name.properties") .setForcedDependencies(List.of( Dependency.of("io.quarkus", "quarkus-kubernetes", Version.getVersion()), - Dependency.of("io.quarkus", "quarkus-container-image-s2i", Version.getVersion()))); + Dependency.of("io.quarkus", "quarkus-container-image-openshift", Version.getVersion()))); @ProdBuildResults private ProdModeTestResults prodModeTestResults; @@ -53,7 +53,7 @@ public void assertGeneratedResources() throws IOException { List openshiftList = DeserializationUtil .deserializeAsList(kubernetesDir.resolve("openshift.yml")); assertThat(openshiftList).allSatisfy(h -> { - assertThat(h.getMetadata().getName()).isIn("ofoo", "foo", "openjdk-11"); + assertThat(h.getMetadata().getName()).isIn("ofoo", "foo", "openjdk-17"); assertThat(h.getMetadata().getLabels()).contains(entry("app.kubernetes.io/name", "ofoo"), entry("app.kubernetes.io/version", "1.0-openshift")); }); diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithSpecifiedContainerNameTest.java b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithSpecifiedContainerNameTest.java index 1008f62907f6d..c1f9422f4ea4a 100644 --- a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithSpecifiedContainerNameTest.java +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithSpecifiedContainerNameTest.java @@ -31,7 +31,7 @@ public class KubernetesWithSpecifiedContainerNameTest { .withConfigurationResource("kubernetes-with-specified-container-name.properties") .setForcedDependencies(List.of( Dependency.of("io.quarkus", "quarkus-kubernetes", Version.getVersion()), - Dependency.of("io.quarkus", "quarkus-container-image-s2i", Version.getVersion()))); + Dependency.of("io.quarkus", "quarkus-container-image-openshift", Version.getVersion()))); @ProdBuildResults private ProdModeTestResults prodModeTestResults; @@ -81,7 +81,7 @@ public void assertGeneratedResources() throws IOException { }); }); - assertThat(h.getMetadata().getName()).isIn("ofoo", "foo", "openjdk-11"); + assertThat(h.getMetadata().getName()).isIn("ofoo", "foo", "openjdk-17"); assertThat(h.getMetadata().getLabels()).contains(entry("app.kubernetes.io/name", "ofoo")); }); } diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/OpenshiftWithS2iTest.java b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/OpenshiftWithS2iTest.java index 136f04936feb5..d3049dc5063de 100644 --- a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/OpenshiftWithS2iTest.java +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/OpenshiftWithS2iTest.java @@ -86,11 +86,6 @@ public void assertGeneratedResources() throws IOException { assertThat(envVar.getName()).isEqualTo("JAVA_APP_JAR"); //assertThat(envVar.getValue()).isEqualTo("/deployments/quarkus-run.jar"); // this is flaky }); - assertThat(envVars).anySatisfy(envVar -> { - assertThat(envVar.getName()).isEqualTo("JAVA_OPTIONS"); - assertThat(envVar.getValue()) - .contains("-Djava.util.logging.manager=org.jboss.logmanager.LogManager"); - }); }); }); diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/openshift-with-legacy-sidecar-and-s2i.properties b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/openshift-with-legacy-sidecar-and-s2i.properties index f293fcce1ccff..8848c5b4df735 100644 --- a/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/openshift-with-legacy-sidecar-and-s2i.properties +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/openshift-with-legacy-sidecar-and-s2i.properties @@ -1,5 +1,5 @@ quarkus.openshift.route.expose=true -quarkus.s2i.base-jvm-image=my.registry.example/openjdk/openjdk-11-rhel7 +quarkus.s2i.base-jvm-image=my.registry.example/openjdk/openjdk-17-rhel8 quarkus.openshift.containers.sc.image=quay.io/sidecar/image:2.1 quarkus.openshift.containers.sc.image-pull-policy=IfNotPresent quarkus.openshift.containers.sc.command=ls diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/openshift-with-sidecar-and-s2i.properties b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/openshift-with-sidecar-and-s2i.properties index f60a7825fec4f..85a695636b57c 100644 --- a/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/openshift-with-sidecar-and-s2i.properties +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/openshift-with-sidecar-and-s2i.properties @@ -1,5 +1,5 @@ quarkus.openshift.route.expose=true -quarkus.s2i.base-jvm-image=my.registry.example/openjdk/openjdk-11-rhel7 +quarkus.s2i.base-jvm-image=my.registry.example/openjdk/openjdk-17-rhel8 quarkus.openshift.sidecars.sc.image=quay.io/sidecar/image:2.1 quarkus.openshift.sidecars.sc.image-pull-policy=IfNotPresent quarkus.openshift.sidecars.sc.command=ls diff --git a/integration-tests/kubernetes/src/it/openshift-s2i-build-and-deploy/invoker.properties b/integration-tests/kubernetes/src/it/openshift-s2i-build-and-deploy/invoker.properties deleted file mode 100644 index bb6b843850719..0000000000000 --- a/integration-tests/kubernetes/src/it/openshift-s2i-build-and-deploy/invoker.properties +++ /dev/null @@ -1,2 +0,0 @@ -# invoker.goals=clean package -Dquarkus.container.build=true -Dquarkus.package.type=native -invoker.goals=clean package -Dquarkus.kubernetes.deploy=true diff --git a/integration-tests/kubernetes/src/it/openshift-s2i-build-and-deploy/pom.xml b/integration-tests/kubernetes/src/it/openshift-s2i-build-and-deploy/pom.xml deleted file mode 100644 index f53561bd00312..0000000000000 --- a/integration-tests/kubernetes/src/it/openshift-s2i-build-and-deploy/pom.xml +++ /dev/null @@ -1,119 +0,0 @@ - - - 4.0.0 - org.acme - openshift-s2i-build-and-deploy - 0.1-SNAPSHOT - - UTF-8 - 3.1.2 - 11 - UTF-8 - 11 - - - - - io.quarkus - quarkus-bom - @project.version@ - pom - import - - - - - - io.quarkus - quarkus-resteasy - - - io.quarkus - quarkus-kubernetes - - - io.quarkus - quarkus-container-image-s2i - - - io.quarkus - quarkus-junit5 - test - - - io.rest-assured - rest-assured - test - - - - - - io.quarkus - quarkus-maven-plugin - @project.version@ - - - - build - - - - - - maven-surefire-plugin - ${surefire-plugin.version} - - - org.jboss.logmanager.LogManager - ${maven.home} - - - - - - - - native - - - native - - - - native - - - - - org.apache.maven.plugins - maven-surefire-plugin - - ${native.surefire.skip} - - - - maven-failsafe-plugin - ${surefire-plugin.version} - - - - integration-test - verify - - - - ${project.build.directory}/${project.build.finalName}-runner - org.jboss.logmanager.LogManager - ${maven.home} - - - - - - - - - - diff --git a/integration-tests/kubernetes/src/it/openshift-s2i-build-and-deploy/src/main/docker/Dockerfile.jvm b/integration-tests/kubernetes/src/it/openshift-s2i-build-and-deploy/src/main/docker/Dockerfile.jvm deleted file mode 100644 index fa97e4b117200..0000000000000 --- a/integration-tests/kubernetes/src/it/openshift-s2i-build-and-deploy/src/main/docker/Dockerfile.jvm +++ /dev/null @@ -1,22 +0,0 @@ -#### -# This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode -# -# Before building the docker image run: -# -# mvn package -# -# Then, build the image with: -# -# docker build -f src/main/docker/Dockerfile.jvm -t quarkus/getting-started-jvm . -# -# Then run the container using: -# -# docker run -i --rm -p 8080:8080 quarkus/getting-started-jvm -# -### -FROM fabric8/java-alpine-openjdk8-jre -ENV JAVA_OPTIONS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager" -ENV AB_ENABLED=jmx_exporter -COPY target/lib/* /deployments/lib/ -COPY target/*-runner.jar /deployments/app.jar -ENTRYPOINT [ "/deployments/run-java.sh" ] \ No newline at end of file diff --git a/integration-tests/kubernetes/src/it/openshift-s2i-build-and-deploy/src/main/java/org/acme/Hello.java b/integration-tests/kubernetes/src/it/openshift-s2i-build-and-deploy/src/main/java/org/acme/Hello.java deleted file mode 100644 index 95b11f63bda44..0000000000000 --- a/integration-tests/kubernetes/src/it/openshift-s2i-build-and-deploy/src/main/java/org/acme/Hello.java +++ /dev/null @@ -1,16 +0,0 @@ -package org.acme; - -import jakarta.ws.rs.GET; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.Produces; -import jakarta.ws.rs.core.MediaType; - -@Path("/hello") -public class Hello { - - @GET - @Produces(MediaType.TEXT_PLAIN) - public String hello() { - return "hello"; - } -} diff --git a/integration-tests/kubernetes/src/it/openshift-s2i-build-and-deploy/src/main/resources/application.properties b/integration-tests/kubernetes/src/it/openshift-s2i-build-and-deploy/src/main/resources/application.properties deleted file mode 100644 index 3d0cdee0c4c10..0000000000000 --- a/integration-tests/kubernetes/src/it/openshift-s2i-build-and-deploy/src/main/resources/application.properties +++ /dev/null @@ -1,4 +0,0 @@ -# Configuration file -# key = value -quarkus.kubernetes.deployment.target=openshift -quarkus.kubernetes-client.trust-certs=true diff --git a/integration-tests/kubernetes/src/it/openshift-s2i-build-and-deploy/verify.groovy b/integration-tests/kubernetes/src/it/openshift-s2i-build-and-deploy/verify.groovy deleted file mode 100644 index 5da558a350b9a..0000000000000 --- a/integration-tests/kubernetes/src/it/openshift-s2i-build-and-deploy/verify.groovy +++ /dev/null @@ -1,19 +0,0 @@ -import io.dekorate.utils.Serialization -import io.fabric8.kubernetes.api.model.KubernetesList -import io.fabric8.openshift.api.model.BuildConfig - -//Check that file exits -String base = basedir -File openshiftYml = new File(base, "target/kubernetes/openshift.yml") -assert openshiftYml.exists() -openshiftYml.withInputStream { stream -> - //Check that its parse-able - KubernetesList list = Serialization.unmarshalAsList(stream) - assert list != null - - BuildConfig buildConfig = list.items.find{r -> r.kind == "BuildConfig"} - - //Check that it contains a BuildConfig named after the project - assert buildConfig != null - assert buildConfig.metadata.name == "openshift-s2i-build-and-deploy" -} diff --git a/integration-tests/main/pom.xml b/integration-tests/main/pom.xml index 0086d9f353c81..ad14c0fceb17b 100644 --- a/integration-tests/main/pom.xml +++ b/integration-tests/main/pom.xml @@ -538,7 +538,7 @@ hibernate_orm_test hibernate_orm_test - org.hibernate.dialect.PostgreSQL10Dialect + org.hibernate.dialect.PostgreSQLDialect 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 8374d41fe2a17..d493be1b826b6 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 @@ -458,24 +458,6 @@ public void testProjectGenerationFromScratchWithAppConfigParameter() throws Mave } - @Test - public void testProjectGenerationFromScratchWithJava11() throws MavenInvocationException, IOException { - testDir = initEmptyProject("projects/project-generation-with-java11"); - assertThat(testDir).isDirectory(); - invoker = initInvoker(testDir); - - Properties properties = new Properties(); - properties.put("javaVersion", "11"); - - 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>11<"); - } - @Test public void testProjectGenerationFromScratchWithJava17() throws MavenInvocationException, IOException { testDir = initEmptyProject("projects/project-generation-with-java17"); @@ -512,25 +494,6 @@ public void testProjectGenerationFromScratchWithJava21() throws MavenInvocationE .contains("maven.compiler.release>21<"); } - @Test - public void testProjectGenerationFromScratchWithGradleJava11() throws MavenInvocationException, IOException { - testDir = initEmptyProject("projects/project-generation-with-gradle-java11"); - assertThat(testDir).isDirectory(); - invoker = initInvoker(testDir); - - Properties properties = new Properties(); - properties.put("javaVersion", "11"); - 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_11"); - } - @Test public void testProjectGenerationFromScratchWithGradleJava17() throws MavenInvocationException, IOException { testDir = initEmptyProject("projects/project-generation-with-gradle-java17"); diff --git a/integration-tests/maven/src/test/java/io/quarkus/maven/it/DevMojoIT.java b/integration-tests/maven/src/test/java/io/quarkus/maven/it/DevMojoIT.java index 3cbe292a417fb..bf298c21d3bf0 100644 --- a/integration-tests/maven/src/test/java/io/quarkus/maven/it/DevMojoIT.java +++ b/integration-tests/maven/src/test/java/io/quarkus/maven/it/DevMojoIT.java @@ -337,7 +337,7 @@ public void testCustomOutputDirSetInProfile() throws MavenInvocationException, I @Test public void testThatNonExistentSrcDirCanBeAdded() throws MavenInvocationException, IOException { - testDir = initProject("projects/classic", "projects/project-classic-run-java-change"); + testDir = initProject("projects/classic", "projects/project-classic-non-existent-src-dir-can-be-added"); File sourceDir = new File(testDir, "src/main/java"); File sourceDirMoved = new File(testDir, "src/main/java-moved"); diff --git a/integration-tests/maven/src/test/java/io/quarkus/maven/it/ImageBuildIT.java b/integration-tests/maven/src/test/java/io/quarkus/maven/it/ImageBuildIT.java index 8c2ea8370fe96..537c81629f547 100644 --- a/integration-tests/maven/src/test/java/io/quarkus/maven/it/ImageBuildIT.java +++ b/integration-tests/maven/src/test/java/io/quarkus/maven/it/ImageBuildIT.java @@ -19,14 +19,14 @@ public class ImageBuildIT extends MojoTestBase { private RunningInvoker running; private File testDir; - // We can only test with jib as its the only extension that has 0 dependencies from the system. + // We can only test with jib as it's the only extension that has 0 dependencies from the system. @Test @EnabledOnOs({ OS.LINUX }) public void testImageBuildWithJib() throws Exception { Properties buildProperties = new Properties(); buildProperties.put("quarkus.container-image.builder", "jib"); - testDir = initProject("projects/classic"); + testDir = initProject("projects/classic", "projects/image-build-with-jib"); running = new RunningInvoker(testDir, false); final MavenProcessInvocationResult result = running.execute(Collections.singletonList("quarkus:image-build"), Collections.emptyMap(), buildProperties); diff --git a/integration-tests/maven/src/test/java/io/quarkus/maven/it/JarRunnerIT.java b/integration-tests/maven/src/test/java/io/quarkus/maven/it/JarRunnerIT.java index 2c7a2a8405352..a60cd61c150bd 100644 --- a/integration-tests/maven/src/test/java/io/quarkus/maven/it/JarRunnerIT.java +++ b/integration-tests/maven/src/test/java/io/quarkus/maven/it/JarRunnerIT.java @@ -3,6 +3,7 @@ import static io.quarkus.maven.it.ApplicationNameAndVersionTestUtil.assertApplicationPropertiesSetCorrectly; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; +import static org.assertj.core.api.Assumptions.assumeThat; import static org.awaitility.Awaitility.await; import java.io.BufferedReader; @@ -617,6 +618,8 @@ public void testThatAppCDSAreUsable() throws Exception { await().atMost(TestUtils.getDefaultTimeout(), TimeUnit.MINUTES) .until(() -> result.getProcess() != null && !result.getProcess().isAlive()); + // just skip if the JDK can't create appcds + assumeThat(running.log()).doesNotContainIgnoringCase("Unable to create AppCDS"); assertThat(running.log()).containsIgnoringCase("BUILD SUCCESS"); running.stop(); diff --git a/integration-tests/maven/src/test/java/io/quarkus/maven/it/TestUtils.java b/integration-tests/maven/src/test/java/io/quarkus/maven/it/TestUtils.java index c9d76849403e7..3f1d8e5d15adb 100644 --- a/integration-tests/maven/src/test/java/io/quarkus/maven/it/TestUtils.java +++ b/integration-tests/maven/src/test/java/io/quarkus/maven/it/TestUtils.java @@ -4,7 +4,7 @@ final class TestUtils { - private static final long DEFAULT_TIMEOUT = OS.current() == OS.WINDOWS ? 3L : 1L; + private static final long DEFAULT_TIMEOUT = OS.current() == OS.WINDOWS ? 3L : 2L; private TestUtils() { } diff --git a/integration-tests/maven/src/test/resources-filtered/expected/create-extension-pom-add-to-bom/add-to-bom/pom.xml b/integration-tests/maven/src/test/resources-filtered/expected/create-extension-pom-add-to-bom/add-to-bom/pom.xml index 319dbedf23060..ce1c6a8583ec8 100644 --- a/integration-tests/maven/src/test/resources-filtered/expected/create-extension-pom-add-to-bom/add-to-bom/pom.xml +++ b/integration-tests/maven/src/test/resources-filtered/expected/create-extension-pom-add-to-bom/add-to-bom/pom.xml @@ -17,8 +17,8 @@ UTF-8 UTF-8 - 11 - 11 + 17 + 17 true diff --git a/integration-tests/maven/src/test/resources-filtered/expected/create-extension-pom-itest/itest/pom.xml b/integration-tests/maven/src/test/resources-filtered/expected/create-extension-pom-itest/itest/pom.xml index 8845208427579..ce69dfc08adb7 100644 --- a/integration-tests/maven/src/test/resources-filtered/expected/create-extension-pom-itest/itest/pom.xml +++ b/integration-tests/maven/src/test/resources-filtered/expected/create-extension-pom-itest/itest/pom.xml @@ -17,8 +17,8 @@ UTF-8 UTF-8 - 11 - 11 + 17 + 17 true diff --git a/integration-tests/maven/src/test/resources-filtered/expected/create-extension-pom-minimal/minimal-extension/pom.xml b/integration-tests/maven/src/test/resources-filtered/expected/create-extension-pom-minimal/minimal-extension/pom.xml index d871a8b9c5d15..dc6bcdb39ff84 100644 --- a/integration-tests/maven/src/test/resources-filtered/expected/create-extension-pom-minimal/minimal-extension/pom.xml +++ b/integration-tests/maven/src/test/resources-filtered/expected/create-extension-pom-minimal/minimal-extension/pom.xml @@ -17,8 +17,8 @@ UTF-8 UTF-8 - 11 - 11 + 17 + 17 true diff --git a/integration-tests/maven/src/test/resources-filtered/expected/create-extension-pom-with-grand-parent/with-grand-parent/pom.xml b/integration-tests/maven/src/test/resources-filtered/expected/create-extension-pom-with-grand-parent/with-grand-parent/pom.xml index d5d412671b2f1..3db396312fb52 100644 --- a/integration-tests/maven/src/test/resources-filtered/expected/create-extension-pom-with-grand-parent/with-grand-parent/pom.xml +++ b/integration-tests/maven/src/test/resources-filtered/expected/create-extension-pom-with-grand-parent/with-grand-parent/pom.xml @@ -18,8 +18,8 @@ UTF-8 UTF-8 - 11 - 11 + 17 + 17 true diff --git a/integration-tests/maven/src/test/resources-filtered/expected/new-extension-current-directory-project/pom.xml b/integration-tests/maven/src/test/resources-filtered/expected/new-extension-current-directory-project/pom.xml index d617e29e4f8ff..a3aa84bdee7ae 100644 --- a/integration-tests/maven/src/test/resources-filtered/expected/new-extension-current-directory-project/pom.xml +++ b/integration-tests/maven/src/test/resources-filtered/expected/new-extension-current-directory-project/pom.xml @@ -14,7 +14,7 @@ UTF-8 UTF-8 - 11 + 17 \${project.version} \${compiler-plugin.version} diff --git a/integration-tests/maven/src/test/resources-filtered/expected/new-extension-project-with-jboss-parent/my-ext/pom.xml b/integration-tests/maven/src/test/resources-filtered/expected/new-extension-project-with-jboss-parent/my-ext/pom.xml index cd2cbbf21187c..218d2d5c53e18 100644 --- a/integration-tests/maven/src/test/resources-filtered/expected/new-extension-project-with-jboss-parent/my-ext/pom.xml +++ b/integration-tests/maven/src/test/resources-filtered/expected/new-extension-project-with-jboss-parent/my-ext/pom.xml @@ -19,8 +19,8 @@ UTF-8 UTF-8 - 11 - 11 + 17 + 17 true \${project.version} \${compiler-plugin.version} diff --git a/integration-tests/maven/src/test/resources-filtered/expected/new-extension-project/my-ext/pom.xml b/integration-tests/maven/src/test/resources-filtered/expected/new-extension-project/my-ext/pom.xml index d617e29e4f8ff..a3aa84bdee7ae 100644 --- a/integration-tests/maven/src/test/resources-filtered/expected/new-extension-project/my-ext/pom.xml +++ b/integration-tests/maven/src/test/resources-filtered/expected/new-extension-project/my-ext/pom.xml @@ -14,7 +14,7 @@ UTF-8 UTF-8 - 11 + 17 \${project.version} \${compiler-plugin.version} diff --git a/integration-tests/maven/src/test/resources-filtered/projects/codegen-config-factory/app/src/main/docker/Dockerfile.jvm b/integration-tests/maven/src/test/resources-filtered/projects/codegen-config-factory/app/src/main/docker/Dockerfile.jvm index 1617fd89fa050..fd5272297c2ef 100644 --- a/integration-tests/maven/src/test/resources-filtered/projects/codegen-config-factory/app/src/main/docker/Dockerfile.jvm +++ b/integration-tests/maven/src/test/resources-filtered/projects/codegen-config-factory/app/src/main/docker/Dockerfile.jvm @@ -75,7 +75,7 @@ # accessed directly. (example: "foo.example.com,bar.example.com") # ### -FROM registry.access.redhat.com/ubi8/openjdk-11:1.14 +FROM registry.access.redhat.com/ubi8/openjdk-17:1.18 ENV LANGUAGE='en_US:en' diff --git a/integration-tests/maven/src/test/resources-filtered/projects/codegen-config-factory/app/src/main/docker/Dockerfile.legacy-jar b/integration-tests/maven/src/test/resources-filtered/projects/codegen-config-factory/app/src/main/docker/Dockerfile.legacy-jar index 189ff4eb040e3..95ce7681973c5 100644 --- a/integration-tests/maven/src/test/resources-filtered/projects/codegen-config-factory/app/src/main/docker/Dockerfile.legacy-jar +++ b/integration-tests/maven/src/test/resources-filtered/projects/codegen-config-factory/app/src/main/docker/Dockerfile.legacy-jar @@ -75,7 +75,7 @@ # accessed directly. (example: "foo.example.com,bar.example.com") # ### -FROM registry.access.redhat.com/ubi8/openjdk-11:1.14 +FROM registry.access.redhat.com/ubi8/openjdk-17:1.18 ENV LANGUAGE='en_US:en' diff --git a/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateQuarkiverseExtension/quarkus-my-quarkiverse-ext_.github_workflows_build.yml b/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateQuarkiverseExtension/quarkus-my-quarkiverse-ext_.github_workflows_build.yml index 5da57b27e4dba..b37f375508f20 100644 --- a/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateQuarkiverseExtension/quarkus-my-quarkiverse-ext_.github_workflows_build.yml +++ b/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateQuarkiverseExtension/quarkus-my-quarkiverse-ext_.github_workflows_build.yml @@ -45,11 +45,11 @@ jobs: if: startsWith(matrix.os, 'windows') - uses: actions/checkout@v3 - - name: Set up JDK 11 + - name: Set up JDK 17 uses: actions/setup-java@v3 with: distribution: temurin - java-version: 11 + java-version: 17 cache: 'maven' - name: Build with Maven diff --git a/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateQuarkiverseExtension/quarkus-my-quarkiverse-ext_.github_workflows_quarkus-snapshot.yaml b/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateQuarkiverseExtension/quarkus-my-quarkiverse-ext_.github_workflows_quarkus-snapshot.yaml index ed1e496f85d6b..a66162f56c867 100644 --- a/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateQuarkiverseExtension/quarkus-my-quarkiverse-ext_.github_workflows_quarkus-snapshot.yaml +++ b/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateQuarkiverseExtension/quarkus-my-quarkiverse-ext_.github_workflows_quarkus-snapshot.yaml @@ -10,7 +10,7 @@ on: env: ECOSYSTEM_CI_REPO: quarkusio/quarkus-ecosystem-ci ECOSYSTEM_CI_REPO_FILE: context.yaml - JAVA_VERSION: 11 + JAVA_VERSION: 17 ######################### # Repo specific setting # diff --git a/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateQuarkiverseExtension/quarkus-my-quarkiverse-ext_.github_workflows_release.yml b/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateQuarkiverseExtension/quarkus-my-quarkiverse-ext_.github_workflows_release.yml index 0a3894f1bdd33..2a3577769d630 100644 --- a/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateQuarkiverseExtension/quarkus-my-quarkiverse-ext_.github_workflows_release.yml +++ b/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateQuarkiverseExtension/quarkus-my-quarkiverse-ext_.github_workflows_release.yml @@ -37,11 +37,11 @@ jobs: gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} passphrase: ${{ secrets.GPG_PASSPHRASE }} - - name: Set up JDK 11 + - name: Set up JDK 17 uses: actions/setup-java@v3 with: distribution: temurin - java-version: 11 + java-version: 17 cache: 'maven' server-id: ossrh server-username: MAVEN_USERNAME @@ -54,7 +54,7 @@ jobs: - name: Update latest release version in docs run: | - mvn -B -ntp -pl docs -am generate-resources -Denforcer.skip -Dformatter.skip -Dimpsort.skip + mvn -B -ntp -pl docs -am package -DskipTests -DskipITs -Denforcer.skip -Dformatter.skip -Dimpsort.skip if ! git diff --quiet docs/modules/ROOT/pages/includes/attributes.adoc; then git add docs/modules/ROOT/pages/includes/attributes.adoc git commit -m "Update the latest release version ${{steps.metadata.outputs.current-version}} in documentation" diff --git a/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateQuarkiverseExtension/quarkus-my-quarkiverse-ext_README.md b/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateQuarkiverseExtension/quarkus-my-quarkiverse-ext_README.md index 5e844fd542126..cb86bfbcd72d6 100644 --- a/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateQuarkiverseExtension/quarkus-my-quarkiverse-ext_README.md +++ b/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateQuarkiverseExtension/quarkus-my-quarkiverse-ext_README.md @@ -1,6 +1,6 @@ # Quarkus My Quarkiverse extension -[![Version](https://img.shields.io/maven-central/v/io.quarkiverse.my-quarkiverse-ext/quarkus-my-quarkiverse-ext?logo=apache-maven&style=flat-square)](https://search.maven.org/artifact/io.quarkiverse.my-quarkiverse-ext/quarkus-my-quarkiverse-ext) +[![Version](https://img.shields.io/maven-central/v/io.quarkiverse.my-quarkiverse-ext/quarkus-my-quarkiverse-ext?logo=apache-maven&style=flat-square)](https://central.sonatype.com/artifact/io.quarkiverse.my-quarkiverse-ext/quarkus-my-quarkiverse-ext-parent) ## Welcome to Quarkiverse! @@ -22,4 +22,6 @@ The documentation for this extension should be maintained as part of this reposi The layout should follow the [Antora's Standard File and Directory Set](https://docs.antora.org/antora/2.3/standard-directories/). -Once the docs are ready to be published, please open a PR including this repository in the [Quarkiverse Docs Antora playbook](https://github.com/quarkiverse/quarkiverse-docs/blob/main/antora-playbook.yml#L7). See an example [here](https://github.com/quarkiverse/quarkiverse-docs/pull/1). +Once the docs are ready to be published, please open a PR including this repository in the [Quarkiverse Docs Antora playbook](https://github.com/quarkiverse/quarkiverse-docs/blob/main/antora-playbook.yml#L7). See an example [here](https://github.com/quarkiverse/quarkiverse-docs/pull/1) + +Your documentation will then be published to the https://docs.quarkiverse.io/ website. diff --git a/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateQuarkiverseExtension/quarkus-my-quarkiverse-ext_pom.xml b/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateQuarkiverseExtension/quarkus-my-quarkiverse-ext_pom.xml index 65d094b29b632..5b3cfb537f46c 100644 --- a/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateQuarkiverseExtension/quarkus-my-quarkiverse-ext_pom.xml +++ b/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateQuarkiverseExtension/quarkus-my-quarkiverse-ext_pom.xml @@ -24,7 +24,7 @@ 3.11.0 - 11 + 17 UTF-8 UTF-8 2.10.5.Final diff --git a/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateStandaloneExtension/my-org-my-own-ext_pom.xml b/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateStandaloneExtension/my-org-my-own-ext_pom.xml index 20efb6a780683..eb38615c379de 100644 --- a/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateStandaloneExtension/my-org-my-own-ext_pom.xml +++ b/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateStandaloneExtension/my-org-my-own-ext_pom.xml @@ -14,7 +14,7 @@ 3.11.0 ${surefire-plugin.version} - 11 + 17 UTF-8 UTF-8 2.10.5.Final diff --git a/integration-tests/micrometer-mp-metrics/src/main/java/io/quarkus/it/micrometer/mpmetrics/MessageResource.java b/integration-tests/micrometer-mp-metrics/src/main/java/io/quarkus/it/micrometer/mpmetrics/MessageResource.java index 2105c7ef33831..6ce3276f3f970 100644 --- a/integration-tests/micrometer-mp-metrics/src/main/java/io/quarkus/it/micrometer/mpmetrics/MessageResource.java +++ b/integration-tests/micrometer-mp-metrics/src/main/java/io/quarkus/it/micrometer/mpmetrics/MessageResource.java @@ -1,34 +1,60 @@ package io.quarkus.it.micrometer.mpmetrics; +import java.util.Collection; +import java.util.Objects; + import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; import jakarta.ws.rs.PathParam; +import org.eclipse.microprofile.metrics.Counter; +import org.eclipse.microprofile.metrics.annotation.Metric; + +import io.micrometer.core.instrument.Meter; import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.search.Search; @Path("/message") public class MessageResource { private final MeterRegistry registry; + private final Counter first; + private final Counter second; - public MessageResource(MeterRegistry registry) { + public MessageResource(MeterRegistry registry, + @Metric(name = "first-counter") final Counter first, + @Metric(name = "second-counter") final Counter second) { this.registry = registry; + this.first = Objects.requireNonNull(first); + this.second = Objects.requireNonNull(second); } @GET public String message() { + first.inc(); + second.inc(); return registry.getClass().getName(); } @GET @Path("fail") public String fail() { + first.inc(); throw new NullPointerException("Failed on purpose"); } @GET @Path("item/{id}") public String item(@PathParam("id") String id) { + second.inc(); return "return message with id " + id; } + + @GET + @Path("mpmetrics") + public String metrics() { + Collection meters = Search.in(registry).name(s -> s.contains("mpmetrics")).meters(); + meters.addAll(Search.in(registry).name(s -> s.endsWith("-counter")).meters()); + return meters.stream().allMatch(x -> x.getId().getTag("scope") != null) ? "OK" : "FAIL"; + } } diff --git a/integration-tests/micrometer-mp-metrics/src/test/java/io/quarkus/it/micrometer/mpmetrics/MPMetricsTest.java b/integration-tests/micrometer-mp-metrics/src/test/java/io/quarkus/it/micrometer/mpmetrics/MPMetricsTest.java index 7d47a0f85cba2..d7c2e9fc02344 100644 --- a/integration-tests/micrometer-mp-metrics/src/test/java/io/quarkus/it/micrometer/mpmetrics/MPMetricsTest.java +++ b/integration-tests/micrometer-mp-metrics/src/test/java/io/quarkus/it/micrometer/mpmetrics/MPMetricsTest.java @@ -70,7 +70,10 @@ void validateMetricsOutput_1() { "io_quarkus_it_micrometer_mpmetrics_PrimeResource_highestPrimeNumberSoFar2{scope=\"application\"} 887.0")) // the counter associated with a timed method should have been removed - .body(not(containsString("io_quarkus_it_micrometer_mpmetrics_PrimeResource_checkPrime"))); + .body(not(containsString("io_quarkus_it_micrometer_mpmetrics_PrimeResource_checkPrime"))) + + // no calls to /message + .body(not(containsString("/message"))); } @Test @@ -83,7 +86,7 @@ void callPrimeGen_4() { } @Test - @Order(8) + @Order(6) void callMessage() { given() .when().get("/message") @@ -91,6 +94,24 @@ void callMessage() { .statusCode(200); } + @Test + @Order(7) + void callMessageFail() { + given() + .when().get("/message/fail") + .then() + .statusCode(500); + } + + @Test + @Order(8) + void callMessageId() { + given() + .when().get("/message/item/35") + .then() + .statusCode(200); + } + @Test @Order(9) void validateMetricsOutput_2() { @@ -106,7 +127,10 @@ void validateMetricsOutput_2() { "highestPrimeNumberSoFar 887.0")) .body(containsString( "io_quarkus_it_micrometer_mpmetrics_InjectedInstance_notPrime_total{scope=\"application\"}")) - .body(not(containsString("/message"))); + .body(containsString( + "first_counter_total{scope=\"application\"}")) + .body(containsString( + "second_counter_total{scope=\"application\"}")); } @Test @@ -123,4 +147,13 @@ void validateJsonOutput() { Matchers.equalTo(887.0f)); } + @Test + @Order(11) + void meters() { + given() + .when().get("/message/mpmetrics") + .then() + .statusCode(200) + .log().body(); + } } diff --git a/integration-tests/micrometer-prometheus/src/main/java/io/quarkus/it/micrometer/prometheus/AnnotatedResource.java b/integration-tests/micrometer-prometheus/src/main/java/io/quarkus/it/micrometer/prometheus/AnnotatedResource.java index 5d9d67c2eaef8..35badea2479c5 100644 --- a/integration-tests/micrometer-prometheus/src/main/java/io/quarkus/it/micrometer/prometheus/AnnotatedResource.java +++ b/integration-tests/micrometer-prometheus/src/main/java/io/quarkus/it/micrometer/prometheus/AnnotatedResource.java @@ -12,6 +12,7 @@ import io.micrometer.core.annotation.Counted; import io.micrometer.core.annotation.Timed; +import io.micrometer.core.aop.MeterTag; @Path("/all-the-things") public class AnnotatedResource { @@ -74,12 +75,12 @@ public Object onlyCountFailures() { } @Counted(value = "metric.all", extraTags = { "extra", "tag" }) - public Object countAllInvocations(boolean fail) { + public Object countAllInvocations(@MeterTag boolean fail) { return new Response(fail).get(); } @Counted(description = "nice description") - public Object emptyMetricName(boolean fail) { + public Object emptyMetricName(@MeterTag(resolver = PrefixingValueResolver.class) boolean fail) { return new Response(fail).get(); } @@ -89,12 +90,12 @@ public CompletableFuture onlyCountAsyncFailures() { } @Counted(value = "async.all", extraTags = { "extra", "tag" }) - public CompletableFuture countAllAsyncInvocations(boolean fail) { + public CompletableFuture countAllAsyncInvocations(@MeterTag(key = "do_fail_call") boolean fail) { return CompletableFuture.supplyAsync(new Response(fail)); } @Counted - public CompletableFuture emptyAsyncMetricName(boolean fail) { + public CompletableFuture emptyAsyncMetricName(@MeterTag(expression = "expression") boolean fail) { return CompletableFuture.supplyAsync(new Response(fail)); } diff --git a/integration-tests/micrometer-prometheus/src/main/java/io/quarkus/it/micrometer/prometheus/AnswerToEverythingExpressionResolver.java b/integration-tests/micrometer-prometheus/src/main/java/io/quarkus/it/micrometer/prometheus/AnswerToEverythingExpressionResolver.java new file mode 100644 index 0000000000000..18643d7bd22a2 --- /dev/null +++ b/integration-tests/micrometer-prometheus/src/main/java/io/quarkus/it/micrometer/prometheus/AnswerToEverythingExpressionResolver.java @@ -0,0 +1,14 @@ +package io.quarkus.it.micrometer.prometheus; + +import jakarta.inject.Singleton; + +import io.micrometer.common.annotation.ValueExpressionResolver; + +@Singleton +public class AnswerToEverythingExpressionResolver implements ValueExpressionResolver { + @Override + public String resolve(String expression, Object parameter) { + // Answer to the Ultimate Question of Life, the Universe, and Everything. + return "42"; + } +} diff --git a/integration-tests/micrometer-prometheus/src/main/java/io/quarkus/it/micrometer/prometheus/PrefixingValueResolver.java b/integration-tests/micrometer-prometheus/src/main/java/io/quarkus/it/micrometer/prometheus/PrefixingValueResolver.java new file mode 100644 index 0000000000000..5f257408909c5 --- /dev/null +++ b/integration-tests/micrometer-prometheus/src/main/java/io/quarkus/it/micrometer/prometheus/PrefixingValueResolver.java @@ -0,0 +1,13 @@ +package io.quarkus.it.micrometer.prometheus; + +import jakarta.inject.Singleton; + +import io.micrometer.common.annotation.ValueResolver; + +@Singleton +public class PrefixingValueResolver implements ValueResolver { + @Override + public String resolve(Object parameter) { + return "prefix " + parameter; + } +} diff --git a/integration-tests/micrometer-prometheus/src/test/java/io/quarkus/it/micrometer/prometheus/PrometheusMetricsRegistryTest.java b/integration-tests/micrometer-prometheus/src/test/java/io/quarkus/it/micrometer/prometheus/PrometheusMetricsRegistryTest.java index 67c1c7cf63d31..186cb2268a90b 100644 --- a/integration-tests/micrometer-prometheus/src/test/java/io/quarkus/it/micrometer/prometheus/PrometheusMetricsRegistryTest.java +++ b/integration-tests/micrometer-prometheus/src/test/java/io/quarkus/it/micrometer/prometheus/PrometheusMetricsRegistryTest.java @@ -148,22 +148,22 @@ void testPrometheusScrapeEndpointTextPlain() { // Annotated counters .body(not(containsString("metric_none"))) .body(containsString( - "metric_all_total{class=\"io.quarkus.it.micrometer.prometheus.AnnotatedResource\",env=\"test\",env2=\"test\",exception=\"none\",extra=\"tag\",method=\"countAllInvocations\",registry=\"prometheus\",result=\"success\",} 1.0")) + "metric_all_total{class=\"io.quarkus.it.micrometer.prometheus.AnnotatedResource\",env=\"test\",env2=\"test\",exception=\"none\",extra=\"tag\",fail=\"false\",method=\"countAllInvocations\",registry=\"prometheus\",result=\"success\",} 1.0")) .body(containsString( - "metric_all_total{class=\"io.quarkus.it.micrometer.prometheus.AnnotatedResource\",env=\"test\",env2=\"test\",exception=\"NullPointerException\",extra=\"tag\",method=\"countAllInvocations\",registry=\"prometheus\",result=\"failure\",} 1.0")) + "metric_all_total{class=\"io.quarkus.it.micrometer.prometheus.AnnotatedResource\",env=\"test\",env2=\"test\",exception=\"NullPointerException\",extra=\"tag\",fail=\"true\",method=\"countAllInvocations\",registry=\"prometheus\",result=\"failure\",} 1.0")) .body(containsString( - "method_counted_total{class=\"io.quarkus.it.micrometer.prometheus.AnnotatedResource\",env=\"test\",env2=\"test\",exception=\"NullPointerException\",method=\"emptyMetricName\",registry=\"prometheus\",result=\"failure\",} 1.0")) + "method_counted_total{class=\"io.quarkus.it.micrometer.prometheus.AnnotatedResource\",env=\"test\",env2=\"test\",exception=\"NullPointerException\",fail=\"prefix true\",method=\"emptyMetricName\",registry=\"prometheus\",result=\"failure\",} 1.0")) .body(containsString( - "method_counted_total{class=\"io.quarkus.it.micrometer.prometheus.AnnotatedResource\",env=\"test\",env2=\"test\",exception=\"none\",method=\"emptyMetricName\",registry=\"prometheus\",result=\"success\",} 1.0")) + "method_counted_total{class=\"io.quarkus.it.micrometer.prometheus.AnnotatedResource\",env=\"test\",env2=\"test\",exception=\"none\",fail=\"prefix false\",method=\"emptyMetricName\",registry=\"prometheus\",result=\"success\",} 1.0")) .body(not(containsString("async_none"))) .body(containsString( - "async_all_total{class=\"io.quarkus.it.micrometer.prometheus.AnnotatedResource\",env=\"test\",env2=\"test\",exception=\"NullPointerException\",extra=\"tag\",method=\"countAllAsyncInvocations\",registry=\"prometheus\",result=\"failure\",} 1.0")) + "async_all_total{class=\"io.quarkus.it.micrometer.prometheus.AnnotatedResource\",do_fail_call=\"true\",env=\"test\",env2=\"test\",exception=\"NullPointerException\",extra=\"tag\",method=\"countAllAsyncInvocations\",registry=\"prometheus\",result=\"failure\",} 1.0")) .body(containsString( - "async_all_total{class=\"io.quarkus.it.micrometer.prometheus.AnnotatedResource\",env=\"test\",env2=\"test\",exception=\"none\",extra=\"tag\",method=\"countAllAsyncInvocations\",registry=\"prometheus\",result=\"success\",} 1.0")) + "async_all_total{class=\"io.quarkus.it.micrometer.prometheus.AnnotatedResource\",do_fail_call=\"false\",env=\"test\",env2=\"test\",exception=\"none\",extra=\"tag\",method=\"countAllAsyncInvocations\",registry=\"prometheus\",result=\"success\",} 1.0")) .body(containsString( - "method_counted_total{class=\"io.quarkus.it.micrometer.prometheus.AnnotatedResource\",env=\"test\",env2=\"test\",exception=\"NullPointerException\",method=\"emptyAsyncMetricName\",registry=\"prometheus\",result=\"failure\",} 1.0")) + "method_counted_total{class=\"io.quarkus.it.micrometer.prometheus.AnnotatedResource\",env=\"test\",env2=\"test\",exception=\"NullPointerException\",fail=\"42\",method=\"emptyAsyncMetricName\",registry=\"prometheus\",result=\"failure\",} 1.0")) .body(containsString( - "method_counted_total{class=\"io.quarkus.it.micrometer.prometheus.AnnotatedResource\",env=\"test\",env2=\"test\",exception=\"none\",method=\"emptyAsyncMetricName\",registry=\"prometheus\",result=\"success\",} 1.0")) + "method_counted_total{class=\"io.quarkus.it.micrometer.prometheus.AnnotatedResource\",env=\"test\",env2=\"test\",exception=\"none\",fail=\"42\",method=\"emptyAsyncMetricName\",registry=\"prometheus\",result=\"success\",} 1.0")) // Annotated Timers .body(containsString( @@ -240,22 +240,22 @@ void testPrometheusScrapeEndpointOpenMetrics() { // Annotated counters .body(not(containsString("metric_none"))) .body(containsString( - "metric_all_total{class=\"io.quarkus.it.micrometer.prometheus.AnnotatedResource\",env=\"test\",env2=\"test\",exception=\"none\",extra=\"tag\",method=\"countAllInvocations\",registry=\"prometheus\",result=\"success\"} 1.0 # {span_id=")) + "metric_all_total{class=\"io.quarkus.it.micrometer.prometheus.AnnotatedResource\",env=\"test\",env2=\"test\",exception=\"none\",extra=\"tag\",fail=\"false\",method=\"countAllInvocations\",registry=\"prometheus\",result=\"success\"} 1.0 # {span_id=")) .body(containsString( - "metric_all_total{class=\"io.quarkus.it.micrometer.prometheus.AnnotatedResource\",env=\"test\",env2=\"test\",exception=\"NullPointerException\",extra=\"tag\",method=\"countAllInvocations\",registry=\"prometheus\",result=\"failure\"} 1.0")) + "metric_all_total{class=\"io.quarkus.it.micrometer.prometheus.AnnotatedResource\",env=\"test\",env2=\"test\",exception=\"NullPointerException\",extra=\"tag\",fail=\"true\",method=\"countAllInvocations\",registry=\"prometheus\",result=\"failure\"} 1.0")) .body(containsString( - "method_counted_total{class=\"io.quarkus.it.micrometer.prometheus.AnnotatedResource\",env=\"test\",env2=\"test\",exception=\"NullPointerException\",method=\"emptyMetricName\",registry=\"prometheus\",result=\"failure\"} 1.0 # {span_id=")) + "method_counted_total{class=\"io.quarkus.it.micrometer.prometheus.AnnotatedResource\",env=\"test\",env2=\"test\",exception=\"NullPointerException\",fail=\"prefix true\",method=\"emptyMetricName\",registry=\"prometheus\",result=\"failure\"} 1.0 # {span_id=")) .body(containsString( - "method_counted_total{class=\"io.quarkus.it.micrometer.prometheus.AnnotatedResource\",env=\"test\",env2=\"test\",exception=\"none\",method=\"emptyMetricName\",registry=\"prometheus\",result=\"success\"} 1.0")) + "method_counted_total{class=\"io.quarkus.it.micrometer.prometheus.AnnotatedResource\",env=\"test\",env2=\"test\",exception=\"none\",fail=\"prefix false\",method=\"emptyMetricName\",registry=\"prometheus\",result=\"success\"} 1.0")) .body(not(containsString("async_none"))) .body(containsString( - "async_all_total{class=\"io.quarkus.it.micrometer.prometheus.AnnotatedResource\",env=\"test\",env2=\"test\",exception=\"NullPointerException\",extra=\"tag\",method=\"countAllAsyncInvocations\",registry=\"prometheus\",result=\"failure\"} 1.0")) + "async_all_total{class=\"io.quarkus.it.micrometer.prometheus.AnnotatedResource\",do_fail_call=\"true\",env=\"test\",env2=\"test\",exception=\"NullPointerException\",extra=\"tag\",method=\"countAllAsyncInvocations\",registry=\"prometheus\",result=\"failure\"} 1.0")) .body(containsString( - "async_all_total{class=\"io.quarkus.it.micrometer.prometheus.AnnotatedResource\",env=\"test\",env2=\"test\",exception=\"none\",extra=\"tag\",method=\"countAllAsyncInvocations\",registry=\"prometheus\",result=\"success\"} 1.0")) + "async_all_total{class=\"io.quarkus.it.micrometer.prometheus.AnnotatedResource\",do_fail_call=\"false\",env=\"test\",env2=\"test\",exception=\"none\",extra=\"tag\",method=\"countAllAsyncInvocations\",registry=\"prometheus\",result=\"success\"} 1.0")) .body(containsString( - "method_counted_total{class=\"io.quarkus.it.micrometer.prometheus.AnnotatedResource\",env=\"test\",env2=\"test\",exception=\"NullPointerException\",method=\"emptyAsyncMetricName\",registry=\"prometheus\",result=\"failure\"} 1.0")) + "method_counted_total{class=\"io.quarkus.it.micrometer.prometheus.AnnotatedResource\",env=\"test\",env2=\"test\",exception=\"NullPointerException\",fail=\"42\",method=\"emptyAsyncMetricName\",registry=\"prometheus\",result=\"failure\"} 1.0")) .body(containsString( - "method_counted_total{class=\"io.quarkus.it.micrometer.prometheus.AnnotatedResource\",env=\"test\",env2=\"test\",exception=\"none\",method=\"emptyAsyncMetricName\",registry=\"prometheus\",result=\"success\"} 1.0")) + "method_counted_total{class=\"io.quarkus.it.micrometer.prometheus.AnnotatedResource\",env=\"test\",env2=\"test\",exception=\"none\",fail=\"42\",method=\"emptyAsyncMetricName\",registry=\"prometheus\",result=\"success\"} 1.0")) // Annotated Timers .body(containsString( diff --git a/integration-tests/java-17/src/main/java/io/quarkus/it/mongodb/panache/person/PersonName.java b/integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/record/PersonName.java similarity index 60% rename from integration-tests/java-17/src/main/java/io/quarkus/it/mongodb/panache/person/PersonName.java rename to integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/record/PersonName.java index dfadda4abc8e1..f3425e5f52939 100644 --- a/integration-tests/java-17/src/main/java/io/quarkus/it/mongodb/panache/person/PersonName.java +++ b/integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/record/PersonName.java @@ -1,7 +1,7 @@ -package io.quarkus.it.mongodb.panache.person; +package io.quarkus.it.mongodb.panache.record; import io.quarkus.mongodb.panache.common.ProjectionFor; -@ProjectionFor(Person.class) +@ProjectionFor(PersonRecord.class) public record PersonName(String firstName, String lastName) { } diff --git a/integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/record/PersonRecord.java b/integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/record/PersonRecord.java new file mode 100644 index 0000000000000..91ab54030c183 --- /dev/null +++ b/integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/record/PersonRecord.java @@ -0,0 +1,4 @@ +package io.quarkus.it.mongodb.panache.record; + +public record PersonRecord(String firstName, String lastName, Status status) { +} diff --git a/integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/record/PersonRecordRepository.java b/integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/record/PersonRecordRepository.java new file mode 100644 index 0000000000000..ceb019b521282 --- /dev/null +++ b/integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/record/PersonRecordRepository.java @@ -0,0 +1,9 @@ +package io.quarkus.it.mongodb.panache.record; + +import jakarta.enterprise.context.ApplicationScoped; + +import io.quarkus.mongodb.panache.PanacheMongoRepository; + +@ApplicationScoped +public class PersonRecordRepository implements PanacheMongoRepository { +} diff --git a/integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/record/PersonResource.java b/integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/record/PersonResource.java new file mode 100644 index 0000000000000..cd49f9dec2993 --- /dev/null +++ b/integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/record/PersonResource.java @@ -0,0 +1,24 @@ +package io.quarkus.it.mongodb.panache.record; + +import java.util.List; + +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; + +@Path("/persons/record") +public class PersonResource { + @Inject + PersonRecordRepository personRecordRepository; + + @GET + public List getPersons() { + return personRecordRepository.findAll().project(PersonName.class).list(); + } + + @POST + public void addPerson(PersonRecord person) { + personRecordRepository.persist(person); + } +} diff --git a/integration-tests/java-17/src/main/java/io/quarkus/it/mongodb/panache/person/Status.java b/integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/record/Status.java similarity index 87% rename from integration-tests/java-17/src/main/java/io/quarkus/it/mongodb/panache/person/Status.java rename to integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/record/Status.java index 0a7180aab25e7..09ccc049a6e0a 100644 --- a/integration-tests/java-17/src/main/java/io/quarkus/it/mongodb/panache/person/Status.java +++ b/integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/record/Status.java @@ -1,4 +1,4 @@ -package io.quarkus.it.mongodb.panache.person; +package io.quarkus.it.mongodb.panache.record; public enum Status { DEAD("I'm a Zombie"), diff --git a/integration-tests/mongodb-panache/src/test/java/io/quarkus/it/mongodb/panache/record/MongodbPanacheRecordIT.java b/integration-tests/mongodb-panache/src/test/java/io/quarkus/it/mongodb/panache/record/MongodbPanacheRecordIT.java new file mode 100644 index 0000000000000..2c87986c3d7a5 --- /dev/null +++ b/integration-tests/mongodb-panache/src/test/java/io/quarkus/it/mongodb/panache/record/MongodbPanacheRecordIT.java @@ -0,0 +1,7 @@ +package io.quarkus.it.mongodb.panache.record; + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +class MongodbPanacheRecordIT extends MongodbPanacheRecordTest { +} diff --git a/integration-tests/java-17/src/test/java/io/quarkus/it/mongodb/panache/person/PersonResourceTest.java b/integration-tests/mongodb-panache/src/test/java/io/quarkus/it/mongodb/panache/record/MongodbPanacheRecordTest.java similarity index 53% rename from integration-tests/java-17/src/test/java/io/quarkus/it/mongodb/panache/person/PersonResourceTest.java rename to integration-tests/mongodb-panache/src/test/java/io/quarkus/it/mongodb/panache/record/MongodbPanacheRecordTest.java index 72c3162cdfdca..b6b1c77d75a65 100644 --- a/integration-tests/java-17/src/test/java/io/quarkus/it/mongodb/panache/person/PersonResourceTest.java +++ b/integration-tests/mongodb-panache/src/test/java/io/quarkus/it/mongodb/panache/record/MongodbPanacheRecordTest.java @@ -1,4 +1,4 @@ -package io.quarkus.it.mongodb.panache.person; +package io.quarkus.it.mongodb.panache.record; import static io.restassured.RestAssured.given; import static io.restassured.RestAssured.when; @@ -8,36 +8,30 @@ import io.quarkus.test.common.QuarkusTestResource; import io.quarkus.test.junit.QuarkusTest; -import io.quarkus.test.mongodb.MongoTestResource; +import io.quarkus.test.mongodb.MongoReplicaSetTestResource; import io.restassured.http.ContentType; @QuarkusTest -@QuarkusTestResource(MongoTestResource.class) -class PersonResourceTest { - private static final String ROOT_URL = "/mongo/persons"; +@QuarkusTestResource(MongoReplicaSetTestResource.class) +class MongodbPanacheRecordTest { + + private static final String ROOT_URL = "/persons/record"; @Test void testRecordInPanache() { - var person1 = new Person(); - person1.firstname = "Loïc"; - person1.lastname = "Mathieu"; - person1.status = Status.ALIVE; - var person2 = new Person(); - person1.firstname = "Zombie"; - person2.lastname = "Zombie"; - person2.status = Status.DEAD; + var person1 = new PersonRecord("Loïc", "Mathieu", Status.ALIVE); + var person2 = new PersonRecord("Zombie", "Zombie", Status.DEAD); given().body(person1).contentType(ContentType.JSON) .when().post(ROOT_URL) - .then().statusCode(201); + .then().statusCode(204); given().body(person2).contentType(ContentType.JSON) .when().post(ROOT_URL) - .then().statusCode(201); + .then().statusCode(204); when().get(ROOT_URL) .then() .statusCode(200) .body("size()", is(2)); } - } diff --git a/integration-tests/mtls-certificates/pom.xml b/integration-tests/mtls-certificates/pom.xml new file mode 100644 index 0000000000000..c62a8e7a0a050 --- /dev/null +++ b/integration-tests/mtls-certificates/pom.xml @@ -0,0 +1,117 @@ + + + 4.0.0 + + + quarkus-integration-tests-parent + io.quarkus + 999-SNAPSHOT + + + quarkus-integration-test-mtls-certificates + Quarkus - Integration Tests - mTLS Client Certificate tests + + + + io.quarkus + quarkus-resteasy-reactive + + + io.quarkus + quarkus-security + + + io.quarkus + quarkus-junit5 + test + + + io.rest-assured + rest-assured + test + + + + + + + io.quarkus + quarkus-security-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-resteasy-reactive-deployment + ${project.version} + pom + test + + + * + * + + + + + + + + + io.quarkus + quarkus-maven-plugin + + + + build + + + + + + + + + native + + + native + + + + + + maven-failsafe-plugin + + + + integration-test + verify + + + + + ${project.build.directory}/${project.build.finalName}-runner + + + + + + + + + + native + + + + diff --git a/integration-tests/mtls-certificates/src/main/java/io/quarkus/it/vertx/CertificateRoleMappingResource.java b/integration-tests/mtls-certificates/src/main/java/io/quarkus/it/vertx/CertificateRoleMappingResource.java new file mode 100644 index 0000000000000..d42ec1fc65425 --- /dev/null +++ b/integration-tests/mtls-certificates/src/main/java/io/quarkus/it/vertx/CertificateRoleMappingResource.java @@ -0,0 +1,42 @@ +package io.quarkus.it.vertx; + +import jakarta.annotation.security.RolesAllowed; +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.security.Authenticated; +import io.quarkus.security.identity.SecurityIdentity; + +@Path("/protected") +public class CertificateRoleMappingResource { + + @Inject + SecurityIdentity identity; + + @Authenticated + @GET + @Produces(MediaType.TEXT_PLAIN) + @Path("/authenticated") + public String name() { + return identity.getPrincipal().getName(); + } + + @GET + @Produces(MediaType.TEXT_PLAIN) + @Path("/authorized-user") + @RolesAllowed("user") + public String authorizedName() { + return identity.getPrincipal().getName(); + } + + @GET + @Produces(MediaType.TEXT_PLAIN) + @Path("/authorized-admin") + @RolesAllowed("admin") + public String authorizedAdmin() { + return identity.getPrincipal().getName(); + } +} diff --git a/integration-tests/mtls-certificates/src/main/resources/application.properties b/integration-tests/mtls-certificates/src/main/resources/application.properties new file mode 100644 index 0000000000000..871c0f0ee71bb --- /dev/null +++ b/integration-tests/mtls-certificates/src/main/resources/application.properties @@ -0,0 +1,10 @@ +quarkus.http.ssl.certificate.key-store-file=server-keystore.jks +quarkus.http.ssl.certificate.key-store-password=password +quarkus.http.ssl.certificate.key-store-key-alias=server +quarkus.http.ssl.certificate.key-store-key-password=serverpw +quarkus.http.ssl.certificate.trust-store-file=server-truststore.jks +quarkus.http.ssl.certificate.trust-store-password=password +quarkus.http.ssl.client-auth=REQUIRED +quarkus.http.auth.certificate-role-properties=role-mappings.txt +quarkus.native.additional-build-args=-H:IncludeResources=.*\\.jks,-H:IncludeResources=.*\\.txt + diff --git a/integration-tests/mtls-certificates/src/main/resources/role-mappings.txt b/integration-tests/mtls-certificates/src/main/resources/role-mappings.txt new file mode 100644 index 0000000000000..506946d5cdf50 --- /dev/null +++ b/integration-tests/mtls-certificates/src/main/resources/role-mappings.txt @@ -0,0 +1,2 @@ +client=user,admin +localhost=user \ No newline at end of file diff --git a/integration-tests/mtls-certificates/src/main/resources/server-keystore.jks b/integration-tests/mtls-certificates/src/main/resources/server-keystore.jks new file mode 100644 index 0000000000000..c7ac8b12c43bf Binary files /dev/null and b/integration-tests/mtls-certificates/src/main/resources/server-keystore.jks differ diff --git a/integration-tests/mtls-certificates/src/main/resources/server-truststore.jks b/integration-tests/mtls-certificates/src/main/resources/server-truststore.jks new file mode 100644 index 0000000000000..87dc406af5528 Binary files /dev/null and b/integration-tests/mtls-certificates/src/main/resources/server-truststore.jks differ diff --git a/integration-tests/mtls-certificates/src/test/java/io/quarkus/it/vertx/CertificateRoleMappingIT.java b/integration-tests/mtls-certificates/src/test/java/io/quarkus/it/vertx/CertificateRoleMappingIT.java new file mode 100644 index 0000000000000..2e33b3dc59c98 --- /dev/null +++ b/integration-tests/mtls-certificates/src/test/java/io/quarkus/it/vertx/CertificateRoleMappingIT.java @@ -0,0 +1,7 @@ +package io.quarkus.it.vertx; + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +public class CertificateRoleMappingIT extends CertificateRoleMappingTest { +} diff --git a/integration-tests/mtls-certificates/src/test/java/io/quarkus/it/vertx/CertificateRoleMappingTest.java b/integration-tests/mtls-certificates/src/test/java/io/quarkus/it/vertx/CertificateRoleMappingTest.java new file mode 100644 index 0000000000000..18484a0fa5f80 --- /dev/null +++ b/integration-tests/mtls-certificates/src/test/java/io/quarkus/it/vertx/CertificateRoleMappingTest.java @@ -0,0 +1,68 @@ +package io.quarkus.it.vertx; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.equalTo; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.net.ConnectException; +import java.net.URL; + +import org.junit.jupiter.api.Test; + +import io.quarkus.test.common.http.TestHTTPResource; +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.builder.RequestSpecBuilder; +import io.restassured.specification.RequestSpecification; + +@QuarkusTest +public class CertificateRoleMappingTest { + + @TestHTTPResource(ssl = true) + URL url; + + @Test + public void testAuthenticated() { + given().spec(getMtlsRequestSpec("client-keystore-1.jks")).get("/protected/authenticated") + .then().body(equalTo("CN=client,OU=cert,O=quarkus,L=city,ST=state,C=AU")); + given().spec(getMtlsRequestSpec("client-keystore-2.jks")).get("/protected/authenticated") + .then().body(equalTo("CN=localhost,OU=quarkus,O=quarkus,L=city,ST=state,C=IE")); + } + + @Test + public void testAuthorizedUser() { + given().spec(getMtlsRequestSpec("client-keystore-1.jks")).get("/protected/authorized-user") + .then().body(equalTo("CN=client,OU=cert,O=quarkus,L=city,ST=state,C=AU")); + given().spec(getMtlsRequestSpec("client-keystore-2.jks")).get("/protected/authorized-user") + .then().body(equalTo("CN=localhost,OU=quarkus,O=quarkus,L=city,ST=state,C=IE")); + } + + @Test + public void testAuthorizedAdmin() { + given().spec(getMtlsRequestSpec("client-keystore-1.jks")).get("/protected/authorized-admin") + .then().body(equalTo("CN=client,OU=cert,O=quarkus,L=city,ST=state,C=AU")); + given().spec(getMtlsRequestSpec("client-keystore-2.jks")).get("/protected/authorized-admin") + .then().statusCode(403); + } + + @Test + public void testNoClientCertificate() { + assertThrows(ConnectException.class, + () -> given().get("/protected/authenticated"), + "Insecure requests must fail at the transport level"); + assertThrows(ConnectException.class, + () -> given().get("/protected/authorized-user"), + "Insecure requests must fail at the transport level"); + assertThrows(ConnectException.class, + () -> given().get("/protected/authorized-admin"), + "Insecure requests must fail at the transport level"); + } + + private RequestSpecification getMtlsRequestSpec(String clientKeyStore) { + return new RequestSpecBuilder() + .setBaseUri(String.format("%s://%s", url.getProtocol(), url.getHost())) + .setPort(url.getPort()) + .setKeyStore(clientKeyStore, "password") + .setTrustStore("client-truststore.jks", "password") + .build(); + } +} diff --git a/integration-tests/mtls-certificates/src/test/resources/client-keystore-1.jks b/integration-tests/mtls-certificates/src/test/resources/client-keystore-1.jks new file mode 100644 index 0000000000000..cf6d6ba454864 Binary files /dev/null and b/integration-tests/mtls-certificates/src/test/resources/client-keystore-1.jks differ diff --git a/integration-tests/mtls-certificates/src/test/resources/client-keystore-2.jks b/integration-tests/mtls-certificates/src/test/resources/client-keystore-2.jks new file mode 100644 index 0000000000000..6961a6b1d0203 Binary files /dev/null and b/integration-tests/mtls-certificates/src/test/resources/client-keystore-2.jks differ diff --git a/integration-tests/mtls-certificates/src/test/resources/client-truststore.jks b/integration-tests/mtls-certificates/src/test/resources/client-truststore.jks new file mode 100644 index 0000000000000..112fb9857fbd7 Binary files /dev/null and b/integration-tests/mtls-certificates/src/test/resources/client-truststore.jks differ diff --git a/integration-tests/oidc-client-reactive/pom.xml b/integration-tests/oidc-client-reactive/pom.xml index 5d504c6944136..ae8c75a0c95c1 100644 --- a/integration-tests/oidc-client-reactive/pom.xml +++ b/integration-tests/oidc-client-reactive/pom.xml @@ -154,11 +154,8 @@ gradle-enterprise-maven-extension - - - - META-INF/ide-deps/** - + + application.properties 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 b0d0f2282c034..513b1584b3a91 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,6 +3,8 @@ import jakarta.enterprise.context.ApplicationScoped; import io.quarkus.arc.Unremovable; +import io.quarkus.oidc.common.OidcEndpoint; +import io.quarkus.oidc.common.OidcEndpoint.Type; import io.quarkus.oidc.common.OidcRequestContextProperties; import io.quarkus.oidc.common.OidcRequestFilter; import io.vertx.mutiny.core.buffer.Buffer; @@ -10,6 +12,7 @@ @ApplicationScoped @Unremovable +@OidcEndpoint(value = Type.TOKEN) public class OidcRequestCustomizer implements OidcRequestFilter { @Override diff --git a/integration-tests/oidc-token-propagation-reactive/src/main/java/io/quarkus/it/keycloak/FrontendResource.java b/integration-tests/oidc-token-propagation-reactive/src/main/java/io/quarkus/it/keycloak/FrontendResource.java index 64d9d13c35a6e..58690fa283fd1 100644 --- a/integration-tests/oidc-token-propagation-reactive/src/main/java/io/quarkus/it/keycloak/FrontendResource.java +++ b/integration-tests/oidc-token-propagation-reactive/src/main/java/io/quarkus/it/keycloak/FrontendResource.java @@ -16,6 +16,10 @@ public class FrontendResource { @RestClient AccessTokenPropagationService accessTokenPropagationService; + @Inject + @RestClient + IdTokenPropagationService idTokenPropagationService; + @Inject @RestClient ServiceWithoutToken serviceWithoutToken; @@ -28,6 +32,14 @@ public Uni userNameAccessTokenPropagation() { return accessTokenPropagationService.getUserName(); } + @GET + @Path("id-token-propagation") + @Produces("text/plain") + @RolesAllowed("user") + public Uni userNameIdTokenPropagation() { + return idTokenPropagationService.getUserName(); + } + @GET @Path("service-without-token") @Produces("text/plain") diff --git a/integration-tests/oidc-token-propagation-reactive/src/main/java/io/quarkus/it/keycloak/IdTokenPropagationService.java b/integration-tests/oidc-token-propagation-reactive/src/main/java/io/quarkus/it/keycloak/IdTokenPropagationService.java new file mode 100644 index 0000000000000..8fe9e0e928cdd --- /dev/null +++ b/integration-tests/oidc-token-propagation-reactive/src/main/java/io/quarkus/it/keycloak/IdTokenPropagationService.java @@ -0,0 +1,20 @@ +package io.quarkus.it.keycloak; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; + +import org.eclipse.microprofile.rest.client.annotation.RegisterProvider; +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; + +import io.smallrye.mutiny.Uni; + +@RegisterRestClient +@RegisterProvider(IdTokenRequestReactiveFilter.class) +@Path("/") +public interface IdTokenPropagationService { + + @GET + @Produces("text/plain") + Uni getUserName(); +} diff --git a/integration-tests/oidc-token-propagation-reactive/src/main/java/io/quarkus/it/keycloak/IdTokenRequestReactiveFilter.java b/integration-tests/oidc-token-propagation-reactive/src/main/java/io/quarkus/it/keycloak/IdTokenRequestReactiveFilter.java new file mode 100644 index 0000000000000..82186e45db0c6 --- /dev/null +++ b/integration-tests/oidc-token-propagation-reactive/src/main/java/io/quarkus/it/keycloak/IdTokenRequestReactiveFilter.java @@ -0,0 +1,23 @@ +package io.quarkus.it.keycloak; + +import jakarta.annotation.Priority; +import jakarta.inject.Inject; +import jakarta.ws.rs.Priorities; +import jakarta.ws.rs.core.HttpHeaders; + +import org.jboss.resteasy.reactive.client.spi.ResteasyReactiveClientRequestContext; +import org.jboss.resteasy.reactive.client.spi.ResteasyReactiveClientRequestFilter; + +import io.quarkus.oidc.IdTokenCredential; + +@Priority(Priorities.AUTHENTICATION) +public class IdTokenRequestReactiveFilter implements ResteasyReactiveClientRequestFilter { + + @Inject + IdTokenCredential idToken; + + @Override + public void filter(ResteasyReactiveClientRequestContext requestContext) { + requestContext.getHeaders().putSingle(HttpHeaders.AUTHORIZATION, "Bearer " + idToken.getToken()); + } +} diff --git a/integration-tests/oidc-token-propagation-reactive/src/main/java/io/quarkus/it/keycloak/ProtectedResource.java b/integration-tests/oidc-token-propagation-reactive/src/main/java/io/quarkus/it/keycloak/ProtectedResource.java index c4cf5320e129a..3b3a2a78883e2 100644 --- a/integration-tests/oidc-token-propagation-reactive/src/main/java/io/quarkus/it/keycloak/ProtectedResource.java +++ b/integration-tests/oidc-token-propagation-reactive/src/main/java/io/quarkus/it/keycloak/ProtectedResource.java @@ -1,13 +1,13 @@ package io.quarkus.it.keycloak; -import java.security.Principal; - import jakarta.annotation.security.RolesAllowed; import jakarta.inject.Inject; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; +import org.eclipse.microprofile.jwt.JsonWebToken; + import io.quarkus.security.Authenticated; import io.smallrye.mutiny.Uni; @@ -16,12 +16,12 @@ public class ProtectedResource { @Inject - Principal principal; + JsonWebToken jwt; @GET @Produces("text/plain") @RolesAllowed("user") public Uni principalName() { - return Uni.createFrom().item(principal.getName()); + return Uni.createFrom().item(jwt.getClaim("typ") + ":" + jwt.getName()); } } diff --git a/integration-tests/oidc-token-propagation-reactive/src/main/resources/application.properties b/integration-tests/oidc-token-propagation-reactive/src/main/resources/application.properties index 321847db72236..36253fa1c4c16 100644 --- a/integration-tests/oidc-token-propagation-reactive/src/main/resources/application.properties +++ b/integration-tests/oidc-token-propagation-reactive/src/main/resources/application.properties @@ -1,4 +1,5 @@ io.quarkus.it.keycloak.AccessTokenPropagationService/mp-rest/uri=http://localhost:8081/protected +io.quarkus.it.keycloak.IdTokenPropagationService/mp-rest/uri=http://localhost:8081/protected io.quarkus.it.keycloak.ServiceWithoutToken/mp-rest/uri=http://localhost:8081/protected quarkus.oidc.application-type=web-app diff --git a/integration-tests/oidc-token-propagation-reactive/src/test/java/io/quarkus/it/keycloak/OidcTokenReactivePropagationTest.java b/integration-tests/oidc-token-propagation-reactive/src/test/java/io/quarkus/it/keycloak/OidcTokenReactivePropagationTest.java index df2184d15568a..7a4cb646a36f0 100644 --- a/integration-tests/oidc-token-propagation-reactive/src/test/java/io/quarkus/it/keycloak/OidcTokenReactivePropagationTest.java +++ b/integration-tests/oidc-token-propagation-reactive/src/test/java/io/quarkus/it/keycloak/OidcTokenReactivePropagationTest.java @@ -29,8 +29,11 @@ public void testGetUserNameWithAccessTokenPropagation() throws Exception { loginForm.getInputByName("password").setValueAttribute("alice"); TextPage textPage = loginForm.getInputByName("login").click(); + assertEquals("Bearer:alice", textPage.getContent()); + + textPage = webClient.getPage("http://localhost:8081/frontend/id-token-propagation"); + assertEquals("ID:alice", textPage.getContent()); - assertEquals("alice", textPage.getContent()); webClient.getCookieManager().clearCookies(); } } diff --git a/integration-tests/oidc-wiremock/pom.xml b/integration-tests/oidc-wiremock/pom.xml index 05a5cafb1d838..6e4c116474ca2 100644 --- a/integration-tests/oidc-wiremock/pom.xml +++ b/integration-tests/oidc-wiremock/pom.xml @@ -80,12 +80,6 @@ - - - src/main/resources - true - - maven-surefire-plugin diff --git a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/AdminResource.java b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/AdminResource.java index 665fbad54356a..a499a3d7c5f62 100644 --- a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/AdminResource.java +++ b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/AdminResource.java @@ -53,6 +53,22 @@ public String adminNoIntrospection() { return "granted:" + identity.getRoles(); } + @Path("bearer-certificate-full-chain") + @GET + @RolesAllowed("admin") + @Produces(MediaType.APPLICATION_JSON) + public String bearerCertificateFullChain() { + return "granted:" + identity.getRoles(); + } + + @Path("bearer-kid-or-chain") + @GET + @RolesAllowed("admin") + @Produces(MediaType.APPLICATION_JSON) + public String bearerKidOrChain() { + return "granted:" + identity.getRoles(); + } + @Path("bearer-role-claim-path") @GET @RolesAllowed("custom") diff --git a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CustomTenantResolver.java b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CustomTenantResolver.java index 34ffd429732e1..2fe6cca9790dc 100644 --- a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CustomTenantResolver.java +++ b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CustomTenantResolver.java @@ -17,54 +17,10 @@ public String resolve(RoutingContext context) { if (path.endsWith("code-flow") || path.endsWith("code-flow/logout")) { return "code-flow"; } - if (path.endsWith("code-flow-encrypted-id-token-jwk")) { - return "code-flow-encrypted-id-token-jwk"; - } - if (path.endsWith("code-flow-encrypted-id-token-pem")) { - return "code-flow-encrypted-id-token-pem"; - } if (path.endsWith("code-flow-form-post") || path.endsWith("code-flow-form-post/front-channel-logout")) { return "code-flow-form-post"; } - if (path.endsWith("code-flow-user-info-only")) { - return "code-flow-user-info-only"; - } - if (path.endsWith("code-flow-user-info-github")) { - return "code-flow-user-info-github"; - } - if (path.endsWith("bearer-user-info-github-service")) { - return "bearer-user-info-github-service"; - } - if (path.endsWith("code-flow-user-info-github-cached-in-idtoken")) { - return "code-flow-user-info-github-cached-in-idtoken"; - } - if (path.endsWith("code-flow-token-introspection")) { - return "code-flow-token-introspection"; - } - if (path.endsWith("bearer")) { - return "bearer"; - } - if (path.endsWith("bearer-id")) { - return "bearer-id"; - } - if (path.endsWith("bearer-required-algorithm")) { - return "bearer-required-algorithm"; - } - if (path.endsWith("bearer-azure")) { - return "bearer-azure"; - } - if (path.endsWith("bearer-no-introspection")) { - return "bearer-no-introspection"; - } - if (path.endsWith("bearer-role-claim-path")) { - return "bearer-role-claim-path"; - } - if (path.endsWith("bearer-key-without-kid-thumbprint")) { - return "bearer-key-without-kid-thumbprint"; - } - if (path.endsWith("bearer-wrong-role-path")) { - return "bearer-wrong-role-path"; - } + return null; } } diff --git a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/OidcDiscoveryRequestCustomizer.java b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/OidcDiscoveryRequestCustomizer.java new file mode 100644 index 0000000000000..069450ce91ec2 --- /dev/null +++ b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/OidcDiscoveryRequestCustomizer.java @@ -0,0 +1,22 @@ +package io.quarkus.it.keycloak; + +import jakarta.enterprise.context.ApplicationScoped; + +import io.quarkus.arc.Unremovable; +import io.quarkus.oidc.common.OidcEndpoint; +import io.quarkus.oidc.common.OidcEndpoint.Type; +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 +@OidcEndpoint(value = Type.DISCOVERY) +public class OidcDiscoveryRequestCustomizer implements OidcRequestFilter { + + @Override + public void filter(HttpRequest request, Buffer buffer, OidcRequestContextProperties contextProps) { + request.putHeader("Discovery", "OK"); + } +} 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/OidcJwksRequestCustomizer.java similarity index 74% rename from integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/OidcRequestCustomizer.java rename to integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/OidcJwksRequestCustomizer.java index 0f76995ecd0ed..2b6080cdfef15 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/OidcJwksRequestCustomizer.java @@ -4,22 +4,25 @@ import io.quarkus.arc.Unremovable; import io.quarkus.oidc.AccessTokenCredential; +import io.quarkus.oidc.OidcConfigurationMetadata; 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 OidcRequestFilter { +// Or @OidcEndpoint(value = Type.JWKS) +public class OidcJwksRequestCustomizer implements OidcRequestFilter { @Override public void filter(HttpRequest request, Buffer buffer, OidcRequestContextProperties contextProps) { - HttpMethod method = request.method(); + OidcConfigurationMetadata metadata = contextProps.get(OidcConfigurationMetadata.class.getName()); + // There are many tenants in the test so the URI check is still required String uri = request.uri(); - if (method == HttpMethod.GET && uri.endsWith("/auth/azure/jwk")) { - String token = contextProps.getString(OidcRequestContextProperties.TOKEN); + if (uri.equals("/auth/azure/jwk") && + metadata.getJsonWebKeySetUri().endsWith(uri)) { + String token = contextProps.get(OidcRequestContextProperties.TOKEN); AccessTokenCredential tokenCred = contextProps.get(OidcRequestContextProperties.TOKEN_CREDENTIAL, AccessTokenCredential.class); // or @@ -37,5 +40,4 @@ public void filter(HttpRequest request, Buffer buffer, OidcRequestContex } } } - } diff --git a/integration-tests/oidc-wiremock/src/main/resources/application.properties b/integration-tests/oidc-wiremock/src/main/resources/application.properties index 6ac2dbb4c4537..f3f8a78afc7ea 100644 --- a/integration-tests/oidc-wiremock/src/main/resources/application.properties +++ b/integration-tests/oidc-wiremock/src/main/resources/application.properties @@ -121,6 +121,15 @@ quarkus.oidc.bearer.credentials.secret=secret quarkus.oidc.bearer.token.audience=https://service.example.com quarkus.oidc.bearer.allow-token-introspection-cache=false +quarkus.oidc.bearer-kid-or-chain.auth-server-url=${keycloak.url}/realms/quarkus/ +quarkus.oidc.bearer-kid-or-chain.client-id=quarkus-app +quarkus.oidc.bearer-kid-or-chain.credentials.secret=secret +quarkus.oidc.bearer-kid-or-chain.token.audience=https://service.example.com +quarkus.oidc.bearer-kid-or-chain.allow-token-introspection-cache=false +quarkus.oidc.bearer-kid-or-chain.certificate-chain.trust-store-file=truststore.p12 +quarkus.oidc.bearer-kid-or-chain.certificate-chain.trust-store-password=storepassword +quarkus.oidc.bearer-kid-or-chain.certificate-chain.trust-store-cert-alias=fullchain + quarkus.oidc.bearer-id.auth-server-url=${keycloak.url}/realms/quarkus/ quarkus.oidc.bearer-id.client-id=quarkus-app quarkus.oidc.bearer-id.credentials.secret=secret @@ -153,6 +162,10 @@ quarkus.oidc.bearer-no-introspection.credentials.secret=secret quarkus.oidc.bearer-no-introspection.token.audience=https://service.example.com quarkus.oidc.bearer-no-introspection.token.allow-jwt-introspection=false +quarkus.oidc.bearer-certificate-full-chain.certificate-chain.trust-store-file=truststore.p12 +quarkus.oidc.bearer-certificate-full-chain.certificate-chain.trust-store-password=storepassword +quarkus.oidc.bearer-certificate-full-chain.certificate-chain.trust-store-cert-alias=fullchain + quarkus.oidc.bearer-key-without-kid-thumbprint.auth-server-url=${keycloak.url}/realms/quarkus/ quarkus.oidc.bearer-key-without-kid-thumbprint.discovery-enabled=false quarkus.oidc.bearer-key-without-kid-thumbprint.jwks-path=single-key-without-kid-thumbprint @@ -183,4 +196,4 @@ quarkus.http.auth.permission.front-channel-logout.policy=authenticated quarkus.http.auth.permission.backchannellogout.paths=/back-channel-logout quarkus.http.auth.permission.backchannellogout.policy=permit -quarkus.native.additional-build-args=-H:IncludeResources=private.*\\.* +quarkus.native.additional-build-args=-H:IncludeResources=private.*\\.*,-H:IncludeResources=.*\\.p12 diff --git a/integration-tests/oidc-wiremock/src/main/resources/truststore.p12 b/integration-tests/oidc-wiremock/src/main/resources/truststore.p12 new file mode 100644 index 0000000000000..81b0be2ede57e Binary files /dev/null and b/integration-tests/oidc-wiremock/src/main/resources/truststore.p12 differ 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 5cf6f2cc4f5f8..67a7903c4d1bd 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 @@ -2,14 +2,21 @@ import static com.github.tomakehurst.wiremock.client.WireMock.matching; import static org.hamcrest.Matchers.equalTo; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.InputStream; import java.nio.charset.StandardCharsets; +import java.security.KeyPairGenerator; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; import java.time.Duration; import java.time.Instant; import java.util.Arrays; import java.util.Base64; import java.util.HashSet; +import java.util.List; import java.util.Set; import org.awaitility.Awaitility; @@ -29,6 +36,9 @@ import io.restassured.RestAssured; import io.smallrye.jwt.algorithm.SignatureAlgorithm; import io.smallrye.jwt.build.Jwt; +import io.smallrye.jwt.util.KeyUtils; +import io.smallrye.jwt.util.ResourceUtils; +import io.vertx.core.json.JsonObject; @QuarkusTest @QuarkusTestResource(OidcWiremockTestResource.class) @@ -166,6 +176,202 @@ public void testAccessAdminResourceWithWrongCertS256Thumbprint() { .statusCode(401); } + @Test + public void testAccessAdminResourceWithFullCertChain() throws Exception { + X509Certificate rootCert = KeyUtils.getCertificate(ResourceUtils.readResource("/ca.cert.pem")); + X509Certificate intermediateCert = KeyUtils.getCertificate(ResourceUtils.readResource("/intermediate.cert.pem")); + X509Certificate subjectCert = KeyUtils.getCertificate(ResourceUtils.readResource("/www.quarkustest.com.cert.pem")); + PrivateKey subjectPrivateKey = KeyUtils.readPrivateKey("/www.quarkustest.com.key.pem"); + // Send the token with the valid certificate chain + String accessToken = getAccessTokenWithCertChain( + List.of(subjectCert, intermediateCert, rootCert), + subjectPrivateKey); + + RestAssured.given().auth().oauth2(accessToken) + .when().get("/api/admin/bearer-certificate-full-chain") + .then() + .statusCode(200) + .body(Matchers.containsString("admin")); + + // send the same token to the endpoint which does not allow a fallback to x5c + RestAssured.given().auth().oauth2(accessToken) + .when().get("/api/admin/bearer") + .then() + .statusCode(401); + + // Send the token with the valid certificate chain, but with the token signed by a non-matching private key + accessToken = getAccessTokenWithCertChain( + List.of(subjectCert, intermediateCert, rootCert), + KeyPairGenerator.getInstance("RSA").generateKeyPair().getPrivate()); + RestAssured.given().auth().oauth2(accessToken) + .when().get("/api/admin/bearer-certificate-full-chain") + .then() + .statusCode(401); + + // Send the token with the valid certificates but which are are in the wrong order in the chain + accessToken = getAccessTokenWithCertChain( + List.of(intermediateCert, subjectCert, rootCert), + subjectPrivateKey); + RestAssured.given().auth().oauth2(accessToken) + .when().get("/api/admin/bearer-certificate-full-chain") + .then() + .statusCode(401); + + // Send the token with the valid certificates but with the intermediate one omitted from the chain + accessToken = getAccessTokenWithCertChain( + List.of(subjectCert, rootCert), + subjectPrivateKey); + RestAssured.given().auth().oauth2(accessToken) + .when().get("/api/admin/bearer-certificate-full-chain") + .then() + .statusCode(401); + + // Send the token with the only the last valid certificate + accessToken = getAccessTokenWithCertChain( + List.of(subjectCert), + subjectPrivateKey); + RestAssured.given().auth().oauth2(accessToken) + .when().get("/api/admin/bearer-certificate-full-chain") + .then() + .statusCode(401); + + } + + @Test + public void testAccessAdminResourceWithKidOrChain() throws Exception { + // token with a matching kid, not x5c + String token = Jwt.preferredUserName("admin") + .groups(Set.of("admin")) + .issuer("https://server.example.com") + .audience("https://service.example.com") + .sign(); + + assertKidOnlyIsPresent(token, "1"); + + RestAssured.given().auth().oauth2(token) + .when().get("/api/admin/bearer-kid-or-chain") + .then() + .statusCode(200) + .body(Matchers.containsString("admin")); + + // token without kid and x5c + token = Jwt.preferredUserName("admin") + .groups(Set.of("admin")) + .issuer("https://server.example.com") + .audience("https://service.example.com") + .sign(KeyPairGenerator.getInstance("RSA").generateKeyPair().getPrivate()); + assertNoKidAndX5cArePresent(token); + + RestAssured.given().auth().oauth2(token) + .when().get("/api/admin/bearer-kid-or-chain") + .then() + .statusCode(401); + + // token with a kid which will resolve to a non-matching public key, no x5c + token = Jwt.preferredUserName("admin") + .groups(Set.of("admin")) + .issuer("https://server.example.com") + .audience("https://service.example.com") + .jws().keyId("1") + .sign(KeyPairGenerator.getInstance("RSA").generateKeyPair().getPrivate()); + + assertKidOnlyIsPresent(token, "1"); + + RestAssured.given().auth().oauth2(token) + .when().get("/api/admin/bearer-kid-or-chain") + .then() + .statusCode(401); + + X509Certificate rootCert = KeyUtils.getCertificate(ResourceUtils.readResource("/ca.cert.pem")); + X509Certificate intermediateCert = KeyUtils.getCertificate(ResourceUtils.readResource("/intermediate.cert.pem")); + X509Certificate subjectCert = KeyUtils.getCertificate(ResourceUtils.readResource("/www.quarkustest.com.cert.pem")); + PrivateKey subjectPrivateKey = KeyUtils.readPrivateKey("/www.quarkustest.com.key.pem"); + + // Send the token with the valid certificate chain + token = getAccessTokenWithCertChain( + List.of(subjectCert, intermediateCert, rootCert), + subjectPrivateKey); + + assertX5cOnlyIsPresent(token); + + RestAssured.given().auth().oauth2(token) + .when().get("/api/admin/bearer-kid-or-chain") + .then() + .statusCode(200) + .body(Matchers.containsString("admin")); + + // send the same token to the endpoint which does not allow a fallback to x5c + RestAssured.given().auth().oauth2(token) + .when().get("/api/admin/bearer") + .then() + .statusCode(401); + + // Send the token with the valid certificate chain with certificates in the wrong order + token = getAccessTokenWithCertChain( + List.of(intermediateCert, subjectCert, rootCert), + subjectPrivateKey); + + assertX5cOnlyIsPresent(token); + + RestAssured.given().auth().oauth2(token) + .when().get("/api/admin/bearer-kid-or-chain") + .then() + .statusCode(401); + + // Send token signed by the subject private key but with a kid which will resolve to + // a non-matching public key and x5c + token = Jwt.preferredUserName("admin") + .groups(Set.of("admin")) + .issuer("https://server.example.com") + .audience("https://service.example.com") + .jws().keyId("1").chain(List.of(intermediateCert, subjectCert, rootCert)) + .sign(subjectPrivateKey); + + assertBothKidAndX5cArePresent(token, "1"); + + RestAssured.given().auth().oauth2(token) + .when().get("/api/admin/bearer-kid-or-chain") + .then() + .statusCode(401); + + // no token + RestAssured.when().get("/api/admin/bearer-kid-or-chain") + .then() + .statusCode(401); + } + + private void assertNoKidAndX5cArePresent(String token) { + JsonObject headers = OidcUtils.decodeJwtHeaders(token); + assertFalse(headers.containsKey("x5c")); + assertFalse(headers.containsKey("kid")); + assertFalse(headers.containsKey("x5t")); + assertFalse(headers.containsKey("x5t#S256")); + } + + private void assertBothKidAndX5cArePresent(String token, String kid) { + JsonObject headers = OidcUtils.decodeJwtHeaders(token); + assertTrue(headers.containsKey("x5c")); + assertEquals(kid, headers.getString("kid")); + assertFalse(headers.containsKey("x5t")); + assertFalse(headers.containsKey("x5t#S256")); + } + + private void assertKidOnlyIsPresent(String token, String kid) { + JsonObject headers = OidcUtils.decodeJwtHeaders(token); + assertFalse(headers.containsKey("x5c")); + assertEquals(kid, headers.getString("kid")); + assertFalse(headers.containsKey("x5t")); + assertFalse(headers.containsKey("x5t#S256")); + } + + private void assertX5cOnlyIsPresent(String token) { + JsonObject headers = OidcUtils.decodeJwtHeaders(token); + assertTrue(headers.containsKey("x5c")); + assertFalse(headers.containsKey("kid")); + assertFalse(headers.containsKey("x5t")); + assertFalse(headers.containsKey("x5t#S256")); + } + @Test public void testAccessAdminResourceWithCustomRolePathForbidden() { RestAssured.given().auth().oauth2(getAccessTokenWithCustomRolePath("admin", Set.of("admin"))) @@ -425,6 +631,16 @@ private String getAccessTokenWithWrongS256Thumbprint(String userName, Set chain, + PrivateKey privateKey) throws Exception { + return Jwt.preferredUserName("alice") + .groups("admin") + .issuer("https://server.example.com") + .audience("https://service.example.com") + .jws().chain(chain) + .sign(privateKey); + } + private String getAccessTokenWithoutKidAndThumbprint(String userName, Set groups) { return Jwt.preferredUserName(userName) .groups(groups) @@ -465,5 +681,4 @@ private String getAccessTokenWrongIssuer(String userName, Set groups) { .audience("https://service.example.com") .sign(); } - } diff --git a/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/WiremockTestResource.java b/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/WiremockTestResource.java index 31f834d31d05b..0c7e47985a5a4 100644 --- a/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/WiremockTestResource.java +++ b/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/WiremockTestResource.java @@ -1,6 +1,7 @@ package io.quarkus.it.keycloak; import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; import static com.github.tomakehurst.wiremock.client.WireMock.get; import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; @@ -24,6 +25,7 @@ public void start() { server.stubFor( get(urlEqualTo("/auth/realms/quarkus2/.well-known/openid-configuration")) + .withHeader("Discovery", equalTo("OK")) .willReturn(aResponse() .withHeader("Content-Type", "application/json") .withBody("{\n" + diff --git a/integration-tests/oidc-wiremock/src/test/resources/ca.cert.pem b/integration-tests/oidc-wiremock/src/test/resources/ca.cert.pem new file mode 100644 index 0000000000000..b8ec4ac6c5dd1 --- /dev/null +++ b/integration-tests/oidc-wiremock/src/test/resources/ca.cert.pem @@ -0,0 +1,34 @@ +-----BEGIN CERTIFICATE----- +MIIF0TCCA7mgAwIBAgIUevkdgNus9CUyOGrDiHwuFFAzSsowDQYJKoZIhvcNAQEL +BQAwcDELMAkGA1UEBhMCSUUxDzANBgNVBAgMBkR1YmxpbjEPMA0GA1UEBwwGRHVi +bGluMRAwDgYDVQQKDAdRdWFya3VzMRswGQYDVQQLDBJRdWFya3VzIERlcGFydG1l +bnQxEDAOBgNVBAMMB1Jvb3QgQ0EwHhcNMjMxMTE3MTIyNzI2WhcNNDMxMTEyMTIy +NzI2WjBwMQswCQYDVQQGEwJJRTEPMA0GA1UECAwGRHVibGluMQ8wDQYDVQQHDAZE +dWJsaW4xEDAOBgNVBAoMB1F1YXJrdXMxGzAZBgNVBAsMElF1YXJrdXMgRGVwYXJ0 +bWVudDEQMA4GA1UEAwwHUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCC +AgoCggIBALQFnHt8yToRlnRKtwcf8yaKzVH53rdv2D9kVzGyuqNVwyPvnPx/Vo88 +lOVeNFY9cj9aTpuVry8wO58Xf6+eMrfiGsHkyW2Fi7PeMyTN5V+smhDonYrZIEKU +UsGuEFwnsAdAPyboQAXG3Xy82OJD3HZfARIoA5l80GtnoeQicKex724bhSohm5ZS +XdGTlHRhTLcG1eaidccUyBAJjMexnCsqHdLfzrKOK/Hl8wPPNXOTPZZ6GmjWub5g +Ti6qYu/tkuC2hlu+rEFVql75cpJ9sA5P/DRF/0A7dJClWSNErG2ATcoImpaxUnpd +jSs76LIx779nOd6zbIaSyIwzbPoTxuoiAK5Fg8dZjK2A+omwfnIHvd30/5D7NcQj +LshRWH/G26/rdpj0c3ZwpW2md065cFVgal/m1nsEqREjHyRvm1PkacKAEw9A4gUQ +Au0NYTX6KWE2TcTQdKbcGlBQPcNkJPKdbv+bfNs6+BreEjltcIMZ0Xl7qPVOU3Hm +d44avBoHQRhHDg2ud7ZFxpvhjxKmUwEGTDdgt2vuXAyEkrfGCQ2AE58nlcLzAdWN +Zaq0o0WObzW5pXjcSslEln/U5x94U7Fnql5/XD27UqvMYTkZAK0fyYnsZcghrf4Q +qq25HipDD9j4YtDvBOYE24nxxVZWK4k5kjc5et6zWRjBt6cH99yXAgMBAAGjYzBh +MB0GA1UdDgQWBBSjdDFDHtjprW5hclFqtSK/sz6gojAfBgNVHSMEGDAWgBSjdDFD +HtjprW5hclFqtSK/sz6gojAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +BjANBgkqhkiG9w0BAQsFAAOCAgEAKha7J68N/SIr/FD/X+moJF7QzCH0fPhvYHJg +l8dOB39b901nJ0mRVxUH5pZwsoi4nxU4t52leLT5hUgyG02H0v4eHYHtkMBAJ0IE +gdmsNRrdZPulPs5/hrLOZJG41X3X485qUouyKjZlDSjr4djFifzOWHL9VonyDSV4 +j7MtKnnTo5UzqKt3fcr0LzP5x4t3M6dZgTMjIG5C6pmar6Qp2htZh1RFx+wW+KJy +ULhfByID5hrA99Q5gS7w27EjvD80tgDZaRbrV/gt4hI/0W0NHvP1m1HX0oe0bhWx +soBMLaaH0F+LSo3jU3e7OakP2/i2Jpz5sIKndL6lIf80o2Ngo+LQr4aPK7lzPPYV +U7I2Il0KfklbFUmbVYNdVtbZKaOwdEU0ADqptJY1cnH9putd5Z1ea9NWENcXFaRs +LfgqFagEKTZZkwkX2oNHH9bwEZsfAgr2OWjzHIcfQ3NnRBPymx1CB2QMYuPbg6ql +6eGjRBWVPpMK/tGp9BDIfPC5Kq4yuAMihKoDuikL12hKKB59VfpybBH+ziVxBcyD +LO8Fsuu5V85TOaZ3DFqp8ZODQWnvDre8o0VxwdH+4SC01qhTZRtqSdoOXGrt+J0B +EsIvFGOOEE5W+23Hcr6Nwl5YFm95f2ZPCkb5Iu2Wp6BtlZQBFfTXiGhP+LT008Yl +W+0+5Lc= +-----END CERTIFICATE----- diff --git a/integration-tests/oidc-wiremock/src/test/resources/intermediate.cert.pem b/integration-tests/oidc-wiremock/src/test/resources/intermediate.cert.pem new file mode 100644 index 0000000000000..27dc46ad3ff27 --- /dev/null +++ b/integration-tests/oidc-wiremock/src/test/resources/intermediate.cert.pem @@ -0,0 +1,33 @@ +-----BEGIN CERTIFICATE----- +MIIFuTCCA6GgAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwcDELMAkGA1UEBhMCSUUx +DzANBgNVBAgMBkR1YmxpbjEPMA0GA1UEBwwGRHVibGluMRAwDgYDVQQKDAdRdWFy +a3VzMRswGQYDVQQLDBJRdWFya3VzIERlcGFydG1lbnQxEDAOBgNVBAMMB1Jvb3Qg +Q0EwHhcNMjMxMTE3MTIzMDIwWhcNMzMxMTE0MTIzMDIwWjBnMQswCQYDVQQGEwJJ +RTEPMA0GA1UECAwGRHVibGluMRAwDgYDVQQKDAdRdWFya3VzMRswGQYDVQQLDBJR +dWFya3VzIERlcGFydG1lbnQxGDAWBgNVBAMMD0ludGVybWVkaWF0ZSBDQTCCAiIw +DQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMFfN7Pi8dF+gQTglUDbnTsunr10 +0Ldeozyg4UK+V59NJ8FSX9k1OZeSIjqEN3kljtjNzKX7eqwIw/8UjboQeusIM9jM +x+cu0ifWlulu/TBLzZfxO3Laq0lSJWuoPRt9tDAdA1PJK62p044zp6i6B9PDldhi +RpZPj7PJcAoEh3NTmzh7icKVmcGfj2Xo+M/TOiGtKsIhH34w/aXi6u/03PsBs0gw +8Lids9WTUFGIvf4jAeCzuxWL7RQVr7qDhEvlKEh1tRknSUf0W2yJCE+aFD3XL/b2 +r6qc+CbsV59n+IcARH7gFDEBkAdk9lBozmF7o7+ADc5CZAjtN2FGWDhLXoCV8fvT +4/sGLsT/MGZPUS4vqw+Gl3+Qx0qk+DgVrwWGFPX168vXBEB/f+AsCX3O9Hn6vNjB +uEoIi6+bZP6P2MHThARzOm705cM9xxvj82qrdpYtxhi4jLAwzZ/Aa+1wTu1Uobub +9LXoyuNHSppuK1gi4DZYUHs9YkQwVTwGu1cyYI9oCy9tZ90YqDHHQCyNqAFEPyjw +C9JT1B3g3gZiiPfpVtzvqxG8qNC6fYyYDSq4aNjlCfv8jYBPylByUyVMG5QnS0ai +5lYhUF6L8v1jWrsMvkBCmua4TvP4ofa8qUNOpS5eKTNiWk8wkuGne8MhSPDAExVO +z82E3S8s4jdpwWgHAgMBAAGjZjBkMB0GA1UdDgQWBBRM+ZXzjUoJLnQsxPsFa6CJ +uIjsyTAfBgNVHSMEGDAWgBSjdDFDHtjprW5hclFqtSK/sz6gojASBgNVHRMBAf8E +CDAGAQH/AgEAMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAAa8u +aDO6nrADunYa6ePJLmxpiMCpaJR6XcM3UkZkQ+zdHsPLPl9lbN2tbr982CWbr89Q +QmkHMxylQQ4QLwUM6USzDohIiQ516I0LcGUatTymVXKRwSZc1xK587v4iI7LVy5P +pQuBMeA+tteldyaaWTDL3ppa1UmNWksS3MDHOYcJ/GWqsCqD1Au0sj1E3bGW4t6b +Wkan6gUG7z1IpXN8XOVSgbzTkWnH89LJns6YUHMnIXb+qQflLCbuj3TYa5H5JgOC +atVJEHOtqCaDxNDH5t99zFrvLkYy/AJ2QMqMlLS0pWRBmRcaBXEQl7npenZOUgn9 +A0AKs3hoYl0aF0aVMmy7R1Rx+V0G7s3AFZ31QUuWRiy50QJmZg4qZYliZFMcFZrg +H4T1IKcF1IddU7/tUodaaCP6DT9HRufJ8VNw8kFeFYK414TgvyuViIpHHGuUOLDl +Ee6ONp3VkzY3sseXpmR14JRnT2JOL5yt1kaDc8VdyLe+v57NURNUB8s5s6x/oIYI +9BDT1paHb38C/g8E72emgRMs+LwABJJm72hBiKo4eI8uDYKSTKuzxF9JANMMXPG9 +wvCgyFzf1ySsFlFueAMDVZJtqD0SPqtbilfJJ6lUmzyWEb2WYaUDpSiqmNU4Dnw0 +b+x+3T3uJ8oAbV9xBA9aba6M+fkzjtZ0VakcFpM= +-----END CERTIFICATE----- diff --git a/integration-tests/oidc-wiremock/src/test/resources/www.quarkustest.com.cert.pem b/integration-tests/oidc-wiremock/src/test/resources/www.quarkustest.com.cert.pem new file mode 100644 index 0000000000000..cb05e37ef44da --- /dev/null +++ b/integration-tests/oidc-wiremock/src/test/resources/www.quarkustest.com.cert.pem @@ -0,0 +1,29 @@ +-----BEGIN CERTIFICATE----- +MIIE6zCCAtOgAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwZzELMAkGA1UEBhMCSUUx +DzANBgNVBAgMBkR1YmxpbjEQMA4GA1UECgwHUXVhcmt1czEbMBkGA1UECwwSUXVh +cmt1cyBEZXBhcnRtZW50MRgwFgYDVQQDDA9JbnRlcm1lZGlhdGUgQ0EwHhcNMjMx +MTE3MTIzNjQ1WhcNMjQxMTI2MTIzNjQ1WjCBgDELMAkGA1UEBhMCSUUxDzANBgNV +BAgMBkR1YmxpbjEPMA0GA1UEBwwGRHVibGluMRAwDgYDVQQKDAdRdWFya3VzMR8w +HQYDVQQLDBZRdWFya3VzSW50ZWdyYXRpb25UZXN0MRwwGgYDVQQDDBN3d3cucXVh +cmt1c3Rlc3QuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuqLr +N3x2QK1oc5FeAiImBSq8ouaUJ5s9wZre7A/2RrM1ZTzUK/VyoynWaVxYIkjdGTpT +H6xYtz3+T6z/l0xHO/tugXGHDGEQOgstvh0E8C1DrdvIOqdPtNYUBW6Nw0NVrVwH +ClBDSFN5Xw89YhjydtETy11joKQ2X9SViDfCICOVpx0ml05Txc45CUJsDofEX5HQ +C0eG32cuemvMLouAFH9fMfoVrx9Yhy5vBrzlX22s0ig9bu53qQlNuzj5AUcNKUCM +NttRltptHmRiAnRzUIGiOhXuaz0oEIU80p82sVM78tfY4qXIu9LVWYM1qqpYielx +BkjC7GElG7Log4lf4QIDAQABo4GGMIGDMAkGA1UdEwQCMAAwEQYJYIZIAYb4QgEB +BAQDAgZAMA4GA1UdDwEB/wQEAwIFoDATBgNVHSUEDDAKBggrBgEFBQcDATAfBgNV +HSMEGDAWgBRM+ZXzjUoJLnQsxPsFa6CJuIjsyTAdBgNVHQ4EFgQUEQ404BYvLmtz +vfS6I3iW4vKM/4QwDQYJKoZIhvcNAQELBQADggIBAFRMZozul2oqjOhQG9todI8w +6woJk5b5VDf0c5zPvQntWeS8vv7Bysy7yqmfdZFqZXZstIfmX+USvE1XOuQl+2X7 +BHn958lwyiKNUwNVm27Luf5yKEtjZHqmCvfCjcGAt5vuyyV1JcmP4gzEvEiTByMq +A4VSw7+u8y0/kEJLpgoikQaBYgp5HPkqJ/EmI55QUKlIElX9cgJxz5ihdHw/EUxD +C0AvKxH4SoMGxAlplz+ncJp6Ru6EI51dE1tIUlLwsFF39GjZ2a7AQCzG3umqM5ui +sKI6l5DFU9HVoDbNrSJ0DWbvevC6jA9sGsQyUjwewrhrsOposR2NOS/RyMw7YdWi +XIg50TmkWOyEScF7PQQ43qYL7JZQx9fB5k5Tscb0tV/anmTjbSQZmAeTsiHkDPeq +hGeP5mnvIdETwS9AZyYFDam1xOPcFpnsN2MGGXIUBvI876rno3zZlNnq4ugbYYWw +GlG/C7dseXP2dyvTsalNNUqSjZoFpwrQDPBFjTNxtKjX3E0J9ATL9QHsvO/+UkdG +FFyKVAGsFkI0kYv6gaPoqPkJoLxK3wZJ/QXMLJPk/jz7jBz5YPwvR1huN4ZgE9A9 +UvFxgcHuDjsBaHd+DJeILv/O47ELLnVKjnmvACZt4WbxMzH4ZpcB1oN9zQ7RP/CK +YZrxGk8DGTBADYV4cHl2 +-----END CERTIFICATE----- diff --git a/integration-tests/oidc-wiremock/src/test/resources/www.quarkustest.com.key.pem b/integration-tests/oidc-wiremock/src/test/resources/www.quarkustest.com.key.pem new file mode 100644 index 0000000000000..38080bc66d484 --- /dev/null +++ b/integration-tests/oidc-wiremock/src/test/resources/www.quarkustest.com.key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC6ous3fHZArWhz +kV4CIiYFKryi5pQnmz3Bmt7sD/ZGszVlPNQr9XKjKdZpXFgiSN0ZOlMfrFi3Pf5P +rP+XTEc7+26BcYcMYRA6Cy2+HQTwLUOt28g6p0+01hQFbo3DQ1WtXAcKUENIU3lf +Dz1iGPJ20RPLXWOgpDZf1JWIN8IgI5WnHSaXTlPFzjkJQmwOh8RfkdALR4bfZy56 +a8wui4AUf18x+hWvH1iHLm8GvOVfbazSKD1u7nepCU27OPkBRw0pQIw221GW2m0e +ZGICdHNQgaI6Fe5rPSgQhTzSnzaxUzvy19jipci70tVZgzWqqliJ6XEGSMLsYSUb +suiDiV/hAgMBAAECggEAA6VZm3agt8A7dWB+WePRYtH0J+mBtOldMjpEhw+Dw9tN +3Hms+mPb1rCjSeEeLqNGQG8pfwmmnQPGw0cxogLBNHyDqt8tIHKH9t5PiTJ3bXqw +4wVTWsP4dGOnNfj0J3+Z/Z452/t36QKKcS8yx4cNu4D3lkYvg0yZ7FLSjfvq3KZU +w8m8/4EPtP2+KxIvFIDU5/5W43wYUv6QctTEuIDRfikdik3oKDiNUGCtfd6OOQtE +mINkDA6nLgOngyxj8jBAgkBIs+FfOoywPZP3cR9MeAdJYPBxEE3NN4wlZlQZCzkd +SA10258coG7bXii8lTrik89v0WhvGOYtoWniddv7MQKBgQDge3fo06q82+QhP2wM +ni99YMiYaW3oaotEAT86C8owDWR3z2+luk3HK34d/2uVhgzftTN3DMM70pZ7arI2 +GJqxNdzK2YGrACNB9bYCLhRj4/ITGannz6cjrHsiRH99BGpyeZQKFNmvrgY8ru+j +GSy17JY0/8Kj7gQYohnvHdh6SQKBgQDU1ylO6ZHZykfXvPgvnFqYUHiOgBwk6Y7+ +ClozmZL48u/42PpsVNuoquzH5V1kIBDwWZjUsOnZX5rbv0YzOJ449PsVw+pM+NAb +Dwtzwgfb9/uBryFKMgiXMToOEAyN+ENRg4PpyHRQu+shVh8/MPdMytA8AaypmZP7 +aPKhw/v42QKBgGG4PcWjxtJ54oA6rJ19iuzIYeo/EvI21zMeW9i34ycx3UdujIqX +ZF5MZ5VFaB7qANateZ7cdmynSoylMLjt0wFLkjbXomO/JpoDDV07k/K7+tgnttfL +hFW6MswDB4BzmKcGl9QfqXeZiOuHt5fHULhNKkIeCCv2Y+AZSLLXyjHJAoGAcmDq +RwkII0UsVIity1A381CTaOj5tvB4spa3oLEwJW7QfSeFdEAqBztLoaTmCk+dKrzL +f8lO8k0JeHwS6qXLiYpFgI3XVOQFWfU8z0l/VbuvQiLuPeQjb7S5oSMIzCaVbrHB +axoZP+Ws1y5j/l5/F5qKSyUPN9lbiCj+8uXSfAkCgYAcZ8i2+4ji9Ntu83G90GWi +hS2JOlZEUOCcE9vRu6HDkWC0qfkGbhjUk5GUHBjFp0shRTR/GnGA0ILAzgoxEK1s +/aDel9XDeuF4DJC+HzvPDoPFYz0UH4CuOYWhAejL81a5/AAHQqm2fpQSNln097rC +KfGyU23XuO7U8BloCy/hCA== +-----END PRIVATE KEY----- diff --git a/integration-tests/openshift-client/pom.xml b/integration-tests/openshift-client/pom.xml index eef764d55f5e6..82629e66d5b56 100644 --- a/integration-tests/openshift-client/pom.xml +++ b/integration-tests/openshift-client/pom.xml @@ -22,11 +22,6 @@ io.quarkus quarkus-resteasy-jackson - - io.quarkus - quarkus-integration-test-kubernetes-client-hack-extension - ${project.version} - io.quarkus quarkus-openshift-client @@ -34,11 +29,15 @@ io.fabric8 - openshift-model-operator + openshift-model-hive io.fabric8 - openshift-model-operator-hub + openshift-model-miscellaneous + + + io.fabric8 + openshift-model-operator @@ -70,19 +69,6 @@ - - io.quarkus - quarkus-integration-test-kubernetes-client-hack-extension-deployment - ${project.version} - pom - test - - - * - * - - - io.quarkus quarkus-openshift-client-deployment diff --git a/integration-tests/opentelemetry-quartz/pom.xml b/integration-tests/opentelemetry-quartz/pom.xml new file mode 100644 index 0000000000000..006bac75a7f1f --- /dev/null +++ b/integration-tests/opentelemetry-quartz/pom.xml @@ -0,0 +1,144 @@ + + + + quarkus-integration-tests-parent + io.quarkus + 999-SNAPSHOT + + 4.0.0 + + quarkus-integration-test-opentelemetry-quartz + Quarkus - Integration Tests - OpenTelemetry Quartz + + + + io.quarkus + quarkus-arc + + + io.quarkus + quarkus-resteasy-reactive + + + io.quarkus + quarkus-quartz + + + io.quarkus + quarkus-opentelemetry + + + io.quarkus + quarkus-resteasy-reactive-jackson + + + + + io.opentelemetry + opentelemetry-sdk-testing + + + + + io.quarkus + quarkus-junit5 + test + + + io.rest-assured + rest-assured + test + + + org.awaitility + awaitility + test + + + + io.quarkus + quarkus-arc-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-quartz-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-resteasy-reactive-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-opentelemetry-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-resteasy-reactive-jackson-deployment + ${project.version} + pom + test + + + * + * + + + + + + + + + io.quarkus + quarkus-maven-plugin + + + + build + + + + + + + + + diff --git a/integration-tests/opentelemetry-quartz/src/main/java/io/quarkus/it/opentelemetry/quartz/CountResource.java b/integration-tests/opentelemetry-quartz/src/main/java/io/quarkus/it/opentelemetry/quartz/CountResource.java new file mode 100644 index 0000000000000..dd6f36e860655 --- /dev/null +++ b/integration-tests/opentelemetry-quartz/src/main/java/io/quarkus/it/opentelemetry/quartz/CountResource.java @@ -0,0 +1,40 @@ +package io.quarkus.it.opentelemetry.quartz; + +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("/scheduler/count") +public class CountResource { + + @Inject + Counter counter; + + @Inject + ManualScheduledCounter manualScheduledCounter; + + @Inject + JobDefinitionCounter jobDefinitionCounter; + + @GET + @Produces(MediaType.TEXT_PLAIN) + public Integer getCount() { + return counter.get(); + } + + @GET + @Path("manual") + @Produces(MediaType.TEXT_PLAIN) + public Integer getManualCount() { + return manualScheduledCounter.get(); + } + + @GET + @Path("job-definition") + @Produces(MediaType.TEXT_PLAIN) + public Integer getJobDefinitionCount() { + return jobDefinitionCounter.get(); + } +} diff --git a/integration-tests/opentelemetry-quartz/src/main/java/io/quarkus/it/opentelemetry/quartz/Counter.java b/integration-tests/opentelemetry-quartz/src/main/java/io/quarkus/it/opentelemetry/quartz/Counter.java new file mode 100644 index 0000000000000..0199a8fb362a5 --- /dev/null +++ b/integration-tests/opentelemetry-quartz/src/main/java/io/quarkus/it/opentelemetry/quartz/Counter.java @@ -0,0 +1,30 @@ +package io.quarkus.it.opentelemetry.quartz; + +import java.util.concurrent.atomic.AtomicInteger; + +import jakarta.annotation.PostConstruct; +import jakarta.enterprise.context.ApplicationScoped; + +import io.quarkus.scheduler.Scheduled; + +@ApplicationScoped +public class Counter { + + AtomicInteger counter; + + @PostConstruct + void init() { + counter = new AtomicInteger(); + } + + public int get() { + return counter.get(); + } + + @Scheduled(cron = "*/1 * * * * ?", identity = "myCounter") + void increment() throws InterruptedException { + Thread.sleep(100l); + counter.incrementAndGet(); + } + +} diff --git a/integration-tests/opentelemetry-quartz/src/main/java/io/quarkus/it/opentelemetry/quartz/ExporterResource.java b/integration-tests/opentelemetry-quartz/src/main/java/io/quarkus/it/opentelemetry/quartz/ExporterResource.java new file mode 100644 index 0000000000000..5f6407cc16167 --- /dev/null +++ b/integration-tests/opentelemetry-quartz/src/main/java/io/quarkus/it/opentelemetry/quartz/ExporterResource.java @@ -0,0 +1,38 @@ +package io.quarkus.it.opentelemetry.quartz; + +import java.util.List; +import java.util.stream.Collectors; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Produces; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter; +import io.opentelemetry.sdk.trace.data.SpanData; + +@Path("") +public class ExporterResource { + @Inject + InMemorySpanExporter inMemorySpanExporter; + + @GET + @Path("/export") + public List export() { // only export scheduled spans + return inMemorySpanExporter.getFinishedSpanItems() + .stream() + .filter(sd -> !sd.getName().contains("export") && !sd.getName().contains("GET")) + .collect(Collectors.toList()); + } + + @ApplicationScoped + static class InMemorySpanExporterProducer { + @Produces + @Singleton + InMemorySpanExporter inMemorySpanExporter() { + return InMemorySpanExporter.create(); + } + } +} diff --git a/integration-tests/opentelemetry-quartz/src/main/java/io/quarkus/it/opentelemetry/quartz/FailedBasicScheduler.java b/integration-tests/opentelemetry-quartz/src/main/java/io/quarkus/it/opentelemetry/quartz/FailedBasicScheduler.java new file mode 100644 index 0000000000000..9450827e7e328 --- /dev/null +++ b/integration-tests/opentelemetry-quartz/src/main/java/io/quarkus/it/opentelemetry/quartz/FailedBasicScheduler.java @@ -0,0 +1,17 @@ +package io.quarkus.it.opentelemetry.quartz; + +import jakarta.enterprise.context.ApplicationScoped; + +import io.quarkus.scheduler.Scheduled; + +@ApplicationScoped +public class FailedBasicScheduler { + + @Scheduled(cron = "*/1 * * * * ?", identity = "myFailedBasicScheduler") + void init() throws InterruptedException { + Thread.sleep(100l); + throw new RuntimeException("error occurred in myFailedBasicScheduler."); + + } + +} diff --git a/integration-tests/opentelemetry-quartz/src/main/java/io/quarkus/it/opentelemetry/quartz/FailedJobDefinitionScheduler.java b/integration-tests/opentelemetry-quartz/src/main/java/io/quarkus/it/opentelemetry/quartz/FailedJobDefinitionScheduler.java new file mode 100644 index 0000000000000..705b5c357c9aa --- /dev/null +++ b/integration-tests/opentelemetry-quartz/src/main/java/io/quarkus/it/opentelemetry/quartz/FailedJobDefinitionScheduler.java @@ -0,0 +1,29 @@ +package io.quarkus.it.opentelemetry.quartz; + +import jakarta.annotation.PostConstruct; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import io.quarkus.quartz.QuartzScheduler; +import io.quarkus.runtime.Startup; + +@ApplicationScoped +@Startup +public class FailedJobDefinitionScheduler { + + @Inject + QuartzScheduler scheduler; + + @PostConstruct + void init() { + scheduler.newJob("myFailedJobDefinition").setCron("*/1 * * * * ?").setTask(ex -> { + try { + Thread.sleep(100l); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + throw new RuntimeException("error occurred in myFailedJobDefinition."); + }).schedule(); + } + +} diff --git a/integration-tests/opentelemetry-quartz/src/main/java/io/quarkus/it/opentelemetry/quartz/FailedManualScheduler.java b/integration-tests/opentelemetry-quartz/src/main/java/io/quarkus/it/opentelemetry/quartz/FailedManualScheduler.java new file mode 100644 index 0000000000000..21dc72c5f5adf --- /dev/null +++ b/integration-tests/opentelemetry-quartz/src/main/java/io/quarkus/it/opentelemetry/quartz/FailedManualScheduler.java @@ -0,0 +1,52 @@ +package io.quarkus.it.opentelemetry.quartz; + +import jakarta.annotation.PostConstruct; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.quartz.Job; +import org.quartz.JobBuilder; +import org.quartz.JobDetail; +import org.quartz.JobExecutionContext; +import org.quartz.SchedulerException; +import org.quartz.SimpleScheduleBuilder; +import org.quartz.Trigger; +import org.quartz.TriggerBuilder; + +import io.quarkus.runtime.Startup; +import io.quarkus.runtime.annotations.RegisterForReflection; + +@Startup +@ApplicationScoped +public class FailedManualScheduler { + @Inject + org.quartz.Scheduler quartz; + + @PostConstruct + void init() throws SchedulerException { + JobDetail job = JobBuilder.newJob(CountingJob.class).withIdentity("myFailedManualJob", "myFailedGroup").build(); + Trigger trigger = TriggerBuilder + .newTrigger() + .withIdentity("myFailedTrigger", "myFailedGroup") + .startNow() + .withSchedule(SimpleScheduleBuilder + .simpleSchedule() + .repeatForever() + .withIntervalInSeconds(1)) + .build(); + quartz.scheduleJob(job, trigger); + } + + @RegisterForReflection + public static class CountingJob implements Job { + @Override + public void execute(JobExecutionContext jobExecutionContext) { + try { + Thread.sleep(100l); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + throw new RuntimeException("error occurred in myFailedManualJob."); + } + } +} diff --git a/integration-tests/opentelemetry-quartz/src/main/java/io/quarkus/it/opentelemetry/quartz/JobDefinitionCounter.java b/integration-tests/opentelemetry-quartz/src/main/java/io/quarkus/it/opentelemetry/quartz/JobDefinitionCounter.java new file mode 100644 index 0000000000000..70d1038080df3 --- /dev/null +++ b/integration-tests/opentelemetry-quartz/src/main/java/io/quarkus/it/opentelemetry/quartz/JobDefinitionCounter.java @@ -0,0 +1,37 @@ +package io.quarkus.it.opentelemetry.quartz; + +import java.util.concurrent.atomic.AtomicInteger; + +import jakarta.annotation.PostConstruct; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import io.quarkus.quartz.QuartzScheduler; +import io.quarkus.runtime.Startup; + +@ApplicationScoped +@Startup +public class JobDefinitionCounter { + + @Inject + QuartzScheduler scheduler; + + AtomicInteger counter; + + @PostConstruct + void init() { + counter = new AtomicInteger(); + scheduler.newJob("myJobDefinition").setCron("*/1 * * * * ?").setTask(ex -> { + try { + Thread.sleep(100l); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + counter.incrementAndGet(); + }).schedule(); + } + + public int get() { + return counter.get(); + } +} diff --git a/integration-tests/opentelemetry-quartz/src/main/java/io/quarkus/it/opentelemetry/quartz/ManualScheduledCounter.java b/integration-tests/opentelemetry-quartz/src/main/java/io/quarkus/it/opentelemetry/quartz/ManualScheduledCounter.java new file mode 100644 index 0000000000000..00aca09f07c11 --- /dev/null +++ b/integration-tests/opentelemetry-quartz/src/main/java/io/quarkus/it/opentelemetry/quartz/ManualScheduledCounter.java @@ -0,0 +1,59 @@ +package io.quarkus.it.opentelemetry.quartz; + +import java.util.concurrent.atomic.AtomicInteger; + +import jakarta.annotation.PostConstruct; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.quartz.Job; +import org.quartz.JobBuilder; +import org.quartz.JobDetail; +import org.quartz.JobExecutionContext; +import org.quartz.SchedulerException; +import org.quartz.SimpleScheduleBuilder; +import org.quartz.Trigger; +import org.quartz.TriggerBuilder; + +import io.quarkus.runtime.Startup; +import io.quarkus.runtime.annotations.RegisterForReflection; + +@Startup +@ApplicationScoped +public class ManualScheduledCounter { + @Inject + org.quartz.Scheduler quartz; + private static AtomicInteger counter = new AtomicInteger(); + + public int get() { + return counter.get(); + } + + @PostConstruct + void init() throws SchedulerException { + JobDetail job = JobBuilder.newJob(CountingJob.class).withIdentity("myManualJob", "myGroup").build(); + Trigger trigger = TriggerBuilder + .newTrigger() + .withIdentity("myTrigger", "myGroup") + .startNow() + .withSchedule(SimpleScheduleBuilder + .simpleSchedule() + .repeatForever() + .withIntervalInSeconds(1)) + .build(); + quartz.scheduleJob(job, trigger); + } + + @RegisterForReflection + public static class CountingJob implements Job { + @Override + public void execute(JobExecutionContext jobExecutionContext) { + try { + Thread.sleep(100l); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + counter.incrementAndGet(); + } + } +} diff --git a/integration-tests/opentelemetry-quartz/src/main/resources/application.properties b/integration-tests/opentelemetry-quartz/src/main/resources/application.properties new file mode 100644 index 0000000000000..b32b9f635240d --- /dev/null +++ b/integration-tests/opentelemetry-quartz/src/main/resources/application.properties @@ -0,0 +1,5 @@ +# speed up build +quarkus.otel.bsp.schedule.delay=100 +quarkus.otel.bsp.export.timeout=5s + +quarkus.scheduler.tracing.enabled=true \ No newline at end of file diff --git a/integration-tests/opentelemetry-quartz/src/test/java/io/quarkus/it/opentelemetry/quartz/OpenTelemetryQuartzIT.java b/integration-tests/opentelemetry-quartz/src/test/java/io/quarkus/it/opentelemetry/quartz/OpenTelemetryQuartzIT.java new file mode 100644 index 0000000000000..e938a8d4f73c7 --- /dev/null +++ b/integration-tests/opentelemetry-quartz/src/test/java/io/quarkus/it/opentelemetry/quartz/OpenTelemetryQuartzIT.java @@ -0,0 +1,16 @@ +package io.quarkus.it.opentelemetry.quartz; + +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.DisabledOnIntegrationTest; +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +public class OpenTelemetryQuartzIT extends OpenTelemetryQuartzTest { + @Test + @DisabledOnIntegrationTest("native mode testing span does not have a field 'exception' (only in integration-test, not in quarkus app)") + @Override + public void quartzSpanTest() { + super.quartzSpanTest(); + } +} diff --git a/integration-tests/opentelemetry-quartz/src/test/java/io/quarkus/it/opentelemetry/quartz/OpenTelemetryQuartzTest.java b/integration-tests/opentelemetry-quartz/src/test/java/io/quarkus/it/opentelemetry/quartz/OpenTelemetryQuartzTest.java new file mode 100644 index 0000000000000..0238f61946b2f --- /dev/null +++ b/integration-tests/opentelemetry-quartz/src/test/java/io/quarkus/it/opentelemetry/quartz/OpenTelemetryQuartzTest.java @@ -0,0 +1,105 @@ +package io.quarkus.it.opentelemetry.quartz; + +import static io.restassured.RestAssured.get; +import static io.restassured.RestAssured.given; +import static org.awaitility.Awaitility.await; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.time.Duration; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.StatusCode; +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.common.mapper.TypeRef; +import io.restassured.response.Response; + +@QuarkusTest +public class OpenTelemetryQuartzTest { + + private static long DURATION_IN_NANOSECONDS = 100_000_000; // Thread.sleep(100l) for each job + + @Test + public void quartzSpanTest() { + // ensure that scheduled job is called + assertCounter("/scheduler/count", 1, Duration.ofSeconds(3)); + // assert programmatically scheduled job is called + assertCounter("/scheduler/count/manual", 1, Duration.ofSeconds(3)); + // assert JobDefinition type scheduler + assertCounter("/scheduler/count/job-definition", 1, Duration.ofSeconds(1)); + + // ------- SPAN ASSERTS ------- + List> spans = getSpans(); + + assertJobSpan(spans, "myCounter", DURATION_IN_NANOSECONDS); // identity + assertJobSpan(spans, "myGroup.myManualJob", DURATION_IN_NANOSECONDS); // group + identity + assertJobSpan(spans, "myJobDefinition", DURATION_IN_NANOSECONDS); // identity + + // errors + assertErrorJobSpan(spans, "myFailedBasicScheduler", DURATION_IN_NANOSECONDS, + "error occurred in myFailedBasicScheduler."); + assertErrorJobSpan(spans, "myFailedGroup.myFailedManualJob", DURATION_IN_NANOSECONDS, + "error occurred in myFailedManualJob."); + assertErrorJobSpan(spans, "myFailedJobDefinition", DURATION_IN_NANOSECONDS, + "error occurred in myFailedJobDefinition."); + + } + + private void assertCounter(String counterPath, int expectedCount, Duration timeout) { + await().atMost(timeout) + .pollInterval(Duration.ofMillis(500)) + .until(() -> { + Response response = given().when().get(counterPath); + int code = response.statusCode(); + if (code != 200) { + return false; + } + String body = response.asString(); + int count = Integer.valueOf(body); + return count >= expectedCount; + }); + + } + + private List> getSpans() { + return get("/export").body().as(new TypeRef<>() { + }); + } + + private void assertJobSpan(List> spans, String expectedName, long expectedDuration) { + assertNotNull(spans); + assertFalse(spans.isEmpty()); + Map span = spans.stream().filter(map -> map.get("name").equals(expectedName)).findFirst().orElse(null); + assertNotNull(span, "Span with name '" + expectedName + "' not found."); + assertEquals(SpanKind.INTERNAL.toString(), span.get("kind"), "Span with name '" + expectedName + "' is not internal."); + + long start = (long) span.get("startEpochNanos"); + long end = (long) span.get("endEpochNanos"); + long delta = (end - start); + assertTrue(delta >= expectedDuration, + "Duration of span with name '" + expectedName + + "' is not longer than 100ms, actual duration: " + delta + " (ns)"); + } + + private void assertErrorJobSpan(List> spans, String expectedName, long expectedDuration, + String expectedErrorMessage) { + assertJobSpan(spans, expectedName, expectedDuration); + Map span = spans.stream().filter(map -> map.get("name").equals(expectedName)).findFirst() + .orElseThrow(AssertionError::new); // this assert should never be thrown, since we already checked it in `assertJobSpan` + + Map statusAttributes = (Map) span.get("status"); + assertNotNull(statusAttributes, "Span with name '" + expectedName + "' is not an ERROR"); + assertEquals(StatusCode.ERROR.toString(), statusAttributes.get("statusCode"), + "Span with name '" + expectedName + "' is not an ERROR"); + Map exception = (Map) ((List>) span.get("events")).stream() + .map(map -> map.get("exception")).findFirst().orElseThrow(AssertionError::new); + assertTrue(((String) exception.get("message")).contains(expectedErrorMessage), + "Span with name '" + expectedName + "' has wrong error message"); + } +} diff --git a/integration-tests/java-17/pom.xml b/integration-tests/opentelemetry-scheduler/pom.xml similarity index 72% rename from integration-tests/java-17/pom.xml rename to integration-tests/opentelemetry-scheduler/pom.xml index 442cf8ebd9720..f0fc716016933 100644 --- a/integration-tests/java-17/pom.xml +++ b/integration-tests/opentelemetry-scheduler/pom.xml @@ -1,41 +1,46 @@ - 4.0.0 - + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> - io.quarkus quarkus-integration-tests-parent + io.quarkus 999-SNAPSHOT + 4.0.0 - quarkus-integration-test-java-17 - - Quarkus - Integration Tests - Java 17 - - - 17 - + quarkus-integration-test-opentelemetry-scheduler + Quarkus - Integration Tests - OpenTelemetry Scheduler io.quarkus - quarkus-resteasy-reactive-jackson + quarkus-arc io.quarkus - quarkus-mongodb-panache + quarkus-resteasy-reactive io.quarkus - quarkus-hibernate-orm-panache + quarkus-scheduler io.quarkus - quarkus-jdbc-h2 + quarkus-opentelemetry + + + io.quarkus + quarkus-resteasy-reactive-jackson + + + + + io.opentelemetry + opentelemetry-sdk-testing + io.quarkus quarkus-junit5 @@ -47,20 +52,27 @@ test - io.quarkus - quarkus-test-mongodb + org.awaitility + awaitility test + io.quarkus - quarkus-test-h2 + quarkus-arc-deployment + ${project.version} + pom test + + + * + * + + - - io.quarkus - quarkus-mongodb-panache-deployment + quarkus-scheduler-deployment ${project.version} pom test @@ -73,7 +85,7 @@ io.quarkus - quarkus-hibernate-orm-panache-deployment + quarkus-resteasy-reactive-deployment ${project.version} pom test @@ -86,7 +98,7 @@ io.quarkus - quarkus-jdbc-h2-deployment + quarkus-opentelemetry-deployment ${project.version} pom test @@ -113,19 +125,7 @@ - - - src/main/resources - true - - - - maven-failsafe-plugin - - true - - io.quarkus quarkus-maven-plugin @@ -140,4 +140,5 @@ - \ No newline at end of file + + diff --git a/integration-tests/opentelemetry-scheduler/src/main/java/io/quarkus/it/opentelemetry/scheduler/CountResource.java b/integration-tests/opentelemetry-scheduler/src/main/java/io/quarkus/it/opentelemetry/scheduler/CountResource.java new file mode 100644 index 0000000000000..90906d9bc9288 --- /dev/null +++ b/integration-tests/opentelemetry-scheduler/src/main/java/io/quarkus/it/opentelemetry/scheduler/CountResource.java @@ -0,0 +1,30 @@ +package io.quarkus.it.opentelemetry.scheduler; + +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("/scheduler/count") +public class CountResource { + + @Inject + Counter counter; + + @Inject + JobDefinitionCounter jobDefinitionCounter; + + @GET + @Produces(MediaType.TEXT_PLAIN) + public Integer getCount() { + return counter.get(); + } + + @GET + @Path("job-definition") + @Produces(MediaType.TEXT_PLAIN) + public Integer getJobDefinitionCount() { + return jobDefinitionCounter.get(); + } +} diff --git a/integration-tests/opentelemetry-scheduler/src/main/java/io/quarkus/it/opentelemetry/scheduler/Counter.java b/integration-tests/opentelemetry-scheduler/src/main/java/io/quarkus/it/opentelemetry/scheduler/Counter.java new file mode 100644 index 0000000000000..c175e7716fe9f --- /dev/null +++ b/integration-tests/opentelemetry-scheduler/src/main/java/io/quarkus/it/opentelemetry/scheduler/Counter.java @@ -0,0 +1,30 @@ +package io.quarkus.it.opentelemetry.scheduler; + +import java.util.concurrent.atomic.AtomicInteger; + +import jakarta.annotation.PostConstruct; +import jakarta.enterprise.context.ApplicationScoped; + +import io.quarkus.scheduler.Scheduled; + +@ApplicationScoped +public class Counter { + + AtomicInteger counter; + + @PostConstruct + void init() { + counter = new AtomicInteger(); + } + + public int get() { + return counter.get(); + } + + @Scheduled(cron = "*/1 * * * * ?", identity = "myCounter") + void increment() throws InterruptedException { + Thread.sleep(100l); + counter.incrementAndGet(); + } + +} diff --git a/integration-tests/opentelemetry-scheduler/src/main/java/io/quarkus/it/opentelemetry/scheduler/ExporterResource.java b/integration-tests/opentelemetry-scheduler/src/main/java/io/quarkus/it/opentelemetry/scheduler/ExporterResource.java new file mode 100644 index 0000000000000..b7d246eb46673 --- /dev/null +++ b/integration-tests/opentelemetry-scheduler/src/main/java/io/quarkus/it/opentelemetry/scheduler/ExporterResource.java @@ -0,0 +1,38 @@ +package io.quarkus.it.opentelemetry.scheduler; + +import java.util.List; +import java.util.stream.Collectors; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Produces; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter; +import io.opentelemetry.sdk.trace.data.SpanData; + +@Path("") +public class ExporterResource { + @Inject + InMemorySpanExporter inMemorySpanExporter; + + @GET + @Path("/export") + public List export() { // only export scheduled spans + return inMemorySpanExporter.getFinishedSpanItems() + .stream() + .filter(sd -> !sd.getName().contains("export") && !sd.getName().contains("GET")) + .collect(Collectors.toList()); + } + + @ApplicationScoped + static class InMemorySpanExporterProducer { + @Produces + @Singleton + InMemorySpanExporter inMemorySpanExporter() { + return InMemorySpanExporter.create(); + } + } +} diff --git a/integration-tests/opentelemetry-scheduler/src/main/java/io/quarkus/it/opentelemetry/scheduler/FailedBasicScheduler.java b/integration-tests/opentelemetry-scheduler/src/main/java/io/quarkus/it/opentelemetry/scheduler/FailedBasicScheduler.java new file mode 100644 index 0000000000000..f03822402cd37 --- /dev/null +++ b/integration-tests/opentelemetry-scheduler/src/main/java/io/quarkus/it/opentelemetry/scheduler/FailedBasicScheduler.java @@ -0,0 +1,17 @@ +package io.quarkus.it.opentelemetry.scheduler; + +import jakarta.enterprise.context.ApplicationScoped; + +import io.quarkus.scheduler.Scheduled; + +@ApplicationScoped +public class FailedBasicScheduler { + + @Scheduled(cron = "*/1 * * * * ?", identity = "myFailedBasicScheduler") + void init() throws InterruptedException { + Thread.sleep(100l); + throw new RuntimeException("error occurred in myFailedBasicScheduler."); + + } + +} diff --git a/integration-tests/opentelemetry-scheduler/src/main/java/io/quarkus/it/opentelemetry/scheduler/FailedJobDefinitionScheduler.java b/integration-tests/opentelemetry-scheduler/src/main/java/io/quarkus/it/opentelemetry/scheduler/FailedJobDefinitionScheduler.java new file mode 100644 index 0000000000000..41d45b6ceff4d --- /dev/null +++ b/integration-tests/opentelemetry-scheduler/src/main/java/io/quarkus/it/opentelemetry/scheduler/FailedJobDefinitionScheduler.java @@ -0,0 +1,29 @@ +package io.quarkus.it.opentelemetry.scheduler; + +import jakarta.annotation.PostConstruct; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import io.quarkus.runtime.Startup; +import io.quarkus.scheduler.Scheduler; + +@ApplicationScoped +@Startup +public class FailedJobDefinitionScheduler { + + @Inject + Scheduler scheduler; + + @PostConstruct + void init() { + scheduler.newJob("myFailedJobDefinition").setCron("*/1 * * * * ?").setTask(ex -> { + try { + Thread.sleep(100l); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + throw new RuntimeException("error occurred in myFailedJobDefinition."); + }).schedule(); + } + +} diff --git a/integration-tests/opentelemetry-scheduler/src/main/java/io/quarkus/it/opentelemetry/scheduler/JobDefinitionCounter.java b/integration-tests/opentelemetry-scheduler/src/main/java/io/quarkus/it/opentelemetry/scheduler/JobDefinitionCounter.java new file mode 100644 index 0000000000000..16ee5db3e1273 --- /dev/null +++ b/integration-tests/opentelemetry-scheduler/src/main/java/io/quarkus/it/opentelemetry/scheduler/JobDefinitionCounter.java @@ -0,0 +1,37 @@ +package io.quarkus.it.opentelemetry.scheduler; + +import java.util.concurrent.atomic.AtomicInteger; + +import jakarta.annotation.PostConstruct; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import io.quarkus.runtime.Startup; +import io.quarkus.scheduler.Scheduler; + +@ApplicationScoped +@Startup +public class JobDefinitionCounter { + + @Inject + Scheduler scheduler; + + AtomicInteger counter; + + @PostConstruct + void init() { + counter = new AtomicInteger(); + scheduler.newJob("myJobDefinition").setCron("*/1 * * * * ?").setTask(ex -> { + try { + Thread.sleep(100l); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + counter.incrementAndGet(); + }).schedule(); + } + + public int get() { + return counter.get(); + } +} diff --git a/integration-tests/opentelemetry-scheduler/src/main/resources/application.properties b/integration-tests/opentelemetry-scheduler/src/main/resources/application.properties new file mode 100644 index 0000000000000..b32b9f635240d --- /dev/null +++ b/integration-tests/opentelemetry-scheduler/src/main/resources/application.properties @@ -0,0 +1,5 @@ +# speed up build +quarkus.otel.bsp.schedule.delay=100 +quarkus.otel.bsp.export.timeout=5s + +quarkus.scheduler.tracing.enabled=true \ No newline at end of file diff --git a/integration-tests/opentelemetry-scheduler/src/test/java/io/quarkus/it/opentelemetry/scheduler/OpenTelemetrySchedulerIT.java b/integration-tests/opentelemetry-scheduler/src/test/java/io/quarkus/it/opentelemetry/scheduler/OpenTelemetrySchedulerIT.java new file mode 100644 index 0000000000000..dd8c254377ca9 --- /dev/null +++ b/integration-tests/opentelemetry-scheduler/src/test/java/io/quarkus/it/opentelemetry/scheduler/OpenTelemetrySchedulerIT.java @@ -0,0 +1,16 @@ +package io.quarkus.it.opentelemetry.scheduler; + +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.DisabledOnIntegrationTest; +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +public class OpenTelemetrySchedulerIT extends OpenTelemetrySchedulerTest { + @Test + @DisabledOnIntegrationTest("native mode testing span does not have a field 'exception' (only in integration-test, not in quarkus app)") + @Override + public void schedulerSpanTest() { + super.schedulerSpanTest(); + } +} diff --git a/integration-tests/opentelemetry-scheduler/src/test/java/io/quarkus/it/opentelemetry/scheduler/OpenTelemetrySchedulerTest.java b/integration-tests/opentelemetry-scheduler/src/test/java/io/quarkus/it/opentelemetry/scheduler/OpenTelemetrySchedulerTest.java new file mode 100644 index 0000000000000..28652a8ddd4c5 --- /dev/null +++ b/integration-tests/opentelemetry-scheduler/src/test/java/io/quarkus/it/opentelemetry/scheduler/OpenTelemetrySchedulerTest.java @@ -0,0 +1,100 @@ +package io.quarkus.it.opentelemetry.scheduler; + +import static io.restassured.RestAssured.get; +import static io.restassured.RestAssured.given; +import static org.awaitility.Awaitility.await; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.time.Duration; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.StatusCode; +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.common.mapper.TypeRef; +import io.restassured.response.Response; + +@QuarkusTest +public class OpenTelemetrySchedulerTest { + + private static long DURATION_IN_NANOSECONDS = 100_000_000; // Thread.sleep(100l) for each job + + @Test + public void schedulerSpanTest() { + // ensure that scheduled job is called + assertCounter("/scheduler/count", 1, Duration.ofSeconds(3)); + // assert JobDefinition type scheduler + assertCounter("/scheduler/count/job-definition", 1, Duration.ofSeconds(3)); + + // ------- SPAN ASSERTS ------- + List> spans = getSpans(); + + assertJobSpan(spans, "myCounter", DURATION_IN_NANOSECONDS); // identity + assertJobSpan(spans, "myJobDefinition", DURATION_IN_NANOSECONDS); // identity + + // errors + assertErrorJobSpan(spans, "myFailedBasicScheduler", DURATION_IN_NANOSECONDS, + "error occurred in myFailedBasicScheduler."); + assertErrorJobSpan(spans, "myFailedJobDefinition", DURATION_IN_NANOSECONDS, + "error occurred in myFailedJobDefinition."); + + } + + private void assertCounter(String counterPath, int expectedCount, Duration timeout) { + await().atMost(timeout) + .pollInterval(Duration.ofMillis(500)) + .until(() -> { + Response response = given().when().get(counterPath); + int code = response.statusCode(); + if (code != 200) { + return false; + } + String body = response.asString(); + int count = Integer.valueOf(body); + return count >= expectedCount; + }); + + } + + private List> getSpans() { + return get("/export").body().as(new TypeRef<>() { + }); + } + + private void assertJobSpan(List> spans, String expectedName, long expectedDuration) { + assertNotNull(spans); + assertFalse(spans.isEmpty()); + Map span = spans.stream().filter(map -> map.get("name").equals(expectedName)).findFirst().orElse(null); + assertNotNull(span, "Span with name '" + expectedName + "' not found."); + assertEquals(SpanKind.INTERNAL.toString(), span.get("kind"), "Span with name '" + expectedName + "' is not internal."); + + long start = (long) span.get("startEpochNanos"); + long end = (long) span.get("endEpochNanos"); + long delta = (end - start); + assertTrue(delta >= expectedDuration, + "Duration of span with name '" + expectedName + + "' is not longer than 100ms, actual duration: " + delta + " (ns)"); + } + + private void assertErrorJobSpan(List> spans, String expectedName, long expectedDuration, + String expectedErrorMessage) { + assertJobSpan(spans, expectedName, expectedDuration); + Map span = spans.stream().filter(map -> map.get("name").equals(expectedName)).findFirst() + .orElseThrow(AssertionError::new); // this assert should never be thrown, since we already checked it in `assertJobSpan` + + Map statusAttributes = (Map) span.get("status"); + assertNotNull(statusAttributes, "Span with name '" + expectedName + "' is not an ERROR"); + assertEquals(StatusCode.ERROR.toString(), statusAttributes.get("statusCode"), + "Span with name '" + expectedName + "' is not an ERROR"); + Map exception = (Map) ((List>) span.get("events")).stream() + .map(map -> map.get("exception")).findFirst().orElseThrow(AssertionError::new); + assertTrue(((String) exception.get("message")).contains(expectedErrorMessage), + "Span with name '" + expectedName + "' has wrong error message"); + } +} diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml index fbf2f5491ac55..40b59a223a9d8 100644 --- a/integration-tests/pom.xml +++ b/integration-tests/pom.xml @@ -118,6 +118,23 @@ + + maven-failsafe-plugin + + + + native-runner + + ${project.build.directory} + + + *-runner + + RELATIVE_PATH + + + + @@ -234,7 +251,7 @@ hibernate-reactive-panache hibernate-reactive-panache-kotlin hibernate-search-orm-elasticsearch - hibernate-search-orm-elasticsearch-coordination-outbox-polling + hibernate-search-orm-elasticsearch-outbox-polling hibernate-search-orm-opensearch hibernate-search-orm-elasticsearch-tenancy hibernate-orm-tenancy @@ -288,7 +305,6 @@ amazon-lambda-http-resteasy amazon-lambda-http-resteasy-reactive container-image - kubernetes-client-hack-extension kubernetes kubernetes-client kubernetes-client-devservices @@ -360,6 +376,8 @@ opentelemetry opentelemetry-spi opentelemetry-jdbc-instrumentation + opentelemetry-quartz + opentelemetry-scheduler opentelemetry-vertx opentelemetry-reactive opentelemetry-grpc @@ -401,25 +419,12 @@ istio management-interface management-interface-auth + mtls-certificates virtual-threads - - - java-17 - - - !no-test-modules - - [17,) - - - java-17 - - - diff --git a/integration-tests/qute/pom.xml b/integration-tests/qute/pom.xml index 0d3f59bf0d5be..1e6870c1a471f 100644 --- a/integration-tests/qute/pom.xml +++ b/integration-tests/qute/pom.xml @@ -8,7 +8,7 @@ 999-SNAPSHOT 4.0.0 - + quarkus-integration-test-qute Quarkus - Integration Tests - Qute Qute integration test module @@ -109,11 +109,6 @@ org.apache.maven.plugins maven-compiler-plugin ${compiler-plugin.version} - - 11 - 11 - true - io.quarkus diff --git a/integration-tests/rest-client-reactive/pom.xml b/integration-tests/rest-client-reactive/pom.xml index e70e0e07b177d..17ca23e967a60 100644 --- a/integration-tests/rest-client-reactive/pom.xml +++ b/integration-tests/rest-client-reactive/pom.xml @@ -154,11 +154,8 @@ gradle-enterprise-maven-extension - - - - META-INF/ide-deps/** - + + application.properties diff --git a/integration-tests/scala/src/test/resources/projects/classic-scala/pom.xml b/integration-tests/scala/src/test/resources/projects/classic-scala/pom.xml index 5ece6f2ef1e7c..ee6ed9e3dc340 100644 --- a/integration-tests/scala/src/test/resources/projects/classic-scala/pom.xml +++ b/integration-tests/scala/src/test/resources/projects/classic-scala/pom.xml @@ -8,9 +8,9 @@ 1.0-SNAPSHOT @project.version@ - 11 + 17 UTF-8 - 11 + 17 @scala.version@ @scala-maven-plugin.version@ @version.surefire.plugin@ @@ -103,7 +103,7 @@ -deprecation -feature -explaintypes - -target:jvm-11 + -release:17 diff --git a/integration-tests/scala/src/test/resources/projects/external-reloadable-artifacts/app/pom.xml b/integration-tests/scala/src/test/resources/projects/external-reloadable-artifacts/app/pom.xml index d814e579300b6..d6ec72e0a9f1e 100644 --- a/integration-tests/scala/src/test/resources/projects/external-reloadable-artifacts/app/pom.xml +++ b/integration-tests/scala/src/test/resources/projects/external-reloadable-artifacts/app/pom.xml @@ -10,9 +10,9 @@ @project.version@ - 11 + 17 UTF-8 - 11 + 17 @scala.version@ @scala-maven-plugin.version@ @version.surefire.plugin@ @@ -91,7 +91,7 @@ -deprecation -feature -explaintypes - -target:jvm-11 + -release:17 diff --git a/integration-tests/scala/src/test/resources/projects/external-reloadable-artifacts/external-lib/pom.xml b/integration-tests/scala/src/test/resources/projects/external-reloadable-artifacts/external-lib/pom.xml index b7ead6173ecc0..1c15f735f9b74 100644 --- a/integration-tests/scala/src/test/resources/projects/external-reloadable-artifacts/external-lib/pom.xml +++ b/integration-tests/scala/src/test/resources/projects/external-reloadable-artifacts/external-lib/pom.xml @@ -10,9 +10,9 @@ @project.version@ - 11 + 17 UTF-8 - 11 + 17 @scala.version@ @scala-maven-plugin.version@ 3.2.2 @@ -50,7 +50,7 @@ -deprecation -feature -explaintypes - -target:jvm-11 + -release:17 diff --git a/integration-tests/spring-data-jpa/src/main/java/io/quarkus/it/spring/data/jpa/MovieRepository.java b/integration-tests/spring-data-jpa/src/main/java/io/quarkus/it/spring/data/jpa/MovieRepository.java index f655f23dbe721..07404debbce32 100644 --- a/integration-tests/spring-data-jpa/src/main/java/io/quarkus/it/spring/data/jpa/MovieRepository.java +++ b/integration-tests/spring-data-jpa/src/main/java/io/quarkus/it/spring/data/jpa/MovieRepository.java @@ -61,7 +61,7 @@ public interface MovieRepository extends CrudRepository { List countByRating(); // issue 13044 - @Query("SELECT DISTINCT m.rating FROM Movie m where m.rating != null") + @Query("SELECT DISTINCT m.rating FROM Movie m where m.rating is not null") List findAllRatings(); @Query("SELECT title, rating from Movie where title = ?1") diff --git a/integration-tests/test-extension/tests/src/test/resources-filtered/projects/project-using-test-callback-from-extension/pom.xml b/integration-tests/test-extension/tests/src/test/resources-filtered/projects/project-using-test-callback-from-extension/pom.xml index cc098681c0c59..52663a431fa0b 100644 --- a/integration-tests/test-extension/tests/src/test/resources-filtered/projects/project-using-test-callback-from-extension/pom.xml +++ b/integration-tests/test-extension/tests/src/test/resources-filtered/projects/project-using-test-callback-from-extension/pom.xml @@ -9,7 +9,7 @@ ${compiler-plugin.version} false quarkus-bom - 11 + 17 UTF-8 UTF-8 3.1.2 diff --git a/integration-tests/test-extension/tests/src/test/resources-filtered/projects/project-using-test-parameter-injection/pom.xml b/integration-tests/test-extension/tests/src/test/resources-filtered/projects/project-using-test-parameter-injection/pom.xml index aac91a5239cc2..94a911512ac8e 100644 --- a/integration-tests/test-extension/tests/src/test/resources-filtered/projects/project-using-test-parameter-injection/pom.xml +++ b/integration-tests/test-extension/tests/src/test/resources-filtered/projects/project-using-test-parameter-injection/pom.xml @@ -9,7 +9,7 @@ ${compiler-plugin.version} false - 11 + 17 UTF-8 UTF-8 quarkus-bom diff --git a/integration-tests/test-extension/tests/src/test/resources-filtered/projects/project-using-test-template-from-extension/pom.xml b/integration-tests/test-extension/tests/src/test/resources-filtered/projects/project-using-test-template-from-extension/pom.xml index eef420dac8589..d18b282118578 100644 --- a/integration-tests/test-extension/tests/src/test/resources-filtered/projects/project-using-test-template-from-extension/pom.xml +++ b/integration-tests/test-extension/tests/src/test/resources-filtered/projects/project-using-test-template-from-extension/pom.xml @@ -9,7 +9,7 @@ ${compiler-plugin.version} false quarkus-bom - 11 + 17 UTF-8 UTF-8 3.1.2 diff --git a/integration-tests/vertx-http/src/main/resources/application.properties b/integration-tests/vertx-http/src/main/resources/application.properties index 0623305e9666b..325197e0c00f7 100644 --- a/integration-tests/vertx-http/src/main/resources/application.properties +++ b/integration-tests/vertx-http/src/main/resources/application.properties @@ -7,6 +7,7 @@ quarkus.http.ssl.certificate.trust-store-file=server-truststore.jks quarkus.http.ssl.certificate.trust-store-password=password quarkus.http.ssl.certificate.trust-store-cert-alias=mykey-1 quarkus.http.ssl.client-auth=REQUIRED +quarkus.http.insecure-requests=ENABLED quarkus.http.access-log.enabled=true quarkus.http.access-log.log-to-file=true quarkus.http.access-log.base-file-name=quarkus-access-log diff --git a/pom.xml b/pom.xml index fc0f5a8d1bd12..5f091a3b95491 100644 --- a/pom.xml +++ b/pom.xml @@ -51,28 +51,23 @@ - 11 - 11 - 11 - true - ${env.GRAALVM_HOME} jdbc:postgresql:hibernate_orm_test 4.5.1 - 0.0.99 + 0.0.101 false false - 8.3.1 + 9.0.1 0.8.11 6.9.2 - 1.59.0 + 1.59.1 1.2.1 3.25.0 ${protoc.version} diff --git a/relocations/pom.xml b/relocations/pom.xml index 0d5a530654d5c..112d7ada36650 100644 --- a/relocations/pom.xml +++ b/relocations/pom.xml @@ -24,6 +24,8 @@ quarkus-jaeger-deployment quarkus-smallrye-opentracing quarkus-smallrye-opentracing-deployment + quarkus-hibernate-search-orm-coordination-outbox-polling + quarkus-hibernate-search-orm-coordination-outbox-polling-deployment diff --git a/relocations/quarkus-hibernate-search-orm-coordination-outbox-polling-deployment/pom.xml b/relocations/quarkus-hibernate-search-orm-coordination-outbox-polling-deployment/pom.xml new file mode 100644 index 0000000000000..de349589a3389 --- /dev/null +++ b/relocations/quarkus-hibernate-search-orm-coordination-outbox-polling-deployment/pom.xml @@ -0,0 +1,22 @@ + + + + quarkus-relocations-parent + io.quarkus + 999-SNAPSHOT + + 4.0.0 + + quarkus-hibernate-search-orm-coordination-outbox-polling-deployment + + + + io.quarkus + quarkus-hibernate-search-orm-outbox-polling-deployment + ${project.version} + ${project.groupId}:${project.artifactId}:${project.version} was relocated to io.quarkus:quarkus-hibernate-search-orm-outbox-polling-deployment:${project.version}. Please update the artifactId of the dependency in your project configuration. For more information about this change, please refer to https://github.com/quarkusio/quarkus/wiki/Migration-Guide-3.7#quarkus-hibernate-search-orm-coordination-outbox-polling-was-renamed + + + diff --git a/relocations/quarkus-hibernate-search-orm-coordination-outbox-polling/pom.xml b/relocations/quarkus-hibernate-search-orm-coordination-outbox-polling/pom.xml new file mode 100644 index 0000000000000..e4f7adaeba992 --- /dev/null +++ b/relocations/quarkus-hibernate-search-orm-coordination-outbox-polling/pom.xml @@ -0,0 +1,22 @@ + + + + quarkus-relocations-parent + io.quarkus + 999-SNAPSHOT + + 4.0.0 + + quarkus-hibernate-search-orm-coordination-outbox-polling + + + + io.quarkus + quarkus-hibernate-search-orm-outbox-polling + ${project.version} + ${project.groupId}:${project.artifactId}:${project.version} was relocated to io.quarkus:quarkus-hibernate-search-orm-outbox-polling:${project.version}. Please update the artifactId of the dependency in your project configuration. For more information about this change, please refer to https://github.com/quarkusio/quarkus/wiki/Migration-Guide-3.7#quarkus-hibernate-search-orm-coordination-outbox-polling-was-renamed + + + diff --git a/test-framework/common/src/main/java/io/quarkus/test/common/DefaultDockerContainerLauncher.java b/test-framework/common/src/main/java/io/quarkus/test/common/DefaultDockerContainerLauncher.java index 96ae3e5fb2b7f..20fc7ed3a6c9d 100644 --- a/test-framework/common/src/main/java/io/quarkus/test/common/DefaultDockerContainerLauncher.java +++ b/test-framework/common/src/main/java/io/quarkus/test/common/DefaultDockerContainerLauncher.java @@ -24,7 +24,7 @@ import org.apache.commons.lang3.RandomStringUtils; import org.jboss.logging.Logger; -import io.quarkus.runtime.util.ContainerRuntimeUtil; +import io.quarkus.deployment.util.ContainerRuntimeUtil; import io.quarkus.test.common.http.TestHTTPResourceManager; import io.smallrye.config.common.utils.StringUtil; diff --git a/test-framework/devmode-test-utils/pom.xml b/test-framework/devmode-test-utils/pom.xml index 4311def04cd6e..563f1268f72c9 100644 --- a/test-framework/devmode-test-utils/pom.xml +++ b/test-framework/devmode-test-utils/pom.xml @@ -14,6 +14,10 @@ Quarkus - Test Framework - Dev Mode Test Utils + + org.jboss.logging + jboss-logging + commons-io commons-io diff --git a/test-framework/devmode-test-utils/src/main/java/io/quarkus/test/devmode/util/DevModeClient.java b/test-framework/devmode-test-utils/src/main/java/io/quarkus/test/devmode/util/DevModeClient.java index 82fe2688d8853..cb71d4e0449f6 100644 --- a/test-framework/devmode-test-utils/src/main/java/io/quarkus/test/devmode/util/DevModeClient.java +++ b/test-framework/devmode-test-utils/src/main/java/io/quarkus/test/devmode/util/DevModeClient.java @@ -19,6 +19,7 @@ import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; +import org.jboss.logging.Logger; import io.smallrye.common.os.OS; @@ -26,6 +27,8 @@ public class DevModeClient { private static final long DEFAULT_TIMEOUT = OS.current() == OS.WINDOWS ? 3L : 1L; + private static final Logger LOG = Logger.getLogger(DevModeClient.class); + static long getDefaultTimeout() { return DEFAULT_TIMEOUT; } @@ -180,6 +183,10 @@ public String getHttpResponse(String path, boolean allowError, Supplier resp.set(content); return true; } catch (Exception e) { + LOG.error( + "An error occurred when DevModeClient accessed " + path + + ". It might be a normal testing behavior but logging the exception for information", + e); return false; } }); @@ -206,6 +213,10 @@ public boolean getHttpResponse(String path, int expectedStatus, long timeout, Ti } return false; } catch (Exception e) { + LOG.error( + "An error occurred when DevModeClient accessed " + path + + ". It might be a normal testing behavior but logging the exception for information", + e); return false; } }); @@ -227,6 +238,10 @@ public boolean getStrictHttpResponse(String path, int expectedStatus) { //complete no matter what the response code was return true; } catch (Exception e) { + LOG.error( + "An error occurred when DevModeClient accessed " + path + + ". It might be a normal testing behavior but logging the exception for information", + e); return false; } }); diff --git a/test-framework/jacoco/runtime/src/main/java/io/quarkus/jacoco/runtime/ReportCreator.java b/test-framework/jacoco/runtime/src/main/java/io/quarkus/jacoco/runtime/ReportCreator.java index e118708a6c684..2d440c3762e13 100644 --- a/test-framework/jacoco/runtime/src/main/java/io/quarkus/jacoco/runtime/ReportCreator.java +++ b/test-framework/jacoco/runtime/src/main/java/io/quarkus/jacoco/runtime/ReportCreator.java @@ -37,6 +37,16 @@ public ReportCreator(ReportInfo reportInfo, JacocoConfig config) { @Override public void run() { + // Ugly workaround: + // Multiple ReportCreator shutdown hooks might run concurrently, possibly corrupting the report file(s) - e.g. when using @TestProfile. + // By locking on a class from the parent CL, all hooks are "serialized", one after another. + // In the long run there should only be as many hooks as there are different Jacoco configs...usually there will be only one config anyway! + synchronized (ExecFileLoader.class) { + doRun(); + } + } + + private void doRun() { File targetdir = new File(reportInfo.reportDir); targetdir.mkdirs(); try { diff --git a/test-framework/junit5-component/pom.xml b/test-framework/junit5-component/pom.xml index fafa7e91c4d8b..8d6bb92298438 100644 --- a/test-framework/junit5-component/pom.xml +++ b/test-framework/junit5-component/pom.xml @@ -61,7 +61,7 @@ org.jboss.logmanager - jboss-logmanager-embedded + jboss-logmanager test diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/IntegrationTestUtil.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/IntegrationTestUtil.java index 47ff74106e7dc..d0642fbf170ec 100644 --- a/test-framework/junit5/src/main/java/io/quarkus/test/junit/IntegrationTestUtil.java +++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/IntegrationTestUtil.java @@ -1,6 +1,6 @@ package io.quarkus.test.junit; -import static io.quarkus.runtime.util.ContainerRuntimeUtil.detectContainerRuntime; +import static io.quarkus.deployment.util.ContainerRuntimeUtil.detectContainerRuntime; import static io.quarkus.test.common.PathTestHelper.getAppClassLocationForTestLocation; import static io.quarkus.test.common.PathTestHelper.getTestClassesLocation; import static java.lang.ProcessBuilder.Redirect.DISCARD; @@ -49,10 +49,10 @@ import io.quarkus.bootstrap.workspace.ArtifactSources; import io.quarkus.bootstrap.workspace.SourceDir; import io.quarkus.deployment.builditem.DevServicesLauncherConfigResultBuildItem; +import io.quarkus.deployment.util.ContainerRuntimeUtil; import io.quarkus.paths.PathList; import io.quarkus.runtime.configuration.ProfileManager; import io.quarkus.runtime.logging.LoggingSetupRecorder; -import io.quarkus.runtime.util.ContainerRuntimeUtil; import io.quarkus.test.common.ArtifactLauncher; import io.quarkus.test.common.LauncherUtil; import io.quarkus.test.common.PathTestHelper; diff --git a/update-version.sh b/update-version.sh index 3eb07d975cbd4..1ebbaf2382826 100755 --- a/update-version.sh +++ b/update-version.sh @@ -9,7 +9,7 @@ if [ $# -eq 0 ]; then fi VERSION=$1 -./mvnw -Dscan=false -Dgradle.cache.local.enabled=false versions:set -Dtcks -DnewVersion="${VERSION}" -DgenerateBackupPoms=false -DprocessAllModules -Prelocations +./mvnw -e -B -Dscan=false -Dgradle.cache.local.enabled=false versions:set -Dtcks -DnewVersion="${VERSION}" -DgenerateBackupPoms=false -DprocessAllModules -Prelocations if [ -f devtools/gradle/gradle.properties ]; then sed -i -r "s/^version( ?= ?).*$/version\1${VERSION}/" devtools/gradle/gradle.properties