diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 8f0c0bc5fddf4..e577070f8f4b1 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -57,6 +57,7 @@ updates: - dependency-name: net.alchim31.maven:scala-maven-plugin # SmallRye - dependency-name: io.smallrye:* + - dependency-name: io.smallrye.beanbag:* - dependency-name: io.smallrye.common:* - dependency-name: io.smallrye.config:* - dependency-name: io.smallrye.reactive:* diff --git a/.github/native-tests.json b/.github/native-tests.json index 1b05bb4e97989..dbe32b8def327 100644 --- a/.github/native-tests.json +++ b/.github/native-tests.json @@ -93,7 +93,7 @@ { "category": "HTTP", "timeout": 110, - "test-modules": "elytron-resteasy, resteasy-jackson, elytron-resteasy-reactive, resteasy-mutiny, resteasy-reactive-kotlin/standard, vertx, vertx-http, vertx-web, vertx-web-jackson, vertx-graphql, virtual-http, rest-client, rest-client-reactive, rest-client-reactive-stork, rest-client-reactive-multipart, websockets, management-interface, management-interface-auth", + "test-modules": "elytron-resteasy, resteasy-jackson, elytron-resteasy-reactive, resteasy-mutiny, resteasy-reactive-kotlin/standard, vertx, vertx-http, vertx-web, vertx-web-jackson, vertx-graphql, virtual-http, rest-client, rest-client-reactive, rest-client-reactive-stork, rest-client-reactive-multipart, websockets, management-interface, management-interface-auth, mutiny-native-jctools", "os-name": "ubuntu-latest" }, { @@ -116,8 +116,8 @@ }, { "category": "Misc4", - "timeout": 125, - "test-modules": "picocli-native, gradle, micrometer-mp-metrics, micrometer-prometheus, logging-json, jaxp, jaxb, opentelemetry, opentelemetry-jdbc-instrumentation, webjars-locator", + "timeout": 130, + "test-modules": "picocli-native, gradle, micrometer-mp-metrics, micrometer-prometheus, logging-json, jaxp, jaxb, opentelemetry, opentelemetry-jdbc-instrumentation, opentelemetry-redis-instrumentation, webjars-locator", "os-name": "ubuntu-latest" }, { @@ -128,8 +128,8 @@ }, { "category": "gRPC", - "timeout": 75, - "test-modules": "grpc-health, grpc-interceptors, grpc-mutual-auth, grpc-plain-text-gzip, grpc-plain-text-mutiny, grpc-proto-v2, grpc-streaming, grpc-tls, grpc-tls-p12", + "timeout": 80, + "test-modules": "grpc-health, grpc-interceptors, grpc-mutual-auth, grpc-plain-text-gzip, grpc-plain-text-mutiny, grpc-proto-v2, grpc-streaming, grpc-tls, grpc-tls-p12, grpc-test-random-port", "os-name": "ubuntu-latest" }, { diff --git a/.github/quarkus-github-bot.yml b/.github/quarkus-github-bot.yml index 7ad82f4879218..06178aae8af14 100644 --- a/.github/quarkus-github-bot.yml +++ b/.github/quarkus-github-bot.yml @@ -474,10 +474,12 @@ triage: notify: [ebullient] - id: config labels: [area/config] + title: "config" directories: - extensions/config-yaml/ - core/deployment/src/main/java/io/quarkus/deployment/configuration/ - core/runtime/src/main/java/io/quarkus/runtime/configuration/ + notify: [radcortez] - id: core labels: [area/core] notify: [aloubyansky, gsmet, geoand, radcortez, Sanne, stuartwdouglas] @@ -645,6 +647,12 @@ triage: directories: - extensions/vault/ - integration-tests/vault + - id: netty + labels: [area/netty] + title: "netty" + notify: [franz1981, cescoffier, jponge] + directories: + - extensions/netty - id: vertx labels: [area/vertx] title: "vert.?x" diff --git a/.github/virtual-threads-tests.json b/.github/virtual-threads-tests.json index 9a8a190876a79..a17c515aeeb5b 100644 --- a/.github/virtual-threads-tests.json +++ b/.github/virtual-threads-tests.json @@ -11,6 +11,12 @@ "timeout": 45, "test-modules": "amqp-virtual-threads, jms-virtual-threads, kafka-virtual-threads", "os-name": "ubuntu-latest" + }, + { + "category": "Security", + "timeout": 20, + "test-modules": "security-webauthn-virtual-threads", + "os-name": "ubuntu-latest" } ] } diff --git a/.github/workflows/ci-actions-incremental.yml b/.github/workflows/ci-actions-incremental.yml index c3a92701dcb76..3faf0721fa9f3 100644 --- a/.github/workflows/ci-actions-incremental.yml +++ b/.github/workflows/ci-actions-incremental.yml @@ -18,6 +18,8 @@ on: - '.github/*.yml' - '.github/*.java' - '.github/*.conf' + - '.github/workflows/doc-build.yml' + - '.github/workflows/preview.yml' pull_request: types: [opened, synchronize, reopened, ready_for_review] paths-ignore: @@ -33,6 +35,8 @@ on: - '.github/*.yml' - '.github/*.java' - '.github/*.conf' + - '.github/workflows/doc-build.yml' + - '.github/workflows/preview.yml' workflow_dispatch: concurrency: @@ -210,7 +214,7 @@ jobs: 7z a -tzip build-reports.zip -r \ 'target/build-report.json' \ 'target/gradle-build-scan-url.txt' \ - LICENSE.txt + LICENSE - name: Upload build reports uses: actions/upload-artifact@v4 if: always() @@ -400,7 +404,7 @@ jobs: '**/target/*-reports/TEST-*.xml' \ 'target/build-report.json' \ 'target/gradle-build-scan-url.txt' \ - LICENSE.txt + LICENSE - name: Upload build reports uses: actions/upload-artifact@v4 if: always() @@ -510,7 +514,7 @@ jobs: '**/target/*-reports/TEST-*.xml' \ 'target/build-report.json' \ 'target/gradle-build-scan-url.txt' \ - LICENSE.txt + LICENSE - name: Upload build reports uses: actions/upload-artifact@v4 if: always() @@ -596,7 +600,7 @@ jobs: '**/target/*-reports/TEST-*.xml' \ 'target/build-report.json' \ 'target/gradle-build-scan-url.txt' \ - LICENSE.txt + LICENSE - name: Upload build reports uses: actions/upload-artifact@v4 if: always() @@ -688,7 +692,7 @@ jobs: '**/target/*-reports/TEST-*.xml' \ 'target/build-report.json' \ 'target/gradle-build-scan-url.txt' \ - LICENSE.txt + LICENSE - name: Upload build reports uses: actions/upload-artifact@v4 if: always() @@ -780,7 +784,7 @@ jobs: '**/target/*-reports/TEST-*.xml' \ 'target/build-report.json' \ 'target/gradle-build-scan-url.txt' \ - LICENSE.txt + LICENSE - name: Upload build reports uses: actions/upload-artifact@v4 if: always() @@ -937,7 +941,7 @@ jobs: 'integration-tests/virtual-threads/**/target/*-reports/TEST-*.xml' \ 'integration-tests/virtual-threads/target/build-report.json' \ 'integration-tests/virtual-threads/target/gradle-build-scan-url.txt' \ - LICENSE.txt + LICENSE - name: Upload build reports uses: actions/upload-artifact@v4 if: always() @@ -1018,7 +1022,7 @@ jobs: '**/target/*-reports/TEST-*.xml' \ 'target/build-report.json' \ 'target/gradle-build-scan-url.txt' \ - LICENSE.txt + LICENSE - name: Upload build reports uses: actions/upload-artifact@v4 if: always() @@ -1130,7 +1134,7 @@ jobs: '**/build/test-results/test/TEST-*.xml' \ 'target/build-report.json' \ 'target/gradle-build-scan-url.txt' \ - LICENSE.txt + LICENSE - name: Upload build reports uses: actions/upload-artifact@v4 if: always() diff --git a/.github/workflows/native-it-selected-graalvm.yml b/.github/workflows/native-it-selected-graalvm.yml new file mode 100644 index 0000000000000..8103432d9f1df --- /dev/null +++ b/.github/workflows/native-it-selected-graalvm.yml @@ -0,0 +1,396 @@ +name: Quarkus CI - Native IT on selected GraalVM + +on: + workflow_dispatch: + inputs: + BRANCH: + description: 'Branch to use' + required: true + default: 'main' + type: string + NATIVE_COMPILER: + description: 'the native compiler to use' + required: true + default: 'mandrel' + type: choice + options: + - mandrel + - graalvm-community + - graalvm + - liberica + NATIVE_COMPILER_VERSION: + description: 'the native compiler version to use' + required: true + default: '21' + type: choice + options: + - '17' + - '21' + - '22' + +env: + # Workaround testsuite locale issue + LANG: en_US.UTF-8 + COMMON_MAVEN_ARGS: "-e -B --settings .github/mvn-settings.xml --fail-at-end" + COMMON_TEST_MAVEN_ARGS: "-Dformat.skip -Denforcer.skip -DskipDocs -Dforbiddenapis.skip -DskipExtensionValidation -DskipCodestartValidation" + NATIVE_TEST_MAVEN_ARGS: "-Dtest-containers -Dstart-containers -Dquarkus.native.native-image-xmx=6g -Dnative -Dnative.surefire.skip -Dno-descriptor-tests clean install -DskipDocs" + JVM_TEST_MAVEN_ARGS: "-Dtest-containers -Dstart-containers -Dquarkus.test.hang-detection-timeout=60" + PTS_MAVEN_ARGS: "-Ddevelocity.pts.enabled=${{ github.event_name == 'pull_request' && github.base_ref == 'main' && 'true' || 'false' }}" + DB_USER: hibernate_orm_test + DB_PASSWORD: hibernate_orm_test + DB_NAME: hibernate_orm_test + DEVELOCITY_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} + PULL_REQUEST_NUMBER: ${{ github.event.number }} + +defaults: + run: + shell: bash + +jobs: + build-jdk17: + name: "Initial JDK 17 Build - ${{ inputs.BRANCH }}" + runs-on: ubuntu-latest + outputs: + gib_args: ${{ steps.get-gib-args.outputs.gib_args }} + gib_impacted: ${{ steps.get-gib-impacted.outputs.impacted_modules }} + m2-cache-key: ${{ steps.m2-cache-key.outputs.key }} + steps: + - name: Gradle Enterprise environment + run: | + 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: + ref: ${{ inputs.BRANCH }} + # this is important for GIB to work + fetch-depth: 0 + - name: Add quarkusio remote + 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 17 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 17 + - name: Generate .m2 cache key + id: m2-cache-key + run: | + echo "key=m2-cache-$(/bin/date -u "+%Y-%U")" >> $GITHUB_OUTPUT + - name: Cache Maven Repository + id: cache-maven + uses: actions/cache@v4 + with: + 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/develocity-actions/maven-setup@v1 + with: + capture-strategy: ON_DEMAND + job-name: "Initial JDK 17 Build" + add-pr-comment: false + add-job-summary: false + - name: Build + env: + CAPTURE_BUILD_SCAN: true + run: | + ./mvnw -T1C $COMMON_MAVEN_ARGS -DskipTests -DskipITs -DskipDocs -Dinvoker.skip -Dskip.gradle.tests -Djbang.skip -Dtruststore.skip -Dno-format -Dtcks -Prelocations clean install + - name: Verify extension dependencies + run: ./update-extension-dependencies.sh $COMMON_MAVEN_ARGS + - name: Get GIB arguments + id: get-gib-args + env: + PULL_REQUEST_BASE: ${{ github.event.pull_request.base.ref }} + run: | + # See also: https://github.com/gitflow-incremental-builder/gitflow-incremental-builder#configuration (GIB) + # Common GIB_ARGS for all CI cases (hint: see also root pom.xml): + # - disableSelectedProjectsHandling: required to detect changes in jobs that use -pl + # - untracked: to ignore files created by jobs (and uncommitted to be consistent) + GIB_ARGS="-Dincremental -Dgib.disableSelectedProjectsHandling -Dgib.untracked=false -Dgib.uncommitted=false" + if [ -n "$PULL_REQUEST_BASE" ] + then + # The PR defines a clear merge target so just use that branch for reference, *unless*: + # - the current branch is a backport branch targeting some released branch like 1.10 (merge target is not main) + GIB_ARGS+=" -Dgib.referenceBranch=origin/$PULL_REQUEST_BASE -Dgib.disableIfReferenceBranchMatches='origin/\d+\.\d+'" + else + # No PR means the merge target is uncertain so fetch & use main of quarkusio/quarkus, *unless*: + # - the current branch is main or some released branch like 1.10 + # - the current branch is a backport branch which is going to target some released branch like 1.10 (merge target is not main) + GIB_ARGS+=" -Dgib.referenceBranch=refs/remotes/quarkusio/main -Dgib.fetchReferenceBranch -Dgib.disableIfBranchMatches='main|\d+\.\d+|.*backport.*'" + fi + echo "GIB_ARGS: $GIB_ARGS" + echo "gib_args=${GIB_ARGS}" >> $GITHUB_OUTPUT + - name: Get GIB impacted modules + id: get-gib-impacted + # mvnw just for creating gib-impacted.log ("validate" should not waste much time if not incremental at all, e.g. on main) + run: | + ./mvnw -q -T1C $COMMON_MAVEN_ARGS -Dscan=false -Dtcks -Dquickly-ci ${{ steps.get-gib-args.outputs.gib_args }} -Dgib.logImpactedTo=gib-impacted.log validate + if [ -f gib-impacted.log ] + then + GIB_IMPACTED=$(cat gib-impacted.log) + else + GIB_IMPACTED='_all_' + fi + echo "GIB_IMPACTED: ${GIB_IMPACTED}" + # three steps to retain linefeeds in output for other jobs + # (see https://github.com/github/docs/issues/21529 and https://github.com/orgs/community/discussions/26288#discussioncomment-3876281) + echo 'impacted_modules<> $GITHUB_OUTPUT + echo "${GIB_IMPACTED}" >> $GITHUB_OUTPUT + echo 'EOF' >> $GITHUB_OUTPUT + - name: Tar .m2/repository/io/quarkus + run: tar -czf m2-io-quarkus.tgz -C ~ .m2/repository/io/quarkus + - name: Upload .m2/repository/io/quarkus + uses: actions/upload-artifact@v4 + with: + name: m2-io-quarkus + path: m2-io-quarkus.tgz + retention-days: 7 + - name: Delete snapshots artifacts from cache + run: find ~/.m2 -name \*-SNAPSHOT -type d -exec rm -rf {} + + - name: Prepare build reports archive + if: always() + run: | + 7z a -tzip build-reports.zip -r \ + 'target/build-report.json' \ + 'target/gradle-build-scan-url.txt' \ + LICENSE + - name: Upload build reports + uses: actions/upload-artifact@v4 + if: always() + with: + name: "build-reports-Initial JDK 17 Build" + path: | + build-reports.zip + retention-days: 7 + + calculate-test-jobs: + name: Calculate Test Jobs + runs-on: ubuntu-latest + needs: build-jdk17 + env: + GIB_IMPACTED_MODULES: ${{ needs.build-jdk17.outputs.gib_impacted }} + outputs: + native_matrix: ${{ steps.calc-native-matrix.outputs.matrix }} + virtual_threads_matrix: ${{ steps.calc-virtual-threads-matrix.outputs.matrix }} + steps: + - uses: actions/checkout@v4 + - name: Calculate matrix from native-tests.json + id: calc-native-matrix + run: | + echo "GIB_IMPACTED_MODULES: ${GIB_IMPACTED_MODULES}" + json=$(.github/filter-native-tests-json.sh "${GIB_IMPACTED_MODULES}" | tr -d '\n') + # Remove Windows from the matrix + json=$(echo $json | jq 'del(.include[] | select(."os-name" == "windows-latest"))') + json=$(echo $json | tr -d '\n') + echo "${json}" + echo "matrix=${json}" >> $GITHUB_OUTPUT + - name: Calculate matrix from virtual-threads-tests.json + id: calc-virtual-threads-matrix + run: | + echo "GIB_IMPACTED_MODULES: ${GIB_IMPACTED_MODULES}" + json=$(.github/filter-virtual-threads-tests-json.sh "${GIB_IMPACTED_MODULES}" | tr -d '\n') + # Remove Windows from the matrix + json=$(echo $json | jq 'del(.include[] | select(."os-name" == "windows-latest"))') + json=$(echo $json | tr -d '\n') + echo "${json}" + echo "matrix=${json}" >> $GITHUB_OUTPUT + + virtual-thread-native-tests: + name: Native Tests - Virtual Thread - ${{matrix.category}} - ${{inputs.NATIVE_COMPILER}} ${{inputs.NATIVE_COMPILER_VERSION}} - ${{inputs.BRANCH}} + runs-on: ${{matrix.os-name}} + needs: [build-jdk17, calculate-test-jobs] + timeout-minutes: ${{matrix.timeout}} + strategy: + max-parallel: 12 + fail-fast: false + matrix: ${{ fromJson(needs.calculate-test-jobs.outputs.virtual_threads_matrix) }} + steps: + - name: Gradle Enterprise environment + run: | + category=$(echo -n '${{matrix.category}}' | tr '[:upper:]' '[:lower:]' | tr -c '[:alnum:]-' '-' | sed -E 's/-+/-/g') + echo "GE_TAGS=virtual-thread-native-${category}" >> "$GITHUB_ENV" + echo "GE_CUSTOM_VALUES=gh-job-name=Native Tests - Virtual Thread - ${{matrix.category}}" >> "$GITHUB_ENV" + - uses: actions/checkout@v4 + - name: Restore Maven Repository + uses: actions/cache/restore@v4 + with: + path: ~/.m2/repository + # refresh cache every week to avoid unlimited growth + key: ${{ needs.build-jdk17.outputs.m2-cache-key }} + - name: Download .m2/repository/io/quarkus + uses: actions/download-artifact@v4 + with: + name: m2-io-quarkus + path: . + - name: Extract .m2/repository/io/quarkus + run: tar -xzf m2-io-quarkus.tgz -C ~ + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 21 + - name: Setup GraalVM + id: setup-graalvm + uses: graalvm/setup-graalvm@v1 + with: + java-version: ${{ inputs.NATIVE_COMPILER_VERSION }} + distribution: ${{ inputs.NATIVE_COMPILER }} + github-token: ${{ secrets.GITHUB_TOKEN }} + # We do this so we can get better analytics for the downloaded version of the build images + - name: Update Docker Client User Agent + run: | + if [ -f ~/.docker/config.json ]; then + cat <<< $(jq '.HttpHeaders += {"User-Agent": "Quarkus-CI-Docker-Client"}' ~/.docker/config.json) > ~/.docker/config.json + fi + - name: Setup Develocity Build Scan capture + uses: gradle/develocity-actions/maven-setup@v1 + with: + capture-strategy: ON_DEMAND + job-name: "Native Tests - Virtual Thread - ${{matrix.category}}" + add-pr-comment: false + add-job-summary: false + - name: Build + env: + TEST_MODULES: ${{matrix.test-modules}} + CAPTURE_BUILD_SCAN: true + run: | + export LANG=en_US && ./mvnw $COMMON_MAVEN_ARGS $COMMON_TEST_MAVEN_ARGS $PTS_MAVEN_ARGS -f integration-tests/virtual-threads -pl "$TEST_MODULES" $NATIVE_TEST_MAVEN_ARGS + - name: Prepare build reports archive + if: always() + run: | + 7z a -tzip build-reports.zip -r \ + 'integration-tests/virtual-threads/**/target/*-reports/TEST-*.xml' \ + 'integration-tests/virtual-threads/target/build-report.json' \ + 'integration-tests/virtual-threads/target/gradle-build-scan-url.txt' \ + LICENSE + - name: Upload build reports + uses: actions/upload-artifact@v4 + if: always() + with: + name: "build-reports-Virtual Thread Support Tests Native - ${{matrix.category}}" + path: | + build-reports.zip + retention-days: 7 + + native-tests: + name: Native Tests - ${{matrix.category}} - ${{inputs.NATIVE_COMPILER}} ${{inputs.NATIVE_COMPILER_VERSION}} - ${{inputs.BRANCH}} + needs: [build-jdk17, calculate-test-jobs] + runs-on: ${{matrix.os-name}} + env: + # leave more space for the actual native compilation and execution + MAVEN_OPTS: -Xmx1g + # Ignore the following YAML Schema error + timeout-minutes: ${{matrix.timeout}} + strategy: + max-parallel: 12 + fail-fast: false + matrix: ${{ fromJson(needs.calculate-test-jobs.outputs.native_matrix) }} + steps: + - name: Gradle Enterprise environment + run: | + category=$(echo -n '${{matrix.category}}' | tr '[:upper:]' '[:lower:]' | tr -c '[:alnum:]-' '-' | sed -E 's/-+/-/g') + echo "GE_TAGS=native-${category}" >> "$GITHUB_ENV" + echo "GE_CUSTOM_VALUES=gh-job-name=Native Tests - ${{matrix.category}}" >> "$GITHUB_ENV" + - uses: actions/checkout@v4 + - name: Reclaim Disk Space + run: .github/ci-prerequisites.sh + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 17 + - name: Setup GraalVM + id: setup-graalvm + uses: graalvm/setup-graalvm@v1 + with: + java-version: ${{ inputs.NATIVE_COMPILER_VERSION }} + distribution: ${{ inputs.NATIVE_COMPILER }} + github-token: ${{ secrets.GITHUB_TOKEN }} + # We do this so we can get better analytics for the downloaded version of the build images + - name: Update Docker Client User Agent + run: | + if [ -f ~/.docker/config.json ]; then + cat <<< $(jq '.HttpHeaders += {"User-Agent": "Quarkus-CI-Docker-Client"}' ~/.docker/config.json) > ~/.docker/config.json + fi + - name: Restore Maven Repository + uses: actions/cache/restore@v4 + with: + path: ~/.m2/repository + # refresh cache every week to avoid unlimited growth + key: ${{ needs.build-jdk17.outputs.m2-cache-key }} + - name: Download .m2/repository/io/quarkus + uses: actions/download-artifact@v4 + with: + name: m2-io-quarkus + path: . + - name: Extract .m2/repository/io/quarkus + run: tar -xzf m2-io-quarkus.tgz -C ~ + - name: Setup Develocity Build Scan capture + uses: gradle/develocity-actions/maven-setup@v1 + with: + capture-strategy: ON_DEMAND + job-name: "Native Tests - ${{matrix.category}}" + add-pr-comment: false + add-job-summary: false + - name: Cache Quarkus metadata + uses: actions/cache@v4 + with: + path: '**/.quarkus/quarkus-prod-config-dump' + key: ${{ runner.os }}-quarkus-metadata + - name: Build + env: + TEST_MODULES: ${{matrix.test-modules}} + CAPTURE_BUILD_SCAN: true + run: ./mvnw $COMMON_MAVEN_ARGS $COMMON_TEST_MAVEN_ARGS $PTS_MAVEN_ARGS -f integration-tests -pl "$TEST_MODULES" $NATIVE_TEST_MAVEN_ARGS + - name: Prepare failure archive (if maven failed) + if: failure() + run: find . -type d -name '*-reports' -o -wholename '*/build/reports/tests/functionalTest' -o -name '*.log' | tar -czf test-reports.tgz -T - + - name: Upload failure Archive (if maven failed) + uses: actions/upload-artifact@v4 + if: failure() + with: + name: test-reports-native-${{matrix.category}} + path: 'test-reports.tgz' + retention-days: 7 + - name: Prepare build reports archive + if: always() + run: | + 7z a -tzip build-reports.zip -r \ + '**/target/*-reports/TEST-*.xml' \ + '**/build/test-results/test/TEST-*.xml' \ + 'target/build-report.json' \ + 'target/gradle-build-scan-url.txt' \ + LICENSE + - name: Upload build reports + uses: actions/upload-artifact@v4 + if: always() + with: + name: "build-reports-Native Tests - ${{matrix.category}}" + path: | + build-reports.zip + retention-days: 7 + + build-report: + runs-on: ubuntu-latest + name: Build report - ${{inputs.NATIVE_COMPILER}} ${{inputs.NATIVE_COMPILER_VERSION}} - ${{inputs.BRANCH}} + needs: [build-jdk17,native-tests,virtual-thread-native-tests] + if: always() + steps: + - uses: actions/download-artifact@v4 + with: + path: build-reports-artifacts + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 17 + - name: Produce report and add it as job summary + uses: quarkusio/action-build-reporter@main + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + build-reports-artifacts-path: build-reports-artifacts diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml index 25b59f46942e8..32208ac9fb5d6 100644 --- a/.github/workflows/preview.yml +++ b/.github/workflows/preview.yml @@ -14,10 +14,22 @@ jobs: - uses: actions/checkout@v4 with: repository: quarkusio/quarkusio.github.io + fetch-depth: 5000 + fetch-tags: false + + - name: Install git-restore-time + run: sudo apt-get install -y git-restore-mtime + + - name: Restore mtime + run: git restore-mtime + + # There is a weird issue with download-artifact@v4 + # keeping the external action for now - name: Download PR Artifact - uses: actions/download-artifact@v4 + uses: dawidd6/action-download-artifact@v3 with: - run-id: ${{ github.event.workflow_run.workflow_id }} + workflow: ${{ github.event.workflow_run.workflow_id }} + workflow_conclusion: success name: documentation path: documentation-temp - name: Store PR id as variable @@ -33,7 +45,7 @@ jobs: - name: Set up ruby uses: ruby/setup-ruby@v1 with: - ruby-version: 2.7 # can change this to 2.7 or whatever version you prefer + ruby-version: 3.2.3 - name: Build Jekyll site uses: limjh16/jekyll-action-ts@v2 with: @@ -72,6 +84,14 @@ jobs: custom_opts: '--config _config.yml,_only_latest_guides_config.yml' ### If you need to specify any Jekyll build options, enable the above input ### Flags accepted can be found here https://jekyllrb.com/docs/configuration/options/#build-command-options + + - name: Reduce the size of the website to be compatible with surge + run: | + find assets/images/posts/ -mindepth 1 -maxdepth 1 -type d -mtime +100 -exec rm -rf _site/{} \; + find newsletter/ -mindepth 1 -maxdepth 1 -type d -mtime +100 -exec rm -rf _site/{} \; + rm -rf _site/assets/images/worldtour/2023 + rm -rf _site/assets/images/desktopwallpapers + - name: Publishing to surge for preview id: deploy run: npx surge ./_site --domain https://quarkus-pr-main-${{ steps.pr.outputs.id }}-preview.surge.sh --token ${{ secrets.SURGE_TOKEN }} @@ -81,6 +101,10 @@ jobs: token: ${{ secrets.GITHUB_TOKEN }} body: | 🎊 PR Preview ${{ github.sha }} has been successfully built and deployed to https://quarkus-pr-main-${{ steps.pr.outputs.id }}-preview.surge.sh/version/main/guides/ + + - Images of blog posts older than 3 months are not available. + - Newsletters older than 3 months are not available. + body-include: '' diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml index 33adbbdecaa23..c7ebbe6f2f743 100644 --- a/.github/workflows/release-build.yml +++ b/.github/workflows/release-build.yml @@ -30,10 +30,14 @@ jobs: run: | ./mvnw --settings .github/mvn-settings.xml \ -B \ + -Dscan=false \ + -Dno-build-cache \ + -Dgradle.cache.local.enabled=false \ + -Dgradle.cache.remote.enabled=false \ -Prelease \ -DskipTests -DskipITs \ -Ddokka \ - -Dmaven.repo.local=$HOME/release/repository \ + -Dno-test-modules \ -Dgpg.skip \ clean install - name: Report diff --git a/.gitignore b/.gitignore index b98a566166d41..84f84c6e88e8a 100644 --- a/.gitignore +++ b/.gitignore @@ -41,4 +41,5 @@ nb-configuration.xml .envrc .jekyll-cache .mvn/.develocity +.mvn/.gradle-enterprise/ .quarkus diff --git a/bom/application/pom.xml b/bom/application/pom.xml index 3c581ea027dd6..841bbd2f06c1f 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -55,14 +55,14 @@ 4.1.0 4.0.0 3.10.0 - 2.8.1 - 6.2.6 - 4.5.0 + 2.8.2 + 6.3.0 + 4.5.1 2.1.0 1.0.13 3.0.1 3.12.0 - 4.20.0 + 4.21.0 2.6.0 2.1.3 2.1.1 @@ -111,7 +111,7 @@ 7.0.1.Final 2.3 8.0.0.Final - 8.13.0 + 8.13.1 2.2.21 2.2.5.Final 2.2.2.Final @@ -138,11 +138,10 @@ 10.14.2.0 11.5.8.0 1.2.6 - 5.4.0 2.2 5.10.2 - 15.0.0.Final - 5.0.1.Final + 15.0.1.Final + 5.0.2.Final 3.1.5 4.1.108.Final 1.16.0 diff --git a/core/deployment/pom.xml b/core/deployment/pom.xml index 0878028369f03..852e752a75c70 100644 --- a/core/deployment/pom.xml +++ b/core/deployment/pom.xml @@ -143,6 +143,30 @@ + + maven-dependency-plugin + + + download-signed-jar + generate-test-resources + + copy + + + + + org.eclipse.jgit + org.eclipse.jgit.ssh.apache + 6.9.0.202403050737-r + jar + signed.jar + + + ${project.build.testOutputDirectory} + + + + maven-surefire-plugin diff --git a/core/deployment/src/main/java/io/quarkus/deployment/BootstrapConfig.java b/core/deployment/src/main/java/io/quarkus/deployment/BootstrapConfig.java index 8f3e059be79f0..fc4e1e776034c 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/BootstrapConfig.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/BootstrapConfig.java @@ -41,6 +41,15 @@ public class BootstrapConfig { @ConfigItem(defaultValue = "false") boolean disableJarCache; + /** + * A temporary option introduced to avoid a logging warning when {@code }-Dquarkus.bootstrap.incubating-model-resolver} + * is added to the build command line. + * This option enables an incubating implementation of the Quarkus Application Model resolver. + * This option will be removed as soon as the incubating implementation becomes the default one. + */ + @ConfigItem(defaultValue = "false") + boolean incubatingModelResolver; + /** * Whether to throw an error, warn or silently ignore misaligned platform BOM imports */ diff --git a/core/deployment/src/main/java/io/quarkus/deployment/builditem/DevServicesSharedNetworkBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/builditem/DevServicesSharedNetworkBuildItem.java index fbce82559bd27..459d3b9605e50 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/builditem/DevServicesSharedNetworkBuildItem.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/builditem/DevServicesSharedNetworkBuildItem.java @@ -9,15 +9,43 @@ import io.quarkus.builder.BuildChainBuilder; import io.quarkus.builder.BuildStepBuilder; import io.quarkus.builder.item.MultiBuildItem; +import io.quarkus.deployment.dev.devservices.GlobalDevServicesConfig; /** * A marker build item that indicates, if any instances are provided during the build, the containers started by DevServices - * will use a shared network. + * may use a shared network. * This is mainly useful in integration tests where the application container needs to be able * to communicate with the service containers. */ public final class DevServicesSharedNetworkBuildItem extends MultiBuildItem { + private final String source; + + /** Create a build item without identifying the creator source. */ + public DevServicesSharedNetworkBuildItem() { + this.source = UNKNOWN_SOURCE; + } + + /** + * Create a build item identifying the creator source. + * + * @param source The identifier of the creator + */ + public DevServicesSharedNetworkBuildItem(String source) { + this.source = source; + } + + /** The creator source of this build item. May be useful to decide whether a DevService should join a shared network. */ + public String getSource() { + return source; + } + + /* Property used by factory to retrieve the source of instanciation. */ + public static final String SOURCE_PROPERTY = "source"; + + /* Value of source field when instanciation origin is unknown. */ + public static final String UNKNOWN_SOURCE = "unknown"; + /** * Generates a {@code List> build chain builder} which creates a build step * producing the {@link DevServicesSharedNetworkBuildItem} build item. @@ -28,10 +56,28 @@ public static final class Factory implements Function, List< public List> apply(final Map props) { return Collections.singletonList((builder) -> { BuildStepBuilder stepBuilder = builder.addBuildStep((ctx) -> { - ctx.produce(new DevServicesSharedNetworkBuildItem()); + DevServicesSharedNetworkBuildItem buildItem; + if (props != null && props.containsKey(SOURCE_PROPERTY)) { + buildItem = new DevServicesSharedNetworkBuildItem(props.get(SOURCE_PROPERTY).toString()); + } else { + buildItem = new DevServicesSharedNetworkBuildItem(); + } + ctx.produce(buildItem); }); stepBuilder.produces(DevServicesSharedNetworkBuildItem.class).build(); }); } } + + /** + * Helper method for DevServices processors that tells if joining the shared network is required. + * Joining this network may be required if explicitily asked by user properties or if running a containerized + * application during integration tests. + */ + public static boolean isSharedNetworkRequired(GlobalDevServicesConfig globalDevServicesConfig, + List devServicesSharedNetworkBuildItem) { + return globalDevServicesConfig.launchOnSharedNetwork || + (!devServicesSharedNetworkBuildItem.isEmpty() + && devServicesSharedNetworkBuildItem.get(0).getSource().equals("io.quarkus.test.junit")); + } } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/configuration/BuildTimeConfigurationReader.java b/core/deployment/src/main/java/io/quarkus/deployment/configuration/BuildTimeConfigurationReader.java index 8b0cb7c4b1076..62ef5f74e3043 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/configuration/BuildTimeConfigurationReader.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/configuration/BuildTimeConfigurationReader.java @@ -1086,6 +1086,7 @@ public String getValue(final String propertyName) { builder.getSources().clear(); builder.getSourceProviders().clear(); builder.setAddDefaultSources(false) + .withInterceptors(ConfigCompatibility.FrontEnd.nonLoggingInstance(), ConfigCompatibility.BackEnd.instance()) .addDiscoveredCustomizers() .withProfiles(config.getProfiles()) .withSources(sourceProperties); @@ -1099,6 +1100,7 @@ public String getValue(final String propertyName) { builder.getSources().clear(); builder.getSourceProviders().clear(); builder.setAddDefaultSources(false) + .withInterceptors(ConfigCompatibility.FrontEnd.nonLoggingInstance(), ConfigCompatibility.BackEnd.instance()) .addDiscoveredCustomizers() .withSources(sourceProperties) .withSources(new MapBackedConfigSource( diff --git a/core/deployment/src/main/java/io/quarkus/deployment/configuration/ConfigCompatibility.java b/core/deployment/src/main/java/io/quarkus/deployment/configuration/ConfigCompatibility.java index 54007f07a354f..20b1853ae0c17 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/configuration/ConfigCompatibility.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/configuration/ConfigCompatibility.java @@ -114,9 +114,13 @@ public static final class FrontEnd implements ConfigSourceInterceptor { @Serial private static final long serialVersionUID = -3438497970389074611L; - private static final FrontEnd instance = new FrontEnd(); + private static final FrontEnd instance = new FrontEnd(true); + private static final FrontEnd nonLoggingInstance = new FrontEnd(false); - private FrontEnd() { + private final boolean logging; + + private FrontEnd(final boolean logging) { + this.logging = logging; } public ConfigValue getValue(final ConfigSourceInterceptorContext context, final String name) { @@ -155,11 +159,13 @@ public boolean hasNext() { // get the replacement names List list = fn.apply(context, new NameIterator(next)); subIter = list.iterator(); - // todo: print these warnings when mapping the configuration so they cannot appear more than once - if (list.isEmpty()) { - log.warnf("Configuration property '%s' has been deprecated and will be ignored", next); - } else { - log.warnf("Configuration property '%s' has been deprecated and replaced by: %s", next, list); + if (logging) { + // todo: print these warnings when mapping the configuration so they cannot appear more than once + if (list.isEmpty()) { + log.warnf("Configuration property '%s' has been deprecated and will be ignored", next); + } else { + log.warnf("Configuration property '%s' has been deprecated and replaced by: %s", next, list); + } } } return true; @@ -179,6 +185,10 @@ public String next() { public static FrontEnd instance() { return instance; } + + public static FrontEnd nonLoggingInstance() { + return nonLoggingInstance; + } } /** diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/RuntimeUpdatesProcessor.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/RuntimeUpdatesProcessor.java index 1b57c9aa9ab57..de09063b148bb 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/dev/RuntimeUpdatesProcessor.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/RuntimeUpdatesProcessor.java @@ -18,6 +18,7 @@ import java.nio.file.PathMatcher; import java.nio.file.Paths; import java.nio.file.SimpleFileVisitor; +import java.nio.file.StandardCopyOption; import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.FileTime; import java.util.ArrayList; @@ -936,11 +937,14 @@ Set checkForFileChange(Function checkForFileChange(Function checkForFileChange(Function checkForFileChange(Function last) { // Use either the absolute path or the OS-agnostic path to match the HotDeploymentWatchedFileBuildItem ret.add(isAbsolute ? watchedPath.filePath.toString() : watchedPath.getOSAgnosticMatchPath()); @@ -1372,7 +1380,8 @@ private boolean isAbsolute() { @Override public String toString() { - return "WatchedPath [matchPath=" + matchPath + ", filePath=" + filePath + ", restartNeeded=" + restartNeeded + "]"; + return "WatchedPath [matchPath=" + matchPath + ", filePath=" + filePath + ", restartNeeded=" + restartNeeded + + ", lastModified=" + lastModified + "]"; } } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/devservices/GlobalDevServicesConfig.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/devservices/GlobalDevServicesConfig.java index c7a51d9fb9619..12102e2d70095 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/dev/devservices/GlobalDevServicesConfig.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/devservices/GlobalDevServicesConfig.java @@ -16,6 +16,12 @@ public class GlobalDevServicesConfig { @ConfigItem(defaultValue = "true") boolean enabled; + /** + * Global flag that can be used to force the attachmment of Dev Services to shared netxork. Default is false. + */ + @ConfigItem(defaultValue = "false") + public boolean launchOnSharedNetwork; + /** * The timeout for starting a container */ 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 ade96118461b7..708dbb4e3c31e 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 @@ -5,7 +5,6 @@ import java.io.BufferedInputStream; import java.io.BufferedWriter; import java.io.ByteArrayOutputStream; -import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.ObjectOutputStream; @@ -42,12 +41,12 @@ import java.util.function.Consumer; import java.util.function.Predicate; import java.util.jar.Attributes; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.jar.JarOutputStream; import java.util.jar.Manifest; import java.util.stream.Collectors; import java.util.stream.Stream; -import java.util.zip.ZipEntry; -import java.util.zip.ZipFile; -import java.util.zip.ZipOutputStream; import org.jboss.logging.Logger; @@ -92,14 +91,14 @@ /** * This build step builds both the thin jars and uber jars. - * + *

* The way this is built is a bit convoluted. In general, we only want a single one built, * as determined by the {@link PackageConfig} (unless the config explicitly asks for both of them) - * + *

* However, we still need an extension to be able to ask for a specific one of these despite the config, * e.g. if a serverless environment needs an uberjar to build its deployment package then we need * to be able to provide this. - * + *

* To enable this we have two build steps that strongly produce the respective artifact type build * items, but not a {@link ArtifactResultBuildItem}. We then * have another two build steps that only run if they are configured to consume these explicit @@ -929,7 +928,7 @@ private void copyDependency(Set parentFirstArtifacts, OutputTargetB } else { // we copy jars for which we remove entries to the same directory // which seems a bit odd to me - filterZipFile(resolvedDep, targetPath, removedFromThisArchive); + filterJarFile(resolvedDep, targetPath, removedFromThisArchive); } } } @@ -1123,7 +1122,7 @@ private void copyLibraryJars(FileSystem runnerZipFs, OutputTargetBuildItem outpu + resolvedDep.getFileName(); final Path targetPath = libDir.resolve(fileName); classPath.append(" lib/").append(fileName); - filterZipFile(resolvedDep, targetPath, transformedFromThisArchive); + filterJarFile(resolvedDep, targetPath, transformedFromThisArchive); } } else { // This case can happen when we are building a jar from inside the Quarkus repository @@ -1237,16 +1236,26 @@ private void handleParent(FileSystem runnerZipFs, String fileName, Map transformedFromThisArchive) { - + static void filterJarFile(Path resolvedDep, Path targetPath, Set transformedFromThisArchive) { try { byte[] buffer = new byte[10000]; - try (ZipFile in = new ZipFile(resolvedDep.toFile())) { - try (ZipOutputStream out = new ZipOutputStream(new FileOutputStream(targetPath.toFile()))) { - Enumeration entries = in.entries(); + try (JarFile in = new JarFile(resolvedDep.toFile(), false)) { + Manifest manifest = in.getManifest(); + if (manifest != null) { + // Remove signature entries + manifest.getEntries().clear(); + } else { + manifest = new Manifest(); + } + try (JarOutputStream out = new JarOutputStream(Files.newOutputStream(targetPath), manifest)) { + Enumeration entries = in.entries(); while (entries.hasMoreElements()) { - ZipEntry entry = entries.nextElement(); - if (!transformedFromThisArchive.contains(entry.getName())) { + JarEntry entry = entries.nextElement(); + String entryName = entry.getName(); + if (!transformedFromThisArchive.contains(entryName) + && !entryName.equals(JarFile.MANIFEST_NAME) + && !entryName.equals("META-INF/INDEX.LIST") + && !isSignatureFile(entryName)) { entry.setCompressedSize(-1); out.putNextEntry(entry); try (InputStream inStream = in.getInputStream(entry)) { @@ -1255,6 +1264,8 @@ private void filterZipFile(Path resolvedDep, Path targetPath, Set transf out.write(buffer, 0, r); } } + } else { + log.debugf("Removed %s from %s", entryName, resolvedDep); } } } @@ -1262,10 +1273,21 @@ private void filterZipFile(Path resolvedDep, Path targetPath, Set transf Files.setLastModifiedTime(targetPath, Files.getLastModifiedTime(resolvedDep)); } } catch (IOException e) { - throw new RuntimeException(e); + throw new UncheckedIOException(e); } } + private static boolean isSignatureFile(String entry) { + entry = entry.toUpperCase(); + if (entry.startsWith("META-INF/") && entry.indexOf('/', "META-INF/".length()) == -1) { + return entry.endsWith(".SF") + || entry.endsWith(".DSA") + || entry.endsWith(".RSA") + || entry.endsWith(".EC"); + } + return false; + } + /** * Manifest generation is quite simple : we just have to push some attributes in manifest. * However, it gets a little more complex if the manifest preexists. @@ -1591,12 +1613,8 @@ public boolean downloadIfNecessary() { "https://repo.maven.apache.org/maven2/org/vineflower/vineflower/%s/vineflower-%s.jar", context.versionStr, context.versionStr); try (BufferedInputStream in = new BufferedInputStream(new URL(downloadURL).openStream()); - FileOutputStream fileOutputStream = new FileOutputStream(decompilerJar.toFile())) { - byte[] dataBuffer = new byte[1024]; - int bytesRead; - while ((bytesRead = in.read(dataBuffer, 0, 1024)) != -1) { - fileOutputStream.write(dataBuffer, 0, bytesRead); - } + OutputStream fileOutputStream = Files.newOutputStream(decompilerJar)) { + in.transferTo(fileOutputStream); return true; } catch (IOException e) { log.error("Unable to download Vineflower from " + downloadURL, e); diff --git a/core/deployment/src/test/java/io/quarkus/deployment/conditionaldeps/CascadingConditionalDependenciesTest.java b/core/deployment/src/test/java/io/quarkus/deployment/conditionaldeps/CascadingConditionalDependenciesTest.java index cb87d130aa054..4e668d0ca9cad 100644 --- a/core/deployment/src/test/java/io/quarkus/deployment/conditionaldeps/CascadingConditionalDependenciesTest.java +++ b/core/deployment/src/test/java/io/quarkus/deployment/conditionaldeps/CascadingConditionalDependenciesTest.java @@ -5,14 +5,16 @@ import java.util.HashSet; import java.util.Set; +import org.eclipse.aether.util.artifact.JavaScopes; + import io.quarkus.bootstrap.model.ApplicationModel; import io.quarkus.bootstrap.resolver.TsArtifact; import io.quarkus.bootstrap.resolver.TsQuarkusExt; import io.quarkus.deployment.runnerjar.BootstrapFromOriginalJarTestBase; +import io.quarkus.maven.dependency.ArtifactCoords; import io.quarkus.maven.dependency.ArtifactDependency; import io.quarkus.maven.dependency.Dependency; import io.quarkus.maven.dependency.DependencyFlags; -import io.quarkus.maven.dependency.GACTV; public class CascadingConditionalDependenciesTest extends BootstrapFromOriginalJarTestBase { @@ -70,22 +72,28 @@ protected TsArtifact composeApplication() { protected void assertAppModel(ApplicationModel model) throws Exception { final Set expected = new HashSet<>(); expected.add(new ArtifactDependency( - new GACTV(TsArtifact.DEFAULT_GROUP_ID, "ext-c-deployment", TsArtifact.DEFAULT_VERSION), "compile", + ArtifactCoords.jar(TsArtifact.DEFAULT_GROUP_ID, "ext-c-deployment", TsArtifact.DEFAULT_VERSION), + JavaScopes.COMPILE, DependencyFlags.DEPLOYMENT_CP)); expected.add(new ArtifactDependency( - new GACTV(TsArtifact.DEFAULT_GROUP_ID, "ext-a-deployment", TsArtifact.DEFAULT_VERSION), "compile", + ArtifactCoords.jar(TsArtifact.DEFAULT_GROUP_ID, "ext-a-deployment", TsArtifact.DEFAULT_VERSION), + JavaScopes.COMPILE, DependencyFlags.DEPLOYMENT_CP)); expected.add(new ArtifactDependency( - new GACTV(TsArtifact.DEFAULT_GROUP_ID, "ext-b-deployment", TsArtifact.DEFAULT_VERSION), "runtime", + ArtifactCoords.jar(TsArtifact.DEFAULT_GROUP_ID, "ext-b-deployment", TsArtifact.DEFAULT_VERSION), + JavaScopes.COMPILE, DependencyFlags.DEPLOYMENT_CP)); expected.add(new ArtifactDependency( - new GACTV(TsArtifact.DEFAULT_GROUP_ID, "ext-d-deployment", TsArtifact.DEFAULT_VERSION), "runtime", + ArtifactCoords.jar(TsArtifact.DEFAULT_GROUP_ID, "ext-d-deployment", TsArtifact.DEFAULT_VERSION), + JavaScopes.COMPILE, DependencyFlags.DEPLOYMENT_CP)); expected.add(new ArtifactDependency( - new GACTV(TsArtifact.DEFAULT_GROUP_ID, "ext-e-deployment", TsArtifact.DEFAULT_VERSION), "compile", + ArtifactCoords.jar(TsArtifact.DEFAULT_GROUP_ID, "ext-e-deployment", TsArtifact.DEFAULT_VERSION), + JavaScopes.COMPILE, DependencyFlags.DEPLOYMENT_CP)); expected.add(new ArtifactDependency( - new GACTV(TsArtifact.DEFAULT_GROUP_ID, "ext-f-deployment", TsArtifact.DEFAULT_VERSION), "runtime", + ArtifactCoords.jar(TsArtifact.DEFAULT_GROUP_ID, "ext-f-deployment", TsArtifact.DEFAULT_VERSION), + JavaScopes.COMPILE, DependencyFlags.DEPLOYMENT_CP)); assertEquals(expected, getDeploymentOnlyDeps(model)); } diff --git a/core/deployment/src/test/java/io/quarkus/deployment/conditionaldeps/ConditionalDependencyScenarioTwoTest.java b/core/deployment/src/test/java/io/quarkus/deployment/conditionaldeps/ConditionalDependencyScenarioTwoTest.java index 4d4bc3f0881b7..a55543f9c1710 100644 --- a/core/deployment/src/test/java/io/quarkus/deployment/conditionaldeps/ConditionalDependencyScenarioTwoTest.java +++ b/core/deployment/src/test/java/io/quarkus/deployment/conditionaldeps/ConditionalDependencyScenarioTwoTest.java @@ -4,16 +4,17 @@ import java.util.HashSet; import java.util.Set; -import java.util.stream.Collectors; + +import org.eclipse.aether.util.artifact.JavaScopes; import io.quarkus.bootstrap.model.ApplicationModel; import io.quarkus.bootstrap.resolver.TsArtifact; import io.quarkus.bootstrap.resolver.TsQuarkusExt; import io.quarkus.deployment.runnerjar.BootstrapFromOriginalJarTestBase; +import io.quarkus.maven.dependency.ArtifactCoords; import io.quarkus.maven.dependency.ArtifactDependency; import io.quarkus.maven.dependency.Dependency; import io.quarkus.maven.dependency.DependencyFlags; -import io.quarkus.maven.dependency.GACTV; public class ConditionalDependencyScenarioTwoTest extends BootstrapFromOriginalJarTestBase { @@ -114,54 +115,68 @@ protected TsArtifact composeApplication() { @Override protected void assertAppModel(ApplicationModel appModel) throws Exception { - final Set deploymentDeps = appModel.getDependencies().stream() - .filter(d -> d.isDeploymentCp() && !d.isRuntimeCp()).map(d -> new ArtifactDependency(d)) - .collect(Collectors.toSet()); + var deploymentDeps = getDeploymentOnlyDeps(appModel); + ; final Set expected = new HashSet<>(); expected.add(new ArtifactDependency( - new GACTV(TsArtifact.DEFAULT_GROUP_ID, "ext-f-deployment", TsArtifact.DEFAULT_VERSION), "compile", + ArtifactCoords.jar(TsArtifact.DEFAULT_GROUP_ID, "ext-f-deployment", TsArtifact.DEFAULT_VERSION), + JavaScopes.COMPILE, DependencyFlags.DEPLOYMENT_CP)); expected.add(new ArtifactDependency( - new GACTV(TsArtifact.DEFAULT_GROUP_ID, "ext-g-deployment", TsArtifact.DEFAULT_VERSION), "compile", + ArtifactCoords.jar(TsArtifact.DEFAULT_GROUP_ID, "ext-g-deployment", TsArtifact.DEFAULT_VERSION), + JavaScopes.COMPILE, DependencyFlags.DEPLOYMENT_CP)); expected.add(new ArtifactDependency( - new GACTV(TsArtifact.DEFAULT_GROUP_ID, "ext-h-deployment", TsArtifact.DEFAULT_VERSION), "runtime", + ArtifactCoords.jar(TsArtifact.DEFAULT_GROUP_ID, "ext-h-deployment", TsArtifact.DEFAULT_VERSION), + JavaScopes.COMPILE, DependencyFlags.DEPLOYMENT_CP)); expected.add(new ArtifactDependency( - new GACTV(TsArtifact.DEFAULT_GROUP_ID, "ext-k-deployment", TsArtifact.DEFAULT_VERSION), "runtime", + ArtifactCoords.jar(TsArtifact.DEFAULT_GROUP_ID, "ext-k-deployment", TsArtifact.DEFAULT_VERSION), + JavaScopes.COMPILE, DependencyFlags.DEPLOYMENT_CP)); expected.add(new ArtifactDependency( - new GACTV(TsArtifact.DEFAULT_GROUP_ID, "ext-l-deployment", TsArtifact.DEFAULT_VERSION), "compile", + ArtifactCoords.jar(TsArtifact.DEFAULT_GROUP_ID, "ext-l-deployment", TsArtifact.DEFAULT_VERSION), + JavaScopes.COMPILE, DependencyFlags.DEPLOYMENT_CP)); expected.add(new ArtifactDependency( - new GACTV(TsArtifact.DEFAULT_GROUP_ID, "ext-j-deployment", TsArtifact.DEFAULT_VERSION), "compile", + ArtifactCoords.jar(TsArtifact.DEFAULT_GROUP_ID, "ext-j-deployment", TsArtifact.DEFAULT_VERSION), + JavaScopes.COMPILE, DependencyFlags.DEPLOYMENT_CP)); expected.add(new ArtifactDependency( - new GACTV(TsArtifact.DEFAULT_GROUP_ID, "ext-m-deployment", TsArtifact.DEFAULT_VERSION), "compile", + ArtifactCoords.jar(TsArtifact.DEFAULT_GROUP_ID, "ext-m-deployment", TsArtifact.DEFAULT_VERSION), + JavaScopes.COMPILE, DependencyFlags.DEPLOYMENT_CP)); expected.add(new ArtifactDependency( - new GACTV(TsArtifact.DEFAULT_GROUP_ID, "ext-n-deployment", TsArtifact.DEFAULT_VERSION), "runtime", + ArtifactCoords.jar(TsArtifact.DEFAULT_GROUP_ID, "ext-n-deployment", TsArtifact.DEFAULT_VERSION), + JavaScopes.COMPILE, DependencyFlags.DEPLOYMENT_CP)); expected.add(new ArtifactDependency( - new GACTV(TsArtifact.DEFAULT_GROUP_ID, "ext-i-deployment", TsArtifact.DEFAULT_VERSION), "runtime", + ArtifactCoords.jar(TsArtifact.DEFAULT_GROUP_ID, "ext-i-deployment", TsArtifact.DEFAULT_VERSION), + JavaScopes.COMPILE, DependencyFlags.DEPLOYMENT_CP)); expected.add(new ArtifactDependency( - new GACTV(TsArtifact.DEFAULT_GROUP_ID, "ext-o-deployment", TsArtifact.DEFAULT_VERSION), "runtime", + ArtifactCoords.jar(TsArtifact.DEFAULT_GROUP_ID, "ext-o-deployment", TsArtifact.DEFAULT_VERSION), + JavaScopes.COMPILE, DependencyFlags.DEPLOYMENT_CP)); expected.add(new ArtifactDependency( - new GACTV(TsArtifact.DEFAULT_GROUP_ID, "ext-p-deployment", TsArtifact.DEFAULT_VERSION), "runtime", + ArtifactCoords.jar(TsArtifact.DEFAULT_GROUP_ID, "ext-p-deployment", TsArtifact.DEFAULT_VERSION), + JavaScopes.COMPILE, DependencyFlags.DEPLOYMENT_CP)); expected.add(new ArtifactDependency( - new GACTV(TsArtifact.DEFAULT_GROUP_ID, "ext-r-deployment", TsArtifact.DEFAULT_VERSION), "runtime", + ArtifactCoords.jar(TsArtifact.DEFAULT_GROUP_ID, "ext-r-deployment", TsArtifact.DEFAULT_VERSION), + JavaScopes.COMPILE, DependencyFlags.DEPLOYMENT_CP)); expected.add(new ArtifactDependency( - new GACTV(TsArtifact.DEFAULT_GROUP_ID, "ext-s-deployment", TsArtifact.DEFAULT_VERSION), "runtime", + ArtifactCoords.jar(TsArtifact.DEFAULT_GROUP_ID, "ext-s-deployment", TsArtifact.DEFAULT_VERSION), + JavaScopes.COMPILE, DependencyFlags.DEPLOYMENT_CP)); expected.add(new ArtifactDependency( - new GACTV(TsArtifact.DEFAULT_GROUP_ID, "ext-t-deployment", TsArtifact.DEFAULT_VERSION), "compile", + ArtifactCoords.jar(TsArtifact.DEFAULT_GROUP_ID, "ext-t-deployment", TsArtifact.DEFAULT_VERSION), + JavaScopes.COMPILE, DependencyFlags.DEPLOYMENT_CP)); expected.add(new ArtifactDependency( - new GACTV(TsArtifact.DEFAULT_GROUP_ID, "ext-u-deployment", TsArtifact.DEFAULT_VERSION), "runtime", + ArtifactCoords.jar(TsArtifact.DEFAULT_GROUP_ID, "ext-u-deployment", TsArtifact.DEFAULT_VERSION), + JavaScopes.COMPILE, DependencyFlags.DEPLOYMENT_CP)); assertEquals(expected, new HashSet<>(deploymentDeps)); } diff --git a/core/deployment/src/test/java/io/quarkus/deployment/conditionaldeps/ConditionalDependencyWithSingleConditionTest.java b/core/deployment/src/test/java/io/quarkus/deployment/conditionaldeps/ConditionalDependencyWithSingleConditionTest.java index a26d42a488b7f..bcd424cd8cbf3 100644 --- a/core/deployment/src/test/java/io/quarkus/deployment/conditionaldeps/ConditionalDependencyWithSingleConditionTest.java +++ b/core/deployment/src/test/java/io/quarkus/deployment/conditionaldeps/ConditionalDependencyWithSingleConditionTest.java @@ -2,18 +2,17 @@ import static org.junit.jupiter.api.Assertions.assertEquals; -import java.util.HashSet; import java.util.Set; -import java.util.stream.Collectors; + +import org.eclipse.aether.util.artifact.JavaScopes; import io.quarkus.bootstrap.model.ApplicationModel; import io.quarkus.bootstrap.resolver.TsArtifact; import io.quarkus.bootstrap.resolver.TsQuarkusExt; import io.quarkus.deployment.runnerjar.BootstrapFromOriginalJarTestBase; +import io.quarkus.maven.dependency.ArtifactCoords; import io.quarkus.maven.dependency.ArtifactDependency; -import io.quarkus.maven.dependency.Dependency; import io.quarkus.maven.dependency.DependencyFlags; -import io.quarkus.maven.dependency.GACTV; public class ConditionalDependencyWithSingleConditionTest extends BootstrapFromOriginalJarTestBase { @@ -42,19 +41,19 @@ protected TsArtifact composeApplication() { @Override protected void assertAppModel(ApplicationModel appModel) throws Exception { - final Set deploymentDeps = appModel.getDependencies().stream() - .filter(d -> d.isDeploymentCp() && !d.isRuntimeCp()).map(d -> new ArtifactDependency(d)) - .collect(Collectors.toSet()); - final Set expected = new HashSet<>(); - expected.add(new ArtifactDependency( - new GACTV(TsArtifact.DEFAULT_GROUP_ID, "ext-c-deployment", TsArtifact.DEFAULT_VERSION), "compile", - DependencyFlags.DEPLOYMENT_CP)); - expected.add(new ArtifactDependency( - new GACTV(TsArtifact.DEFAULT_GROUP_ID, "ext-a-deployment", TsArtifact.DEFAULT_VERSION), "compile", - DependencyFlags.DEPLOYMENT_CP)); - expected.add(new ArtifactDependency( - new GACTV(TsArtifact.DEFAULT_GROUP_ID, "ext-b-deployment", TsArtifact.DEFAULT_VERSION), "runtime", - DependencyFlags.DEPLOYMENT_CP)); - assertEquals(expected, deploymentDeps); + var expected = Set.of( + new ArtifactDependency( + ArtifactCoords.jar(TsArtifact.DEFAULT_GROUP_ID, "ext-c-deployment", TsArtifact.DEFAULT_VERSION), + JavaScopes.COMPILE, + DependencyFlags.DEPLOYMENT_CP), + new ArtifactDependency( + ArtifactCoords.jar(TsArtifact.DEFAULT_GROUP_ID, "ext-a-deployment", TsArtifact.DEFAULT_VERSION), + JavaScopes.COMPILE, + DependencyFlags.DEPLOYMENT_CP), + new ArtifactDependency( + ArtifactCoords.jar(TsArtifact.DEFAULT_GROUP_ID, "ext-b-deployment", TsArtifact.DEFAULT_VERSION), + JavaScopes.COMPILE, + DependencyFlags.DEPLOYMENT_CP)); + assertEquals(expected, getDeploymentOnlyDeps(appModel)); } } diff --git a/core/deployment/src/test/java/io/quarkus/deployment/conditionaldeps/ConditionalDependencyWithTwoConditionsTest.java b/core/deployment/src/test/java/io/quarkus/deployment/conditionaldeps/ConditionalDependencyWithTwoConditionsTest.java index 9f73397e60e3d..aa6a14336d556 100644 --- a/core/deployment/src/test/java/io/quarkus/deployment/conditionaldeps/ConditionalDependencyWithTwoConditionsTest.java +++ b/core/deployment/src/test/java/io/quarkus/deployment/conditionaldeps/ConditionalDependencyWithTwoConditionsTest.java @@ -5,14 +5,16 @@ import java.util.HashSet; import java.util.Set; +import org.eclipse.aether.util.artifact.JavaScopes; + import io.quarkus.bootstrap.model.ApplicationModel; import io.quarkus.bootstrap.resolver.TsArtifact; import io.quarkus.bootstrap.resolver.TsQuarkusExt; import io.quarkus.deployment.runnerjar.BootstrapFromOriginalJarTestBase; +import io.quarkus.maven.dependency.ArtifactCoords; import io.quarkus.maven.dependency.ArtifactDependency; import io.quarkus.maven.dependency.Dependency; import io.quarkus.maven.dependency.DependencyFlags; -import io.quarkus.maven.dependency.GACTV; public class ConditionalDependencyWithTwoConditionsTest extends BootstrapFromOriginalJarTestBase { @@ -46,16 +48,20 @@ protected TsArtifact composeApplication() { protected void assertAppModel(ApplicationModel model) throws Exception { final Set expected = new HashSet<>(); expected.add(new ArtifactDependency( - new GACTV(TsArtifact.DEFAULT_GROUP_ID, "ext-c-deployment", TsArtifact.DEFAULT_VERSION), "compile", + ArtifactCoords.jar(TsArtifact.DEFAULT_GROUP_ID, "ext-c-deployment", TsArtifact.DEFAULT_VERSION), + JavaScopes.COMPILE, DependencyFlags.DEPLOYMENT_CP)); expected.add(new ArtifactDependency( - new GACTV(TsArtifact.DEFAULT_GROUP_ID, "ext-a-deployment", TsArtifact.DEFAULT_VERSION), "compile", + ArtifactCoords.jar(TsArtifact.DEFAULT_GROUP_ID, "ext-a-deployment", TsArtifact.DEFAULT_VERSION), + JavaScopes.COMPILE, DependencyFlags.DEPLOYMENT_CP)); expected.add(new ArtifactDependency( - new GACTV(TsArtifact.DEFAULT_GROUP_ID, "ext-b-deployment", TsArtifact.DEFAULT_VERSION), "runtime", + ArtifactCoords.jar(TsArtifact.DEFAULT_GROUP_ID, "ext-b-deployment", TsArtifact.DEFAULT_VERSION), + JavaScopes.COMPILE, DependencyFlags.DEPLOYMENT_CP)); expected.add(new ArtifactDependency( - new GACTV(TsArtifact.DEFAULT_GROUP_ID, "ext-d-deployment", TsArtifact.DEFAULT_VERSION), "compile", + ArtifactCoords.jar(TsArtifact.DEFAULT_GROUP_ID, "ext-d-deployment", TsArtifact.DEFAULT_VERSION), + JavaScopes.COMPILE, DependencyFlags.DEPLOYMENT_CP)); assertEquals(expected, getDeploymentOnlyDeps(model)); } diff --git a/core/deployment/src/test/java/io/quarkus/deployment/pkg/steps/JarResultBuildStepTest.java b/core/deployment/src/test/java/io/quarkus/deployment/pkg/steps/JarResultBuildStepTest.java new file mode 100644 index 0000000000000..7cfb2c4ece496 --- /dev/null +++ b/core/deployment/src/test/java/io/quarkus/deployment/pkg/steps/JarResultBuildStepTest.java @@ -0,0 +1,34 @@ +package io.quarkus.deployment.pkg.steps; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.nio.file.Path; +import java.util.Set; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.jar.Manifest; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +/** + * Test for {@link JarResultBuildStep} + */ +class JarResultBuildStepTest { + + @Test + void should_unsign_jar_when_filtered(@TempDir Path tempDir) throws Exception { + Path signedJarFilePath = Path.of(getClass().getClassLoader().getResource("signed.jar").toURI()); + Path jarFilePath = tempDir.resolve("unsigned.jar"); + JarResultBuildStep.filterJarFile(signedJarFilePath, jarFilePath, + Set.of("org/eclipse/jgit/transport/sshd/SshdSessionFactory.class")); + try (JarFile jarFile = new JarFile(jarFilePath.toFile())) { + assertThat(jarFile.stream().map(JarEntry::getName)).doesNotContain("META-INF/ECLIPSE_.RSA", "META-INF/ECLIPSE_.SF"); + // Check that the manifest is still present + Manifest manifest = jarFile.getManifest(); + assertThat(manifest.getMainAttributes()).isNotEmpty(); + assertThat(manifest.getEntries()).isEmpty(); + } + } + +} diff --git a/core/deployment/src/test/java/io/quarkus/deployment/runnerjar/BootstrapFromOriginalJarTestBase.java b/core/deployment/src/test/java/io/quarkus/deployment/runnerjar/BootstrapFromOriginalJarTestBase.java index 0b7a314795338..d9886d3bdd8e0 100644 --- a/core/deployment/src/test/java/io/quarkus/deployment/runnerjar/BootstrapFromOriginalJarTestBase.java +++ b/core/deployment/src/test/java/io/quarkus/deployment/runnerjar/BootstrapFromOriginalJarTestBase.java @@ -75,24 +75,27 @@ protected QuarkusBootstrap.Builder initBootstrapBuilder() throws Exception { .setAppModelResolver(resolver) .setTest(isBootstrapForTestMode()); - if (createWorkspace()) { + if (createWorkspace() || !wsModules.isEmpty()) { System.setProperty("basedir", ws.toAbsolutePath().toString()); final Model appPom = appJar.getPomModel(); - final List bomModules = (appPom.getDependencyManagement() == null ? List. of() - : appPom.getDependencyManagement().getDependencies()).stream() - .filter(d -> "import".equals(d.getScope()) - && d.getGroupId().equals(appPom.getGroupId())) - .collect(Collectors.toList()); - - final List depModules = appPom.getDependencies().stream() - .filter(d -> d.getGroupId().equals(appPom.getGroupId()) && - (d.getType().isEmpty() || ArtifactCoords.TYPE_JAR.equals(d.getType()))) - .collect(Collectors.toList()); + List bomModules = List.of(); + List depModules = List.of(); + if (createWorkspace()) { + bomModules = (appPom.getDependencyManagement() == null ? List. of() + : appPom.getDependencyManagement().getDependencies()).stream() + .filter(d -> "import".equals(d.getScope()) + && d.getGroupId().equals(appPom.getGroupId())) + .collect(Collectors.toList()); + depModules = appPom.getDependencies().stream() + .filter(d -> d.getGroupId().equals(appPom.getGroupId()) && + (d.getType().isEmpty() || ArtifactCoords.TYPE_JAR.equals(d.getType()))) + .collect(Collectors.toList()); + } final Path appModule; final Path appPomXml; - if (depModules.isEmpty() && bomModules.isEmpty() || appPom.getParent() != null) { + if (depModules.isEmpty() && bomModules.isEmpty() && wsModules.isEmpty() || appPom.getParent() != null) { appModule = ws; appPomXml = ws.resolve("pom.xml"); ModelUtils.persistModel(appPomXml, appPom); @@ -130,31 +133,32 @@ protected QuarkusBootstrap.Builder initBootstrapBuilder() throws Exception { ModelUtils.persistModel(appPomXml, appPom); // dependency modules - final Map managedVersions = new HashMap<>(); - collectManagedDeps(appPom, managedVersions); - for (Dependency moduleDep : depModules) { - parentPom.getModules().add(moduleDep.getArtifactId()); - final String moduleVersion = moduleDep.getVersion() == null - ? managedVersions.get(ArtifactKey.of(moduleDep.getGroupId(), moduleDep.getArtifactId(), - moduleDep.getClassifier(), moduleDep.getType())) - : moduleDep.getVersion(); - Model modulePom = ModelUtils.readModel(resolver - .resolve(ArtifactCoords.pom(moduleDep.getGroupId(), moduleDep.getArtifactId(), moduleVersion)) - .getResolvedPaths().getSinglePath()); - modulePom.setParent(parent); - final Path moduleDir = IoUtils.mkdirs(ws.resolve(modulePom.getArtifactId())); - ModelUtils.persistModel(moduleDir.resolve("pom.xml"), modulePom); - final Path resolvedJar = resolver - .resolve(ArtifactCoords.of(modulePom.getGroupId(), modulePom.getArtifactId(), - moduleDep.getClassifier(), moduleDep.getType(), modulePom.getVersion())) - .getResolvedPaths() - .getSinglePath(); - final Path moduleTargetDir = moduleDir.resolve("target"); - ZipUtils.unzip(resolvedJar, moduleTargetDir.resolve("classes")); - IoUtils.copy(resolvedJar, - moduleTargetDir.resolve(modulePom.getArtifactId() + "-" + modulePom.getVersion() + ".jar")); + if (!depModules.isEmpty()) { + final Map managedVersions = new HashMap<>(); + collectManagedDeps(appPom, managedVersions); + for (Dependency moduleDep : depModules) { + parentPom.getModules().add(moduleDep.getArtifactId()); + final String moduleVersion = moduleDep.getVersion() == null + ? managedVersions.get(ArtifactKey.of(moduleDep.getGroupId(), moduleDep.getArtifactId(), + moduleDep.getClassifier(), moduleDep.getType())) + : moduleDep.getVersion(); + Model modulePom = ModelUtils.readModel(resolver + .resolve(ArtifactCoords.pom(moduleDep.getGroupId(), moduleDep.getArtifactId(), moduleVersion)) + .getResolvedPaths().getSinglePath()); + modulePom.setParent(parent); + final Path moduleDir = IoUtils.mkdirs(ws.resolve(modulePom.getArtifactId())); + ModelUtils.persistModel(moduleDir.resolve("pom.xml"), modulePom); + final Path resolvedJar = resolver + .resolve(ArtifactCoords.of(modulePom.getGroupId(), modulePom.getArtifactId(), + moduleDep.getClassifier(), moduleDep.getType(), modulePom.getVersion())) + .getResolvedPaths() + .getSinglePath(); + final Path moduleTargetDir = moduleDir.resolve("target"); + ZipUtils.unzip(resolvedJar, moduleTargetDir.resolve("classes")); + IoUtils.copy(resolvedJar, + moduleTargetDir.resolve(modulePom.getArtifactId() + "-" + modulePom.getVersion() + ".jar")); + } } - for (TsArtifact module : wsModules) { parentPom.getModules().add(module.getArtifactId()); Model modulePom = module.getPomModel(); diff --git a/core/deployment/src/test/java/io/quarkus/deployment/runnerjar/DeploymentDependencyConvergenceTest.java b/core/deployment/src/test/java/io/quarkus/deployment/runnerjar/DeploymentDependencyConvergenceTest.java new file mode 100644 index 0000000000000..77d6c52bee6c1 --- /dev/null +++ b/core/deployment/src/test/java/io/quarkus/deployment/runnerjar/DeploymentDependencyConvergenceTest.java @@ -0,0 +1,68 @@ +package io.quarkus.deployment.runnerjar; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Set; + +import org.eclipse.aether.util.artifact.JavaScopes; + +import io.quarkus.bootstrap.model.ApplicationModel; +import io.quarkus.bootstrap.resolver.TsArtifact; +import io.quarkus.bootstrap.resolver.TsQuarkusExt; +import io.quarkus.maven.dependency.*; + +public class DeploymentDependencyConvergenceTest extends BootstrapFromOriginalJarTestBase { + + @Override + protected TsArtifact composeApplication() { + + var libE10 = install(TsArtifact.jar("lib-e", "1.0")); + var libE20 = install(TsArtifact.jar("lib-e", "2.0")); + var libE30 = install(TsArtifact.jar("lib-e", "3.0")); + + var libD10 = install(TsArtifact.jar("lib-d", "1.0")); + var libD20 = install(TsArtifact.jar("lib-d", "2.0")); + + var libC10 = install(TsArtifact.jar("lib-c", "1.0") + .addDependency(libD10) + .addDependency(libE10)); + + var libB10 = install(TsArtifact.jar("lib-b", "1.0")); + var libB20 = install(TsArtifact.jar("lib-b", "2.0") + .addDependency(libC10)); + + var extA = new TsQuarkusExt("ext-a"); + addToExpectedLib(extA.getRuntime()); + extA.getDeployment() + .addManagedDependency(libD20) + .addManagedDependency(libE20) + .addDependency(libB10); + + return TsArtifact.jar("app") + .addManagedDependency(platformDescriptor()) + .addManagedDependency(platformProperties()) + .addManagedDependency(libB20) + .addManagedDependency(libE30) + .addDependency(extA); + } + + @Override + protected void assertAppModel(ApplicationModel model) throws Exception { + final Set expected = Set.of( + new ArtifactDependency(ArtifactCoords.jar("io.quarkus.bootstrap.test", "ext-a", "1"), JavaScopes.COMPILE, + DependencyFlags.RUNTIME_CP, DependencyFlags.DEPLOYMENT_CP, DependencyFlags.DIRECT, + DependencyFlags.RUNTIME_EXTENSION_ARTIFACT, DependencyFlags.TOP_LEVEL_RUNTIME_EXTENSION_ARTIFACT), + new ArtifactDependency(ArtifactCoords.jar("io.quarkus.bootstrap.test", "ext-a-deployment", "1"), + JavaScopes.COMPILE, + DependencyFlags.DEPLOYMENT_CP), + new ArtifactDependency(ArtifactCoords.jar("io.quarkus.bootstrap.test", "lib-b", "2.0"), JavaScopes.COMPILE, + DependencyFlags.DEPLOYMENT_CP), + new ArtifactDependency(ArtifactCoords.jar("io.quarkus.bootstrap.test", "lib-c", "1.0"), JavaScopes.COMPILE, + DependencyFlags.DEPLOYMENT_CP), + new ArtifactDependency(ArtifactCoords.jar("io.quarkus.bootstrap.test", "lib-d", "2.0"), JavaScopes.COMPILE, + DependencyFlags.DEPLOYMENT_CP), + new ArtifactDependency(ArtifactCoords.jar("io.quarkus.bootstrap.test", "lib-e", "3.0"), JavaScopes.COMPILE, + DependencyFlags.DEPLOYMENT_CP)); + assertEquals(expected, getDependenciesWithFlag(model, DependencyFlags.DEPLOYMENT_CP)); + } +} diff --git a/core/deployment/src/test/java/io/quarkus/deployment/runnerjar/ReloadableFlagsTest.java b/core/deployment/src/test/java/io/quarkus/deployment/runnerjar/ReloadableFlagsTest.java new file mode 100644 index 0000000000000..5614d62716483 --- /dev/null +++ b/core/deployment/src/test/java/io/quarkus/deployment/runnerjar/ReloadableFlagsTest.java @@ -0,0 +1,71 @@ +package io.quarkus.deployment.runnerjar; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Set; +import java.util.stream.Collectors; + +import org.eclipse.aether.util.artifact.JavaScopes; + +import io.quarkus.bootstrap.model.ApplicationModel; +import io.quarkus.bootstrap.resolver.TsArtifact; +import io.quarkus.bootstrap.resolver.TsQuarkusExt; +import io.quarkus.bootstrap.workspace.WorkspaceModule; +import io.quarkus.bootstrap.workspace.WorkspaceModuleId; +import io.quarkus.maven.dependency.ArtifactCoords; +import io.quarkus.maven.dependency.ArtifactDependency; +import io.quarkus.maven.dependency.Dependency; +import io.quarkus.maven.dependency.DependencyFlags; + +public class ReloadableFlagsTest extends BootstrapFromOriginalJarTestBase { + + @Override + protected TsArtifact composeApplication() { + + var transitive = TsArtifact.jar("acme-transitive"); + addWorkspaceModule(transitive); + addToExpectedLib(transitive); + + var common = TsArtifact.jar("acme-common"); + common.addDependency(transitive); + addWorkspaceModule(common); + addToExpectedLib(common); + + var lib = TsArtifact.jar("acme-lib"); + lib.addDependency(common); + addWorkspaceModule(lib); + addToExpectedLib(lib); + + var externalLib = TsArtifact.jar("external-lib"); + externalLib.addDependency(common); + addToExpectedLib(externalLib); + + var myExt = new TsQuarkusExt("my-ext"); + addToExpectedLib(myExt.getRuntime()); + + return TsArtifact.jar("app") + .addManagedDependency(platformDescriptor()) + .addManagedDependency(platformProperties()) + .addDependency(common) + .addDependency(lib) + .addDependency(externalLib) + .addDependency(myExt); + } + + @Override + protected void assertAppModel(ApplicationModel model) { + assertThat(model.getWorkspaceModules().stream().map(WorkspaceModule::getId).collect(Collectors.toSet())) + .isEqualTo(Set.of( + WorkspaceModuleId.of(TsArtifact.DEFAULT_GROUP_ID, "acme-transitive", TsArtifact.DEFAULT_VERSION), + WorkspaceModuleId.of(TsArtifact.DEFAULT_GROUP_ID, "acme-common", TsArtifact.DEFAULT_VERSION), + WorkspaceModuleId.of(TsArtifact.DEFAULT_GROUP_ID, "acme-lib", TsArtifact.DEFAULT_VERSION), + WorkspaceModuleId.of(TsArtifact.DEFAULT_GROUP_ID, "app", TsArtifact.DEFAULT_VERSION))); + + final Set expected = Set.of( + new ArtifactDependency(ArtifactCoords.jar("io.quarkus.bootstrap.test", "acme-lib", "1"), JavaScopes.COMPILE, + DependencyFlags.RUNTIME_CP, DependencyFlags.DEPLOYMENT_CP, DependencyFlags.DIRECT, + DependencyFlags.RELOADABLE, DependencyFlags.WORKSPACE_MODULE)); + + assertThat(getDependenciesWithFlag(model, DependencyFlags.RELOADABLE)).isEqualTo(expected); + } +} diff --git a/core/processor/pom.xml b/core/processor/pom.xml index 8427a552eb841..aee2ce4090171 100644 --- a/core/processor/pom.xml +++ b/core/processor/pom.xml @@ -59,6 +59,17 @@ jboss-logmanager test + + com.karuslabs + elementary + 2.0.1 + test + + + io.quarkus + quarkus-builder + test + diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/ExtensionAnnotationProcessor.java b/core/processor/src/main/java/io/quarkus/annotation/processor/ExtensionAnnotationProcessor.java index 08a043382dbe5..3d978555bbecc 100644 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/ExtensionAnnotationProcessor.java +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/ExtensionAnnotationProcessor.java @@ -130,7 +130,8 @@ public Iterable getCompletions(Element element, Annotation public void doProcess(Set annotations, RoundEnvironment roundEnv) { for (TypeElement annotation : annotations) { - switch (annotation.getQualifiedName().toString()) { + switch (annotation.getQualifiedName() + .toString()) { case Constants.ANNOTATION_BUILD_STEP: trackAnnotationUsed(Constants.ANNOTATION_BUILD_STEP); processBuildStep(roundEnv, annotation); @@ -162,16 +163,19 @@ void doFinish() { try { tempResource = filer.createResource(StandardLocation.SOURCE_OUTPUT, Constants.EMPTY, "ignore.tmp"); } catch (IOException e) { - processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Unable to create temp output file: " + e); + processingEnv.getMessager() + .printMessage(Diagnostic.Kind.ERROR, "Unable to create temp output file: " + e); return; } final URI uri = tempResource.toUri(); // tempResource.delete(); Path path; try { - path = Paths.get(uri).getParent(); + path = Paths.get(uri) + .getParent(); } catch (RuntimeException e) { - processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Resource path URI is invalid: " + uri); + processingEnv.getMessager() + .printMessage(Diagnostic.Kind.ERROR, "Resource path URI is invalid: " + uri); return; } Collection bscListClasses = new TreeSet<>(); @@ -179,13 +183,14 @@ void doFinish() { Properties javaDocProperties = new Properties(); try { - Files.walkFileTree(path, new FileVisitor() { + Files.walkFileTree(path, new FileVisitor<>() { public FileVisitResult preVisitDirectory(final Path dir, final BasicFileAttributes attrs) { return FileVisitResult.CONTINUE; } public FileVisitResult visitFile(final Path file, final BasicFileAttributes attrs) { - final String nameStr = file.getFileName().toString(); + final String nameStr = file.getFileName() + .toString(); if (nameStr.endsWith(".bsc")) { readFile(file, bscListClasses); } else if (nameStr.endsWith(".cr")) { @@ -195,8 +200,9 @@ public FileVisitResult visitFile(final Path file, final BasicFileAttributes attr try (BufferedReader br = Files.newBufferedReader(file, StandardCharsets.UTF_8)) { p.load(br); } catch (IOException e) { - processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, - "Failed to read file " + file + ": " + e); + processingEnv.getMessager() + .printMessage(Diagnostic.Kind.ERROR, + "Failed to read file " + file + ": " + e); } final Set names = p.stringPropertyNames(); for (String name : names) { @@ -208,8 +214,9 @@ public FileVisitResult visitFile(final Path file, final BasicFileAttributes attr } public FileVisitResult visitFileFailed(final Path file, final IOException exc) { - processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, - "Failed to visit file " + file + ": " + exc); + processingEnv.getMessager() + .printMessage(Diagnostic.Kind.ERROR, + "Failed to visit file " + file + ": " + exc); return FileVisitResult.CONTINUE; } @@ -218,7 +225,8 @@ public FileVisitResult postVisitDirectory(final Path dir, final IOException exc) } }); } catch (IOException e) { - processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "File walk failed: " + e); + processingEnv.getMessager() + .printMessage(Diagnostic.Kind.ERROR, "File walk failed: " + e); } if (!bscListClasses.isEmpty()) try { @@ -226,7 +234,8 @@ public FileVisitResult postVisitDirectory(final Path dir, final IOException exc) "META-INF/quarkus-build-steps.list"); writeListResourceFile(bscListClasses, listResource); } catch (IOException e) { - processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Failed to write build steps listing: " + e); + processingEnv.getMessager() + .printMessage(Diagnostic.Kind.ERROR, "Failed to write build steps listing: " + e); return; } if (!crListClasses.isEmpty()) { @@ -235,7 +244,8 @@ public FileVisitResult postVisitDirectory(final Path dir, final IOException exc) "META-INF/quarkus-config-roots.list"); writeListResourceFile(crListClasses, listResource); } catch (IOException e) { - processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Failed to write config roots listing: " + e); + processingEnv.getMessager() + .printMessage(Diagnostic.Kind.ERROR, "Failed to write config roots listing: " + e); return; } } @@ -254,7 +264,8 @@ public FileVisitResult postVisitDirectory(final Path dir, final IOException exc) } } } catch (IOException e) { - processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Failed to write javadoc properties: " + e); + processingEnv.getMessager() + .printMessage(Diagnostic.Kind.ERROR, "Failed to write javadoc properties: " + e); return; } } @@ -269,16 +280,18 @@ public FileVisitResult postVisitDirectory(final Path dir, final IOException exc) } } } catch (IOException e) { - processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Failed to generate extension doc: " + e); - return; - + processingEnv.getMessager() + .printMessage(Diagnostic.Kind.ERROR, "Failed to generate extension doc: " + e); } } private void validateAnnotationUsage() { if (isAnnotationUsed(Constants.ANNOTATION_BUILD_STEP) && isAnnotationUsed(Constants.ANNOTATION_RECORDER)) { - processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, - "Detected use of @Recorder annotation in 'deployment' module. Classes annotated with @Recorder must be part of the extension's 'runtime' module"); + processingEnv.getMessager() + .printMessage(Diagnostic.Kind.ERROR, + "Detected use of @Recorder annotation in 'deployment' module. Classes annotated with @Recorder must be " + + + "part of the extension's 'runtime' module"); } } @@ -315,8 +328,9 @@ private void readFile(Path file, Collection bscListClasses) { } } } catch (IOException e) { - processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, - "Failed to read file " + file + ": " + e); + processingEnv.getMessager() + .printMessage(Diagnostic.Kind.ERROR, + "Failed to read file " + file + ": " + e); } } @@ -329,29 +343,35 @@ private void processBuildStep(RoundEnvironment roundEnv, TypeElement annotation) continue; } - final PackageElement pkg = processingEnv.getElementUtils().getPackageOf(clazz); + final PackageElement pkg = processingEnv.getElementUtils() + .getPackageOf(clazz); if (pkg == null) { - processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, - "Element " + clazz + " has no enclosing package"); + processingEnv.getMessager() + .printMessage(Diagnostic.Kind.ERROR, + "Element " + clazz + " has no enclosing package"); continue; } - final String binaryName = processingEnv.getElementUtils().getBinaryName(clazz).toString(); + final String binaryName = processingEnv.getElementUtils() + .getBinaryName(clazz) + .toString(); if (processorClassNames.add(binaryName)) { validateRecordBuildSteps(clazz); recordConfigJavadoc(clazz); generateAccessor(clazz); final StringBuilder rbn = getRelativeBinaryName(clazz, new StringBuilder()); try { - final FileObject itemResource = processingEnv.getFiler().createResource( - StandardLocation.SOURCE_OUTPUT, - pkg.getQualifiedName().toString(), - rbn.toString() + ".bsc", - clazz); + final FileObject itemResource = processingEnv.getFiler() + .createResource( + StandardLocation.SOURCE_OUTPUT, + pkg.getQualifiedName() + .toString(), + rbn + ".bsc", clazz); writeResourceFile(binaryName, itemResource); } catch (IOException e1) { - processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, - "Failed to create " + rbn + " in " + pkg + ": " + e1, clazz); + processingEnv.getMessager() + .printMessage(Diagnostic.Kind.ERROR, + "Failed to create " + rbn + " in " + pkg + ": " + e1, clazz); } } } @@ -373,21 +393,28 @@ private void validateRecordBuildSteps(TypeElement clazz) { boolean hasRecorder = false; boolean allTypesResolvable = true; for (VariableElement parameter : ex.getParameters()) { - String parameterClassName = parameter.asType().toString(); - TypeElement parameterTypeElement = processingEnv.getElementUtils().getTypeElement(parameterClassName); + String parameterClassName = parameter.asType() + .toString(); + TypeElement parameterTypeElement = processingEnv.getElementUtils() + .getTypeElement(parameterClassName); if (parameterTypeElement == null) { allTypesResolvable = false; } else { if (isAnnotationPresent(parameterTypeElement, Constants.ANNOTATION_RECORDER)) { - if (parameterTypeElement.getModifiers().contains(Modifier.FINAL)) { - processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, - "Class '" + parameterTypeElement.getQualifiedName() - + "' is annotated with @Recorder and therefore cannot be made as a final class."); + if (parameterTypeElement.getModifiers() + .contains(Modifier.FINAL)) { + processingEnv.getMessager() + .printMessage(Diagnostic.Kind.ERROR, + "Class '" + parameterTypeElement.getQualifiedName() + + "' is annotated with @Recorder and therefore cannot be made as a final class."); } else if (getPackageName(clazz).equals(getPackageName(parameterTypeElement))) { - processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING, - "Build step class '" + clazz.getQualifiedName() - + "' and recorder '" + parameterTypeElement - + "' share the same package. This is highly discouraged as it can lead to unexpected results."); + processingEnv.getMessager() + .printMessage(Diagnostic.Kind.WARNING, + "Build step class '" + clazz.getQualifiedName() + + "' and recorder '" + parameterTypeElement + + "' share the same package. This is highly discouraged as it can lead to " + + + "unexpected results."); } hasRecorder = true; break; @@ -396,15 +423,20 @@ private void validateRecordBuildSteps(TypeElement clazz) { } if (!hasRecorder && allTypesResolvable) { - processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Build Step '" + clazz.getQualifiedName() + "#" - + ex.getSimpleName() - + "' which is annotated with '@Record' does not contain a method parameter whose type is annotated with '@Recorder'."); + processingEnv.getMessager() + .printMessage(Diagnostic.Kind.ERROR, "Build Step '" + clazz.getQualifiedName() + "#" + + ex.getSimpleName() + + "' which is annotated with '@Record' does not contain a method parameter whose type is annotated " + + + "with '@Recorder'."); } } } private Name getPackageName(TypeElement clazz) { - return processingEnv.getElementUtils().getPackageOf(clazz).getQualifiedName(); + return processingEnv.getElementUtils() + .getPackageOf(clazz) + .getQualifiedName(); } private StringBuilder getRelativeBinaryName(TypeElement te, StringBuilder b) { @@ -421,7 +453,8 @@ private TypeElement getClassOf(Element e) { Element t = e; while (!(t instanceof TypeElement)) { if (t == null) { - processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Element " + e + " has no enclosing class"); + processingEnv.getMessager() + .printMessage(Diagnostic.Kind.ERROR, "Element " + e + " has no enclosing class"); return null; } t = t.getEnclosingElement(); @@ -430,7 +463,8 @@ private TypeElement getClassOf(Element e) { } private void recordConfigJavadoc(TypeElement clazz) { - String className = clazz.getQualifiedName().toString(); + String className = clazz.getQualifiedName() + .toString(); if (!generatedJavaDocs.add(className)) return; Properties javadocProps = new Properties(); @@ -470,7 +504,8 @@ private void recordConfigJavadoc(TypeElement clazz) { } private void recordMappingJavadoc(TypeElement clazz) { - String className = clazz.getQualifiedName().toString(); + String className = clazz.getQualifiedName() + .toString(); if (!generatedJavaDocs.add(className)) return; if (!isAnnotationPresent(clazz, ANNOTATION_CONFIG_MAPPING)) { @@ -484,7 +519,8 @@ private void recordMappingJavadoc(TypeElement clazz) { } private void recordMappingJavadoc(final TypeElement clazz, final Properties javadocProps) { - String className = clazz.getQualifiedName().toString(); + String className = clazz.getQualifiedName() + .toString(); for (Element e : clazz.getEnclosedElements()) { switch (e.getKind()) { case INTERFACE: { @@ -504,9 +540,11 @@ private void recordMappingJavadoc(final TypeElement clazz, final Properties java } private boolean isEnclosedByMapping(Element clazz) { - if (clazz.getKind().equals(ElementKind.INTERFACE)) { + if (clazz.getKind() + .equals(ElementKind.INTERFACE)) { Element enclosingElement = clazz.getEnclosingElement(); - if (enclosingElement.getKind().equals(ElementKind.INTERFACE)) { + if (enclosingElement.getKind() + .equals(ElementKind.INTERFACE)) { if (isAnnotationPresent(enclosingElement, ANNOTATION_CONFIG_MAPPING)) { return true; } else { @@ -520,30 +558,37 @@ private boolean isEnclosedByMapping(Element clazz) { private void writeJavadocProperties(final TypeElement clazz, final Properties javadocProps) { if (javadocProps.isEmpty()) return; - final PackageElement pkg = processingEnv.getElementUtils().getPackageOf(clazz); - final String rbn = getRelativeBinaryName(clazz, new StringBuilder()).append(".jdp").toString(); + final PackageElement pkg = processingEnv.getElementUtils() + .getPackageOf(clazz); + final String rbn = getRelativeBinaryName(clazz, new StringBuilder()).append(".jdp") + .toString(); try { - FileObject file = processingEnv.getFiler().createResource( - StandardLocation.SOURCE_OUTPUT, - pkg.getQualifiedName().toString(), - rbn, - clazz); + FileObject file = processingEnv.getFiler() + .createResource( + StandardLocation.SOURCE_OUTPUT, + pkg.getQualifiedName() + .toString(), + rbn, + clazz); try (Writer writer = file.openWriter()) { PropertyUtils.store(javadocProps, writer); } } catch (IOException e) { - processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Failed to persist resource " + rbn + ": " + e); + processingEnv.getMessager() + .printMessage(Diagnostic.Kind.ERROR, "Failed to persist resource " + rbn + ": " + e); } } private void processFieldConfigItem(VariableElement field, Properties javadocProps, String className) { - javadocProps.put(className + Constants.DOT + field.getSimpleName().toString(), getRequiredJavadoc(field)); + javadocProps.put(className + Constants.DOT + field.getSimpleName() + .toString(), getRequiredJavadoc(field)); } private void processEnumConstant(Element field, Properties javadocProps, String className) { String javaDoc = getJavadoc(field); if (javaDoc != null && !javaDoc.isBlank()) { - javadocProps.put(className + Constants.DOT + field.getSimpleName().toString(), javaDoc); + javadocProps.put(className + Constants.DOT + field.getSimpleName() + .toString(), javaDoc); } } @@ -557,20 +602,26 @@ private void processCtorConfigItem(ExecutableElement ctor, Properties javadocPro private void processMethodConfigItem(ExecutableElement method, Properties javadocProps, String className) { final String docComment = getRequiredJavadoc(method); final StringBuilder buf = new StringBuilder(); - buf.append(method.getSimpleName().toString()); + buf.append(method.getSimpleName() + .toString()); appendParamTypes(method, buf); javadocProps.put(className + Constants.DOT + buf, docComment); } private void processMethodConfigMapping(ExecutableElement method, Properties javadocProps, String className) { - if (method.getModifiers().contains(Modifier.ABSTRACT)) { + if (method.getModifiers() + .contains(Modifier.ABSTRACT)) { // Skip toString method, because mappings can include it and generate it - if (method.getSimpleName().contentEquals("toString") && method.getParameters().size() == 0) { + if (method.getSimpleName() + .contentEquals("toString") + && method.getParameters() + .isEmpty()) { return; } String docComment = getRequiredJavadoc(method); - javadocProps.put(className + Constants.DOT + method.getSimpleName().toString(), docComment); + javadocProps.put(className + Constants.DOT + method.getSimpleName() + .toString(), docComment); // Find groups without annotation TypeMirror returnType = method.getReturnType(); @@ -593,9 +644,10 @@ private TypeElement unwrapConfigGroup(TypeMirror typeMirror) { } DeclaredType declaredType = (DeclaredType) typeMirror; - String name = declaredType.asElement().toString(); + String name = declaredType.asElement() + .toString(); List typeArguments = declaredType.getTypeArguments(); - if (typeArguments.size() == 0) { + if (typeArguments.isEmpty()) { if (!name.startsWith("java.")) { return (TypeElement) declaredType.asElement(); } @@ -616,9 +668,11 @@ private TypeElement unwrapConfigGroup(TypeMirror typeMirror) { private void processConfigGroup(RoundEnvironment roundEnv, TypeElement annotation) { final Set groupClassNames = new HashSet<>(); for (TypeElement i : typesIn(roundEnv.getElementsAnnotatedWith(annotation))) { - if (groupClassNames.add(i.getQualifiedName().toString())) { + if (groupClassNames.add(i.getQualifiedName() + .toString())) { generateAccessor(i); - if (isEnclosedByMapping(i) || i.getKind().equals(ElementKind.INTERFACE)) { + if (isEnclosedByMapping(i) || i.getKind() + .equals(ElementKind.INTERFACE)) { recordMappingJavadoc(i); } else { recordConfigJavadoc(i); @@ -634,10 +688,12 @@ private void processConfigRoot(RoundEnvironment roundEnv, TypeElement annotation final Set rootClassNames = new HashSet<>(); for (TypeElement clazz : typesIn(roundEnv.getElementsAnnotatedWith(annotation))) { - final PackageElement pkg = processingEnv.getElementUtils().getPackageOf(clazz); + final PackageElement pkg = processingEnv.getElementUtils() + .getPackageOf(clazz); if (pkg == null) { - processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, - "Element " + clazz + " has no enclosing package"); + processingEnv.getMessager() + .printMessage(Diagnostic.Kind.ERROR, + "Element " + clazz + " has no enclosing package"); continue; } @@ -645,7 +701,9 @@ private void processConfigRoot(RoundEnvironment roundEnv, TypeElement annotation configDocItemScanner.addConfigRoot(pkg, clazz); } - final String binaryName = processingEnv.getElementUtils().getBinaryName(clazz).toString(); + final String binaryName = processingEnv.getElementUtils() + .getBinaryName(clazz) + .toString(); if (rootClassNames.add(binaryName)) { // new class if (isAnnotationPresent(clazz, ANNOTATION_CONFIG_MAPPING)) { @@ -656,15 +714,18 @@ private void processConfigRoot(RoundEnvironment roundEnv, TypeElement annotation } final StringBuilder rbn = getRelativeBinaryName(clazz, new StringBuilder()); try { - final FileObject itemResource = processingEnv.getFiler().createResource( - StandardLocation.SOURCE_OUTPUT, - pkg.getQualifiedName().toString(), - rbn + ".cr", - clazz); + final FileObject itemResource = processingEnv.getFiler() + .createResource( + StandardLocation.SOURCE_OUTPUT, + pkg.getQualifiedName() + .toString(), + rbn + ".cr", + clazz); writeResourceFile(binaryName, itemResource); } catch (IOException e1) { - processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, - "Failed to create " + rbn + " in " + pkg + ": " + e1, clazz); + processingEnv.getMessager() + .printMessage(Diagnostic.Kind.ERROR, + "Failed to create " + rbn + " in " + pkg + ": " + e1, clazz); } } } @@ -686,7 +747,8 @@ private void writeResourceFile(String binaryName, FileObject itemResource) throw private void processRecorder(RoundEnvironment roundEnv, TypeElement annotation) { final Set groupClassNames = new HashSet<>(); for (TypeElement i : typesIn(roundEnv.getElementsAnnotatedWith(annotation))) { - if (groupClassNames.add(i.getQualifiedName().toString())) { + if (groupClassNames.add(i.getQualifiedName() + .toString())) { generateAccessor(i); recordConfigJavadoc(i); } @@ -694,28 +756,35 @@ private void processRecorder(RoundEnvironment roundEnv, TypeElement annotation) } private void generateAccessor(final TypeElement clazz) { - if (!generatedAccessors.add(clazz.getQualifiedName().toString())) + if (!generatedAccessors.add(clazz.getQualifiedName() + .toString())) return; final FormatPreferences fp = new FormatPreferences(); final JSources sources = JDeparser.createSources(JFiler.newInstance(processingEnv.getFiler()), fp); - final PackageElement packageElement = processingEnv.getElementUtils().getPackageOf(clazz); - final String className = getRelativeBinaryName(clazz, new StringBuilder()).append("$$accessor").toString(); - final JSourceFile sourceFile = sources.createSourceFile(packageElement.getQualifiedName().toString(), className); + final PackageElement packageElement = processingEnv.getElementUtils() + .getPackageOf(clazz); + final String className = getRelativeBinaryName(clazz, new StringBuilder()).append("$$accessor") + .toString(); + final JSourceFile sourceFile = sources.createSourceFile(packageElement.getQualifiedName() + .toString(), className); JType clazzType = JTypes.typeOf(clazz.asType()); if (clazz.asType() instanceof DeclaredType) { DeclaredType declaredType = ((DeclaredType) clazz.asType()); TypeMirror enclosingType = declaredType.getEnclosingType(); if (enclosingType != null && enclosingType.getKind() == TypeKind.DECLARED - && clazz.getModifiers().contains(Modifier.STATIC)) { + && clazz.getModifiers() + .contains(Modifier.STATIC)) { // Ugly workaround for Eclipse APT and static nested types clazzType = unnestStaticNestedType(declaredType); } } final JClassDef classDef = sourceFile._class(JMod.PUBLIC | JMod.FINAL, className); classDef.constructor(JMod.PRIVATE); // no construction - classDef.annotate(QUARKUS_GENERATED).value("Quarkus annotation processor"); + classDef.annotate(QUARKUS_GENERATED) + .value("Quarkus annotation processor"); final JAssignableExpr instanceName = JExprs.name(Constants.INSTANCE_SYM); - boolean isEnclosingClassPublic = clazz.getModifiers().contains(Modifier.PUBLIC); + boolean isEnclosingClassPublic = clazz.getModifiers() + .contains(Modifier.PUBLIC); // iterate fields boolean generationNeeded = false; for (VariableElement field : fieldsIn(clazz.getEnclosedElements())) { @@ -733,7 +802,8 @@ private void generateAccessor(final TypeElement clazz) { if (fieldType instanceof DeclaredType) { final DeclaredType declaredType = (DeclaredType) fieldType; final TypeElement typeElement = (TypeElement) declaredType.asElement(); - if (typeElement.getModifiers().contains(Modifier.PUBLIC)) { + if (typeElement.getModifiers() + .contains(Modifier.PUBLIC)) { continue; } } else { @@ -746,24 +816,32 @@ private void generateAccessor(final TypeElement clazz) { final JType realType = JTypes.typeOf(fieldType); final JType publicType = fieldType instanceof PrimitiveType ? realType : JType.OBJECT; - final String fieldName = field.getSimpleName().toString(); + final String fieldName = field.getSimpleName() + .toString(); final JMethodDef getter = classDef.method(JMod.PUBLIC | JMod.STATIC, publicType, "get_" + fieldName); - getter.annotate(SuppressWarnings.class).value("unchecked"); + getter.annotate(SuppressWarnings.class) + .value("unchecked"); getter.param(JType.OBJECT, Constants.INSTANCE_SYM); - getter.body()._return(instanceName.cast(clazzType).field(fieldName)); + getter.body() + ._return(instanceName.cast(clazzType) + .field(fieldName)); final JMethodDef setter = classDef.method(JMod.PUBLIC | JMod.STATIC, JType.VOID, "set_" + fieldName); - setter.annotate(SuppressWarnings.class).value("unchecked"); + setter.annotate(SuppressWarnings.class) + .value("unchecked"); setter.param(JType.OBJECT, Constants.INSTANCE_SYM); setter.param(publicType, fieldName); final JAssignableExpr fieldExpr = JExprs.name(fieldName); - setter.body().assign(instanceName.cast(clazzType).field(fieldName), - (publicType.equals(realType) ? fieldExpr : fieldExpr.cast(realType))); + setter.body() + .assign(instanceName.cast(clazzType) + .field(fieldName), + (publicType.equals(realType) ? fieldExpr : fieldExpr.cast(realType))); } // we need to generate an accessor if the class isn't public if (!isEnclosingClassPublic) { for (ExecutableElement ctor : constructorsIn(clazz.getEnclosedElements())) { - if (ctor.getModifiers().contains(Modifier.PRIVATE)) { + if (ctor.getModifiers() + .contains(Modifier.PRIVATE)) { // skip it continue; } @@ -771,7 +849,9 @@ private void generateAccessor(final TypeElement clazz) { StringBuilder b = new StringBuilder(); for (VariableElement parameter : ctor.getParameters()) { b.append('_'); - b.append(parameter.asType().toString().replace('.', '_')); + b.append(parameter.asType() + .toString() + .replace('.', '_')); } String codedName = b.toString(); final JMethodDef ctorMethod = classDef.method(JMod.PUBLIC | JMod.STATIC, JType.OBJECT, "construct" + codedName); @@ -780,12 +860,14 @@ private void generateAccessor(final TypeElement clazz) { final TypeMirror paramType = parameter.asType(); final JType realType = JTypes.typeOf(paramType); final JType publicType = paramType instanceof PrimitiveType ? realType : JType.OBJECT; - final String name = parameter.getSimpleName().toString(); + final String name = parameter.getSimpleName() + .toString(); ctorMethod.param(publicType, name); final JAssignableExpr nameExpr = JExprs.name(name); ctorCall.arg(publicType.equals(realType) ? nameExpr : nameExpr.cast(realType)); } - ctorMethod.body()._return(ctorCall); + ctorMethod.body() + ._return(ctorCall); } } @@ -794,7 +876,8 @@ private void generateAccessor(final TypeElement clazz) { try { sources.writeSources(); } catch (IOException e) { - processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Failed to generate source file: " + e, clazz); + processingEnv.getMessager() + .printMessage(Diagnostic.Kind.ERROR, "Failed to generate source file: " + e, clazz); } } } @@ -802,7 +885,8 @@ private void generateAccessor(final TypeElement clazz) { private JType unnestStaticNestedType(DeclaredType declaredType) { final TypeElement typeElement = (TypeElement) declaredType.asElement(); - final String name = typeElement.getQualifiedName().toString(); + final String name = typeElement.getQualifiedName() + .toString(); final JType rawType = JTypes.typeNamed(name); final List typeArguments = declaredType.getTypeArguments(); if (typeArguments.isEmpty()) { @@ -819,18 +903,25 @@ private JType unnestStaticNestedType(DeclaredType declaredType) { private void appendParamTypes(ExecutableElement ex, final StringBuilder buf) { final List params = ex.getParameters(); if (params.isEmpty()) { - processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Expected at least one parameter", ex); + processingEnv.getMessager() + .printMessage(Diagnostic.Kind.ERROR, "Expected at least one parameter", ex); return; } VariableElement param = params.get(0); DeclaredType dt = (DeclaredType) param.asType(); - String typeName = processingEnv.getElementUtils().getBinaryName(((TypeElement) dt.asElement())).toString(); - buf.append('(').append(typeName); + String typeName = processingEnv.getElementUtils() + .getBinaryName(((TypeElement) dt.asElement())) + .toString(); + buf.append('(') + .append(typeName); for (int i = 1; i < params.size(); ++i) { param = params.get(i); dt = (DeclaredType) param.asType(); - typeName = processingEnv.getElementUtils().getBinaryName(((TypeElement) dt.asElement())).toString(); - buf.append(',').append(typeName); + typeName = processingEnv.getElementUtils() + .getBinaryName(((TypeElement) dt.asElement())) + .toString(); + buf.append(',') + .append(typeName); } buf.append(')'); } @@ -839,15 +930,17 @@ private String getRequiredJavadoc(Element e) { String javaDoc = getJavadoc(e); if (javaDoc == null) { - processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, - "Unable to find javadoc for config item " + e.getEnclosingElement() + " " + e, e); + processingEnv.getMessager() + .printMessage(Diagnostic.Kind.ERROR, + "Unable to find javadoc for config item " + e.getEnclosingElement() + " " + e, e); return ""; } return javaDoc; } private String getJavadoc(Element e) { - String docComment = processingEnv.getElementUtils().getDocComment(e); + String docComment = processingEnv.getElementUtils() + .getDocComment(e); if (docComment == null) { return null; @@ -855,14 +948,18 @@ private String getJavadoc(Element e) { // javax.lang.model keeps the leading space after the "*" so we need to remove it. - return REMOVE_LEADING_SPACE.matcher(docComment).replaceAll("").trim(); + return REMOVE_LEADING_SPACE.matcher(docComment) + .replaceAll("") + .trim(); } private static boolean isDocumentedConfigItem(Element element) { boolean hasAnnotation = false; for (AnnotationMirror annotationMirror : element.getAnnotationMirrors()) { - String annotationName = ((TypeElement) annotationMirror.getAnnotationType().asElement()) - .getQualifiedName().toString(); + String annotationName = ((TypeElement) annotationMirror.getAnnotationType() + .asElement()) + .getQualifiedName() + .toString(); if (Constants.ANNOTATION_CONFIG_ITEM.equals(annotationName)) { hasAnnotation = true; Object generateDocumentation = getAnnotationAttribute(annotationMirror, "generateDocumentation()"); @@ -879,8 +976,10 @@ private static boolean isDocumentedConfigItem(Element element) { private static boolean isConfigMappingMethodIgnored(Element element) { for (AnnotationMirror annotationMirror : element.getAnnotationMirrors()) { - String annotationName = ((TypeElement) annotationMirror.getAnnotationType().asElement()) - .getQualifiedName().toString(); + String annotationName = ((TypeElement) annotationMirror.getAnnotationType() + .asElement()) + .getQualifiedName() + .toString(); if (Constants.ANNOTATION_CONFIG_DOC_IGNORE.equals(annotationName)) { return true; } @@ -890,9 +989,12 @@ private static boolean isConfigMappingMethodIgnored(Element element) { private static Object getAnnotationAttribute(AnnotationMirror annotationMirror, String attributeName) { for (Map.Entry entry : annotationMirror - .getElementValues().entrySet()) { - final String key = entry.getKey().toString(); - final Object value = entry.getValue().getValue(); + .getElementValues() + .entrySet()) { + final String key = entry.getKey() + .toString(); + final Object value = entry.getValue() + .getValue(); if (attributeName.equals(key)) { return value; } @@ -913,7 +1015,9 @@ private static boolean hasParameterDocumentedConfigItem(ExecutableElement ex) { private static boolean isAnnotationPresent(Element element, String... annotationNames) { Set annotations = new HashSet<>(Arrays.asList(annotationNames)); for (AnnotationMirror i : element.getAnnotationMirrors()) { - String annotationName = ((TypeElement) i.getAnnotationType().asElement()).getQualifiedName().toString(); + String annotationName = ((TypeElement) i.getAnnotationType() + .asElement()).getQualifiedName() + .toString(); if (annotations.contains(annotationName)) { return true; } diff --git a/core/processor/src/test/java/io/quarkus/annotation/processor/ExtensionAnnotationProcessorTest.java b/core/processor/src/test/java/io/quarkus/annotation/processor/ExtensionAnnotationProcessorTest.java new file mode 100644 index 0000000000000..d9d85a19ab53a --- /dev/null +++ b/core/processor/src/test/java/io/quarkus/annotation/processor/ExtensionAnnotationProcessorTest.java @@ -0,0 +1,76 @@ +package io.quarkus.annotation.processor; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.List; + +import javax.tools.JavaFileObject; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import com.karuslabs.elementary.Results; +import com.karuslabs.elementary.junit.JavacExtension; +import com.karuslabs.elementary.junit.annotations.Classpath; +import com.karuslabs.elementary.junit.annotations.Processors; + +import io.quarkus.annotation.processor.fs.CustomMemoryFileSystemProvider; + +@ExtendWith(JavacExtension.class) +@Processors({ ExtensionAnnotationProcessor.class }) +class ExtensionAnnotationProcessorTest { + + @BeforeEach + void beforeEach() { + // This is of limited use, since the filesystem doesn't seem to directly generate files, in the current usage + CustomMemoryFileSystemProvider.reset(); + } + + @Test + @Classpath("org.acme.examples.ClassWithBuildStep") + void shouldProcessClassWithBuildStepWithoutErrors(Results results) throws IOException { + assertNoErrrors(results); + } + + @Test + @Classpath("org.acme.examples.ClassWithBuildStep") + void shouldGenerateABscFile(Results results) throws IOException { + assertNoErrrors(results); + List sources = results.sources; + JavaFileObject bscFile = sources.stream() + .filter(source -> source.getName() + .endsWith(".bsc")) + .findAny() + .orElse(null); + assertNotNull(bscFile); + + String contents = removeLineBreaks(new String(bscFile + .openInputStream() + .readAllBytes(), StandardCharsets.UTF_8)); + assertEquals("org.acme.examples.ClassWithBuildStep", contents); + } + + private String removeLineBreaks(String s) { + return s.replace(System.getProperty("line.separator"), "") + .replace("\n", ""); + } + + @Test + @Classpath("org.acme.examples.ClassWithoutBuildStep") + void shouldProcessEmptyClassWithoutErrors(Results results) { + assertNoErrrors(results); + } + + private static void assertNoErrrors(Results results) { + assertEquals(0, results.find() + .errors() + .count(), + "Errors were: " + results.find() + .errors() + .diagnostics()); + } +} diff --git a/core/processor/src/test/java/io/quarkus/annotation/processor/fs/CustomMemoryFileSystem.java b/core/processor/src/test/java/io/quarkus/annotation/processor/fs/CustomMemoryFileSystem.java new file mode 100644 index 0000000000000..432bd86b334e9 --- /dev/null +++ b/core/processor/src/test/java/io/quarkus/annotation/processor/fs/CustomMemoryFileSystem.java @@ -0,0 +1,158 @@ +package io.quarkus.annotation.processor.fs; + +import java.io.IOException; +import java.net.URI; +import java.nio.ByteBuffer; +import java.nio.channels.SeekableByteChannel; +import java.nio.file.FileStore; +import java.nio.file.FileSystem; +import java.nio.file.Path; +import java.nio.file.PathMatcher; +import java.nio.file.Paths; +import java.nio.file.WatchService; +import java.nio.file.attribute.UserPrincipalLookupService; +import java.nio.file.spi.FileSystemProvider; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +public class CustomMemoryFileSystem extends FileSystem { + + private final Map fileContents = new HashMap<>(); + private final CustomMemoryFileSystemProvider provider; + + public CustomMemoryFileSystem(CustomMemoryFileSystemProvider provider) { + this.provider = provider; + } + + @Override + public FileSystemProvider provider() { + return provider; + } + + @Override + public void close() throws IOException { + // No resources to close + } + + @Override + public boolean isOpen() { + return true; // Always open + } + + @Override + public boolean isReadOnly() { + return false; // This filesystem is writable + } + + @Override + public String getSeparator() { + return "/"; // Unix-style separator + } + + @Override + public Iterable getRootDirectories() { + return Collections.singleton(Paths.get("/")); // Single root directory + } + + @Override + public Iterable getFileStores() { + return Collections.emptyList(); // No file stores + } + + @Override + public Set supportedFileAttributeViews() { + return Collections.emptySet(); // No supported file attribute views + } + + @Override + public Path getPath(String first, String... more) { + String path = first; + for (String segment : more) { + path += "/" + segment; + } + return Paths.get(path); + } + + @Override + public PathMatcher getPathMatcher(String syntaxAndPattern) { + return null; + } + + @Override + public UserPrincipalLookupService getUserPrincipalLookupService() { + return null; + } + + @Override + public WatchService newWatchService() throws IOException { + return null; + } + + public void addFile(URI uri, byte[] content) { + fileContents.put(uri, ByteBuffer.wrap(content)); + } + + static class CustomMemorySeekableByteChannel implements SeekableByteChannel { + + private final ByteBuffer buffer; + + CustomMemorySeekableByteChannel(ByteBuffer buffer) { + this.buffer = buffer; + } + + @Override + public int read(ByteBuffer dst) throws IOException { + int remaining = buffer.remaining(); + int count = Math.min(remaining, dst.remaining()); + if (count > 0) { + ByteBuffer slice = buffer.slice(); + slice.limit(count); + dst.put(slice); + buffer.position(buffer.position() + count); + } + return count; + } + + @Override + public int write(ByteBuffer src) throws IOException { + int count = src.remaining(); + buffer.put(src); + return count; + } + + @Override + public long position() throws IOException { + return buffer.position(); + } + + @Override + public SeekableByteChannel position(long newPosition) throws IOException { + buffer.position((int) newPosition); + return this; + } + + @Override + public long size() throws IOException { + return buffer.limit(); + } + + @Override + public SeekableByteChannel truncate(long size) throws IOException { + buffer.limit((int) size); + return this; + } + + @Override + public boolean isOpen() { + return true; // Always open + } + + @Override + public void close() throws IOException { + // No resources to close + } + } + +} diff --git a/core/processor/src/test/java/io/quarkus/annotation/processor/fs/CustomMemoryFileSystemProvider.java b/core/processor/src/test/java/io/quarkus/annotation/processor/fs/CustomMemoryFileSystemProvider.java new file mode 100644 index 0000000000000..8d28a7ae672a6 --- /dev/null +++ b/core/processor/src/test/java/io/quarkus/annotation/processor/fs/CustomMemoryFileSystemProvider.java @@ -0,0 +1,152 @@ +package io.quarkus.annotation.processor.fs; + +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.nio.ByteBuffer; +import java.nio.channels.SeekableByteChannel; +import java.nio.file.AccessMode; +import java.nio.file.CopyOption; +import java.nio.file.DirectoryStream; +import java.nio.file.FileStore; +import java.nio.file.FileSystem; +import java.nio.file.LinkOption; +import java.nio.file.NoSuchFileException; +import java.nio.file.OpenOption; +import java.nio.file.Path; +import java.nio.file.attribute.BasicFileAttributes; +import java.nio.file.attribute.FileAttribute; +import java.nio.file.spi.FileSystemProvider; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +public class CustomMemoryFileSystemProvider extends FileSystemProvider { + + private static final String MEM = "mem"; + + private static Map fileContents = new HashMap(); + + public static void reset() { + fileContents = new HashMap(); + } + + public static Set getCreatedFiles() { + return fileContents.keySet(); + } + + @Override + public String getScheme() { + return MEM; + } + + @Override + public FileSystem newFileSystem(URI uri, Map env) throws IOException { + // There's a bit of a disconnect here between the Elementary JavaFileManager and the memory filesystem, + // even though both are in-memory filesystems + return new CustomMemoryFileSystem(this); + } + + @Override + public FileSystem getFileSystem(URI uri) { + throw new UnsupportedOperationException(); + } + + @Override + public Path getPath(URI uri) { + + if (uri.getScheme() == null || !uri.getScheme() + .equalsIgnoreCase(MEM)) { + throw new IllegalArgumentException("For URI " + uri + ", URI scheme is not '" + MEM + "'"); + + } + + // TODO what should we do here? Can we use the java file manager used by Elementary? + try { + return Path.of(File.createTempFile("mem-fs", "adhoc") + .toURI()); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public SeekableByteChannel newByteChannel(Path path, Set options, FileAttribute... attrs) + throws IOException { + if (fileContents.containsKey(path.toUri())) { + ByteBuffer buffer = fileContents.get(path.toUri()); + return new CustomMemoryFileSystem.CustomMemorySeekableByteChannel(buffer); + } else { + throw new NoSuchFileException(path.toString()); + } + } + + @Override + public DirectoryStream newDirectoryStream(Path dir, DirectoryStream.Filter filter) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public void createDirectory(Path dir, FileAttribute... attrs) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public void delete(Path path) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public void copy(Path source, Path target, CopyOption... options) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public void move(Path source, Path target, CopyOption... options) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isSameFile(Path path1, Path path2) throws IOException { + return path1.equals(path2); + } + + @Override + public boolean isHidden(Path path) throws IOException { + return false; + } + + @Override + public FileStore getFileStore(Path path) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public void checkAccess(Path path, AccessMode... modes) throws IOException { + if (!fileContents.containsKey(path.toUri())) { + throw new NoSuchFileException(path.toString()); + } + } + + @Override + public V getFileAttributeView(Path path, Class type, + LinkOption... options) { + throw new UnsupportedOperationException(); + } + + @Override + public A readAttributes(Path path, Class type, LinkOption... options) + throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public Map readAttributes(Path path, String attributes, LinkOption... options) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public void setAttribute(Path path, String attribute, Object value, LinkOption... options) throws IOException { + throw new UnsupportedOperationException(); + } +} diff --git a/core/processor/src/test/resources/META-INF/services/java.nio.file.spi.FileSystemProvider b/core/processor/src/test/resources/META-INF/services/java.nio.file.spi.FileSystemProvider new file mode 100644 index 0000000000000..9582882517a77 --- /dev/null +++ b/core/processor/src/test/resources/META-INF/services/java.nio.file.spi.FileSystemProvider @@ -0,0 +1 @@ +io.quarkus.annotation.processor.fs.CustomMemoryFileSystemProvider \ No newline at end of file diff --git a/core/processor/src/test/resources/io/quarkus/deployment/annotations/BuildStep.java b/core/processor/src/test/resources/io/quarkus/deployment/annotations/BuildStep.java new file mode 100644 index 0000000000000..944813a9d720a --- /dev/null +++ b/core/processor/src/test/resources/io/quarkus/deployment/annotations/BuildStep.java @@ -0,0 +1,13 @@ +package io.quarkus.deployment.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface BuildStep { + // FAKE! FAKE! This is only here so we can test without introducing a circular dependency + +} \ No newline at end of file diff --git a/core/processor/src/test/resources/org/acme/examples/ArbitraryBuildItem.java b/core/processor/src/test/resources/org/acme/examples/ArbitraryBuildItem.java new file mode 100644 index 0000000000000..ecb7fb4ee6ee6 --- /dev/null +++ b/core/processor/src/test/resources/org/acme/examples/ArbitraryBuildItem.java @@ -0,0 +1,6 @@ +package org.acme.examples; + +import io.quarkus.builder.item.MultiBuildItem; + +public final class ArbitraryBuildItem extends MultiBuildItem { +} \ No newline at end of file diff --git a/core/processor/src/test/resources/org/acme/examples/ClassWithBuildStep.java b/core/processor/src/test/resources/org/acme/examples/ClassWithBuildStep.java new file mode 100644 index 0000000000000..8dbecc4bff2bc --- /dev/null +++ b/core/processor/src/test/resources/org/acme/examples/ClassWithBuildStep.java @@ -0,0 +1,10 @@ +package org.acme.examples; + +import io.quarkus.deployment.annotations.BuildStep; + +public class ClassWithBuildStep { + @BuildStep + ArbitraryBuildItem feature() { + return new ArbitraryBuildItem(); + } +} diff --git a/core/processor/src/test/resources/org/acme/examples/ClassWithoutBuildStep.java b/core/processor/src/test/resources/org/acme/examples/ClassWithoutBuildStep.java new file mode 100644 index 0000000000000..b40b6d25d2059 --- /dev/null +++ b/core/processor/src/test/resources/org/acme/examples/ClassWithoutBuildStep.java @@ -0,0 +1,6 @@ +package org.acme.examples; + +public class ClassWithoutBuildStep { + + +} diff --git a/core/runtime/src/main/java/io/quarkus/runtime/annotations/RuntimeInit.java b/core/runtime/src/main/java/io/quarkus/runtime/annotations/RuntimeInit.java new file mode 100644 index 0000000000000..9f57f399efdc6 --- /dev/null +++ b/core/runtime/src/main/java/io/quarkus/runtime/annotations/RuntimeInit.java @@ -0,0 +1,14 @@ +package io.quarkus.runtime.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marker annotation used to indicate that a recorder method is called during the runtime init phase + */ +@Retention(RetentionPolicy.SOURCE) +@Target(ElementType.METHOD) +public @interface RuntimeInit { +} diff --git a/core/runtime/src/main/java/io/quarkus/runtime/annotations/StaticInit.java b/core/runtime/src/main/java/io/quarkus/runtime/annotations/StaticInit.java new file mode 100644 index 0000000000000..90a3793ec2963 --- /dev/null +++ b/core/runtime/src/main/java/io/quarkus/runtime/annotations/StaticInit.java @@ -0,0 +1,14 @@ +package io.quarkus.runtime.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marker annotation used to indicate that a recorder method is called during the static init phase + */ +@Retention(RetentionPolicy.SOURCE) +@Target(ElementType.METHOD) +public @interface StaticInit { +} diff --git a/core/runtime/src/main/java/io/quarkus/runtime/logging/LoggingSetupRecorder.java b/core/runtime/src/main/java/io/quarkus/runtime/logging/LoggingSetupRecorder.java index 6b37b15685d7d..6108893f04013 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/logging/LoggingSetupRecorder.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/logging/LoggingSetupRecorder.java @@ -6,6 +6,7 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.io.UnsupportedEncodingException; +import java.math.BigInteger; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -687,6 +688,24 @@ private static Handler configureSyslogHandler(final SyslogConfig config, final E handler.setTruncate(config.truncate); handler.setUseCountingFraming(config.useCountingFraming); handler.setLevel(config.level); + if (config.maxLength.isPresent()) { + BigInteger maxLen = config.maxLength.get().asBigInteger(); + if (maxLen.compareTo(BigInteger.valueOf(Integer.MAX_VALUE)) > 0) { + errorManager.error( + "Using 2GB as the value of maxLength for SyslogHandler as it is the maximum allowed value", null, + ErrorManager.GENERIC_FAILURE); + maxLen = BigInteger.valueOf(Integer.MAX_VALUE); + } else { + BigInteger minimumAllowedMaxLength = BigInteger.valueOf(128); + if (maxLen.compareTo(minimumAllowedMaxLength) < 0) { + errorManager.error( + "Using 128 as the value of maxLength for SyslogHandler as using a smaller value is not allowed", + null, ErrorManager.GENERIC_FAILURE); + maxLen = minimumAllowedMaxLength; + } + } + handler.setMaxLength(maxLen.intValue()); + } Formatter formatter = null; boolean formatterWarning = false; diff --git a/core/runtime/src/main/java/io/quarkus/runtime/logging/SyslogConfig.java b/core/runtime/src/main/java/io/quarkus/runtime/logging/SyslogConfig.java index 0411d345fdcc3..767a0a5b92c85 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/logging/SyslogConfig.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/logging/SyslogConfig.java @@ -10,6 +10,7 @@ import io.quarkus.runtime.annotations.ConfigGroup; import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.configuration.MemorySize; @ConfigGroup public class SyslogConfig { @@ -95,6 +96,15 @@ public class SyslogConfig { @ConfigItem Optional filter; + /** + * The maximum length, in bytes, of the message allowed to be sent. The length includes the header and the message. + *

+ * If not set, the default value is {@code 2048} when {@code sys-log-type} is {@code rfc5424} (which is the default) + * and {@code 1024} when {@code sys-log-type} is {@code rfc3164} + */ + @ConfigItem + Optional maxLength; + /** * Syslog async logging config */ diff --git a/devtools/cli/distribution/jreleaser-maintenance.yml b/devtools/cli/distribution/jreleaser-maintenance.yml index 15772e91a8ba7..5847f0b4229eb 100644 --- a/devtools/cli/distribution/jreleaser-maintenance.yml +++ b/devtools/cli/distribution/jreleaser-maintenance.yml @@ -18,7 +18,7 @@ project: - java links: homepage: https://quarkus.io - license: https://github.com/quarkusio/quarkus/blob/main/LICENSE.txt + license: https://github.com/quarkusio/quarkus/blob/main/LICENSE release: github: diff --git a/devtools/cli/distribution/jreleaser-preview.yml b/devtools/cli/distribution/jreleaser-preview.yml index cbfeab135468c..1de4d9a832d17 100644 --- a/devtools/cli/distribution/jreleaser-preview.yml +++ b/devtools/cli/distribution/jreleaser-preview.yml @@ -18,7 +18,7 @@ project: - java links: homepage: https://quarkus.io - license: https://github.com/quarkusio/quarkus/blob/main/LICENSE.txt + license: https://github.com/quarkusio/quarkus/blob/main/LICENSE release: github: diff --git a/devtools/cli/distribution/jreleaser.yml b/devtools/cli/distribution/jreleaser.yml index d8be28fdd50d2..e4e37b0216b86 100644 --- a/devtools/cli/distribution/jreleaser.yml +++ b/devtools/cli/distribution/jreleaser.yml @@ -18,7 +18,7 @@ project: - java links: homepage: https://quarkus.io - license: https://github.com/quarkusio/quarkus/blob/main/LICENSE.txt + license: https://github.com/quarkusio/quarkus/blob/main/LICENSE release: github: diff --git a/devtools/gradle/settings.gradle.kts b/devtools/gradle/settings.gradle.kts index c372ad435ac07..8871081257ab4 100644 --- a/devtools/gradle/settings.gradle.kts +++ b/devtools/gradle/settings.gradle.kts @@ -1,5 +1,5 @@ plugins { - id("com.gradle.develocity") version "3.17" + id("com.gradle.develocity") version "3.17.2" } develocity { diff --git a/devtools/maven/src/main/java/io/quarkus/maven/DependencyTreeMojo.java b/devtools/maven/src/main/java/io/quarkus/maven/DependencyTreeMojo.java index 22891e8f44de4..6e027daff769d 100644 --- a/devtools/maven/src/main/java/io/quarkus/maven/DependencyTreeMojo.java +++ b/devtools/maven/src/main/java/io/quarkus/maven/DependencyTreeMojo.java @@ -4,6 +4,7 @@ import java.io.File; import java.io.IOException; import java.nio.file.Files; +import java.nio.file.OpenOption; import java.nio.file.StandardOpenOption; import java.util.List; import java.util.function.Consumer; @@ -21,7 +22,9 @@ import org.eclipse.aether.repository.RemoteRepository; import io.quarkus.bootstrap.resolver.BootstrapAppModelResolver; +import io.quarkus.bootstrap.resolver.maven.ApplicationDependencyModelResolver; import io.quarkus.bootstrap.resolver.maven.BootstrapMavenContext; +import io.quarkus.bootstrap.resolver.maven.DependencyLoggingConfig; import io.quarkus.bootstrap.resolver.maven.MavenArtifactResolver; import io.quarkus.maven.components.QuarkusWorkspaceProvider; import io.quarkus.maven.dependency.ArtifactCoords; @@ -48,9 +51,27 @@ public class DependencyTreeMojo extends AbstractMojo { * Target launch mode corresponding to {@link io.quarkus.runtime.LaunchMode} for which the dependency tree should be built. * {@code io.quarkus.runtime.LaunchMode.NORMAL} is the default. */ - @Parameter(property = "mode", required = false, defaultValue = "prod") + @Parameter(property = "mode", defaultValue = "prod") String mode; + /** + * INCUBATING option, enabled with @{code -Dquarkus.bootstrap.incubating-model-resolver} system or project property. + *

+ * Whether to log dependency properties, such as on which classpath they belong, whether they are hot-reloadable in dev + * mode, etc. + */ + @Parameter(property = "verbose") + boolean verbose; + + /** + * INCUBATING option, enabled with @{code -Dquarkus.bootstrap.incubating-model-resolver} system or project property. + *

+ * Whether to log all dependencies of each dependency node in a tree, adding {@code [+]} suffix + * to those whose dependencies are not expanded. + */ + @Parameter(property = "graph") + boolean graph; + /** * If specified, this parameter will cause the dependency tree to be written to the path specified, instead of writing to * the console. @@ -77,8 +98,10 @@ public void execute() throws MojoExecutionException, MojoFailureException { final BufferedWriter bw; try { Files.createDirectories(outputFile.toPath().getParent()); - bw = writer = Files.newBufferedWriter(outputFile.toPath(), - appendOutput && outputFile.exists() ? StandardOpenOption.APPEND : StandardOpenOption.CREATE); + final OpenOption[] openOptions = appendOutput && outputFile.exists() + ? new OpenOption[] { StandardOpenOption.APPEND } + : new OpenOption[0]; + bw = writer = Files.newBufferedWriter(outputFile.toPath(), openOptions); } catch (IOException e) { throw new MojoExecutionException("Failed to initialize file output writer", e); } @@ -124,7 +147,13 @@ private void logTree(final Consumer log) throws MojoExecutionException { "Parameter 'mode' was set to '" + mode + "' while expected one of 'dev', 'test' or 'prod'"); } } - modelResolver.setBuildTreeLogger(log); + modelResolver.setIncubatingModelResolver( + ApplicationDependencyModelResolver.isIncubatingEnabled(project.getProperties())); + modelResolver.setDepLogConfig(DependencyLoggingConfig.builder() + .setMessageConsumer(log) + .setVerbose(verbose) + .setGraph(graph) + .build()); modelResolver.resolveModel(appArtifact); } catch (Exception e) { throw new MojoExecutionException("Failed to resolve application model " + appArtifact + " dependencies", e); diff --git a/devtools/maven/src/main/java/io/quarkus/maven/DevMojo.java b/devtools/maven/src/main/java/io/quarkus/maven/DevMojo.java index bdcd48eeff5d3..05bd7d3b81622 100644 --- a/devtools/maven/src/main/java/io/quarkus/maven/DevMojo.java +++ b/devtools/maven/src/main/java/io/quarkus/maven/DevMojo.java @@ -96,6 +96,7 @@ import io.quarkus.bootstrap.model.ApplicationModel; import io.quarkus.bootstrap.model.PathsCollection; import io.quarkus.bootstrap.resolver.BootstrapAppModelResolver; +import io.quarkus.bootstrap.resolver.maven.ApplicationDependencyModelResolver; import io.quarkus.bootstrap.resolver.maven.BootstrapMavenContext; import io.quarkus.bootstrap.resolver.maven.BootstrapMavenContextConfig; import io.quarkus.bootstrap.resolver.maven.MavenArtifactResolver; @@ -155,11 +156,6 @@ public class DevMojo extends AbstractMojo { private static final String ORG_JETBRAINS_KOTLIN = "org.jetbrains.kotlin"; private static final String KOTLIN_MAVEN_PLUGIN = "kotlin-maven-plugin"; - private static final String IO_SMALLRYE = "io.smallrye"; - private static final String ORG_JBOSS_JANDEX = "org.jboss.jandex"; - private static final String JANDEX_MAVEN_PLUGIN = "jandex-maven-plugin"; - private static final String JANDEX = "jandex"; - private static final String BOOTSTRAP_ID = "DevMojo"; /** @@ -367,6 +363,17 @@ public class DevMojo extends AbstractMojo { @Component BuildAnalyticsProvider analyticsProvider; + /** + * A comma-separated list of Maven plugin keys in {@code groupId:artifactId} format + * (for example {@code org.codehaus.mojo:flatten-maven-plugin} and/or goal prefixes, + * (for example {@code flatten}) that should be skipped when {@code quarkus:dev} identifies + * Maven plugin goals that should be executed before the application is launched in dev mode. + *

+ * Only the {@code flatten} Maven plugin is skipped by default. + */ + @Parameter(defaultValue = "org.codehaus.mojo:flatten-maven-plugin") + Set skipPlugins; + /** * console attributes, used to restore the console state */ @@ -587,6 +594,12 @@ private String handleAutoCompile() throws MojoExecutionException { if (p.getExecutions().isEmpty()) { continue; } + if (skipPlugins.contains(p.getKey())) { + if (getLog().isDebugEnabled()) { + getLog().debug("Skipping " + p.getId() + " execution according to skipPlugins value"); + } + continue; + } for (PluginExecution e : p.getExecutions()) { if (e.getPhase() != null && !PRE_DEV_MODE_PHASES.contains(e.getPhase())) { // skip executions with phases post quarkus:dev, such as install, deploy, site, etc @@ -598,6 +611,13 @@ private String handleAutoCompile() throws MojoExecutionException { String goalPrefix = null; if (!e.getGoals().isEmpty()) { goalPrefix = getMojoDescriptor(p, e.getGoals().get(0)).getPluginDescriptor().getGoalPrefix(); + if (skipPlugins.contains(goalPrefix)) { + if (getLog().isDebugEnabled()) { + getLog().debug("Skipping " + goalPrefix + " execution according to skipPlugins value"); + continue; + } + continue; + } pluginPrefixes.put(goalPrefix, p); pluginPrefixes.put(p.getId(), p); } @@ -1341,6 +1361,7 @@ private QuarkusDevModeLauncher newLauncher(Boolean debugPortOk, String bootstrap .setDevMode(true) .setTest(LaunchMode.TEST.equals(getLaunchModeClasspath())) .setCollectReloadableDependencies(!noDeps) + .setIncubatingModelResolver(ApplicationDependencyModelResolver.isIncubatingEnabled(project.getProperties())) .resolveModel(mvnCtx.getCurrentProject().getAppArtifact()); } diff --git a/devtools/maven/src/main/java/io/quarkus/maven/QuarkusBootstrapProvider.java b/devtools/maven/src/main/java/io/quarkus/maven/QuarkusBootstrapProvider.java index 1d8f7559b2fb2..d83cb4fa9f46b 100644 --- a/devtools/maven/src/main/java/io/quarkus/maven/QuarkusBootstrapProvider.java +++ b/devtools/maven/src/main/java/io/quarkus/maven/QuarkusBootstrapProvider.java @@ -37,6 +37,7 @@ import io.quarkus.bootstrap.model.ApplicationModel; import io.quarkus.bootstrap.resolver.AppModelResolverException; import io.quarkus.bootstrap.resolver.BootstrapAppModelResolver; +import io.quarkus.bootstrap.resolver.maven.ApplicationDependencyModelResolver; import io.quarkus.bootstrap.resolver.maven.BootstrapMavenContext; import io.quarkus.bootstrap.resolver.maven.BootstrapMavenException; import io.quarkus.bootstrap.resolver.maven.MavenArtifactResolver; @@ -204,7 +205,10 @@ private MavenArtifactResolver artifactResolver(QuarkusBootstrapMojo mojo, Launch private CuratedApplication doBootstrap(QuarkusBootstrapMojo mojo, LaunchMode mode) throws MojoExecutionException { + final BootstrapAppModelResolver modelResolver = new BootstrapAppModelResolver(artifactResolver(mojo, mode)) + .setIncubatingModelResolver( + ApplicationDependencyModelResolver.isIncubatingEnabled(mojo.mavenProject().getProperties())) .setDevMode(mode == LaunchMode.DEVELOPMENT) .setTest(mode == LaunchMode.TEST) .setCollectReloadableDependencies(mode == LaunchMode.DEVELOPMENT || mode == LaunchMode.TEST); diff --git a/devtools/maven/src/main/java/io/quarkus/maven/UpdateMojo.java b/devtools/maven/src/main/java/io/quarkus/maven/UpdateMojo.java index 4bc790ec24f7f..2812fc1be4db7 100644 --- a/devtools/maven/src/main/java/io/quarkus/maven/UpdateMojo.java +++ b/devtools/maven/src/main/java/io/quarkus/maven/UpdateMojo.java @@ -42,7 +42,7 @@ public class UpdateMojo extends QuarkusProjectStateMojoBase { /** * Version of the target platform (e.g: 2.0.0.Final) - * You may instead use streamId to target the latest version of a specific platform stream. + * You may instead use stream to target the latest version of a specific platform stream. */ @Parameter(property = "platformVersion", required = false) private String platformVersion; diff --git a/devtools/maven/src/test/java/io/quarkus/maven/BasicDependencyTreeTestBase.java b/devtools/maven/src/test/java/io/quarkus/maven/BasicDependencyTreeTestBase.java new file mode 100644 index 0000000000000..ef929a5567a5e --- /dev/null +++ b/devtools/maven/src/test/java/io/quarkus/maven/BasicDependencyTreeTestBase.java @@ -0,0 +1,26 @@ +package io.quarkus.maven; + +import io.quarkus.bootstrap.resolver.TsArtifact; +import io.quarkus.bootstrap.resolver.TsDependency; +import io.quarkus.bootstrap.resolver.TsQuarkusExt; + +abstract class BasicDependencyTreeTestBase extends DependencyTreeMojoTestBase { + + @Override + protected void initRepo() { + + final TsQuarkusExt coreExt = new TsQuarkusExt("test-core-ext"); + app = TsArtifact.jar("test-app") + .addDependency(new TsArtifact(TsArtifact.DEFAULT_GROUP_ID, "artifact-with-classifier", "classifier", "jar", + TsArtifact.DEFAULT_VERSION)) + .addDependency(new TsQuarkusExt("test-ext2") + .addDependency(new TsQuarkusExt("test-ext1").addDependency(coreExt))) + .addDependency(new TsDependency(TsArtifact.jar("optional"), true)) + .addDependency(new TsQuarkusExt("test-ext3").addDependency(coreExt)) + .addDependency(new TsDependency(TsArtifact.jar("provided"), "provided")) + .addDependency(new TsDependency(TsArtifact.jar("runtime"), "runtime")) + .addDependency(new TsDependency(TsArtifact.jar("test"), "test")); + appModel = app.getPomModel(); + app.install(repoBuilder); + } +} diff --git a/devtools/maven/src/test/java/io/quarkus/maven/ConditionalDependencyGraphMojoTest.java b/devtools/maven/src/test/java/io/quarkus/maven/ConditionalDependencyGraphMojoTest.java new file mode 100644 index 0000000000000..26509fd19828f --- /dev/null +++ b/devtools/maven/src/test/java/io/quarkus/maven/ConditionalDependencyGraphMojoTest.java @@ -0,0 +1,53 @@ +package io.quarkus.maven; + +import io.quarkus.bootstrap.resolver.TsArtifact; +import io.quarkus.bootstrap.resolver.TsQuarkusExt; + +public class ConditionalDependencyGraphMojoTest extends DependencyTreeMojoTestBase { + @Override + protected String mode() { + return "prod"; + } + + @Override + protected boolean isGraph() { + return true; + } + + @Override + protected boolean isIncubatingModelResolver() { + return true; + } + + @Override + protected void initRepo() { + + final TsQuarkusExt coreExt = new TsQuarkusExt("test-core-ext"); + + var tomatoExt = new TsQuarkusExt("quarkus-tomato").addDependency(coreExt); + var mozzarellaExt = new TsQuarkusExt("quarkus-mozzarella").addDependency(coreExt); + var basilExt = new TsQuarkusExt("quarkus-basil").addDependency(coreExt); + + var oilJar = TsArtifact.jar("quarkus-oil"); + + var capreseExt = new TsQuarkusExt("quarkus-caprese") + .setDependencyCondition(tomatoExt, mozzarellaExt, basilExt) + .addDependency(coreExt); + capreseExt.getDeployment().addDependency(oilJar); + capreseExt.install(repoBuilder); + + var saladExt = new TsQuarkusExt("quarkus-salad") + .setConditionalDeps(capreseExt) + .addDependency(coreExt); + + app = TsArtifact.jar("app-with-conditional-graph") + .addDependency(tomatoExt) + .addDependency(mozzarellaExt) + .addDependency(basilExt) + .addDependency(saladExt) + .addDependency(oilJar); + + appModel = app.getPomModel(); + app.install(repoBuilder); + } +} diff --git a/devtools/maven/src/test/java/io/quarkus/maven/ConditionalDependencyTreeMojoTest.java b/devtools/maven/src/test/java/io/quarkus/maven/ConditionalDependencyTreeMojoTest.java new file mode 100644 index 0000000000000..45d10a0417b62 --- /dev/null +++ b/devtools/maven/src/test/java/io/quarkus/maven/ConditionalDependencyTreeMojoTest.java @@ -0,0 +1,48 @@ +package io.quarkus.maven; + +import io.quarkus.bootstrap.resolver.TsArtifact; +import io.quarkus.bootstrap.resolver.TsQuarkusExt; + +public class ConditionalDependencyTreeMojoTest extends DependencyTreeMojoTestBase { + @Override + protected String mode() { + return "prod"; + } + + @Override + protected boolean isIncubatingModelResolver() { + return true; + } + + @Override + protected void initRepo() { + + final TsQuarkusExt coreExt = new TsQuarkusExt("test-core-ext"); + + var tomatoExt = new TsQuarkusExt("quarkus-tomato").addDependency(coreExt); + var mozzarellaExt = new TsQuarkusExt("quarkus-mozzarella").addDependency(coreExt); + var basilExt = new TsQuarkusExt("quarkus-basil").addDependency(coreExt); + + var oilJar = TsArtifact.jar("quarkus-oil"); + + var capreseExt = new TsQuarkusExt("quarkus-caprese") + .setDependencyCondition(tomatoExt, mozzarellaExt, basilExt) + .addDependency(coreExt); + capreseExt.getDeployment().addDependency(oilJar); + capreseExt.install(repoBuilder); + + var saladExt = new TsQuarkusExt("quarkus-salad") + .setConditionalDeps(capreseExt) + .addDependency(coreExt); + + app = TsArtifact.jar("app-with-conditional-deps") + .addDependency(tomatoExt) + .addDependency(mozzarellaExt) + .addDependency(basilExt) + .addDependency(saladExt) + .addDependency(oilJar); + + appModel = app.getPomModel(); + app.install(repoBuilder); + } +} diff --git a/devtools/maven/src/test/java/io/quarkus/maven/DependencyTreeMojoTestBase.java b/devtools/maven/src/test/java/io/quarkus/maven/DependencyTreeMojoTestBase.java index bd99f28420ec7..db3a359c1d6d3 100644 --- a/devtools/maven/src/test/java/io/quarkus/maven/DependencyTreeMojoTestBase.java +++ b/devtools/maven/src/test/java/io/quarkus/maven/DependencyTreeMojoTestBase.java @@ -1,32 +1,28 @@ package io.quarkus.maven; -import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.assertj.core.api.Assertions.assertThat; -import java.io.BufferedReader; -import java.io.IOException; import java.io.PrintStream; -import java.nio.file.Files; +import java.nio.charset.StandardCharsets; import java.nio.file.Path; -import java.util.ArrayList; import java.util.Collections; -import java.util.List; import org.apache.maven.artifact.DefaultArtifact; import org.apache.maven.artifact.handler.DefaultArtifactHandler; import org.apache.maven.model.Model; import org.apache.maven.project.MavenProject; +import org.eclipse.aether.util.artifact.JavaScopes; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import io.quarkus.bootstrap.resolver.TsArtifact; -import io.quarkus.bootstrap.resolver.TsDependency; -import io.quarkus.bootstrap.resolver.TsQuarkusExt; import io.quarkus.bootstrap.resolver.TsRepoBuilder; import io.quarkus.bootstrap.resolver.maven.MavenArtifactResolver; import io.quarkus.bootstrap.util.IoUtils; +import io.quarkus.maven.dependency.ArtifactCoords; -public abstract class DependencyTreeMojoTestBase { +abstract class DependencyTreeMojoTestBase { protected Path workDir; protected Path repoHome; @@ -50,22 +46,6 @@ public void setup() throws Exception { initRepo(); } - protected void initRepo() throws Exception { - final TsQuarkusExt coreExt = new TsQuarkusExt("test-core-ext"); - app = TsArtifact.jar("test-app") - .addDependency(new TsArtifact(TsArtifact.DEFAULT_GROUP_ID, "artifact-with-classifier", "classifier", "jar", - TsArtifact.DEFAULT_VERSION)) - .addDependency(new TsQuarkusExt("test-ext2") - .addDependency(new TsQuarkusExt("test-ext1").addDependency(coreExt))) - .addDependency(new TsDependency(TsArtifact.jar("optional"), true)) - .addDependency(new TsQuarkusExt("test-ext3").addDependency(coreExt)) - .addDependency(new TsDependency(TsArtifact.jar("provided"), "provided")) - .addDependency(new TsDependency(TsArtifact.jar("runtime"), "runtime")) - .addDependency(new TsDependency(TsArtifact.jar("test"), "test")); - appModel = app.getPomModel(); - app.install(repoBuilder); - } - @AfterEach public void cleanup() { if (workDir != null) { @@ -73,44 +53,47 @@ public void cleanup() { } } + protected abstract void initRepo(); + protected abstract String mode(); + protected boolean isGraph() { + return false; + } + + protected boolean isIncubatingModelResolver() { + return false; + } + @Test public void test() throws Exception { final DependencyTreeMojo mojo = new DependencyTreeMojo(); mojo.project = new MavenProject(); - mojo.project.setArtifact(new DefaultArtifact(app.getGroupId(), app.getArtifactId(), app.getVersion(), "compile", - app.getType(), app.getClassifier(), new DefaultArtifactHandler("jar"))); + mojo.project.setArtifact(new DefaultArtifact(app.getGroupId(), app.getArtifactId(), app.getVersion(), + JavaScopes.COMPILE, app.getType(), app.getClassifier(), + new DefaultArtifactHandler(ArtifactCoords.TYPE_JAR))); mojo.project.setModel(appModel); mojo.project.setOriginalModel(appModel); + if (isIncubatingModelResolver()) { + mojo.project.getProperties().setProperty("quarkus.bootstrap.incubating-model-resolver", "true"); + } mojo.resolver = mvnResolver; mojo.mode = mode(); + mojo.graph = isGraph(); - final Path mojoLog = workDir.resolve("mojo.log"); + final Path mojoLog = workDir.resolve(getClass().getName() + ".log"); final PrintStream defaultOut = System.out; - try (PrintStream logOut = new PrintStream(mojoLog.toFile(), "UTF-8")) { + try (PrintStream logOut = new PrintStream(mojoLog.toFile(), StandardCharsets.UTF_8)) { System.setOut(logOut); mojo.execute(); } finally { System.setOut(defaultOut); } - assertEquals(readInLowCase(Path.of("").normalize().toAbsolutePath() - .resolve("target").resolve("test-classes") - .resolve(app.getArtifactFileName() + "." + mode())), readInLowCase(mojoLog)); - } - - private static List readInLowCase(Path p) throws IOException { - final List list = new ArrayList<>(); - try (BufferedReader reader = Files.newBufferedReader(p)) { - String line = reader.readLine(); - while (line != null) { - list.add(line.toLowerCase()); - line = reader.readLine(); - } - } - return list; + assertThat(mojoLog).hasSameTextualContentAs( + Path.of("").normalize().toAbsolutePath() + .resolve("target").resolve("test-classes").resolve(app.getArtifactFileName() + "." + mode())); } } diff --git a/devtools/maven/src/test/java/io/quarkus/maven/DevDependencyTreeMojoTest.java b/devtools/maven/src/test/java/io/quarkus/maven/DevDependencyTreeMojoTest.java index bef26188da25e..5dd5c52df7412 100644 --- a/devtools/maven/src/test/java/io/quarkus/maven/DevDependencyTreeMojoTest.java +++ b/devtools/maven/src/test/java/io/quarkus/maven/DevDependencyTreeMojoTest.java @@ -1,6 +1,6 @@ package io.quarkus.maven; -public class DevDependencyTreeMojoTest extends DependencyTreeMojoTestBase { +public class DevDependencyTreeMojoTest extends BasicDependencyTreeTestBase { @Override protected String mode() { return "dev"; diff --git a/devtools/maven/src/test/java/io/quarkus/maven/ProdDependencyTreeMojoTest.java b/devtools/maven/src/test/java/io/quarkus/maven/ProdDependencyTreeMojoTest.java index 81aa3c190b258..7e93473d3dc1b 100644 --- a/devtools/maven/src/test/java/io/quarkus/maven/ProdDependencyTreeMojoTest.java +++ b/devtools/maven/src/test/java/io/quarkus/maven/ProdDependencyTreeMojoTest.java @@ -1,6 +1,6 @@ package io.quarkus.maven; -public class ProdDependencyTreeMojoTest extends DependencyTreeMojoTestBase { +public class ProdDependencyTreeMojoTest extends BasicDependencyTreeTestBase { @Override protected String mode() { return "prod"; diff --git a/devtools/maven/src/test/java/io/quarkus/maven/TestDependencyTreeMojoTest.java b/devtools/maven/src/test/java/io/quarkus/maven/TestDependencyTreeMojoTest.java index fab83b72936e5..4ee403ebf5cf5 100644 --- a/devtools/maven/src/test/java/io/quarkus/maven/TestDependencyTreeMojoTest.java +++ b/devtools/maven/src/test/java/io/quarkus/maven/TestDependencyTreeMojoTest.java @@ -1,6 +1,6 @@ package io.quarkus.maven; -public class TestDependencyTreeMojoTest extends DependencyTreeMojoTestBase { +public class TestDependencyTreeMojoTest extends BasicDependencyTreeTestBase { @Override protected String mode() { return "test"; diff --git a/devtools/maven/src/test/resources/app-with-conditional-deps-1.jar.prod b/devtools/maven/src/test/resources/app-with-conditional-deps-1.jar.prod new file mode 100644 index 0000000000000..5780caf8fafca --- /dev/null +++ b/devtools/maven/src/test/resources/app-with-conditional-deps-1.jar.prod @@ -0,0 +1,15 @@ +[info] Quarkus application PROD mode build dependency tree: +[info] io.quarkus.bootstrap.test:app-with-conditional-deps:pom:1 +[info] ├─ io.quarkus.bootstrap.test:quarkus-tomato-deployment:jar:1 (compile) +[info] │ ├─ io.quarkus.bootstrap.test:quarkus-tomato:jar:1 (compile) +[info] │ │ └─ io.quarkus.bootstrap.test:test-core-ext:jar:1 (compile) +[info] │ └─ io.quarkus.bootstrap.test:test-core-ext-deployment:jar:1 (compile) +[info] ├─ io.quarkus.bootstrap.test:quarkus-mozzarella-deployment:jar:1 (compile) +[info] │ └─ io.quarkus.bootstrap.test:quarkus-mozzarella:jar:1 (compile) +[info] ├─ io.quarkus.bootstrap.test:quarkus-basil-deployment:jar:1 (compile) +[info] │ └─ io.quarkus.bootstrap.test:quarkus-basil:jar:1 (compile) +[info] ├─ io.quarkus.bootstrap.test:quarkus-salad-deployment:jar:1 (compile) +[info] │ ├─ io.quarkus.bootstrap.test:quarkus-salad:jar:1 (compile) +[info] │ │ └─ io.quarkus.bootstrap.test:quarkus-caprese:jar:1 (compile) +[info] │ └─ io.quarkus.bootstrap.test:quarkus-caprese-deployment:jar:1 (compile) +[info] └─ io.quarkus.bootstrap.test:quarkus-oil:jar:1 (compile) \ No newline at end of file diff --git a/devtools/maven/src/test/resources/app-with-conditional-graph-1.jar.prod b/devtools/maven/src/test/resources/app-with-conditional-graph-1.jar.prod new file mode 100644 index 0000000000000..77508e6965d59 --- /dev/null +++ b/devtools/maven/src/test/resources/app-with-conditional-graph-1.jar.prod @@ -0,0 +1,30 @@ +[info] Quarkus application PROD mode build dependency tree: +[info] io.quarkus.bootstrap.test:app-with-conditional-graph:pom:1 +[info] ├─ io.quarkus.bootstrap.test:quarkus-basil::jar:1 (compile) [+] +[info] ├─ io.quarkus.bootstrap.test:quarkus-mozzarella::jar:1 (compile) [+] +[info] ├─ io.quarkus.bootstrap.test:quarkus-salad::jar:1 (compile) [+] +[info] ├─ io.quarkus.bootstrap.test:quarkus-tomato::jar:1 (compile) [+] +[info] ├─ io.quarkus.bootstrap.test:quarkus-tomato-deployment:jar:1 (compile) +[info] │ ├─ io.quarkus.bootstrap.test:quarkus-tomato:jar:1 (compile) +[info] │ │ └─ io.quarkus.bootstrap.test:test-core-ext:jar:1 (compile) +[info] │ └─ io.quarkus.bootstrap.test:test-core-ext-deployment:jar:1 (compile) +[info] │ └─ io.quarkus.bootstrap.test:test-core-ext::jar:1 (compile) [+] +[info] ├─ io.quarkus.bootstrap.test:quarkus-mozzarella-deployment:jar:1 (compile) +[info] │ ├─ io.quarkus.bootstrap.test:test-core-ext-deployment::jar:1 (compile) [+] +[info] │ └─ io.quarkus.bootstrap.test:quarkus-mozzarella:jar:1 (compile) +[info] │ └─ io.quarkus.bootstrap.test:test-core-ext::jar:1 (compile) [+] +[info] ├─ io.quarkus.bootstrap.test:quarkus-basil-deployment:jar:1 (compile) +[info] │ ├─ io.quarkus.bootstrap.test:test-core-ext-deployment::jar:1 (compile) [+] +[info] │ └─ io.quarkus.bootstrap.test:quarkus-basil:jar:1 (compile) +[info] │ └─ io.quarkus.bootstrap.test:test-core-ext::jar:1 (compile) [+] +[info] ├─ io.quarkus.bootstrap.test:quarkus-salad-deployment:jar:1 (compile) +[info] │ ├─ io.quarkus.bootstrap.test:test-core-ext-deployment::jar:1 (compile) [+] +[info] │ ├─ io.quarkus.bootstrap.test:quarkus-salad:jar:1 (compile) +[info] │ │ ├─ io.quarkus.bootstrap.test:test-core-ext::jar:1 (compile) [+] +[info] │ │ └─ io.quarkus.bootstrap.test:quarkus-caprese:jar:1 (compile) +[info] │ │ └─ io.quarkus.bootstrap.test:test-core-ext::jar:1 (compile) [+] +[info] │ └─ io.quarkus.bootstrap.test:quarkus-caprese-deployment:jar:1 (compile) +[info] │ ├─ io.quarkus.bootstrap.test:quarkus-caprese::jar:1 (compile) [+] +[info] │ ├─ io.quarkus.bootstrap.test:quarkus-oil::jar:1 (compile) [+] +[info] │ └─ io.quarkus.bootstrap.test:test-core-ext-deployment::jar:1 (compile) [+] +[info] └─ io.quarkus.bootstrap.test:quarkus-oil:jar:1 (compile) \ No newline at end of file diff --git a/devtools/maven/src/test/resources/test-app-1.jar.dev b/devtools/maven/src/test/resources/test-app-1.jar.dev index 1fb959fdae99b..d43162a11db94 100644 --- a/devtools/maven/src/test/resources/test-app-1.jar.dev +++ b/devtools/maven/src/test/resources/test-app-1.jar.dev @@ -1,4 +1,4 @@ -[info] quarkus application dev mode build dependency tree: +[info] Quarkus application DEV mode build dependency tree: [info] io.quarkus.bootstrap.test:test-app:pom:1 [info] ├─ io.quarkus.bootstrap.test:artifact-with-classifier:jar:classifier:1 (compile) [info] ├─ io.quarkus.bootstrap.test:test-ext2-deployment:jar:1 (compile) diff --git a/devtools/maven/src/test/resources/test-app-1.jar.prod b/devtools/maven/src/test/resources/test-app-1.jar.prod index 5c460135a1273..bf7d0a9836aae 100644 --- a/devtools/maven/src/test/resources/test-app-1.jar.prod +++ b/devtools/maven/src/test/resources/test-app-1.jar.prod @@ -1,4 +1,4 @@ -[info] quarkus application prod mode build dependency tree: +[info] Quarkus application PROD mode build dependency tree: [info] io.quarkus.bootstrap.test:test-app:pom:1 [info] ├─ io.quarkus.bootstrap.test:artifact-with-classifier:jar:classifier:1 (compile) [info] ├─ io.quarkus.bootstrap.test:test-ext2-deployment:jar:1 (compile) diff --git a/devtools/maven/src/test/resources/test-app-1.jar.test b/devtools/maven/src/test/resources/test-app-1.jar.test index 40bc76f40c28c..832397770b902 100644 --- a/devtools/maven/src/test/resources/test-app-1.jar.test +++ b/devtools/maven/src/test/resources/test-app-1.jar.test @@ -1,4 +1,4 @@ -[info] quarkus application test mode build dependency tree: +[info] Quarkus application TEST mode build dependency tree: [info] io.quarkus.bootstrap.test:test-app:pom:1 [info] ├─ io.quarkus.bootstrap.test:artifact-with-classifier:jar:classifier:1 (compile) [info] ├─ io.quarkus.bootstrap.test:test-ext2-deployment:jar:1 (compile) diff --git a/docs/src/main/asciidoc/aws-lambda-http.adoc b/docs/src/main/asciidoc/aws-lambda-http.adoc index 517a31bf99545..158eaefd97739 100644 --- a/docs/src/main/asciidoc/aws-lambda-http.adoc +++ b/docs/src/main/asciidoc/aws-lambda-http.adoc @@ -265,7 +265,7 @@ Do not change the Lambda handler name. ---- Properties: Handler: io.quarkus.amazon.lambda.runtime.QuarkusStreamHandler::handleRequest - Runtime: java11 + Runtime: java17 ---- This handler is a bridge between the lambda runtime and the Quarkus HTTP framework you are using (Jakarta REST, Servlet, etc.) diff --git a/docs/src/main/asciidoc/cassandra.adoc b/docs/src/main/asciidoc/cassandra.adoc index 4168a3d906050..42cb2c8ba61f0 100644 --- a/docs/src/main/asciidoc/cassandra.adoc +++ b/docs/src/main/asciidoc/cassandra.adoc @@ -317,7 +317,7 @@ public class FruitDto { The translation to and from JSON is done automatically by the Quarkus REST (formerly RESTEasy Reactive) extension, which is included in this guide's pom.xml file. If you want to add it manually to your application, add the -below snippet to your application's ppm.xml file: +below snippet to your application's pom.xml file: [source,xml] ---- diff --git a/docs/src/main/asciidoc/config-reference.adoc b/docs/src/main/asciidoc/config-reference.adoc index f4f36dcdcb925..b5a5b0f87ef21 100644 --- a/docs/src/main/asciidoc/config-reference.adoc +++ b/docs/src/main/asciidoc/config-reference.adoc @@ -341,6 +341,7 @@ With the `dev` profile enabled, the property `bar` has the value `hallo`, but th `bonjour`. If the `prod` profile is enabled, `bar` has the value `hello` (as there is no specific value for the `prod` profile), and `baz` the value `bonjour`. +[[default-profiles]] === Default Profiles By default, Quarkus provides three profiles, that activate automatically in certain conditions: diff --git a/docs/src/main/asciidoc/config-yaml.adoc b/docs/src/main/asciidoc/config-yaml.adoc index 074132c8bc895..8d420476d9517 100644 --- a/docs/src/main/asciidoc/config-yaml.adoc +++ b/docs/src/main/asciidoc/config-yaml.adoc @@ -79,7 +79,8 @@ quarkus: ---- quarkus: datasource: - url: jdbc:postgresql://localhost:5432/quarkus_test + jdbc: + url: jdbc:postgresql://localhost:5432/quarkus_test hibernate-orm: database: diff --git a/docs/src/main/asciidoc/deploying-to-openshift.adoc b/docs/src/main/asciidoc/deploying-to-openshift.adoc index 09eb23d61ae75..b4500631fa757 100644 --- a/docs/src/main/asciidoc/deploying-to-openshift.adoc +++ b/docs/src/main/asciidoc/deploying-to-openshift.adoc @@ -86,7 +86,7 @@ You can trigger a build and deployment in a single step or build the container i To trigger a build and deployment in a single step: -:build-additional-parameters: -Dquarkus.kubernetes.deploy=true +:build-additional-parameters: -Dquarkus.openshift.deploy=true include::{includes}/devtools/build.adoc[] :!build-additional-parameters: diff --git a/docs/src/main/asciidoc/dev-services.adoc b/docs/src/main/asciidoc/dev-services.adoc index f8aea5e5c5f14..01fd6fdf4e77b 100644 --- a/docs/src/main/asciidoc/dev-services.adoc +++ b/docs/src/main/asciidoc/dev-services.adoc @@ -6,27 +6,56 @@ https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc = Dev Services Overview include::_attributes.adoc[] :categories: core -:summary: A list of all extensions that support Dev Services and their configuration options. +:summary: An introduction to dev services and a list of all extensions that support Dev Services and their configuration options. :topics: dev-services,dev-mode,testing +== What Are Dev Services? + Quarkus supports the automatic provisioning of unconfigured services in development and test mode. We refer to this capability -as Dev Services. From a developer's perspective this means that if you include an extension and don't configure it then +as Dev Services. If you include an extension and don't configure it then Quarkus will automatically start the relevant service (usually using https://www.testcontainers.org/[Testcontainers] behind the scenes) and wire up your application to use this service. +For a tutorial showing how to get started writing an application with persistence and Dev Services, see xref:getting-started-dev-services.adoc[Your Second Quarkus Application]. + + +== Using Dev Services + +Dev Services are designed to be frictionless, so they will be automatically started any time you include an extension which supports +Dev Services, as long as you don't configure a connection to an external service. + +NOTE: In order to use most Dev Services you will need a working container environment (remote environments are supported). +If you don't have a container environment, such as Docker or Podman, installed you will need to configure your services normally. + +The default startup timeout for Dev Services is 60s, if this is not enough you can increase it with the `quarkus.devservices.timeout` property. + +To configure a production service but continue to use Dev Services in development and test modes, use xref:config-reference.adoc#default-profiles[configuration profiles]. + +For example, + +[source, properties] +---- +# configure your datasource +%prod.quarkus.datasource.db-kind = postgresql +%prod.quarkus.datasource.username = prod-admin +%prod.quarkus.datasource.password = super-secret +%prod.quarkus.datasource.jdbc.url = jdbc:postgresql://localhost:5432/mydatabase +---- + +== Disabling Dev Services + All this functionality is part of the Quarkus `deployment` modules, so does not affect the production application in any way. If you want to disable all Dev Services you can use the `quarkus.devservices.enabled=false` config property, although in most cases this is not necessary as simply configuring the service will result in the Dev Service being disabled automatically. -Note that the default startup timeout is 60s, if this is not enough you can increase it with the `quarkus.devservices.timeout` property. -This page lists all the Dev Services that Quarkus supports. +== Platform Dev Services + +This section lists all the Dev Services available in the Quarkus Platform. -NOTE: In order to use Dev Services you will generally need a working Docker environment (remote environments are supported). -If you don't have Docker installed you will need to configure your services normally. -== AMQP +=== AMQP The AMQP Dev Service will be enabled when the `quarkus-messaging-amqp` extension is present in your application, and the broker address has not been explicitly configured. More information can be found in the @@ -34,7 +63,7 @@ xref:amqp-dev-services.adoc[AMQP Dev Services Guide]. include::{generated-dir}/config/quarkus-smallrye-reactivemessaging-amqp-config-group-amqp-dev-services-build-time-config.adoc[opts=optional, leveloffset=+1] -== Apicurio Registry +=== Apicurio Registry The Apicurio Dev Service will be enabled when the `quarkus-apicurio-registry-avro` extension is present in your application, and it's address has not been explicitly configured. More information can be found in the @@ -42,7 +71,7 @@ xref:apicurio-registry-dev-services.adoc[Apicurio Registry Dev Services Guide]. include::{generated-dir}/config/quarkus-apicurio-registry-devservices-apicurio-registry-devservice-apicurio-registry-dev-services-build-time-config.adoc[opts=optional, leveloffset=+1] -== Databases +=== Databases The database Dev Services will be enabled when a reactive or JDBC datasource extension is present in the application, and the database URL has not been configured. More information can be found in the @@ -60,7 +89,7 @@ N.B. if you opt in for this feature, Quarkus will not reset the state of the dat include::{generated-dir}/config/quarkus-datasource-config-group-dev-services-build-time-config.adoc[opts=optional, leveloffset=+1] -== Kafka +=== Kafka The Kafka Dev Service will be enabled when the `quarkus-kafka-client` extension is present in your application, and the broker address has not been explicitly configured. More information can be found in the @@ -68,7 +97,7 @@ xref:kafka-dev-services.adoc[Kafka Dev Services Guide]. include::{generated-dir}/config/quarkus-kafka-client-config-group-kafka-dev-services-build-time-config.adoc[opts=optional, leveloffset=+1] -== Keycloak +=== Keycloak The Keycloak Dev Service will be enabled when the `quarkus-oidc` extension is present in your application, and the server address has not been explicitly configured. More information can be found in the @@ -76,7 +105,7 @@ xref:security-openid-connect-dev-services.adoc[OIDC Dev Services Guide]. include::{generated-dir}/config/quarkus-keycloak-devservices-keycloak-keycloak-build-time-config.adoc[opts=optional, leveloffset=+1] -== Kubernetes +=== Kubernetes The Kubernetes Dev Service will be enabled when `kubernetes-client` extension is present in your application, and the API server address has not been explicitly configured. More information can be found in the @@ -84,7 +113,7 @@ xref:kubernetes-dev-services.adoc[Kubernetes Dev Services Guide]. include::{generated-dir}/config/quarkus-kubernetes-client-config-group-kubernetes-dev-services-build-time-config.adoc[opts=optional, leveloffset=+1] -== MongoDB +=== MongoDB The MongoDB Dev Service will be enabled when the `quarkus-mongodb-client` extension is present in your application, and the server address has not been explicitly configured. More information can be found in the @@ -92,7 +121,7 @@ xref:mongodb.adoc#dev-services[MongoDB Guide]. include::{generated-dir}/config/quarkus-mongodb-config-group-dev-services-build-time-config.adoc[opts=optional, leveloffset=+1] -== RabbitMQ +=== RabbitMQ The RabbitMQ Dev Service will be enabled when the `quarkus-messaging-rabbitmq` extension is present in your application, and the broker address has not been explicitly configured. More information can be found in the @@ -100,13 +129,13 @@ xref:rabbitmq-dev-services.adoc[RabbitMQ Dev Services Guide]. include::{generated-dir}/config/quarkus-smallrye-reactivemessaging-rabbitmq-config-group-rabbit-mq-dev-services-build-time-config.adoc[opts=optional, leveloffset=+1] -== Pulsar +=== Pulsar The Pulsar Dev Service will be enabled when the `quarkus-messaging-pulsar` extension is present in your application, and the broker address has not been explicitly configured. More information can be found in the xref:pulsar-dev-services.adoc[Pulsar Dev Services Guide]. -== Redis +=== Redis The Redis Dev Service will be enabled when the `quarkus-redis-client` extension is present in your application, and the server address has not been explicitly configured. More information can be found in the @@ -114,19 +143,13 @@ xref:redis-dev-services.adoc[Redis Dev Services Guide]. include::{generated-dir}/config/quarkus-redis-config-group-client-dev-services-config.adoc[opts=optional, leveloffset=+1] -== Vault +=== Vault The Vault Dev Service will be enabled when the `quarkus-vault` extension is present in your application, and the server address has not been explicitly configured. More information can be found in the link:{vault-guide}#dev-services[Vault Guide]. -== Neo4j - -The Neo4j Dev Service will be enabled when the `quarkus-neo4j` extension is present in your application, and -the server address has not been explicitly configured. More information can be found in the -link:{neo4j-guide}#dev-services[Neo4j Guide]. - -== Infinispan +=== Infinispan The Infinispan Dev Service will be enabled when the `quarkus-infinispan-client` extension is present in your application, and the server address has not been explicitly configured. More information can be found in the @@ -134,10 +157,32 @@ xref:infinispan-dev-services.adoc[Infinispan Dev Services Guide]. include::{generated-dir}/config/quarkus-infinispan-client-config-group-infinispan-client-build-time-config-dev-service-configuration.adoc[opts=optional, leveloffset=+1] -== Elasticsearch +=== Elasticsearch The Elasticsearch Dev Service will be enabled when one of the Elasticsearch based extensions (Elasticsearch client or Hibernate Search ORM Elasticsearch) is present in your application, and the server address has not been explicitly configured. More information can be found in the xref:elasticsearch-dev-services.adoc[Elasticsearch Dev Services Guide]. include::{generated-dir}/config/quarkus-elasticsearch-devservices-elasticsearch-dev-services-build-time-config.adoc[opts=optional, leveloffset=+1] + +== Dev Services beyond the Quarkus Platform + +Many Quarkiverse extensions which are not in the Quarkus Platform also offer Dev Services. + +Here are some highlights. + +=== Neo4j + +The Neo4j Dev Service will be enabled when the `quarkus-neo4j` extension is present in your application, and +the server address has not been explicitly configured. More information can be found in the +link:{neo4j-guide}#dev-services[Neo4j Guide]. + +=== WireMock + +The WireMock extension starts WireMock as a Dev Service. It is a test-focussed extension, designed to run in dev and test mode only. +More information can be found in the https://docs.quarkiverse.io/quarkus-wiremock/dev/index.html[WireMock Guide]. + +=== Microcks + +The Microcks Quarkus extension includes a Microcks Dev Service. The Dev Service manages mocks for dependencies and contract-testing your API endpoints. +See the extension https://github.com/microcks/microcks-quarkus[README.md] for more information. \ No newline at end of file diff --git a/docs/src/main/asciidoc/http-reference.adoc b/docs/src/main/asciidoc/http-reference.adoc index 6f90ed502831c..5201ebbc4df5e 100644 --- a/docs/src/main/asciidoc/http-reference.adoc +++ b/docs/src/main/asciidoc/http-reference.adoc @@ -21,7 +21,9 @@ For Servlet support, Quarkus employs a customized Undertow version that operates When Undertow is present, RESTEasy functions as a Servlet filter. In its absence, RESTEasy operates directly on Vert.x without involving Servlets. -== Serving Static Resources +== Serving static resources + +If you are looking to use Quarkus for a web application, look at the xref:web.adoc[Quarkus for the Web] guide. === From the application jar @@ -30,6 +32,36 @@ was chosen as it is the standard location for resources in `jar` files as define Quarkus can be used without Servlet, following this convention allows existing code that places its resources in this location to function correctly. +[[from-mvnpm]] +=== From mvnpm + +If you are using https://mvnpm.org/[mvnpm], as for the following JQuery dependency: + +[source,xml,role="primary asciidoc-tabs-target-sync-cli asciidoc-tabs-target-sync-maven"] +.pom.xml +---- + + org.mvnpm + bootstrap + 5.3.3 + runtime + +---- + +[source,gradle,role="secondary asciidoc-tabs-target-sync-gradle"] +.build.gradle +---- +runtimeOnly("org.mvnpm:bootstrap:5.3.3") +---- + +You can import it in your HTML like this: +[source,html] +---- + +---- + + +[[from-webjars]] === From WebJars If you are using webjars, like the following JQuery one: diff --git a/docs/src/main/asciidoc/images/web-bundle-transition.png b/docs/src/main/asciidoc/images/web-bundle-transition.png new file mode 100644 index 0000000000000..def6da4167c26 Binary files /dev/null and b/docs/src/main/asciidoc/images/web-bundle-transition.png differ diff --git a/docs/src/main/asciidoc/javascript/config.js b/docs/src/main/asciidoc/javascript/config.js index 8761d8f9c1fdc..e72b1c18a794e 100644 --- a/docs/src/main/asciidoc/javascript/config.js +++ b/docs/src/main/asciidoc/javascript/config.js @@ -270,13 +270,15 @@ function makeCollapsibleHandler(descDiv, td, row, if( isCollapsed ) { collapsibleSpan.childNodes.item(0).nodeValue = 'Show less'; iconDecoration.classList.replace('fa-chevron-down', 'fa-chevron-up'); + descDiv.classList.remove('description-collapsed'); + descDiv.classList.add('description-expanded'); } else { collapsibleSpan.childNodes.item(0).nodeValue = 'Show more'; iconDecoration.classList.replace('fa-chevron-up', 'fa-chevron-down'); + descDiv.classList.add('description-collapsed'); + descDiv.classList.remove('description-expanded'); } - descDiv.classList.toggle('description-collapsed'); - descDiv.classList.toggle('description-expanded'); row.classList.toggle('row-collapsed'); }; } diff --git a/docs/src/main/asciidoc/kafka.adoc b/docs/src/main/asciidoc/kafka.adoc index a9d105aa244ab..ff532b0c198ab 100644 --- a/docs/src/main/asciidoc/kafka.adoc +++ b/docs/src/main/asciidoc/kafka.adoc @@ -1017,7 +1017,7 @@ In this case the producer will use this method as generator to create an infinit @Outgoing("prices-out") CompletionStage> generate(); ---- -=== Sending messages with @Emitter +=== Sending messages with Emitter Sometimes, you need to have an imperative way of sending messages. diff --git a/docs/src/main/asciidoc/logging.adoc b/docs/src/main/asciidoc/logging.adoc index 0472c66f03973..4c0068b89ac8c 100644 --- a/docs/src/main/asciidoc/logging.adoc +++ b/docs/src/main/asciidoc/logging.adoc @@ -74,6 +74,7 @@ The same flow can be applied with any of the <>. - -Configure the runtime logging in the `application.properties` file. +JBoss Logging, integrated into Quarkus, offers a unified configuration for all <> through a single configuration file that sets up all available extensions. +To adjust runtime logging, modify the `application.properties` file. .An example of how you can set the default log level to `INFO` logging and include Hibernate `DEBUG` logs: [source, properties] @@ -347,9 +349,9 @@ The logging format string supports the following symbols: |%t|Thread name|Render the thread name. |%t{id}|Thread ID|Render the thread ID. |%z{}|Time zone|Set the time zone of the output to ``. -|%X{}|Mapped Diagnostic Context Value|Renders the value from Mapped Diagnostic Context -|%X|Mapped Diagnostic Context Values|Renders all the values from Mapped Diagnostic Context in format {property.key=property.value} -|%x|Nested Diagnostics context values|Renders all the values from Nested Diagnostics Context in format {value1.value2} +|%X{}|Mapped Diagnostic Context Value|Renders the value from Mapped Diagnostic Context. +|%X|Mapped Diagnostic Context Values|Renders all the values from Mapped Diagnostic Context in format `{property.key=property.value}`. +|%x|Nested Diagnostics context values|Renders all the values from Nested Diagnostics Context in format `{value1.value2}`. |=== @@ -364,8 +366,8 @@ Changing the console log format is useful, for example, when the console output The `quarkus-logging-json` extension may be employed to add support for the JSON logging format and its related configuration. -Add this extension to your build file as the following snippet illustrates: - +. Add this extension to your build file as the following snippet illustrates: ++ [source,xml,role="primary asciidoc-tabs-target-sync-cli asciidoc-tabs-target-sync-maven"] .pom.xml ---- @@ -374,20 +376,21 @@ Add this extension to your build file as the following snippet illustrates: quarkus-logging-json ---- - ++ [source,gradle,role="secondary asciidoc-tabs-target-sync-gradle"] .build.gradle ---- implementation("io.quarkus:quarkus-logging-json") ---- - ++ By default, the presence of this extension replaces the output format configuration from the console configuration, and the format string and the color settings (if any) are ignored. The other console configuration items, including those controlling asynchronous logging and the log level, will continue to be applied. - ++ For some, it will make sense to use humanly readable (unstructured) logging in dev mode and JSON logging (structured) in production mode. This can be achieved using different profiles, as shown in the following configuration. - -.Disable JSON logging in application.properties for dev and test mode ++ +. Disable JSON logging in application.properties for dev and test mode: ++ [source, properties] ---- %dev.quarkus.log.console.json=false @@ -513,6 +516,8 @@ To register a logging filter: .An example of writing a filter: [source,java] ---- +package com.example; + import io.quarkus.logging.LoggingFilter; import java.util.logging.Filter; import java.util.logging.LogRecord; diff --git a/docs/src/main/asciidoc/opentelemetry.adoc b/docs/src/main/asciidoc/opentelemetry.adoc index d6422cddce96f..8616bad99244c 100644 --- a/docs/src/main/asciidoc/opentelemetry.adoc +++ b/docs/src/main/asciidoc/opentelemetry.adoc @@ -628,7 +628,7 @@ The exporter is automatically wired with CDI, that's why the `quarkus.otel.trace The `quarkus.otel.exporter.otlp.traces.protocol` default to `grpc` and `http/protobuf` can also be used. === On Quarkiverse -Additional exporters will be available in the Quarkiverse https://github.com/quarkiverse/quarkus-opentelemetry-exporter/blob/main/README.md[quarkus-opentelemetry-exporter] project. +Additional exporters will be available in the Quarkiverse https://docs.quarkiverse.io/quarkus-opentelemetry-exporter/dev/index.html[quarkus-opentelemetry-exporter] project. [[quarkus-extensions-using-opentelemetry]] == Quarkus core extensions instrumented with OpenTelemetry tracing diff --git a/docs/src/main/asciidoc/qute-reference.adoc b/docs/src/main/asciidoc/qute-reference.adoc index 5050754b75d3f..8b31a7c50f139 100644 --- a/docs/src/main/asciidoc/qute-reference.adoc +++ b/docs/src/main/asciidoc/qute-reference.adoc @@ -1659,7 +1659,7 @@ public class CustomLocator implements TemplateLocator { === Template Variants Sometimes it's useful to render a specific variant of the template based on the content negotiation. -This can be done by setting a special attribute via `TemplateInstance.setAttribute()`: +This can be done by setting a special attribute via `TemplateInstance.setVariant()`: [source,java] ---- @@ -1672,7 +1672,9 @@ class MyService { ItemManager manager; String renderItems() { - return items.data("items",manager.findItems()).setAttribute(TemplateInstance.SELECTED_VARIANT, new Variant(Locale.getDefault(),"text/html","UTF-8")).render(); + return items.data("items", manager.findItems()) + .setVariant(new Variant(Locale.getDefault(), "text/html", "UTF-8")) + .render(); } } ---- @@ -2436,9 +2438,11 @@ The `io.quarkus.qute.TemplateGlobal` annotation can be used to denote static fie Global variables are: -* added to the data map of any `TemplateInstance` during initialization, +* added as _computed data_ of any `TemplateInstance` during initialization, * accessible with the `global:` namespace. +NOTE: When using `TemplateInstance#computedData(String, Function)` a mapping function is associated with a specific key and this function is used each time a value for the given key is requested. In case of global variables, a static method is called or a static field is read in the mapping function. + .Global Variables Definition [source,java] ---- @@ -2827,7 +2831,7 @@ public class MyBean { Template hello; String render() { - return hello.instance().setAttribute("locale", Locale.forLanguageTag("cs")).render(); <1> + return hello.instance().setLocale("cs").render(); <1> } } ---- @@ -2854,6 +2858,40 @@ public class MyBean { ---- <1> The annotation value is a locale tag string (IETF). +===== Enums + +There is a convenient way to localize enums. +If there is a message bundle method that accepts a single parameter of an enum type and has no message template defined: + +[source,java] +---- +@Message <1> +String methodName(MyEnum enum); +---- +<1> The value is intentionally not provided. There's also no key for the method in a localized file. + +Then it receives a generated template: + +[source,html] +---- +{#when enumParamName} + {#is CONSTANT1}{msg:methodName_CONSTANT1} + {#is CONSTANT2}{msg:methodName_CONSTANT2} +{/when} +---- + +Furthermore, a special message method is generated for each enum constant. Finally, each localized file must contain keys and values for all constant message keys: + +[source,poperties] +---- +methodName_CONSTANT1=Value 1 +methodName_CONSTANT2=Value 2 +---- + +In a template, an enum constant can be localized with a message bundle method like `{msg:methodName(enumConstant)}`. + +TIP: There is also <> - a convenient annotation to access enum constants in a template. + ==== Message Templates Every method of a message bundle interface must define a message template. The value is normally defined by `io.quarkus.qute.i18n.Message#value()`, diff --git a/docs/src/main/asciidoc/qute.adoc b/docs/src/main/asciidoc/qute.adoc index e2947d7d4b57d..1a20c1d74f0fa 100644 --- a/docs/src/main/asciidoc/qute.adoc +++ b/docs/src/main/asciidoc/qute.adoc @@ -26,45 +26,70 @@ Clone the Git repository: `git clone {quickstarts-clone-url}`, or download an {q The solution is located in the `qute-quickstart` link:{quickstarts-tree-url}/qute-quickstart[directory]. -== Hello World with Jakarta REST +[[serving-templates]] +== Serving Qute templates via http -If you want to use Qute in your Jakarta REST application, you need to add an extension first: +If you want to serve your templates via http: + +1. The Qute Web extension allows you to directly serve via http templates located in `src/main/resource/templates/pub/`. In that case you don't need any Java code to "plug" the template, for example, the template `src/main/resource/templates/pub/foo.html` will be served from the paths `/foo` and `/foo.html` by default. +2. For finer control, you can combine it with Quarkus REST to control how your template will be served. All files located in the `src/main/resources/templates` directory and its subdirectories are registered as templates and can be injected in a REST resource. -* either `quarkus-rest-qute` if you are using Quarkus REST (formerly RESTEasy Reactive): -+ [source,xml,role="primary asciidoc-tabs-target-sync-cli asciidoc-tabs-target-sync-maven"] .pom.xml ---- - io.quarkus - quarkus-rest-qute + io.quarkiverse.qute.web + quarkus-qute-web ---- -+ + [source,gradle,role="secondary asciidoc-tabs-target-sync-gradle"] .build.gradle ---- -implementation("io.quarkus:quarkus-rest-qute") +implementation("io.quarkiverse.qute.web:quarkus-qute-web") +---- + +NOTE: The Qute Web extension while using the quarkiverse group-id, it is still part of the Quarkus platform. + +[[hello-qute-web]] +=== Serving Hello World with Qute + +Let's start with a Hello World template: + +.src/main/resources/templates/pub/hello.html +[source] +---- +

Hello {http:param('name', 'Quarkus')}!

<1> ---- +<1> `{http:param('name', 'Quarkus')}` is an expression that is evaluated when the template is rendered (Quarkus is the default value). + +NOTE: Templates located in the `pub` directory are served via HTTP. Automatically, no controllers needed. For example, the template src/main/resource/templates/pub/foo.html will be served from the paths /foo and /foo.html by default. + +If your application is running, you can open your browser and hit: http://localhost:8080/hello?name=Martin + +For more information about Qute Web options, see the https://docs.quarkiverse.io/quarkus-qute-web/dev/index.html[Qute Web guide]. + +[[hello-qute-rest]] +=== Hello Qute and REST + +For finer control, you can combine Qute Web with Quarkus REST or Quarkus RESTEasy to control how your template will be served -* or `quarkus-resteasy-qute` if you are using RESTEasy Classic: -+ [source,xml,role="primary asciidoc-tabs-target-sync-cli asciidoc-tabs-target-sync-maven"] .pom.xml ---- io.quarkus - quarkus-resteasy-qute + quarkus-rest ---- -+ + [source,gradle,role="secondary asciidoc-tabs-target-sync-gradle"] .build.gradle ---- -implementation("io.quarkus:quarkus-resteasy-qute") +implementation("io.quarkus:quarkus-rest") ---- -We'll start with a very simple template: +A very simple text template: .hello.txt [source] @@ -73,8 +98,6 @@ Hello {name}! <1> ---- <1> `{name}` is a value expression that is evaluated when the template is rendered. -NOTE: By default, all files located in the `src/main/resources/templates` directory and its subdirectories are registered as templates. Templates are validated during startup and watched for changes in the development mode. - Now let's inject the "compiled" template in the resource class. .HelloResource.java diff --git a/docs/src/main/asciidoc/rest-client.adoc b/docs/src/main/asciidoc/rest-client.adoc index 549b2fd6fa886..3f06f8291c7e0 100644 --- a/docs/src/main/asciidoc/rest-client.adoc +++ b/docs/src/main/asciidoc/rest-client.adoc @@ -325,106 +325,6 @@ public interface ExtensionsService { } ---- - -=== Using ClientMultipartForm - -MultipartForm can be built using the Class `ClientMultipartForm` which supports building the form as needed: - -`ClientMultipartForm` can be programmatically created with custom inputs and/or from `MultipartFormDataInput` and/or from custom Quarkus REST Input annotated with `@RestForm` if received. - -[source, java] ----- -public interface MultipartService { - - @POST - @Path("/multipart") - @Consumes(MediaType.MULTIPART_FORM_DATA) - @Produces(MediaType.APPLICATION_JSON) - Map multipart(ClientMultipartForm dataParts); // <1> -} ----- - -<1> input to the method is a custom Generic `ClientMultipartForm` which matches external application api contract. - - -More information about this Class and supported methods can be found on the javadoc of link:https://javadoc.io/doc/io.quarkus.resteasy.reactive/resteasy-reactive-client/latest/org/jboss/resteasy/reactive/client/api/ClientMultipartForm.html[`ClientMultipartForm`]. - - -Build `ClientMultipartForm` from `MultipartFormDataInput` programmatically - -[source, java] ----- -public ClientMultipartForm buildClientMultipartForm(MultipartFormDataInput inputForm) // <1> - throws IOException { - ClientMultipartForm multiPartForm = ClientMultipartForm.create(); // <2> - for (Entry> attribute : inputForm.getValues().entrySet()) { - for (FormValue fv : attribute.getValue()) { - if (fv.isFileItem()) { - final FileItem fi = fv.getFileItem(); - String mediaType = Objects.toString(fv.getHeaders().getFirst(HttpHeaders.CONTENT_TYPE), - MediaType.APPLICATION_OCTET_STREAM); - if (fi.isInMemory()) { - multiPartForm.binaryFileUpload(attribute.getKey(), fv.getFileName(), - Buffer.buffer(IOUtils.toByteArray(fi.getInputStream())), mediaType); // <3> - } else { - multiPartForm.binaryFileUpload(attribute.getKey(), fv.getFileName(), - fi.getFile().toString(), mediaType); // <4> - } - } else { - multiPartForm.attribute(attribute.getKey(), fv.getValue(), fv.getFileName()); // <5> - } - } - } - return multiPartForm; -} ----- - -<1> `MultipartFormDataInput` inputForm supported by Quarkus REST (Server). -<2> Creating a `ClientMultipartForm` object to populate with various dataparts. -<3> Adding InMemory `FileItem` to `ClientMultipartForm` -<4> Adding physical `FileItem` to `ClientMultipartForm` -<5> Adding any attribute directly to `ClientMultipartForm` if not `FileItem`. - -Build `ClientMultipartForm` from custom Attributes annotated with `@RestForm` - -[source, java] ----- -public class MultiPartPayloadFormData { // <1> - - @RestForm("files") - @PartType(MediaType.APPLICATION_OCTET_STREAM) - List files; - - @RestForm("jsonPayload") - @PartType(MediaType.TEXT_PLAIN) - String jsonPayload; -} - -/* - * Generate ClientMultipartForm from custom attributes annotated with @RestForm - */ -public ClientMultipartForm buildClientMultipartForm(MultiPartPayloadFormData inputForm) { // <1> - ClientMultipartForm multiPartForm = ClientMultipartForm.create(); - multiPartForm.attribute("jsonPayload", inputForm.getJsonPayload(), "jsonPayload"); // <2> - inputForm.getFiles().forEach(fu -> { - multiPartForm.binaryFileUpload("file", fu.name(), fu.filePath().toString(), fu.contentType()); // <3> - }); - return multiPartForm; -} ----- - -<1> `MultiPartPayloadFormData` custom Object created to match the API contract for calling service which needs to be converted to `ClientMultipartForm` -<2> Adding attribute `jsonPayload` directly to `ClientMultipartForm` -<3> Adding `FileUpload` objects to `ClientMultipartForm` as binaryFileUpload with contentType. - -[NOTE] -==== -When sending multipart data that uses the same name, problems can arise if the client and server do not use the same multipart encoder mode. -By default, the REST Client uses `RFC1738`, but depending on the situation, clients may need to be configured with `HTML5` or `RFC3986` mode. - -This configuration can be achieved via the `quarkus.rest-client.multipart-post-encoder-mode` property. -==== - === Sending large payloads The REST Client is capable of sending arbitrarily large HTTP bodies without buffering the contents in memory, if one of the following types is used: @@ -1486,8 +1386,6 @@ public interface EchoClient { [[multipart]] == Multipart Form support -REST Client support multipart messages. - === Sending Multipart messages REST Client allows sending data as multipart forms. This way you can for example @@ -1502,7 +1400,7 @@ To send data as a multipart form, you can just use the regular `@RestForm` (or ` String sendMultipart(@RestForm File file, @RestForm String otherField); ---- -Parameters specified as `File`, `Path`, `byte[]` or `Buffer` are sent as files and default to the +Parameters specified as `File`, `Path`, `byte[]`, `Buffer` or `FileUpload` are sent as files and default to the `application/octet-stream` MIME type. Other `@RestForm` parameter types default to the `text/plain` MIME type. You can override these defaults with the `@PartType` annotation. @@ -1523,7 +1421,7 @@ Naturally, you can also group these parameters into a containing class: String sendMultipart(Parameters parameters); ---- -Any `@RestForm` parameter of the type `File`, `Path`, `byte[]` or `Buffer`, as well as any +Any `@RestForm` parameter of the type `File`, `Path`, `byte[]`, `Buffer` or `FileUpload`, as well as any annotated with `@PartType` automatically imply a `@Consumes(MediaType.MULTIPART_FORM_DATA)` on the method if there is no `@Consumes` present. @@ -1554,6 +1452,105 @@ You can also send JSON multiparts by specifying the `@PartType` annotation: String sendMultipart(@RestForm @PartType(MediaType.APPLICATION_JSON) Person person); ---- +==== Programmatically creating the Multipart form + +In cases where the multipart content needs to be built up programmatically, the REST Client provides `ClientMultipartForm` which can be used in the REST Client like so: + +[source, java] +---- +public interface MultipartService { + + @POST + @Path("/multipart") + @Consumes(MediaType.MULTIPART_FORM_DATA) + @Produces(MediaType.APPLICATION_JSON) + Map multipart(ClientMultipartForm dataParts); +} +---- + + +More information about this class and supported methods can be found on the javadoc of link:https://javadoc.io/doc/io.quarkus.resteasy.reactive/resteasy-reactive-client/latest/org/jboss/resteasy/reactive/client/api/ClientMultipartForm.html[`ClientMultipartForm`]. + +===== Converting a received multipart object into a client request + +A good example of creating `ClientMultipartForm` is one where it is created from the server's `MultipartFormDataInput` (which represents a multipart request received by xref:rest.adoc#multipart[Quarkus REST]) - the purpose being to propagate the request downstream while allowing for arbitrary modifications: + +[source, java] +---- +public ClientMultipartForm buildClientMultipartForm(MultipartFormDataInput inputForm) // <1> + throws IOException { + ClientMultipartForm multiPartForm = ClientMultipartForm.create(); // <2> + for (Entry> attribute : inputForm.getValues().entrySet()) { + for (FormValue fv : attribute.getValue()) { + if (fv.isFileItem()) { + final FileItem fi = fv.getFileItem(); + String mediaType = Objects.toString(fv.getHeaders().getFirst(HttpHeaders.CONTENT_TYPE), + MediaType.APPLICATION_OCTET_STREAM); + if (fi.isInMemory()) { + multiPartForm.binaryFileUpload(attribute.getKey(), fv.getFileName(), + Buffer.buffer(IOUtils.toByteArray(fi.getInputStream())), mediaType); // <3> + } else { + multiPartForm.binaryFileUpload(attribute.getKey(), fv.getFileName(), + fi.getFile().toString(), mediaType); // <4> + } + } else { + multiPartForm.attribute(attribute.getKey(), fv.getValue(), fv.getFileName()); // <5> + } + } + } + return multiPartForm; +} +---- + +<1> `MultipartFormDataInput` is a Quarkus REST (Server) type representing a received multipart request. +<2> A `ClientMultipartForm` is created. +<3> `FileItem` attribute is created for the request attribute that represented an in memory file attribute +<4> `FileItem` attribute is created for the request attribute that represented a file attribute saved on the file system +<5> Non-file attributes added directly to `ClientMultipartForm` if not `FileItem`. + + +In a similar fashion if the received server multipart request is known and looks something like: + +[source, java] +---- +public class Request { // <1> + + @RestForm("files") + @PartType(MediaType.APPLICATION_OCTET_STREAM) + List files; + + @RestForm("jsonPayload") + @PartType(MediaType.TEXT_PLAIN) + String jsonPayload; +} +---- + +the `ClientMultipartForm` can be created easily as follows: + +[source, java] +---- +public ClientMultipartForm buildClientMultipartForm(Request request) { // <1> + ClientMultipartForm multiPartForm = ClientMultipartForm.create(); + multiPartForm.attribute("jsonPayload", request.getJsonPayload(), "jsonPayload"); // <2> + request.getFiles().forEach(fu -> { + multiPartForm.fileUpload(fu); // <3> + }); + return multiPartForm; +} +---- + +<1> `Request` representing the request the server parts accepts +<2> A `jsonPayload` attribute is added directly to `ClientMultipartForm` +<3> A `fileUpload` is created from the request's `FileUpload` + +[NOTE] +==== +When sending multipart data that uses the same name, problems can arise if the client and server do not use the same multipart encoder mode. +By default, the REST Client uses `RFC1738`, but depending on the situation, clients may need to be configured with `HTML5` or `RFC3986` mode. + +This configuration can be achieved via the `quarkus.rest-client.multipart-post-encoder-mode` property. +==== + === Receiving Multipart Messages REST Client also supports receiving multipart messages. As with sending, to parse a multipart response, you need to create a class that describes the response data, e.g. diff --git a/docs/src/main/asciidoc/rest.adoc b/docs/src/main/asciidoc/rest.adoc index 15469f285e738..968213774a87b 100644 --- a/docs/src/main/asciidoc/rest.adoc +++ b/docs/src/main/asciidoc/rest.adoc @@ -2608,7 +2608,7 @@ public class CheeseBodyHandler implements MessageBodyReader, } ---- -If you want to get the most performance our of your writer, you can extend the +If you want to get the most performance out of your writer, you can extend the link:{resteasy-reactive-api}/org/jboss/resteasy/reactive/server/spi/ServerMessageBodyWriter.html[`ServerMessageBodyWriter`] instead of link:{jaxrsapi}/jakarta/ws/rs/ext/MessageBodyWriter.html[`MessageBodyWriter`] where you will be able to use less reflection and bypass the blocking IO layer: diff --git a/docs/src/main/asciidoc/security-jdbc.adoc b/docs/src/main/asciidoc/security-jdbc.adoc index 7a3e8a906f15d..fba33d27df325 100644 --- a/docs/src/main/asciidoc/security-jdbc.adoc +++ b/docs/src/main/asciidoc/security-jdbc.adoc @@ -209,8 +209,6 @@ quarkus.security.jdbc.enabled=true quarkus.security.jdbc.principal-query.sql=SELECT u.password, u.role FROM test_user u WHERE u.username=? <1> quarkus.security.jdbc.principal-query.bcrypt-password-mapper.enabled=true <2> quarkus.security.jdbc.principal-query.bcrypt-password-mapper.password-index=1 -quarkus.security.jdbc.principal-query.bcrypt-password-mapper.salt-index=-1 -quarkus.security.jdbc.principal-query.bcrypt-password-mapper.iteration-count-index=-1 quarkus.security.jdbc.principal-query.attribute-mappings.0.index=2 <3> quarkus.security.jdbc.principal-query.attribute-mappings.0.to=groups ---- @@ -218,7 +216,7 @@ quarkus.security.jdbc.principal-query.attribute-mappings.0.to=groups The `elytron-security-jdbc` extension requires at least one principal query to authenticate the user and its identity. <1> We define a parameterized SQL statement (with exactly 1 parameter) which should return the user's password plus any additional information you want to load. -<2> We configure the password mapper with the position of the password field in the `SELECT` fields and other information like salt, hash encoding, etc. Setting the salt and iteration count indexes to `-1` is required for MCF. +<2> The password mapper is configured with the position of the password field in the `SELECT` fields. The hash is stored in the Modular Crypt Format (MCF) because the salt and iteration count indexes are set to `-1` by default. You can override them in order to decompose each element into three separate columns. <3> We use `attribute-mappings` to bind the `SELECT` projection fields (i.e. `u.role` here) to the target Principal representation attributes. [NOTE] @@ -311,8 +309,6 @@ quarkus.security.jdbc.enabled=true quarkus.security.jdbc.principal-query.sql=SELECT u.password FROM test_user u WHERE u.username=? quarkus.security.jdbc.principal-query.bcrypt-password-mapper.enabled=true quarkus.security.jdbc.principal-query.bcrypt-password-mapper.password-index=1 -quarkus.security.jdbc.principal-query.bcrypt-password-mapper.salt-index=-1 -quarkus.security.jdbc.principal-query.bcrypt-password-mapper.iteration-count-index=-1 quarkus.security.jdbc.principal-query.roles.sql=SELECT r.role_name FROM test_role r, test_user_role ur WHERE ur.username=? AND ur.role_id = r.id quarkus.security.jdbc.principal-query.roles.datasource=permissions 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 2a02b3292ca03..4d764c915a9e9 100644 --- a/docs/src/main/asciidoc/security-oidc-bearer-token-authentication.adoc +++ b/docs/src/main/asciidoc/security-oidc-bearer-token-authentication.adoc @@ -470,6 +470,66 @@ quarkus.oidc.introspection-path=/protocol/openid-connect/tokens/introspect For information about bearer access token propagation to the downstream services, see the xref:security-openid-connect-client-reference.adoc#token-propagation[Token propagation] section of the Quarkus "OpenID Connect (OIDC) and OAuth2 client and filters reference" guide. +=== JWT token certificate chain + +In some cases, JWT bearer tokens have an `x5c` header which represents an X509 certificate chain whose leaf certificate contains a public key that must be used to verify this token's signature. +Before this public key can be accepted to verify the signature, the certificate chain must be validated first. +The certificate chain validation involves several steps: + +1. Confirm that every certificate but the root one is signed by the parent certificate. + +2. Confirm the chain's root certificate is also imported in the truststore. + +3. Validate the chain's leaf certificate. If a common name of the leaf certificate is configured then a common name of the chain's leaf certificate must match it. Otherwise the chain's leaf certificate must also be avaiable in the truststore, unless one or more custom `TokenCertificateValidator` implementations are registered. + +4. `quarkus.oidc.TokenCertificateValidator` can be used to add a custom certificate chain validation step. It can be used by all tenants expecting tokens with the certificate chain or bound to specific OIDC tenants with the `@quarkus.oidc.TenantFeature` annotation. + +For example, here is how you can configure Quarkus OIDC to verify the token's certificate chain, without using `quarkus.oidc.TokenCertificateValidator`: + +[source,properties] +---- +quarkus.oidc.certificate-chain.trust-store-file=truststore-rootcert.p12 <1> +quarkus.oidc.certificate-chain.trust-store-password=storepassword +quarkus.oidc.certificate-chain.leaf-certificate-name=www.quarkusio.com <2> +---- +<1> The truststore must contain the certificate chain's root certificate. +<2> The certificate chain's leaf certificate must have a common name equal to `www.quarkusio.com`. If this property is not configured then the truststore must contain the certificate chain's leaf certificate unless one or more custom `TokenCertificateValidator` implementations are registered. + +You can add a custom certificate chain validation step by registering a custom `quarkus.oidc.TokenCertificateValidator`, for example: + +[source,java] +---- +package io.quarkus.it.keycloak; + +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.List; + +import jakarta.enterprise.context.ApplicationScoped; + +import io.quarkus.arc.Unremovable; +import io.quarkus.oidc.OidcTenantConfig; +import io.quarkus.oidc.TokenCertificateValidator; +import io.quarkus.oidc.runtime.TrustStoreUtils; +import io.vertx.core.json.JsonObject; + +@ApplicationScoped +@Unremovable +public class BearerGlobalTokenChainValidator implements TokenCertificateValidator { + + @Override + public void validate(OidcTenantConfig oidcConfig, List chain, String tokenClaims) throws CertificateException { + String rootCertificateThumbprint = TrustStoreUtils.calculateThumprint(chain.get(chain.size() - 1)); + JsonObject claims = new JsonObject(tokenClaims); + if (!rootCertificateThumbprint.equals(claims.getString("root-certificate-thumbprint"))) { <1> + throw new CertificateException("Invalid root certificate"); + } + } +} + +---- +<1> Confirm that the certificate chain's root certificate is bound to the custom JWT token's claim. + === OIDC provider client authentication `quarkus.oidc.runtime.OidcProviderClient` is used when a remote request to an OIDC provider is required. 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 299c83c7321fb..e2bda15eeca66 100644 --- a/docs/src/main/asciidoc/security-oidc-code-flow-authentication.adoc +++ b/docs/src/main/asciidoc/security-oidc-code-flow-authentication.adoc @@ -169,6 +169,15 @@ quarkus.oidc.credentials.jwt.secret-provider.key=mysecret-key quarkus.oidc.credentials.jwt.secret-provider.name=oidc-credentials-provider ---- +Example of `private_key_jwt` with the PEM key inlined in application.properties, and where the signature algorithm is `RS256`: + +[source,properties] +---- +quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus/ +quarkus.oidc.client-id=quarkus-app +quarkus.oidc.credentials.jwt.key=Base64-encoded private key representation +---- + Example of `private_key_jwt` with the PEM key file, and where the signature algorithm is RS256: [source,properties] @@ -689,7 +698,7 @@ You can disable token encryption in the session cookie by setting `quarkus.oidc. [[custom-token-state-manager]] ==== Session cookie and custom TokenStateManager -If you want to customize the way the tokens are associated with the session cookie, register a custom `io.quarkus.oidc.TokenStateManager' implementation as an `@ApplicationScoped` CDI bean. +If you want to customize the way the tokens are associated with the session cookie, register a custom `io.quarkus.oidc.TokenStateManager` implementation as an `@ApplicationScoped` CDI bean. For example, you might want to keep the tokens in a cache cluster and have only a key stored in a session cookie. Note that this approach might introduce some challenges if you need to make the tokens available across multiple microservices nodes. @@ -763,7 +772,7 @@ To use this feature, add the following extension to your project: :add-extension-extensions: oidc-db-token-state-manager include::{includes}/devtools/extension-add.adoc[] -This extension will replace the default `io.quarkus.oidc.TokenStateManager' with a database-based one. +This extension will replace the default `io.quarkus.oidc.TokenStateManager` with a database-based one. OIDC Database Token State Manager uses a Reactive SQL client under the hood to avoid blocking because the authentication is likely to happen on an IO thread. @@ -1102,8 +1111,12 @@ To support the integration with such OAuth2 servers, `quarkus-oidc` needs to be [NOTE] ==== Even though you configure the extension to support the authorization code flows without `IdToken`, an internal `IdToken` is generated to standardize the way `quarkus-oidc` operates. -You use an `IdToken` to support the authentication session and to avoid redirecting the user to the provider, such as GitHub, on every request. -In this case, the session lifespan is set to 5 minutes, which you can extend further as described in the <> section. +You use an internal `IdToken` to support the authentication session and to avoid redirecting the user to the provider, such as GitHub, on every request. +In this case, the `IdToken` age is set to the value of a standard `expires_in` property in the authorization code flow response. +You can use a `quarkus.oidc.authentication.internal-id-token-lifespan`property to customize the ID token age. +The default ID token age is 5 minutes. + +, which you can extend further as described in the <> section. This simplifies how you handle an application that supports multiple OIDC providers. ==== @@ -1119,13 +1132,13 @@ For GitHub, since it does not have an introspection endpoint, requesting the Use [NOTE] ==== Requiring <> involves making a remote call on every request. -Therefore, you might want to consider caching `UserInfo` data. + +Therefore, `UserInfo` is embedded in the internal generated `IdToken` and saved in the encrypted session cookie. It can be disabled with `quarkus.oidc.cache-user-info-in-idtoken=false`. + +Alternatively, you might want to consider caching `UserInfo` using a default or custom UserInfo cache provider. For more information, see the xref:security-oidc-bearer-token-authentication.adoc#token-introspection-userinfo-cache[Token Introspection and UserInfo cache] section of the "OpenID Connect (OIDC) Bearer token authentication" guide. -Alternatively, you might want to request that `UserInfo` is embedded into the internal generated `IdToken` with the `quarkus.oidc.cache-user-info-in-idtoken=true` property. -The advantage of this approach is that, by default, no cached `UserInfo` state will be kept with the endpoint - instead it will be stored in a session cookie. -You might also want to consider encrypting `IdToken` in this case if `UserInfo` contains sensitive data. -For more information, see <>. +Most well-known social OAuth2 providers enforce rate-limiting so there is a high chance you will prefer to have UserInfo cached. ==== OAuth2 servers might not support a well-known configuration endpoint. 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 02b723f77e488..db214a15dbe15 100644 --- a/docs/src/main/asciidoc/security-openid-connect-client-reference.adoc +++ b/docs/src/main/asciidoc/security-openid-connect-client-reference.adoc @@ -410,7 +410,7 @@ import io.quarkus.oidc.client.runtime.TokensHelper; public class OidcClientResource { @Inject - OidcClientCreator clients; + OidcClientCreator oidcClientCreator; TokensHelper tokenHelper = new TokensHelper(); @Inject @@ -757,6 +757,15 @@ quarkus.oidc-client.credentials.jwt.secret-provider.key=mysecret-key quarkus.oidc-client.credentials.jwt.secret-provider.name=oidc-credentials-provider ---- +`private_key_jwt` with the PEM key inlined in application.properties, and where the signature algorithm is `RS256`: + +[source,properties] +---- +quarkus.oidc-client.auth-server-url=http://localhost:8180/auth/realms/quarkus/ +quarkus.oidc-client.client-id=quarkus-app +quarkus.oidc-client.credentials.jwt.key=Base64-encoded private key representation +---- + `private_key_jwt` with the PEM key file, signature algorithm is `RS256`: [source,properties] diff --git a/docs/src/main/asciidoc/security-openid-connect-multitenancy.adoc b/docs/src/main/asciidoc/security-openid-connect-multitenancy.adoc index 983073ba4453f..cb2ea8024881c 100644 --- a/docs/src/main/asciidoc/security-openid-connect-multitenancy.adoc +++ b/docs/src/main/asciidoc/security-openid-connect-multitenancy.adoc @@ -590,14 +590,20 @@ However, this time, you are going to authenticate by using a different realm. In both cases, the landing page shows the user's name and email if the user is successfully authenticated. Although `alice` exists in both tenants, the application treats them as distinct users in separate realms. +== Tenant resolution + [[tenant-resolution-order]] -== Tenant resolution order +=== Tenant resolution order OIDC tenants are resolved in the following order: -* `io.quarkus.oidc.Tenant` annotation is checked first if the proactive authentication is disabled. -* Dynamic tenant resolution using a custom `TenantConfigResolver`. -* Static tenant resolution using one of these options: custom `TenantResolver`, configured tenant paths, and defaulting to the last request path segment as a tenant id. -* Default OIDC tenant is selected if a tenant id has not been resolved after the preceeding steps. + +1. `io.quarkus.oidc.Tenant` annotation is checked first if the proactive authentication is disabled. + +2. Dynamic tenant resolution using a custom `TenantConfigResolver`. + +3. Static tenant resolution using one of these options: custom `TenantResolver`, configured tenant paths, and defaulting to the last request path segment as a tenant id. + +Finally, the default OIDC tenant is selected if a tenant id has not been resolved after the preceeding steps. See the following sections for more information: @@ -605,6 +611,8 @@ See the following sections for more information: * <> * <> +Additionally, for the OIDC `web-app` applications, the state and session cookies also provide a hint about the tenant resolved with one of the above mentioned options at the time when the authorization code flow started. See the <> section for more information. + [[annotations-tenant-resolver]] === Resolve with annotations @@ -657,7 +665,7 @@ quarkus.http.auth.permission.authenticated.applies-to=JAXRS <1> <1> Tell Quarkus to run the HTTP permission check after the tenant has been selected with the `@Tenant` annotation. [[tenant-config-resolver]] -== Dynamic tenant configuration resolution +=== Dynamic tenant configuration resolution If you need a more dynamic configuration for the different tenants you want to support and don't want to end up with multiple entries in your configuration file, you can use the `io.quarkus.oidc.TenantConfigResolver`. @@ -725,7 +733,7 @@ You can populate it by using any settings supported by the `quarkus-oidc` extens If the dynamic tenant resolver returns `null`, a <> is attempted next. [[static-tenant-resolution]] -== Static tenant configuration resolution +=== Static tenant configuration resolution 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: @@ -739,7 +747,7 @@ These tenant resolution options are tried in the order they are listed until the If the tenant id remains unresolved (`null`), the default (unnamed) tenant configuration is selected. [[tenant-resolver]] -=== Resolve with `TenantResolver` +==== Resolve with `TenantResolver` The following `application.properties` example shows how you can resolve the tenant identifier of two tenants named `a` and `b` by using the `TenantResolver` method: @@ -782,7 +790,7 @@ 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. [[configure-tenant-paths]] -=== Configure tenant paths +==== Configure tenant paths You can use the `quarkus.oidc.tenant-paths` configuration property for resolving the tenant identifier as an alternative to using `io.quarkus.oidc.TenantResolver`. Here is how you can select the `hr` tenant for the `sayHello` endpoint of the `HelloResource` resource used in the previous example: @@ -800,7 +808,7 @@ quarkus.oidc.b.tenant-paths=/*/hello <3> TIP: Path-matching mechanism works exactly same as in the xref:security-authorize-web-endpoints-reference.adoc#authorization-using-configuration[Authorization using configuration]. [[default-tenant-resolver]] -=== Default resolution +==== Use last request path segment as tenant id 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. @@ -840,7 +848,7 @@ Default resolution can also work for Bearer token authentication. Still, it might be less practical because a tenant identifier must always be set as the last path segment value. [[issuer-based-tenant-resolver]] -=== Resolve tenants with a token issuer claim +==== Resolve tenants with a token issuer claim OIDC tenants which support Bearer token authentication can be resolved using the access token's issuer. The following conditions must be met for the issuer-based resolution to work: @@ -870,6 +878,7 @@ quarkus.oidc.tenant-b.credentials.secret=${tenant-b-client-secret} <2> Tenant `tenant-a` discovers the `issuer` from the OIDC provider's well-known configuration endpoint. <3> Tenant `tenant-b` configures the `issuer` because its OIDC provider does not support the discovery. +[[tenant-resolution-for-web-app]] === Tenant resolution for OIDC web-app applications Tenant resolution for the OIDC `web-app` applications must be done at least 3 times during an authorization code flow, when the OIDC tenant-specific configuration affects how each of the following steps is run. diff --git a/docs/src/main/asciidoc/security-openid-connect-providers.adoc b/docs/src/main/asciidoc/security-openid-connect-providers.adoc index 0d997d668b268..d6df9101c8a95 100644 --- a/docs/src/main/asciidoc/security-openid-connect-providers.adoc +++ b/docs/src/main/asciidoc/security-openid-connect-providers.adoc @@ -739,19 +739,9 @@ Depending on your developer API subscription level, some providers may enforce a It may not be a problem when Quarkus fetches public verification keys from OIDC-compliant providers like the <> provider and keeps verifying the user session with these keys locally. However, for pure OAuth2 providers where only an access token is available and which has to be verified indirectly by requesting UserInfo from the provider endpoint on every request done by an already authenticated user, it can become a problem. -In such cases consider xref:security-oidc-bearer-token-authentication#token-introspection-userinfo-cache[caching UserInfo], using either a default or custom cache implementation or even embedding UserInfo in an internally generated ID token which is encrypted by default, for example: +Therefore, UserInfo is embedded in an internally generated ID token and is encrypted in the session cookie. You can disable it with `quarkus.oidc.cache-user-info-in-idtoken=false`. -[source,properties] ----- -quarkus.oidc.provider=x -quarkus.oidc.client-id= -quarkus.oidc.credentials.secret= -quarkus.oidc.authentication.extra-params.scope=tweet.write -quarkus.rest-client.twitter-client.url=https://api.twitter.com/2 - -quarkus.oidc.cache-user-info-in-idtoken=true <1> ----- -<1> Cache UserInfo by embedding it in the internally generated ID token +Alternatively, use a default or custom UserInfo cache provider, please see the xref:security-oidc-bearer-token-authentication#token-introspection-userinfo-cache[Token Introspection and UserInfo cache] section of the "OpenID Connect (OIDC) Bearer token authentication" guide. == References diff --git a/docs/src/main/asciidoc/smallrye-fault-tolerance.adoc b/docs/src/main/asciidoc/smallrye-fault-tolerance.adoc index 568489a3f8f84..ea0c8b6dc7aa2 100644 --- a/docs/src/main/asciidoc/smallrye-fault-tolerance.adoc +++ b/docs/src/main/asciidoc/smallrye-fault-tolerance.adoc @@ -503,7 +503,7 @@ implementation("io.quarkus:quarkus-smallrye-fault-tolerance") == Additional resources SmallRye Fault Tolerance has more features than shown here. -Please check the link:https://smallrye.io/docs/smallrye-fault-tolerance/6.2.6/index.html[SmallRye Fault Tolerance documentation] to learn about them. +Please check the link:https://smallrye.io/docs/smallrye-fault-tolerance/6.3.0/index.html[SmallRye Fault Tolerance documentation] to learn about them. In Quarkus, you can use the SmallRye Fault Tolerance optional features out of the box. @@ -535,7 +535,7 @@ smallrye.faulttolerance.mp-compatibility=true ---- ==== -The link:https://smallrye.io/docs/smallrye-fault-tolerance/6.2.6/reference/programmatic-api.html[programmatic API] is present, including Mutiny support, and integrated with the declarative, annotation-based API. +The link:https://smallrye.io/docs/smallrye-fault-tolerance/6.3.0/reference/programmatic-api.html[programmatic API] is present, including Mutiny support, and integrated with the declarative, annotation-based API. You can use the `FaultTolerance` and `MutinyFaultTolerance` APIs out of the box. Support for Kotlin is present (assuming you use the Quarkus extension for Kotlin), so you can guard your `suspend` functions with fault tolerance annotations. diff --git a/docs/src/main/asciidoc/smallrye-graphql-client.adoc b/docs/src/main/asciidoc/smallrye-graphql-client.adoc index de25e13e8ff09..4aa0a118e0d6b 100644 --- a/docs/src/main/asciidoc/smallrye-graphql-client.adoc +++ b/docs/src/main/asciidoc/smallrye-graphql-client.adoc @@ -200,6 +200,7 @@ public class Planet { Now that we have the model classes, we can create the interface that represents the actual set of operations we want to call on the remote GraphQL service. +[source,java] ---- @GraphQLClientApi(configKey = "star-wars-typesafe") public interface StarWarsClientApi { @@ -256,6 +257,18 @@ With this REST endpoint included in your application, you can simply send a GET and the application will use an injected typesafe client instance to call the remote service, obtain the films and planets, and return the JSON representation of the resulting list. +=== Logging + +For debugging purpose, it is possible to log the request generated by the typesafe client and the response sent back by the server by changing the log level of the `io.smallrye.graphql.client` category to `TRACE` (see the xref:logging.adoc#configure-the-log-level-category-and-format[Logging guide] for more details about how to configure logging). + +This can be achieved by adding the following lines to the `application.properties`: + +[source,properties] +---- +quarkus.log.category."io.smallrye.graphql.client".level=TRACE +quarkus.log.category."io.smallrye.graphql.client".min-level=TRACE +---- + == Using the Dynamic client For the dynamic client, the model classes are optional, because we can work with abstract diff --git a/docs/src/main/asciidoc/spring-data-jpa.adoc b/docs/src/main/asciidoc/spring-data-jpa.adoc index 201ae4df8ab29..cb41a8bc350d0 100644 --- a/docs/src/main/asciidoc/spring-data-jpa.adoc +++ b/docs/src/main/asciidoc/spring-data-jpa.adoc @@ -162,6 +162,8 @@ INSERT INTO fruit(id, name, color) VALUES (5, 'Strawberry', 'Red'); Hibernate ORM will execute these queries on application startup. +TIP: Users can also use a file named `data.sql` in addition to `import.sql` + == Define the repository It is now time to define the repository that will be used to access `Fruit`. diff --git a/docs/src/main/asciidoc/validation.adoc b/docs/src/main/asciidoc/validation.adoc index 60bbc6be1eb68..425d97b01fc7b 100644 --- a/docs/src/main/asciidoc/validation.adoc +++ b/docs/src/main/asciidoc/validation.adoc @@ -7,7 +7,7 @@ https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc include::_attributes.adoc[] :categories: web, data :summary: This guide covers how to use Hibernate Validator/Bean Validation in your REST services. -:topics: bean-validation,hibernate-validator,validation +:topics: bean-validation,hibernate-validator,validation,validator,constraints :extensions: io.quarkus:quarkus-hibernate-validator This guide covers how to use Hibernate Validator/Bean Validation for: diff --git a/docs/src/main/asciidoc/web.adoc b/docs/src/main/asciidoc/web.adoc new file mode 100644 index 0000000000000..1f58a62ef29fb --- /dev/null +++ b/docs/src/main/asciidoc/web.adoc @@ -0,0 +1,272 @@ +//// +This guide is maintained in the main Quarkus repository +and pull requests should be submitted there: +https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc +//// += Quarkus for the Web +include::_attributes.adoc[] +:categories: web +:summary: Learn more about creating all kinds of Web applications with Quarkus. +:numbered: +:sectnums: +:sectnumlevels: 3 +:topics: http,web,renarde,full-stack,qute,quinoa,web-bundler,mvc,ssr,nodejs,npm,javascript,css,jsf,faces +:extensions: io.quarkiverse.qute.web:quarkus-qute-web,io.quarkiverse.renarde:quarkus-renarde,io.quarkiverse.web-bundler:quarkus-web-bundler,io.quarkiverse.quinoa:quarkus-quinoa + +Quarkus provides several extensions to create web applications, this document aims to provide directions on which extension to use for different use cases. + +== The basics + +=== Serving static resources + +Let's assume you have a Quarkus backend, and you want to serve static files. This is the most basic case, it is supported out of the box with all our Vert.x based extensions, you must place them in the `META-INF/resources` directory of your application. + +You can find more information in the xref:http-reference#serving-static-resources[HTTP reference guide]. + +=== Serving scripts, styles, and web libraries + +However, if you want to insert scripts, styles, and libraries in your web pages, you have 3 options: + +a. Consume libraries from public CDNs such as cdnjs, unpkg, jsDelivr and more, or copy them to your `META-INF/resources` directory. +b. Use runtime web dependencies such as mvnpm.org or webjars, when added to your pom.xml or build.gradle they can be directly xref:http-reference#from-mvnpm[accessed from your web pages]. +c. Package your scripts (js, ts), styles (css, scss), and web dependencies together using a bundler (see xref:#bundling[below]). + +NOTE: *We recommend using a bundler for production* as it offers better control, consistency, security, and performance. The good news is that Quarkus makes it really easy and fast with the https://docs.quarkiverse.io/quarkus-web-bundler/dev/[Quarkus Web Bundler extension]. + +[[bundling]] +=== Bundling scripts, styles, and libraries + +There are two ways to bundle your web assets: + +a. Using https://docs.quarkiverse.io/quarkus-web-bundler/dev/[the Quarkus Web Bundler extension], which is the recommended way. Without any configuration, it puts everything together in an instant, and follows good practices such as dead-code elimination, minification, caching, and more. +b. Using a custom bundler such as Webpack, Parcel, Rollup, etc. This can be easily integrated with Quarkus using the https://quarkiverse.github.io/quarkiverse-docs/quarkus-quinoa/dev/[Quarkus Quinoa extension]. + +image::web-bundle-transition.png[Web Bundle Transition] + +== Server-side rendering (SSR) + +For templating and server-side rendering with Quarkus, there are different engines available such as xref:qute.adoc[Qute] or https://docs.quarkiverse.io/quarkus-freemarker/dev/[Freemarker] and others. + +=== Qute Web + +Qute is designed specifically to meet the Quarkus needs, and help you deal with templates, snippets, and partials and render the data from your storage. It is inspired by the most famous template engines, it is fast, type-safe, works in native, and has a lot of nice features. + +To install Qute Web, follow xref:qute.adoc#serving-templates[the instructions]. + +Here is a simple example of a Qute template: + +.src/main/resources/templates/pub/index.html +[source,html] +---- + + + + + Qute Page + {#bundle /} <1> + + +

Hello {http:param('name', 'Quarkus')}

<2> +
    + {#for item in cdi:Product.items} <3> +
  • {item.name} {#if item.active}{item.price}{/if}
  • <4> + {/for} +
+ + +---- + +<1> With the https://docs.quarkiverse.io/quarkus-web-bundler/dev/[Web Bundler extension], this expression will be replaced by the bundled scripts and styles. +<2> You can directly use the HTTP parameters in your templates. +<3> This expression is validated. Try to change the expression to `cdi:Product.notHere` and the build will fail. +<4> If you install xref:ide-tooling.adoc[Quarkus IDEs plugins], you will have autocompletion, link to implementation and validation. + +=== Model-View-Controller (MVC) + +The MVC approach is also made very easy with Quarkus thanks to https://docs.quarkiverse.io/quarkus-renarde/dev/index.html[the Renarde extension], a Rails-like framework using Qute. + +Associated with the https://docs.quarkiverse.io/quarkus-web-bundler/dev/[Web Bundler extension], the road is open to build modern web applications for all your needs. Here is what a simple Renarde controller looks like: + +.src/main/java/rest/Todos.java +[source,java] +---- +package rest; + +[...] + +public class Todos extends Controller { + + @CheckedTemplate + static class Templates { + public static native TemplateInstance index(List todos); + } + + public TemplateInstance index() { + // list every todo + List todos = Todo.listAll(); + // render the index template + return Templates.index(todos); + } + + @POST + public void add(@NotBlank @RestForm String task) { + // check if there are validation issues + if(validationFailed()) { + // go back to the index page + index(); + } + // create a new Todo + Todo todo = new Todo(); + todo.task = task; + todo.persist(); + // send loving message + flash("message", "Task added"); + // redirect to index page + index(); + } + + @POST + public void delete(@RestPath Long id) { + // find the Todo + Todo todo = Todo.findById(id); + notFoundIfNull(todo); + // delete it + todo.delete(); + // send loving message + flash("message", "Task deleted"); + // redirect to index page + index(); + } + + @POST + public void done(@RestPath Long id) { + // find the Todo + Todo todo = Todo.findById(id); + notFoundIfNull(todo); + // switch its done state + todo.done = !todo.done; + if(todo.done) + todo.doneDate = new Date(); + // send loving message + flash("message", "Task updated"); + // redirect to index page + index(); + } +} +---- + +== Single Page Applications + +Quarkus provides very solid tools for creating or integrating Single Page Applications to Quarkus (React, Angular, Vue, …), here are 3 options: + +* https://quarkiverse.github.io/quarkiverse-docs/quarkus-quinoa/dev/[Quarkus Quinoa] bridges your npm-compatible web application and Quarkus for both dev and prod. No need to install Node.js or configure your framework, it will detect it and use sensible defaults. +* The https://docs.quarkiverse.io/quarkus-web-bundler/dev/[Quarkus Web Bundler] is also a good approach, it is closer to the Java ecosystem and removes a lot of boilerplate and configuration, it is fast and efficient. For examples of such SPAs, see https://github.com/quarkusio/code.quarkus.io[code.quarkus.io] and https://github.com/mvnpm/mvnpm[mvnpm.org]. +* Your automation using the https://github.com/eirslett/frontend-maven-plugin[maven-frontend-plugin] or similar tools. + +== Full-stack microservices (Micro-frontends) + +Quarkus is an excellent choice for both full-stack web components and full-stack microservices aka Micro-frontends. By utilizing the Web Bundler or Quinoa, you can significantly reduce boilerplate code and manage multiple services efficiently without much configuration duplication. + +For example the https://github.com/quarkusio/search.quarkus.io[Quarkus documentation search engine] on https://quarkus.io[quarkus.io] uses the Web Bundler to create a full-stack web-component. With Lit Element for the web-component and OpenSearch for the indexation it is a nice way to enhance the static web-site experience in a dynamic way. + +More content about this is coming soon... +// Blog article in prep: https://github.com/quarkusio/quarkusio.github.io/issues/1934 + +== Other ways + +We described Quarkus most common ways to create web applications but there are other options: + +* https://quarkus.io/extensions/com.vaadin/vaadin-quarkus-extension/[Vaadin Flow extension], for this unique framework that lets you build web apps directly from Java code without writing HTML or JavaScript. +* JavaServer Faces (jsf) is a specification for building component-based web apps in Java. It available in Quarkus, the https://quarkus.io/extensions/org.apache.myfaces.core.extensions.quarkus/myfaces-quarkus/[MyFaces] extension is an implementation of Faces for Quarkus. https://quarkus.io/extensions/io.quarkiverse.primefaces/quarkus-primefaces/[PrimeFaces] is a Faces components suite, and https://quarkus.io/extensions/io.quarkiverse.omnifaces/quarkus-omnifaces/[OmniFaces], a utility library. More information is available in https://www.melloware.com/quarkus-faces-using-jsf-with-quarkus/[this blog post]. +* Create xref:building-my-first-extension.adoc[a new extension] for your favorite web framework. + +== Testing your web applications + +For testing web applications, https://docs.quarkiverse.io/quarkus-playwright/dev/[Quarkus Playwright] is very easy to use. You can create effective cross-browser end-to-end tests mimicking user interaction and making sure your web application is working as a whole. The big advantage is that it benefits from all dev-services and Quarkus mocking features. + +[source,java] +---- +@QuarkusTest +@WithPlaywright +public class WebApplicationTest { + + @InjectPlaywright + BrowserContext context; + + @TestHTTPResource("/") + URL index; + + @Test + public void testIndex() { + final Page page = context.newPage(); + Response response = page.navigate(index.toString()); + Assertions.assertEquals("OK", response.statusText()); + + page.waitForLoadState(); + + String title = page.title(); + Assertions.assertEquals("My Awesome App", title); + + // Make sure the web app is loaded and hits the backend + final ElementHandle quinoaEl = page.waitForSelector(".toast-body.received"); + String greeting = quinoaEl.innerText(); + Assertions.assertEquals("Hello from REST", greeting); + } +} +---- + +== Q&A + +=== Why is Quarkus a very good option for Web Applications compared to other frameworks? + +Quarkus is well known for its backend extensions ecosystem and developer experience, if you combine it with great extensions for frontend, then it is a perfect mix! All the testing and dev-mode features are now available for both frontend and backend. + +=== What are the advantages of SSR (Server Side Rendering) over SPA (Single Page App)? +Here are the benefits of performing rendering work on the server: + +*Data Retrieval:* Fetching data on the server, closer to the data source. This enhances performance by reducing the time needed to retrieve data for rendering and minimizes client requests. + +*Enhanced Security:* Storage of sensitive data and logic is happening on the server, such as tokens and API keys, without exposing them to client-side risks. + +*Caching Efficiency:* Server-side rendering allows for result caching, which can be reused across users and subsequent requests. This optimizes performance and lowers costs by reducing rendering and data fetching per request. + +*Improved Initial Page Load and First Contentful Paint (FCP):* Generating HTML on the server enables users to view the page immediately, eliminating the need to wait for client-side JavaScript to download, parse, and execute for rendering. + +*Search Engine Optimization (SEO) and Social Media Shareability:* The rendered HTML aids search engine indexing and social network previews, enhancing discoverability and shareability. + + +=== I am hesitating between Quinoa and the Web Bundler, how should I make my decision? + +You have to think that the bundled output is essentially the same with both solutions. Also, switching from one to the other is not a big deal, the choice is about the developer experience and finding the best fit for your team. + +Some guidelines: + +*Go for Quinoa:* + +* You have an existing frontend configured with a npm-compatible build tool, Quinoa is the most direct option. +* You have a dedicated frontend team familiar with tools such as NPM, Yarn and other for building Single Page Apps. +* You want to write Javascript unit tests (such as Jest, Jasmine, ..), it is not possible with the Web Bundler. However, you could publish a components library on NPM and consume it from the Web Bundler. +* You use very specific bundling options or specific tools in your build process +* You love package.json and configurations tweaking + +*Go for Web Bundler:* + +* For simple web applications, the Web Bundler is the easiest and fastest way to get started +* You prefer to stay close to the Maven/Gradle ecosystem +(Node.js is not needed), it uses an extremely fast bundler for the web (esbuild) +* You want to reduce boilerplate and configuration + + +=== How do I scale a Quarkus Web Application? + +Serving a few static pages and scripts from an existing Quarkus backend is not a big overhead, so scaling the full app is usually the simplest option. +You could also split it in two services: one for the backend and one for the frontend. However, in most cases, this approach wouldn’t yield substantial benefits compared to the initial method. + +If your application involves a substantial number of static resources, consider using a CDN. Both the Web Bundler and Quinoa can be configured to work seamlessly with a CDN, providing improved performance and distribution of assets. + +// It would be nice to have a blog article and benchmark about this topic. + + + + + diff --git a/docs/src/main/java/io/quarkus/docs/generation/AssembleDownstreamDocumentation.java b/docs/src/main/java/io/quarkus/docs/generation/AssembleDownstreamDocumentation.java index f2438f7f08a56..6f23fd809cd4c 100755 --- a/docs/src/main/java/io/quarkus/docs/generation/AssembleDownstreamDocumentation.java +++ b/docs/src/main/java/io/quarkus/docs/generation/AssembleDownstreamDocumentation.java @@ -61,6 +61,7 @@ public class AssembleDownstreamDocumentation { Pattern.CASE_INSENSITIVE + Pattern.MULTILINE); private static final String SOURCE_BLOCK_PREFIX = "[source"; private static final String SOURCE_BLOCK_DELIMITER = "--"; + private static final Pattern FOOTNOTE_PATTERN = Pattern.compile("footnote:([a-z0-9_-]+)\\[(\\])?"); private static final String PROJECT_NAME_ATTRIBUTE = "{project-name}"; private static final String RED_HAT_BUILD_OF_QUARKUS = "Red Hat build of Quarkus"; @@ -386,7 +387,7 @@ private static void copyAsciidoc(Path sourceFile, Path targetFile, Set d if (currentBuffer.length() > 0) { rewrittenGuide.append( - rewriteLinks(sourceFile.getFileName().toString(), currentBuffer.toString(), downstreamGuides, + rewriteContent(sourceFile.getFileName().toString(), currentBuffer.toString(), downstreamGuides, titlesByReference, linkRewritingErrors)); currentBuffer.setLength(0); } @@ -399,7 +400,7 @@ private static void copyAsciidoc(Path sourceFile, Path targetFile, Set d if (currentBuffer.length() > 0) { rewrittenGuide.append( - rewriteLinks(sourceFile.getFileName().toString(), currentBuffer.toString(), downstreamGuides, + rewriteContent(sourceFile.getFileName().toString(), currentBuffer.toString(), downstreamGuides, titlesByReference, linkRewritingErrors)); } @@ -413,7 +414,7 @@ private static void copyAsciidoc(Path sourceFile, Path targetFile, Set d Files.writeString(targetFile, rewrittenGuideWithoutTabs.trim()); } - private static String rewriteLinks(String fileName, + private static String rewriteContent(String fileName, String content, Set downstreamGuides, Map titlesByReference, @@ -454,6 +455,14 @@ private static String rewriteLinks(String fileName, return "[[" + mr.group(1) + "]]"; }); + content = FOOTNOTE_PATTERN.matcher(content).replaceAll(mr -> { + if (mr.group(2) != null) { + return "footnoteref:[" + mr.group(1) + "]"; + } + + return "footnoteref:[" + mr.group(1) + ", "; + }); + return content; } diff --git a/extensions/amazon-lambda-http/deployment/src/main/resources/http/sam.jvm.yaml b/extensions/amazon-lambda-http/deployment/src/main/resources/http/sam.jvm.yaml index 0d41bbbf07590..60b4175a98b92 100644 --- a/extensions/amazon-lambda-http/deployment/src/main/resources/http/sam.jvm.yaml +++ b/extensions/amazon-lambda-http/deployment/src/main/resources/http/sam.jvm.yaml @@ -12,7 +12,7 @@ Type: AWS::Serverless::Function Properties: Handler: io.quarkus.amazon.lambda.runtime.QuarkusStreamHandler::handleRequest - Runtime: java11 + Runtime: java17 CodeUri: function.zip MemorySize: 512 Policies: AWSLambdaBasicExecutionRole diff --git a/extensions/amazon-lambda-rest/deployment/src/main/resources/http/sam.jvm.yaml b/extensions/amazon-lambda-rest/deployment/src/main/resources/http/sam.jvm.yaml index a2476a2a1a648..b965648cd1c7b 100644 --- a/extensions/amazon-lambda-rest/deployment/src/main/resources/http/sam.jvm.yaml +++ b/extensions/amazon-lambda-rest/deployment/src/main/resources/http/sam.jvm.yaml @@ -12,7 +12,7 @@ Type: AWS::Serverless::Function Properties: Handler: io.quarkus.amazon.lambda.runtime.QuarkusStreamHandler::handleRequest - Runtime: java11 + Runtime: java17 CodeUri: function.zip MemorySize: 512 Policies: AWSLambdaBasicExecutionRole diff --git a/extensions/amazon-lambda/common-deployment/src/main/resources/lambda/manage.sh b/extensions/amazon-lambda/common-deployment/src/main/resources/lambda/manage.sh index 6990202eca9ea..acfca769bf970 100644 --- a/extensions/amazon-lambda/common-deployment/src/main/resources/lambda/manage.sh +++ b/extensions/amazon-lambda/common-deployment/src/main/resources/lambda/manage.sh @@ -51,7 +51,7 @@ function cmd_update() { FUNCTION_NAME=${lambdaName} HANDLER=${handler} -RUNTIME=java11 +RUNTIME=java17 ZIP_FILE=${targetUri} function usage() { diff --git a/extensions/amazon-lambda/common-deployment/src/main/resources/lambda/sam.jvm.yaml b/extensions/amazon-lambda/common-deployment/src/main/resources/lambda/sam.jvm.yaml index 7efbe0de60fc7..b4d67fca6a950 100644 --- a/extensions/amazon-lambda/common-deployment/src/main/resources/lambda/sam.jvm.yaml +++ b/extensions/amazon-lambda/common-deployment/src/main/resources/lambda/sam.jvm.yaml @@ -12,7 +12,7 @@ Resources: Type: AWS::Serverless::Function Properties: Handler: ${handler} - Runtime: java11 + Runtime: java17 CodeUri: function.zip MemorySize: 256 Timeout: 15 diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/StartupBuildSteps.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/StartupBuildSteps.java index 477eee7c6d506..53923c2136643 100644 --- a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/StartupBuildSteps.java +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/StartupBuildSteps.java @@ -149,8 +149,12 @@ void registerStartupObservers(ObserverRegistrationPhaseBuildItem observerRegistr && !annotationStore.hasAnnotation(method, DotNames.PRODUCES)) { startupMethods.add(method); } else { - LOG.warnf("Ignored an invalid @Startup method declared on %s: %s", method.declaringClass().name(), - method); + if (!annotationStore.hasAnnotation(method, DotNames.PRODUCES)) { + // Producer methods annotated with @Startup are valid and processed above + LOG.warnf("Ignored an invalid @Startup method declared on %s: %s", + method.declaringClass().name(), + method); + } } } } diff --git a/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/ConfigBeanCreator.java b/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/ConfigBeanCreator.java index 548d2afe985c2..03b232b3111bc 100644 --- a/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/ConfigBeanCreator.java +++ b/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/ConfigBeanCreator.java @@ -27,7 +27,7 @@ public Object create(CreationalContext creationalContext, Map CompletableFuture getIfPresent(Object key) { Objects.requireNonNull(key, NULL_KEYS_NOT_SUPPORTED_MSG); CompletableFuture existingCacheValue = cache.getIfPresent(key); - // record metrics, if not null apply casting if (existingCacheValue == null) { - statsCounter.recordMisses(1); return null; } else { LOGGER.tracef("Key [%s] found in cache [%s]", key, cacheInfo.name); - statsCounter.recordHits(1); // cast, but still throw the CacheException in case it fails return unwrapCacheValueOrThrowable(existingCacheValue) diff --git a/extensions/datasource/deployment/src/main/java/io/quarkus/datasource/deployment/devservices/DevServicesDatasourceProcessor.java b/extensions/datasource/deployment/src/main/java/io/quarkus/datasource/deployment/devservices/DevServicesDatasourceProcessor.java index 559fcbe6fa3d0..a5ab0f15c4e3e 100644 --- a/extensions/datasource/deployment/src/main/java/io/quarkus/datasource/deployment/devservices/DevServicesDatasourceProcessor.java +++ b/extensions/datasource/deployment/src/main/java/io/quarkus/datasource/deployment/devservices/DevServicesDatasourceProcessor.java @@ -316,6 +316,8 @@ private RunningDevService startDevDb( e.getValue()); } } + setDataSourceProperties(propertiesMap, dbName, devServicesPrefix + "reuse", + String.valueOf(dataSourceBuildTimeConfig.devservices().reuse())); Map devDebProperties = new HashMap<>(); for (DevServicesDatasourceConfigurationHandlerBuildItem devDbConfigurationHandlerBuildItem : configHandlers) { diff --git a/extensions/devservices/common/src/main/java/io/quarkus/devservices/common/ConfigureUtil.java b/extensions/devservices/common/src/main/java/io/quarkus/devservices/common/ConfigureUtil.java index 0049cc1348834..fee779b8bfab1 100644 --- a/extensions/devservices/common/src/main/java/io/quarkus/devservices/common/ConfigureUtil.java +++ b/extensions/devservices/common/src/main/java/io/quarkus/devservices/common/ConfigureUtil.java @@ -3,17 +3,22 @@ import java.io.IOException; import java.io.InputStream; import java.io.UncheckedIOException; +import java.lang.reflect.Field; import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Properties; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Consumer; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.Network; import org.testcontainers.utility.Base58; +import com.github.dockerjava.api.command.CreateNetworkCmd; + public final class ConfigureUtil { private static final Map DEVSERVICES_PROPS = new ConcurrentHashMap<>(); @@ -35,6 +40,12 @@ public static String configureSharedNetwork(GenericContainer container, Strin Class networkClass = tccl.getParent() .loadClass("org.testcontainers.containers.Network"); Object sharedNetwork = networkClass.getField("SHARED").get(null); + Consumer addDevservicesLabel = cmd -> cmd + .withLabels(Map.of("quarkus.devservices.network", "shared")); + Field createNetworkCmdModifiersField = sharedNetwork.getClass().getSuperclass() + .getDeclaredField("createNetworkCmdModifiers"); + createNetworkCmdModifiersField.setAccessible(true); + createNetworkCmdModifiersField.set(sharedNetwork, Set.of(addDevservicesLabel)); container.setNetwork((Network) sharedNetwork); } catch (Exception e) { throw new IllegalStateException("Unable to obtain SHARED network from testcontainers", e); diff --git a/extensions/devservices/db2/src/main/java/io/quarkus/devservices/db2/deployment/DB2DevServicesProcessor.java b/extensions/devservices/db2/src/main/java/io/quarkus/devservices/db2/deployment/DB2DevServicesProcessor.java index 9940177775761..0eb630a700ee6 100644 --- a/extensions/devservices/db2/src/main/java/io/quarkus/devservices/db2/deployment/DB2DevServicesProcessor.java +++ b/extensions/devservices/db2/src/main/java/io/quarkus/devservices/db2/deployment/DB2DevServicesProcessor.java @@ -20,6 +20,7 @@ import io.quarkus.datasource.deployment.spi.DevServicesDatasourceProviderBuildItem; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.builditem.DevServicesSharedNetworkBuildItem; +import io.quarkus.deployment.dev.devservices.GlobalDevServicesConfig; import io.quarkus.devservices.common.ConfigureUtil; import io.quarkus.devservices.common.ContainerShutdownCloseable; import io.quarkus.devservices.common.Labels; @@ -32,15 +33,19 @@ public class DB2DevServicesProcessor { @BuildStep DevServicesDatasourceProviderBuildItem setupDB2( - List devServicesSharedNetworkBuildItem) { + List devServicesSharedNetworkBuildItem, + GlobalDevServicesConfig globalDevServicesConfig) { return new DevServicesDatasourceProviderBuildItem(DatabaseKind.DB2, new DevServicesDatasourceProvider() { @Override public RunningDevServicesDatasource startDatabase(Optional username, Optional password, String datasourceName, DevServicesDatasourceContainerConfig containerConfig, LaunchMode launchMode, Optional startupTimeout) { + + boolean useSharedNetwork = DevServicesSharedNetworkBuildItem.isSharedNetworkRequired(globalDevServicesConfig, + devServicesSharedNetworkBuildItem); QuarkusDb2Container container = new QuarkusDb2Container(containerConfig.getImageName(), containerConfig.getFixedExposedPort(), - !devServicesSharedNetworkBuildItem.isEmpty()); + useSharedNetwork); startupTimeout.ifPresent(container::withStartupTimeout); String effectiveUsername = containerConfig.getUsername().orElse(username.orElse(DEFAULT_DATABASE_USERNAME)); diff --git a/extensions/devservices/mariadb/src/main/java/io/quarkus/devservices/mariadb/deployment/MariaDBDevServicesProcessor.java b/extensions/devservices/mariadb/src/main/java/io/quarkus/devservices/mariadb/deployment/MariaDBDevServicesProcessor.java index eb859ae79f1fb..3ec8c1362a462 100644 --- a/extensions/devservices/mariadb/src/main/java/io/quarkus/devservices/mariadb/deployment/MariaDBDevServicesProcessor.java +++ b/extensions/devservices/mariadb/src/main/java/io/quarkus/devservices/mariadb/deployment/MariaDBDevServicesProcessor.java @@ -20,6 +20,7 @@ import io.quarkus.datasource.deployment.spi.DevServicesDatasourceProviderBuildItem; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.builditem.DevServicesSharedNetworkBuildItem; +import io.quarkus.deployment.dev.devservices.GlobalDevServicesConfig; import io.quarkus.devservices.common.ConfigureUtil; import io.quarkus.devservices.common.ContainerShutdownCloseable; import io.quarkus.devservices.common.Labels; @@ -35,16 +36,20 @@ public class MariaDBDevServicesProcessor { @BuildStep DevServicesDatasourceProviderBuildItem setupMariaDB( - List devServicesSharedNetworkBuildItem) { + List devServicesSharedNetworkBuildItem, + GlobalDevServicesConfig globalDevServicesConfig) { return new DevServicesDatasourceProviderBuildItem(DatabaseKind.MARIADB, new DevServicesDatasourceProvider() { @SuppressWarnings("unchecked") @Override public RunningDevServicesDatasource startDatabase(Optional username, Optional password, String datasourceName, DevServicesDatasourceContainerConfig containerConfig, LaunchMode launchMode, Optional startupTimeout) { + + boolean useSharedNetwork = DevServicesSharedNetworkBuildItem.isSharedNetworkRequired(globalDevServicesConfig, + devServicesSharedNetworkBuildItem); QuarkusMariaDBContainer container = new QuarkusMariaDBContainer(containerConfig.getImageName(), containerConfig.getFixedExposedPort(), - !devServicesSharedNetworkBuildItem.isEmpty()); + useSharedNetwork); startupTimeout.ifPresent(container::withStartupTimeout); String effectiveUsername = containerConfig.getUsername().orElse(username.orElse(DEFAULT_DATABASE_USERNAME)); diff --git a/extensions/devservices/mssql/src/main/java/io/quarkus/devservices/mssql/deployment/MSSQLDevServicesProcessor.java b/extensions/devservices/mssql/src/main/java/io/quarkus/devservices/mssql/deployment/MSSQLDevServicesProcessor.java index 8296bdfdd7f0b..40baa951bd618 100644 --- a/extensions/devservices/mssql/src/main/java/io/quarkus/devservices/mssql/deployment/MSSQLDevServicesProcessor.java +++ b/extensions/devservices/mssql/src/main/java/io/quarkus/devservices/mssql/deployment/MSSQLDevServicesProcessor.java @@ -17,6 +17,7 @@ import io.quarkus.datasource.deployment.spi.DevServicesDatasourceProviderBuildItem; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.builditem.DevServicesSharedNetworkBuildItem; +import io.quarkus.deployment.dev.devservices.GlobalDevServicesConfig; import io.quarkus.devservices.common.ConfigureUtil; import io.quarkus.devservices.common.ContainerShutdownCloseable; import io.quarkus.devservices.common.Labels; @@ -34,13 +35,17 @@ public class MSSQLDevServicesProcessor { @BuildStep DevServicesDatasourceProviderBuildItem setupMSSQL( - List devServicesSharedNetworkBuildItem) { + List devServicesSharedNetworkBuildItem, + GlobalDevServicesConfig globalDevServicesConfig) { return new DevServicesDatasourceProviderBuildItem(DatabaseKind.MSSQL, new DevServicesDatasourceProvider() { @SuppressWarnings("unchecked") @Override public RunningDevServicesDatasource startDatabase(Optional username, Optional password, String datasourceName, DevServicesDatasourceContainerConfig containerConfig, LaunchMode launchMode, Optional startupTimeout) { + + boolean useSharedNetwork = DevServicesSharedNetworkBuildItem.isSharedNetworkRequired(globalDevServicesConfig, + devServicesSharedNetworkBuildItem); QuarkusMSSQLServerContainer container = new QuarkusMSSQLServerContainer(containerConfig.getImageName(), containerConfig.getFixedExposedPort(), !devServicesSharedNetworkBuildItem.isEmpty()); diff --git a/extensions/devservices/mysql/src/main/java/io/quarkus/devservices/mysql/deployment/MySQLDevServicesProcessor.java b/extensions/devservices/mysql/src/main/java/io/quarkus/devservices/mysql/deployment/MySQLDevServicesProcessor.java index b951e08147f4a..1b9341a7a8eac 100644 --- a/extensions/devservices/mysql/src/main/java/io/quarkus/devservices/mysql/deployment/MySQLDevServicesProcessor.java +++ b/extensions/devservices/mysql/src/main/java/io/quarkus/devservices/mysql/deployment/MySQLDevServicesProcessor.java @@ -20,6 +20,7 @@ import io.quarkus.datasource.deployment.spi.DevServicesDatasourceProviderBuildItem; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.builditem.DevServicesSharedNetworkBuildItem; +import io.quarkus.deployment.dev.devservices.GlobalDevServicesConfig; import io.quarkus.devservices.common.ConfigureUtil; import io.quarkus.devservices.common.ContainerShutdownCloseable; import io.quarkus.devservices.common.Labels; @@ -34,16 +35,20 @@ public class MySQLDevServicesProcessor { @BuildStep DevServicesDatasourceProviderBuildItem setupMysql( - List devServicesSharedNetworkBuildItem) { + List devServicesSharedNetworkBuildItem, + GlobalDevServicesConfig globalDevServicesConfig) { return new DevServicesDatasourceProviderBuildItem(DatabaseKind.MYSQL, new DevServicesDatasourceProvider() { @SuppressWarnings("unchecked") @Override public RunningDevServicesDatasource startDatabase(Optional username, Optional password, String datasourceName, DevServicesDatasourceContainerConfig containerConfig, LaunchMode launchMode, Optional startupTimeout) { + + boolean useSharedNetwork = DevServicesSharedNetworkBuildItem.isSharedNetworkRequired(globalDevServicesConfig, + devServicesSharedNetworkBuildItem); QuarkusMySQLContainer container = new QuarkusMySQLContainer(containerConfig.getImageName(), containerConfig.getFixedExposedPort(), - !devServicesSharedNetworkBuildItem.isEmpty()); + useSharedNetwork); startupTimeout.ifPresent(container::withStartupTimeout); String effectiveUsername = containerConfig.getUsername().orElse(username.orElse(DEFAULT_DATABASE_USERNAME)); diff --git a/extensions/devservices/oracle/src/main/java/io/quarkus/devservices/oracle/deployment/OracleDevServicesProcessor.java b/extensions/devservices/oracle/src/main/java/io/quarkus/devservices/oracle/deployment/OracleDevServicesProcessor.java index 34d6e32cd8398..58448a484745a 100644 --- a/extensions/devservices/oracle/src/main/java/io/quarkus/devservices/oracle/deployment/OracleDevServicesProcessor.java +++ b/extensions/devservices/oracle/src/main/java/io/quarkus/devservices/oracle/deployment/OracleDevServicesProcessor.java @@ -20,6 +20,7 @@ import io.quarkus.datasource.deployment.spi.DevServicesDatasourceProviderBuildItem; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.builditem.DevServicesSharedNetworkBuildItem; +import io.quarkus.deployment.dev.devservices.GlobalDevServicesConfig; import io.quarkus.devservices.common.ConfigureUtil; import io.quarkus.devservices.common.ContainerShutdownCloseable; import io.quarkus.devservices.common.Labels; @@ -39,15 +40,19 @@ public class OracleDevServicesProcessor { @BuildStep DevServicesDatasourceProviderBuildItem setupOracle( - List devServicesSharedNetworkBuildItem) { + List devServicesSharedNetworkBuildItem, + GlobalDevServicesConfig globalDevServicesConfig) { return new DevServicesDatasourceProviderBuildItem(DatabaseKind.ORACLE, new DevServicesDatasourceProvider() { @Override public RunningDevServicesDatasource startDatabase(Optional username, Optional password, String datasourceName, DevServicesDatasourceContainerConfig containerConfig, LaunchMode launchMode, Optional startupTimeout) { + + boolean useSharedNetwork = DevServicesSharedNetworkBuildItem.isSharedNetworkRequired(globalDevServicesConfig, + devServicesSharedNetworkBuildItem); QuarkusOracleServerContainer container = new QuarkusOracleServerContainer(containerConfig.getImageName(), containerConfig.getFixedExposedPort(), - !devServicesSharedNetworkBuildItem.isEmpty()); + useSharedNetwork); startupTimeout.ifPresent(container::withStartupTimeout); String effectiveUsername = containerConfig.getUsername().orElse(username.orElse(DEFAULT_DATABASE_USERNAME)); diff --git a/extensions/devservices/postgresql/src/main/java/io/quarkus/devservices/postgresql/deployment/PostgresqlDevServicesProcessor.java b/extensions/devservices/postgresql/src/main/java/io/quarkus/devservices/postgresql/deployment/PostgresqlDevServicesProcessor.java index af09e7e9fc499..d02a3699d844f 100644 --- a/extensions/devservices/postgresql/src/main/java/io/quarkus/devservices/postgresql/deployment/PostgresqlDevServicesProcessor.java +++ b/extensions/devservices/postgresql/src/main/java/io/quarkus/devservices/postgresql/deployment/PostgresqlDevServicesProcessor.java @@ -25,6 +25,7 @@ import io.quarkus.deployment.builditem.ConsoleCommandBuildItem; import io.quarkus.deployment.builditem.DevServicesLauncherConfigResultBuildItem; import io.quarkus.deployment.builditem.DevServicesSharedNetworkBuildItem; +import io.quarkus.deployment.dev.devservices.GlobalDevServicesConfig; import io.quarkus.devservices.common.ConfigureUtil; import io.quarkus.devservices.common.ContainerShutdownCloseable; import io.quarkus.devservices.common.Labels; @@ -42,16 +43,20 @@ ConsoleCommandBuildItem psqlCommand(DevServicesLauncherConfigResultBuildItem dev @BuildStep DevServicesDatasourceProviderBuildItem setupPostgres( - List devServicesSharedNetworkBuildItem) { + List devServicesSharedNetworkBuildItem, + GlobalDevServicesConfig globalDevServicesConfig) { return new DevServicesDatasourceProviderBuildItem(DatabaseKind.POSTGRESQL, new DevServicesDatasourceProvider() { @SuppressWarnings("unchecked") @Override public RunningDevServicesDatasource startDatabase(Optional username, Optional password, String datasourceName, DevServicesDatasourceContainerConfig containerConfig, LaunchMode launchMode, Optional startupTimeout) { + + boolean useSharedNetwork = DevServicesSharedNetworkBuildItem.isSharedNetworkRequired(globalDevServicesConfig, + devServicesSharedNetworkBuildItem); QuarkusPostgreSQLContainer container = new QuarkusPostgreSQLContainer(containerConfig.getImageName(), containerConfig.getFixedExposedPort(), - !devServicesSharedNetworkBuildItem.isEmpty()); + useSharedNetwork); startupTimeout.ifPresent(container::withStartupTimeout); String effectiveUsername = containerConfig.getUsername().orElse(username.orElse(DEFAULT_DATABASE_USERNAME)); 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 eee0fdf9d98ac..769dd2f7205a8 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 @@ -95,8 +95,10 @@ public DevServicesResultBuildItem startElasticsearchDevService( (launchMode.isTest() ? "(test) " : "") + "Dev Services for Elasticsearch starting:", consoleInstalledBuildItem, loggingSetupBuildItem); try { + boolean useSharedNetwork = DevServicesSharedNetworkBuildItem.isSharedNetworkRequired(devServicesConfig, + devServicesSharedNetworkBuildItem); devService = startElasticsearch(dockerStatusBuildItem, configuration, buildItemsConfig, launchMode, - !devServicesSharedNetworkBuildItem.isEmpty(), + useSharedNetwork, devServicesConfig.timeout); if (devService == null) { compressor.closeAndDumpCaptured(); diff --git a/extensions/elytron-security-jdbc/runtime/src/main/java/io/quarkus/elytron/security/jdbc/BcryptPasswordKeyMapperConfig.java b/extensions/elytron-security-jdbc/runtime/src/main/java/io/quarkus/elytron/security/jdbc/BcryptPasswordKeyMapperConfig.java index 5848f998f5836..d7c43a70a89f6 100644 --- a/extensions/elytron-security-jdbc/runtime/src/main/java/io/quarkus/elytron/security/jdbc/BcryptPasswordKeyMapperConfig.java +++ b/extensions/elytron-security-jdbc/runtime/src/main/java/io/quarkus/elytron/security/jdbc/BcryptPasswordKeyMapperConfig.java @@ -34,9 +34,10 @@ public interface BcryptPasswordKeyMapperConfig { Encoding hashEncoding(); /** - * The index (1 based numbering) of the column containing the Bcrypt salt + * The index (1 based numbering) of the column containing the Bcrypt salt. The default value of `-1` implies that the salt + * is stored in the password column using the Modular Crypt Format (MCF) standard. */ - @WithDefault("0") + @WithDefault("-1") int saltIndex(); /** @@ -46,9 +47,10 @@ public interface BcryptPasswordKeyMapperConfig { Encoding saltEncoding(); /** - * The index (1 based numbering) of the column containing the Bcrypt iteration count + * The index (1 based numbering) of the column containing the Bcrypt iteration count. The default value of `-1` implies that + * the iteration count is stored in the password column using the Modular Crypt Format (MCF) standard. */ - @WithDefault("0") + @WithDefault("-1") int iterationCountIndex(); default PasswordKeyMapper toPasswordKeyMapper() { diff --git a/extensions/elytron-security-ldap/runtime/src/main/java/io/quarkus/elytron/security/ldap/DelegatingLdapContext.java b/extensions/elytron-security-ldap/runtime/src/main/java/io/quarkus/elytron/security/ldap/DelegatingLdapContext.java index c90a9da278e2c..6f43ceb731a5a 100644 --- a/extensions/elytron-security-ldap/runtime/src/main/java/io/quarkus/elytron/security/ldap/DelegatingLdapContext.java +++ b/extensions/elytron-security-ldap/runtime/src/main/java/io/quarkus/elytron/security/ldap/DelegatingLdapContext.java @@ -1,7 +1,5 @@ package io.quarkus.elytron.security.ldap; -import java.security.AccessController; -import java.security.PrivilegedAction; import java.util.Hashtable; import javax.naming.Binding; @@ -26,7 +24,6 @@ import org.wildfly.common.Assert; import org.wildfly.security.auth.realm.ldap.ThreadLocalSSLSocketFactory; -import org.wildfly.security.manager.action.SetContextClassLoaderAction; class DelegatingLdapContext implements LdapContext { @@ -46,7 +43,7 @@ interface CloseHandler { } // for needs of newInstance() - private DelegatingLdapContext(DirContext delegating, SocketFactory socketFactory) throws NamingException { + private DelegatingLdapContext(DirContext delegating, SocketFactory socketFactory) { this.delegating = delegating; this.closeHandler = null; // close handler should not be applied to copy this.socketFactory = socketFactory; @@ -488,10 +485,10 @@ private ClassLoader getSocketFactoryClassLoader() { } private ClassLoader setClassLoaderTo(final ClassLoader targetClassLoader) { - return doPrivileged(new SetContextClassLoaderAction(targetClassLoader)); + final Thread currentThread = Thread.currentThread(); + final ClassLoader original = currentThread.getContextClassLoader(); + currentThread.setContextClassLoader(targetClassLoader); + return original; } - private static T doPrivileged(final PrivilegedAction action) { - return System.getSecurityManager() != null ? AccessController.doPrivileged(action) : action.run(); - } } diff --git a/extensions/elytron-security-ldap/runtime/src/main/java/io/quarkus/elytron/security/ldap/QuarkusDirContextFactory.java b/extensions/elytron-security-ldap/runtime/src/main/java/io/quarkus/elytron/security/ldap/QuarkusDirContextFactory.java index 36118d8864f6a..1fe3324d0aa50 100644 --- a/extensions/elytron-security-ldap/runtime/src/main/java/io/quarkus/elytron/security/ldap/QuarkusDirContextFactory.java +++ b/extensions/elytron-security-ldap/runtime/src/main/java/io/quarkus/elytron/security/ldap/QuarkusDirContextFactory.java @@ -1,7 +1,5 @@ package io.quarkus.elytron.security.ldap; -import java.security.AccessController; -import java.security.PrivilegedAction; import java.time.Duration; import java.util.Hashtable; @@ -15,7 +13,6 @@ import javax.security.auth.callback.PasswordCallback; import org.wildfly.security.auth.realm.ldap.DirContextFactory; -import org.wildfly.security.manager.action.SetContextClassLoaderAction; public class QuarkusDirContextFactory implements DirContextFactory { // private static final ElytronMessages log = Logger.getMessageLogger(ElytronMessages.class, "org.wildfly.security"); @@ -142,10 +139,10 @@ public void returnContext(DirContext context) { } private ClassLoader setClassLoaderTo(final ClassLoader targetClassLoader) { - return doPrivileged(new SetContextClassLoaderAction(targetClassLoader)); + final Thread currentThread = Thread.currentThread(); + final ClassLoader original = currentThread.getContextClassLoader(); + currentThread.setContextClassLoader(targetClassLoader); + return original; } - private static T doPrivileged(final PrivilegedAction action) { - return System.getSecurityManager() != null ? AccessController.doPrivileged(action) : action.run(); - } } diff --git a/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/GrpcServiceTestBase.java b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/GrpcServiceTestBase.java index 271921f5e5504..94e30cb55ad80 100644 --- a/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/GrpcServiceTestBase.java +++ b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/GrpcServiceTestBase.java @@ -30,7 +30,7 @@ import io.smallrye.mutiny.Multi; import io.smallrye.mutiny.Uni; -public class GrpcServiceTestBase { +public abstract class GrpcServiceTestBase { protected static final Duration TIMEOUT = Duration.ofSeconds(5); protected ManagedChannel channel; diff --git a/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/blocking/BlockingMethodsTest.java b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/blocking/BlockingMethodsTest.java index d0747f500bf0d..fd75ab680a6e1 100644 --- a/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/blocking/BlockingMethodsTest.java +++ b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/blocking/BlockingMethodsTest.java @@ -1,6 +1,8 @@ package io.quarkus.grpc.server.blocking; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; import java.time.Duration; import java.util.Iterator; @@ -17,6 +19,7 @@ import com.google.protobuf.ByteString; import com.google.protobuf.EmptyProtos; +import io.grpc.Status; import io.grpc.StatusRuntimeException; import io.grpc.testing.integration.Messages; import io.quarkus.grpc.GrpcClient; @@ -38,7 +41,8 @@ public class BlockingMethodsTest { .addPackage(EmptyProtos.class.getPackage()) .addPackage(Messages.class.getPackage()) .addPackage(BlockingTestServiceGrpc.class.getPackage()) - .addClasses(BlockingTestService.class, AssertHelper.class)) + .addClasses(BlockingTestService.class, AssertHelper.class, + io.quarkus.grpc.blocking.BlockingTestService.class)) .withConfigurationResource("blocking-test-config.properties"); protected static final Duration TIMEOUT = Duration.ofSeconds(5); @@ -49,6 +53,9 @@ public class BlockingMethodsTest { @GrpcClient("blocking-test") MutinyBlockingTestServiceGrpc.MutinyBlockingTestServiceStub mutiny; + @GrpcClient("blocking-test") + io.quarkus.grpc.blocking.BlockingTestService client; + @Test @Timeout(5) public void testEmpty() { @@ -81,6 +88,20 @@ public void testUnaryMethodBlocking() { assertThat(response).isNotNull(); } + @Test + @Timeout(5) + public void testUnaryMethodBlockingClient() { + Messages.SimpleRequest request = Messages.SimpleRequest.newBuilder() + .setMsg("IllegalArgument") + .build(); + try { + client.unaryCallBlocking(request).await().indefinitely(); + fail(); // should get SRE ... + } catch (StatusRuntimeException e) { + assertEquals(Status.INVALID_ARGUMENT.getCode(), e.getStatus().getCode()); + } + } + @Test @Timeout(5) public void testStreamingOutMethod() { diff --git a/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/services/BlockingTestService.java b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/services/BlockingTestService.java index 15e3ece324f0b..83f8e00482d90 100644 --- a/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/services/BlockingTestService.java +++ b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/services/BlockingTestService.java @@ -59,6 +59,10 @@ public void unaryCallBlocking(Messages.SimpleRequest request, assertThat(request).isNotNull(); assertRunOnWorker(); assertRunOnDuplicatedContext(); + String msg = request.getMsg(); + if ("IllegalArgument".equals(msg)) { + throw new IllegalArgumentException("You're not allowed!"); + } responseObserver.onNext(Messages.SimpleResponse.newBuilder().build()); responseObserver.onCompleted(); } diff --git a/extensions/grpc/deployment/src/test/proto/messages.proto b/extensions/grpc/deployment/src/test/proto/messages.proto index 5110719e822b8..de2fbf4ed2839 100644 --- a/extensions/grpc/deployment/src/test/proto/messages.proto +++ b/extensions/grpc/deployment/src/test/proto/messages.proto @@ -94,6 +94,9 @@ message SimpleRequest { // Whether server should return a given status EchoStatus response_status = 7; + + // Simple string + string msg = 8; } // Unary response, as configured by the request. diff --git a/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/GrpcServerRecorder.java b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/GrpcServerRecorder.java index 2f95cf0248e87..54e83ead17432 100644 --- a/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/GrpcServerRecorder.java +++ b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/GrpcServerRecorder.java @@ -159,11 +159,11 @@ private void buildGrpcServer(Vertx vertx, GrpcServerConfiguration configuration, CompressionInterceptor compressionInterceptor = prepareCompressionInterceptor(configuration); for (GrpcServiceDefinition service : toBeRegistered) { - ServerServiceDefinition defWithInterceptors = serviceWithInterceptors( - vertx, grpcContainer, blockingMethodsPerService, virtualMethodsPerService, compressionInterceptor, service, + ServerServiceDefinition serviceDefinition = serviceWithInterceptors( + vertx, grpcContainer, blockingMethodsPerService, virtualMethodsPerService, compressionInterceptor, + globalInterceptors, service, launchMode == LaunchMode.DEVELOPMENT); LOGGER.debugf("Registered gRPC service '%s'", service.definition.getServiceDescriptor().getName()); - ServerServiceDefinition serviceDefinition = ServerInterceptors.intercept(defWithInterceptors, globalInterceptors); GrpcServiceBridge bridge = GrpcServiceBridge.bridge(serviceDefinition); bridge.bind(server); definitions.add(service.definition); @@ -427,12 +427,14 @@ private void devModeReload(GrpcContainer grpcContainer, Vertx vertx, GrpcServerC definitions.add(service.definition); } + List globalInterceptors = grpcContainer.getSortedGlobalInterceptors(); + List servicesWithInterceptors = new ArrayList<>(); CompressionInterceptor compressionInterceptor = prepareCompressionInterceptor(configuration); for (GrpcServiceDefinition service : services) { servicesWithInterceptors.add( serviceWithInterceptors(vertx, grpcContainer, blockingMethodsPerService, virtualMethodsPerService, - compressionInterceptor, service, true)); + compressionInterceptor, globalInterceptors, service, true)); } // add after actual services, so we don't inspect them for interceptors, etc @@ -447,15 +449,14 @@ private void devModeReload(GrpcContainer grpcContainer, Vertx vertx, GrpcServerC initHealthStorage(); - List globalInterceptors = grpcContainer.getSortedGlobalInterceptors(); - + List devModeInterceptors = new ArrayList<>(); if (provider != null) { - globalInterceptors.add(new DevModeInterceptor(Thread.currentThread().getContextClassLoader())); - globalInterceptors.add(new GrpcHotReplacementInterceptor()); - provider.devModeReload(servicesWithInterceptors, methods, globalInterceptors, shutdown); + devModeInterceptors.add(new DevModeInterceptor(Thread.currentThread().getContextClassLoader())); + devModeInterceptors.add(new GrpcHotReplacementInterceptor()); + provider.devModeReload(servicesWithInterceptors, methods, devModeInterceptors, shutdown); } else { devModeWrapper = new DevModeWrapper(Thread.currentThread().getContextClassLoader()); - GrpcServerReloader.reinitialize(servicesWithInterceptors, methods, globalInterceptors); + GrpcServerReloader.reinitialize(servicesWithInterceptors, methods, devModeInterceptors); shutdown.addShutdownTask(GrpcServerReloader::reset); } } @@ -522,23 +523,21 @@ private Map.Entry buildServer(Vertx vertx, GrpcServerConfigurat CompressionInterceptor compressionInterceptor = prepareCompressionInterceptor(configuration); + List globalInterceptors = grpcContainer.getSortedGlobalInterceptors(); + for (GrpcServiceDefinition service : toBeRegistered) { builder.addService( serviceWithInterceptors(vertx, grpcContainer, blockingMethodsPerService, virtualMethodsPerService, - compressionInterceptor, service, launchMode == LaunchMode.DEVELOPMENT)); + compressionInterceptor, globalInterceptors, service, launchMode == LaunchMode.DEVELOPMENT)); LOGGER.debugf("Registered gRPC service '%s'", service.definition.getServiceDescriptor().getName()); definitions.add(service.definition); } if (reflectionServiceEnabled) { LOGGER.info("Registering gRPC reflection service"); - builder.addService(new ReflectionServiceV1(definitions)); - builder.addService(new ReflectionServiceV1alpha(definitions)); - } - - for (ServerInterceptor serverInterceptor : grpcContainer.getSortedGlobalInterceptors()) { - builder.intercept(serverInterceptor); + builder.addService(ServerInterceptors.intercept(new ReflectionServiceV1(definitions), globalInterceptors)); + builder.addService(ServerInterceptors.intercept(new ReflectionServiceV1alpha(definitions), globalInterceptors)); } String msg = "Starting "; @@ -569,12 +568,13 @@ private ServerServiceDefinition serviceWithInterceptors(Vertx vertx, GrpcContain Map> blockingMethodsPerService, Map> virtualMethodsPerService, CompressionInterceptor compressionInterceptor, + List globalInterceptors, GrpcServiceDefinition service, boolean devMode) { List interceptors = new ArrayList<>(); if (compressionInterceptor != null) { interceptors.add(compressionInterceptor); } - + interceptors.addAll(globalInterceptors); interceptors.addAll(grpcContainer.getSortedPerServiceInterceptors(service.getImplementationClassName())); // We only register the blocking interceptor if needed by at least one method of the service (either blocking or runOnVirtualThread) @@ -587,6 +587,7 @@ private ServerServiceDefinition serviceWithInterceptors(Vertx vertx, GrpcContain VirtualThreadsRecorder.getCurrent(), devMode)); } } + interceptors.sort(Interceptors.INTERCEPTOR_COMPARATOR); return ServerInterceptors.intercept(service.definition, interceptors); } diff --git a/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/Interceptors.java b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/Interceptors.java index 559944d23e72a..64e05244925eb 100644 --- a/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/Interceptors.java +++ b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/Interceptors.java @@ -15,8 +15,9 @@ public final class Interceptors { public static final int DUPLICATE_CONTEXT = Integer.MAX_VALUE - 5; - public static final int EXCEPTION_HANDLER = Integer.MAX_VALUE - 10; public static final int REQUEST_CONTEXT = Integer.MAX_VALUE - 50; + public static final int BLOCKING_HANDLER = Integer.MAX_VALUE - 60; + public static final int EXCEPTION_HANDLER = Integer.MAX_VALUE - 70; static List getSortedPerServiceInterceptors(String name, Set> interceptorClasses) { if (interceptorClasses.isEmpty()) { diff --git a/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/supports/blocking/BlockingServerInterceptor.java b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/supports/blocking/BlockingServerInterceptor.java index 358cf9931e0c1..047d67adce374 100644 --- a/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/supports/blocking/BlockingServerInterceptor.java +++ b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/supports/blocking/BlockingServerInterceptor.java @@ -12,6 +12,8 @@ import java.util.function.Consumer; import java.util.function.Function; +import jakarta.enterprise.inject.spi.Prioritized; + import org.jboss.logging.Logger; import io.grpc.Context; @@ -23,6 +25,7 @@ import io.quarkus.arc.InjectableContext; import io.quarkus.arc.InjectableContext.ContextState; import io.quarkus.arc.ManagedContext; +import io.quarkus.grpc.runtime.Interceptors; import io.vertx.core.Vertx; /** @@ -31,7 +34,7 @@ *

* For non-annotated methods, the interceptor acts as a pass-through. */ -public class BlockingServerInterceptor implements ServerInterceptor, Function { +public class BlockingServerInterceptor implements ServerInterceptor, Function, Prioritized { private static final Logger log = Logger.getLogger(BlockingServerInterceptor.class); private final Vertx vertx; @@ -133,6 +136,11 @@ public ServerCall.Listener interceptCall(ServerCall instance) { } } - /** - * Runs the given privileged action, using a privileged block if required. - *

- * NOTE: This must never be changed into a publicly available method to avoid execution of arbitrary - * privileged actions within HV's protection domain. - */ - private T run(PrivilegedAction action) { - return System.getSecurityManager() != null ? AccessController.doPrivileged(action) : action.run(); - } } 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 f872ab8a71a51..a52ff559827ff 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 @@ -365,8 +365,9 @@ private Set infinispanClientNames(CombinedIndexBuildItem indexBuildItem, for (AnnotationInstance annotation : infinispanClientAnnotations) { clientNames.add(annotation.value().asString()); } - // dev mode client name for default - if (infinispanClientsBuildTimeConfig.defaultInfinispanClient.devService.devservices.enabled) { + // dev mode client name for default - 0 config + if (infinispanClientsBuildTimeConfig.defaultInfinispanClient.devService.devservices.enabled + && infinispanClientsBuildTimeConfig.defaultInfinispanClient.devService.devservices.createDefaultClient) { clientNames.add(DEFAULT_INFINISPAN_CLIENT_NAME); } diff --git a/extensions/infinispan-client/deployment/src/main/java/io/quarkus/infinispan/client/deployment/devservices/InfinispanDevServiceProcessor.java b/extensions/infinispan-client/deployment/src/main/java/io/quarkus/infinispan/client/deployment/devservices/InfinispanDevServiceProcessor.java index 36b689d36a143..45afb622a8c82 100644 --- a/extensions/infinispan-client/deployment/src/main/java/io/quarkus/infinispan/client/deployment/devservices/InfinispanDevServiceProcessor.java +++ b/extensions/infinispan-client/deployment/src/main/java/io/quarkus/infinispan/client/deployment/devservices/InfinispanDevServiceProcessor.java @@ -259,7 +259,7 @@ private static class QuarkusInfinispanContainer extends InfinispanContainer { public QuarkusInfinispanContainer(String clientName, InfinispanDevServicesConfig config, LaunchMode launchMode, boolean useSharedNetwork) { - super(config.imageName.orElse(IMAGE_BASENAME + ":" + Version.getMajorMinor())); + super(config.imageName.orElse(IMAGE_BASENAME + ":" + Version.getVersion())); this.fixedExposedPort = config.port; this.useSharedNetwork = useSharedNetwork; if (launchMode == DEVELOPMENT) { diff --git a/extensions/infinispan-client/deployment/src/test/java/io/quarkus/infinispan/test/RunDevServiceWithoutClientBeanCreationTest.java b/extensions/infinispan-client/deployment/src/test/java/io/quarkus/infinispan/test/RunDevServiceWithoutClientBeanCreationTest.java new file mode 100644 index 0000000000000..60956c23b0cb3 --- /dev/null +++ b/extensions/infinispan-client/deployment/src/test/java/io/quarkus/infinispan/test/RunDevServiceWithoutClientBeanCreationTest.java @@ -0,0 +1,29 @@ + +package io.quarkus.infinispan.test; + +import static org.assertj.core.api.Assertions.assertThat; + +import jakarta.enterprise.inject.Default; + +import org.infinispan.client.hotrod.RemoteCacheManager; +import org.infinispan.counter.api.CounterManager; +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 RunDevServiceWithoutClientBeanCreationTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withConfigurationResource("disable-default-client-devservices.properties"); + + @Test + public void remoteCacheManagerDefaultBeansAccessible() { + assertThat(Arc.container().instance(RemoteCacheManager.class, Default.Literal.INSTANCE).get()).isNull(); + assertThat(Arc.container().instance(CounterManager.class, Default.Literal.INSTANCE).get()).isNull(); + assertThat(Arc.container().listAll(RemoteCacheManager.class).size()).isZero(); + assertThat(Arc.container().listAll(CounterManager.class).size()).isZero(); + } +} diff --git a/extensions/infinispan-client/deployment/src/test/resources/disable-default-client-devservices.properties b/extensions/infinispan-client/deployment/src/test/resources/disable-default-client-devservices.properties new file mode 100644 index 0000000000000..d1867570e5357 --- /dev/null +++ b/extensions/infinispan-client/deployment/src/test/resources/disable-default-client-devservices.properties @@ -0,0 +1,2 @@ +quarkus.infinispan-client.devservices.enabled=true +quarkus.infinispan-client.devservices.create-default-client=false diff --git a/extensions/infinispan-client/runtime/src/main/java/io/quarkus/infinispan/client/runtime/InfinispanDevServicesConfig.java b/extensions/infinispan-client/runtime/src/main/java/io/quarkus/infinispan/client/runtime/InfinispanDevServicesConfig.java index 1877049ec344d..83f70143409bd 100644 --- a/extensions/infinispan-client/runtime/src/main/java/io/quarkus/infinispan/client/runtime/InfinispanDevServicesConfig.java +++ b/extensions/infinispan-client/runtime/src/main/java/io/quarkus/infinispan/client/runtime/InfinispanDevServicesConfig.java @@ -22,6 +22,15 @@ public class InfinispanDevServicesConfig { @ConfigItem(defaultValue = "true") public boolean enabled; + /** + * When the configuration is empty, an Infinispan default client is automatically created to connect + * to the running dev service. However, there are scenarios where creating this client is unnecessary, + * yet we still need to spin up an Infinispan Server. In such cases, this property serves to determine + * whether the client should be created by default or not by the extension. + */ + @ConfigItem(defaultValue = "true") + public boolean createDefaultClient; + /** * Optional fixed port the dev service will listen to. *

diff --git a/extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/KafkaProcessor.java b/extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/KafkaProcessor.java index 18549b1f92eb4..298fbfd6de13f 100644 --- a/extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/KafkaProcessor.java +++ b/extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/KafkaProcessor.java @@ -165,6 +165,7 @@ void logging(BuildProducer log) { log.produce(new LogCategoryBuildItem("org.apache.kafka.clients", Level.WARNING)); log.produce(new LogCategoryBuildItem("org.apache.kafka.common.utils", Level.WARNING)); log.produce(new LogCategoryBuildItem("org.apache.kafka.common.metrics", Level.WARNING)); + log.produce(new LogCategoryBuildItem("org.apache.kafka.common.telemetry", Level.WARNING)); } @BuildStep diff --git a/extensions/liquibase/deployment/src/test/java/io/quarkus/liquibase/test/LiquibaseExtensionConfigFixture.java b/extensions/liquibase/deployment/src/test/java/io/quarkus/liquibase/test/LiquibaseExtensionConfigFixture.java index 258c33815d843..1e4f916ab50d9 100644 --- a/extensions/liquibase/deployment/src/test/java/io/quarkus/liquibase/test/LiquibaseExtensionConfigFixture.java +++ b/extensions/liquibase/deployment/src/test/java/io/quarkus/liquibase/test/LiquibaseExtensionConfigFixture.java @@ -121,6 +121,14 @@ public String defaultSchemaName(String datasourceName) { return getStringValue("quarkus.liquibase.%s.default-schema-name", datasourceName); } + public String username(String datasourceName) { + return getStringValue("quarkus.liquibase.%s.username", datasourceName); + } + + public String password(String datasourceName) { + return getStringValue("quarkus.liquibase.%s.password", datasourceName); + } + public String liquibaseCatalogName(String datasourceName) { return getStringValue("quarkus.liquibase.%s.liquibase-catalog-name", datasourceName); } diff --git a/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/LiquibaseFactory.java b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/LiquibaseFactory.java index d26d6d25c480a..db1c460fa1b64 100644 --- a/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/LiquibaseFactory.java +++ b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/LiquibaseFactory.java @@ -2,10 +2,13 @@ import java.io.FileNotFoundException; import java.nio.file.Paths; +import java.sql.Connection; +import java.sql.DriverManager; import java.util.Map; import javax.sql.DataSource; +import io.agroal.api.AgroalDataSource; import io.quarkus.liquibase.runtime.LiquibaseConfig; import io.quarkus.runtime.util.StringUtil; import liquibase.Contexts; @@ -78,8 +81,22 @@ public Liquibase createLiquibase() { try (ResourceAccessor resourceAccessor = resolveResourceAccessor()) { String parsedChangeLog = parseChangeLog(config.changeLog); - Database database = DatabaseFactory.getInstance() - .findCorrectDatabaseImplementation(new JdbcConnection(dataSource.getConnection())); + Database database; + + if (config.username.isPresent() && config.password.isPresent()) { + AgroalDataSource agroalDataSource = dataSource.unwrap(AgroalDataSource.class); + String jdbcUrl = agroalDataSource.getConfiguration().connectionPoolConfiguration() + .connectionFactoryConfiguration().jdbcUrl(); + Connection connection = DriverManager.getConnection(jdbcUrl, config.username.get(), config.password.get()); + + database = DatabaseFactory.getInstance() + .findCorrectDatabaseImplementation( + new JdbcConnection(connection)); + + } else { + database = DatabaseFactory.getInstance() + .findCorrectDatabaseImplementation(new JdbcConnection(dataSource.getConnection())); + } if (database != null) { database.setDatabaseChangeLogLockTableName(config.databaseChangeLogLockTableName); diff --git a/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/runtime/LiquibaseConfig.java b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/runtime/LiquibaseConfig.java index 3af8105e1c621..de1f2e47a1f36 100644 --- a/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/runtime/LiquibaseConfig.java +++ b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/runtime/LiquibaseConfig.java @@ -85,4 +85,16 @@ public class LiquibaseConfig { */ public Optional liquibaseTablespaceName = Optional.empty(); + /** + * The username that Liquibase uses to connect to the database. + * If no username is configured, falls back to the datasource username and password. + */ + public Optional username = Optional.empty(); + + /** + * The password that Liquibase uses to connect to the database. + * If no password is configured, falls back to the datasource username and password. + */ + public Optional password = Optional.empty(); + } diff --git a/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/runtime/LiquibaseCreator.java b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/runtime/LiquibaseCreator.java index c3d20635e8612..a6c06d69fb47a 100644 --- a/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/runtime/LiquibaseCreator.java +++ b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/runtime/LiquibaseCreator.java @@ -33,6 +33,8 @@ public LiquibaseFactory createLiquibaseFactory(DataSource dataSource, String dat if (liquibaseRuntimeConfig.databaseChangeLogTableName.isPresent()) { config.databaseChangeLogTableName = liquibaseRuntimeConfig.databaseChangeLogTableName.get(); } + config.password = liquibaseRuntimeConfig.password; + config.username = liquibaseRuntimeConfig.username; config.defaultSchemaName = liquibaseRuntimeConfig.defaultSchemaName; config.defaultCatalogName = liquibaseRuntimeConfig.defaultCatalogName; config.liquibaseTablespaceName = liquibaseRuntimeConfig.liquibaseTablespaceName; diff --git a/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/runtime/LiquibaseDataSourceRuntimeConfig.java b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/runtime/LiquibaseDataSourceRuntimeConfig.java index 639590f5d5b58..c8cb0d1cf3e56 100644 --- a/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/runtime/LiquibaseDataSourceRuntimeConfig.java +++ b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/runtime/LiquibaseDataSourceRuntimeConfig.java @@ -101,6 +101,20 @@ public static final LiquibaseDataSourceRuntimeConfig defaultConfig() { @ConfigItem public Optional defaultSchemaName = Optional.empty(); + /** + * The username that Liquibase uses to connect to the database. + * If no specific username is configured, falls back to the datasource username and password. + */ + @ConfigItem + public Optional username = Optional.empty(); + + /** + * The password that Liquibase uses to connect to the database. + * If no specific password is configured, falls back to the datasource username and password. + */ + @ConfigItem + public Optional password = Optional.empty(); + /** * The name of the catalog with the liquibase tables. */ diff --git a/extensions/logging-json/runtime/src/main/java/io/quarkus/logging/json/runtime/AdditionalFieldConfig.java b/extensions/logging-json/runtime/src/main/java/io/quarkus/logging/json/runtime/AdditionalFieldConfig.java index ec95d7fb5a59c..dba33ea8e0d72 100644 --- a/extensions/logging-json/runtime/src/main/java/io/quarkus/logging/json/runtime/AdditionalFieldConfig.java +++ b/extensions/logging-json/runtime/src/main/java/io/quarkus/logging/json/runtime/AdditionalFieldConfig.java @@ -16,7 +16,7 @@ public class AdditionalFieldConfig { /** * Additional field type specification. - * Supported types: string, int, long + * Supported types: {@code string}, {@code int}, and {@code long}. * String is the default if not specified. */ @ConfigItem(defaultValue = "string") diff --git a/extensions/mongodb-client/deployment/src/main/java/io/quarkus/mongodb/deployment/DevServicesMongoProcessor.java b/extensions/mongodb-client/deployment/src/main/java/io/quarkus/mongodb/deployment/DevServicesMongoProcessor.java index 20fae0ff4aef7..9b35f3954a706 100644 --- a/extensions/mongodb-client/deployment/src/main/java/io/quarkus/mongodb/deployment/DevServicesMongoProcessor.java +++ b/extensions/mongodb-client/deployment/src/main/java/io/quarkus/mongodb/deployment/DevServicesMongoProcessor.java @@ -93,8 +93,10 @@ public List startMongo(List tokenFromPrivateKeyUni() { + return keyClient.getTokens().flatMap(tokens -> Uni.createFrom().item(tokens.getAccessToken())); + } + @GET @Path("token") public Uni tokenUni() { @@ -23,13 +33,13 @@ public Uni tokenUni() { @GET @Path("tokens") public Uni grantTokensUni() { - return client.getTokens().flatMap(tokens -> createTokensString(tokens)); + return client.getTokens().flatMap(this::createTokensString); } @GET @Path("refresh-tokens") public Uni refreshGrantTokens(@QueryParam("refreshToken") String refreshToken) { - return client.refreshTokens(refreshToken).flatMap(tokens -> createTokensString(tokens)); + return client.refreshTokens(refreshToken).flatMap(this::createTokensString); } private Uni createTokensString(Tokens tokens) { diff --git a/extensions/oidc-client/deployment/src/test/java/io/quarkus/oidc/client/OidcClientsResource.java b/extensions/oidc-client/deployment/src/test/java/io/quarkus/oidc/client/OidcClientsResource.java index e575a1e45fa92..2cb8795032d7a 100644 --- a/extensions/oidc-client/deployment/src/test/java/io/quarkus/oidc/client/OidcClientsResource.java +++ b/extensions/oidc-client/deployment/src/test/java/io/quarkus/oidc/client/OidcClientsResource.java @@ -27,7 +27,7 @@ public Uni tokenUni(@PathParam("id") String oidcClientId) { @GET @Path("tokens/{id}") public Uni grantTokensUni(@PathParam("id") String oidcClientId) { - return getClient(oidcClientId).getTokens().flatMap(tokens -> createTokensString(tokens)); + return getClient(oidcClientId).getTokens().flatMap(this::createTokensString); } @GET diff --git a/extensions/oidc-client/deployment/src/test/resources/application-oidc-client-credentials-jwt-private-key.properties b/extensions/oidc-client/deployment/src/test/resources/application-oidc-client-credentials-jwt-private-key.properties index 593d2f8161cdf..93ce6084ed4f8 100644 --- a/extensions/oidc-client/deployment/src/test/resources/application-oidc-client-credentials-jwt-private-key.properties +++ b/extensions/oidc-client/deployment/src/test/resources/application-oidc-client-credentials-jwt-private-key.properties @@ -4,3 +4,7 @@ quarkus.oidc.client-id=quarkus-app quarkus.oidc-client.auth-server-url=${quarkus.oidc.auth-server-url} quarkus.oidc-client.client-id=${quarkus.oidc.client-id} quarkus.oidc-client.credentials.jwt.key-file=/privateKey.pem + +quarkus.oidc-client.key.auth-server-url=${quarkus.oidc.auth-server-url} +quarkus.oidc-client.key.client-id=${quarkus.oidc.client-id} +quarkus.oidc-client.key.credentials.jwt.key=MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCyXwKqKL/hQWDkurdHyRn/9aZqmrgpCfiT5+gQ7KZ9RvDjgTqkJT6IIrRFvIpeBMwSsw3dkUPGmgN1J4QOhLaR2VEXhc20UbxFbr6HXAskZGPuCL1tzRWDkLNMZaEO8jqhPbcq1Ro4GMhaSdm0sBHmcQnu8wAOrdAowdzGh/HUaaYBDY0OZVAm9N8zzBXTahna9frJCMHq3e9szIiv6HYZTy1672/+hR/0D1HY+bqpQtJnzSrKjkFeXDAbYPgewYLEJ2Dk+oo6L1I6S+UTrl4FRHw1fHAd2i75JD+vL/8w/AtKkej0CCBUSZJiV+KDJWjnDUVRWjq5hQb9pu4qEJKhAgMBAAECggEAJvBs4X7B3MfsAiLszgQN4/3ZlZ4vI+5kUM2osMEo22J4RgI5Lgpfa1LALhUp07qSXmauWTdUJ3AJ3zKANrcsMAzUEiGItZu+UR4LA/vJBunPkvBfgi/qSW12ZvAsx9mDiR2y9evNrH9khalnmHVzgu4ccAimc43oSm1/5+tXlLoZ1QK/FohxBxAshtuDHGs8yKUL0jpv7dOrjhCj2ibmPYe6AUk9F61sVWO0/i0Q8UAOcYT3L5nCS5WnLhdCdYpIJJ7xl2PrVE/BAD+JEG5uCOYfVeYh+iCZVfpX17ryfNNUaBtyxKEGVtHbje3mO86mYN3noaS0w/zpUjBPgV+KEQKBgQDsp6VTmDIqHFTp2cC2yrDMxRznif92EGv7ccJDZtbTC37mAuf2J7x5b6AiE1EfxEXyGYzSk99sCns+GbL1EHABUt5pimDCl33b6XvuccQNpnJ0MfM5eRX9Ogyt/OKdDRnQsvrTPNCWOyJjvG01HQM4mfxaBBnxnvl5meH2pyG/ZQKBgQDA87DnyqEFhTDLX5c1TtwHSRj2xeTPGKG0GyxOJXcxR8nhtY9ee0kyLZ14RytnOxKarCFgYXeG4IoGEc/I42WbA4sq88tZcbe4IJkdX0WLMqOTdMrdx9hMU1ytKVUglUJZBVm7FaTQjA+ArMwqkXAA5HBMtArUsfJKUt3l0hMIjQKBgQDS1vmAZJQs2Fj+jzYWpLaneOWrk1K5yR+rQUql6jVyiUdhfS1ULUrJlh3Avh0EhEUc0I6Z/YyMITpztUmu9BoV09K7jMFwHK/RAU+cvFbDIovN4cKkbbCdjt5FFIyBB278dLjrAb+EWOLmoLVbIKICB47AU+8ZSV1SbTrYGUcD0QKBgQCAliZv4na6sg9ZiUPAr+QsKserNSiN5zFkULOPBKLRQbFFbPS1l12pRgLqNCu1qQV19H5tt6arSRpSfy5FB14gFxV4s23yFrnDyF2h2GsFH+MpEq1bbaI1A10AvUnQ5AeKQemRpxPmM2DldMK/H5tPzO0WAOoy4r/ATkc4sG4kxQKBgBL9neT0TmJtxlYGzjNcjdJXs3Q91+nZt3DRMGT9s0917SuP77+FdJYocDiH1rVa9sGG8rkh1jTdqliAxDXwIm5IGS/0OBnkaN1nnGDk5yTiYxOutC5NSj7ecI5Erud8swW6iGqgz2ioFpGxxIYqRlgTv/6mVt41KALfKrYIkVLw \ No newline at end of file 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 93834a53fb41e..959265accfed3 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 @@ -15,7 +15,7 @@ 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 requestBody 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 */ 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 2da30b8da5bf5..c16645774ba65 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 @@ -274,6 +274,14 @@ public static enum Source { @ConfigItem public Provider secretProvider = new Provider(); + /** + * String representation of a private key. If provided, indicates that JWT is signed using a private key in PEM or + * JWK format. + * You can use the {@link #signatureAlgorithm} property to override the default key algorithm, `RS256`. + */ + @ConfigItem + public Optional key = Optional.empty(); + /** * If provided, indicates that JWT is signed using a private key in PEM or JWK format. * You can use the {@link #signatureAlgorithm} property to override the default key algorithm, `RS256`. @@ -399,6 +407,14 @@ public void setAudience(String audience) { this.audience = Optional.of(audience); } + public Optional getKey() { + return key; + } + + public void setKey(String key) { + this.key = Optional.of(key); + } + public Optional getKeyFile() { return keyFile; } 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 6997a29ec767c..fa855e47ec827 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 @@ -280,8 +280,8 @@ public static boolean isClientSecretBasicAuthRequired(Credentials creds) { } public static boolean isClientJwtAuthRequired(Credentials creds) { - return creds.jwt.secret.isPresent() || creds.jwt.secretProvider.key.isPresent() || creds.jwt.keyFile.isPresent() - || creds.jwt.keyStoreFile.isPresent(); + return creds.jwt.secret.isPresent() || creds.jwt.secretProvider.key.isPresent() || creds.jwt.key.isPresent() + || creds.jwt.keyFile.isPresent() || creds.jwt.keyStoreFile.isPresent(); } public static boolean isClientSecretPostAuthRequired(Credentials creds) { @@ -329,7 +329,10 @@ public static Key clientJwtKey(Credentials creds) { } else { Key key = null; try { - if (creds.jwt.getKeyFile().isPresent()) { + if (creds.jwt.getKey().isPresent()) { + key = KeyUtils.tryAsPemSigningPrivateKey(creds.jwt.getKey().get(), + getSignatureAlgorithm(creds, SignatureAlgorithm.RS256)); + } else if (creds.jwt.getKeyFile().isPresent()) { key = KeyUtils.readSigningKey(creds.jwt.getKeyFile().get(), creds.jwt.keyId.orElse(null), getSignatureAlgorithm(creds, SignatureAlgorithm.RS256)); } else if (creds.jwt.keyStoreFile.isPresent()) { diff --git a/extensions/oidc/deployment/pom.xml b/extensions/oidc/deployment/pom.xml index 78334a851e1d3..8abab01676220 100644 --- a/extensions/oidc/deployment/pom.xml +++ b/extensions/oidc/deployment/pom.xml @@ -100,6 +100,11 @@ quarkus-resteasy-jackson-deployment test + + io.quarkus + quarkus-elytron-security-properties-file-deployment + test + diff --git a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/OidcBuildStep.java b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/OidcBuildStep.java index b4827b8306bf4..9bc2cae847574 100644 --- a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/OidcBuildStep.java +++ b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/OidcBuildStep.java @@ -65,6 +65,7 @@ import io.quarkus.oidc.runtime.TenantConfigBean; import io.quarkus.oidc.runtime.providers.AzureAccessTokenCustomizer; import io.quarkus.runtime.TlsConfig; +import io.quarkus.smallrye.context.deployment.ContextPropagationInitializedBuildItem; import io.quarkus.vertx.core.deployment.CoreVertxBuildItem; import io.quarkus.vertx.http.deployment.EagerSecurityInterceptorBindingBuildItem; import io.quarkus.vertx.http.deployment.HttpAuthMechanismAnnotationBuildItem; @@ -220,7 +221,9 @@ public SyntheticBeanBuildItem setup( OidcConfig config, OidcRecorder recorder, CoreVertxBuildItem vertxBuildItem, - TlsConfig tlsConfig) { + TlsConfig tlsConfig, + // this is required for setup ordering: we need CP set up + ContextPropagationInitializedBuildItem cpInitializedBuildItem) { return SyntheticBeanBuildItem.configure(TenantConfigBean.class).unremovable().types(TenantConfigBean.class) .supplier(recorder.setup(config, vertxBuildItem.getVertx(), tlsConfig)) .destroyer(TenantConfigBean.Destroyer.class) diff --git a/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/ImplicitBasicAuthAndBearerAuthCombinationTest.java b/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/ImplicitBasicAuthAndBearerAuthCombinationTest.java new file mode 100644 index 0000000000000..857130a08ef65 --- /dev/null +++ b/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/ImplicitBasicAuthAndBearerAuthCombinationTest.java @@ -0,0 +1,77 @@ +package io.quarkus.oidc.test; + +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +import org.eclipse.microprofile.jwt.JsonWebToken; +import org.hamcrest.Matchers; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.oidc.BearerTokenAuthentication; +import io.quarkus.test.QuarkusDevModeTest; +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.keycloak.server.KeycloakTestResourceLifecycleManager; +import io.quarkus.vertx.http.runtime.security.annotation.BasicAuthentication; +import io.restassured.RestAssured; + +@QuarkusTestResource(KeycloakTestResourceLifecycleManager.class) +public class ImplicitBasicAuthAndBearerAuthCombinationTest { + + @RegisterExtension + static final QuarkusDevModeTest test = new QuarkusDevModeTest() + .withApplicationRoot((jar) -> jar + .addClasses(BasicBearerResource.class) + .addAsResource( + new StringAsset(""" + quarkus.security.users.embedded.enabled=true + quarkus.security.users.embedded.plain-text=true + quarkus.security.users.embedded.users.alice=alice + quarkus.oidc.auth-server-url=${keycloak.url}/realms/quarkus + quarkus.oidc.client-id=quarkus-service-app + quarkus.oidc.credentials.secret=secret + quarkus.http.auth.proactive=false + """), + "application.properties")); + + @Test + public void testBasicEnabledAsSelectedWithAnnotation() { + // endpoint is annotated with 'BasicAuthentication', so basic auth must be enabled + RestAssured.given().auth().oauth2(getAccessToken()).get("/basic-bearer/bearer") + .then().statusCode(200).body(Matchers.is("alice")); + RestAssured.given().auth().basic("alice", "alice").get("/basic-bearer/basic") + .then().statusCode(204); + RestAssured.given().auth().basic("alice", "alice").get("/basic-bearer/bearer") + .then().statusCode(401); + RestAssured.given().auth().oauth2(getAccessToken()).get("/basic-bearer/basic") + .then().statusCode(401); + } + + private static String getAccessToken() { + return KeycloakTestResourceLifecycleManager.getAccessToken("alice"); + } + + @BearerTokenAuthentication + @Path("basic-bearer") + public static class BasicBearerResource { + + @Inject + JsonWebToken accessToken; + + @GET + @BasicAuthentication + @Path("basic") + public String basic() { + return accessToken.getName(); + } + + @GET + @Path("bearer") + public String bearer() { + return accessToken.getName(); + } + } + +} diff --git a/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/ImplicitBasicAuthAndCodeFlowAuthCombinationTest.java b/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/ImplicitBasicAuthAndCodeFlowAuthCombinationTest.java new file mode 100644 index 0000000000000..917cfe5b57dc1 --- /dev/null +++ b/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/ImplicitBasicAuthAndCodeFlowAuthCombinationTest.java @@ -0,0 +1,118 @@ +package io.quarkus.oidc.test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +import java.io.IOException; + +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +import org.eclipse.microprofile.jwt.JsonWebToken; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException; +import com.gargoylesoftware.htmlunit.SilentCssErrorHandler; +import com.gargoylesoftware.htmlunit.WebClient; +import com.gargoylesoftware.htmlunit.html.HtmlForm; +import com.gargoylesoftware.htmlunit.html.HtmlPage; + +import io.quarkus.oidc.IdToken; +import io.quarkus.test.QuarkusDevModeTest; +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.keycloak.server.KeycloakTestResourceLifecycleManager; +import io.restassured.RestAssured; + +@QuarkusTestResource(KeycloakTestResourceLifecycleManager.class) +public class ImplicitBasicAuthAndCodeFlowAuthCombinationTest { + + @RegisterExtension + static final QuarkusDevModeTest test = new QuarkusDevModeTest() + .withApplicationRoot((jar) -> jar + .addClasses(BasicCodeFlowResource.class) + .addAsResource( + new StringAsset(""" + quarkus.security.users.embedded.enabled=true + quarkus.security.users.embedded.plain-text=true + quarkus.security.users.embedded.users.alice=alice + quarkus.oidc.auth-server-url=${keycloak.url}/realms/quarkus + quarkus.oidc.client-id=quarkus-web-app + quarkus.oidc.credentials.secret=secret + quarkus.oidc.application-type=web-app + quarkus.http.auth.permission.code-flow.paths=/basic-code-flow/code-flow + quarkus.http.auth.permission.code-flow.policy=authenticated + quarkus.http.auth.permission.code-flow.auth-mechanism=code + quarkus.http.auth.permission.basic.paths=/basic-code-flow/basic + quarkus.http.auth.permission.basic.policy=authenticated + quarkus.http.auth.permission.basic.auth-mechanism=basic + """), + "application.properties")); + + @Test + public void testBasicEnabledAsSelectedWithHttpPerm() throws IOException, InterruptedException { + // endpoint is annotated with 'BasicAuthentication', so basic auth must be enabled + RestAssured.given().auth().basic("alice", "alice").get("/basic-code-flow/basic") + .then().statusCode(204); + RestAssured.given().auth().basic("alice", "alice").redirects().follow(false) + .get("/basic-code-flow/code-flow").then().statusCode(302); + + try (final WebClient webClient = createWebClient()) { + + try { + webClient.getPage("http://localhost:8080/basic-code-flow/basic"); + fail("Exception is expected because by the basic auth is required"); + } catch (FailingHttpStatusCodeException ex) { + // Reported by Quarkus + assertEquals(401, ex.getStatusCode()); + } + HtmlPage page = webClient.getPage("http://localhost:8080/basic-code-flow/code-flow"); + + assertEquals("Sign in to quarkus", page.getTitleText()); + + HtmlForm loginForm = page.getForms().get(0); + + loginForm.getInputByName("username").setValueAttribute("alice"); + loginForm.getInputByName("password").setValueAttribute("alice"); + + page = loginForm.getInputByName("login").click(); + + assertEquals("alice", page.getBody().asNormalizedText()); + + webClient.getCookieManager().clearCookies(); + } + } + + private WebClient createWebClient() { + WebClient webClient = new WebClient(); + webClient.setCssErrorHandler(new SilentCssErrorHandler()); + return webClient; + } + + private static String getAccessToken() { + return KeycloakTestResourceLifecycleManager.getAccessToken("alice"); + } + + @Path("basic-code-flow") + public static class BasicCodeFlowResource { + + @Inject + @IdToken + JsonWebToken idToken; + + @GET + @Path("basic") + public String basic() { + return idToken.getName(); + } + + @GET + @Path("code-flow") + public String codeFlow() { + return idToken.getName(); + } + } + +} diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/AuthorizationCodeTokens.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/AuthorizationCodeTokens.java index f9e2a1b5d2f53..367ec33bb36ca 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/AuthorizationCodeTokens.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/AuthorizationCodeTokens.java @@ -8,37 +8,94 @@ public class AuthorizationCodeTokens { private String idToken; private String accessToken; private String refreshToken; + private Long accessTokenExpiresIn; public AuthorizationCodeTokens() { } public AuthorizationCodeTokens(String idToken, String accessToken, String refreshToken) { - this.setIdToken(idToken); - this.setAccessToken(accessToken); - this.setRefreshToken(refreshToken); + this(idToken, accessToken, refreshToken, null); } + public AuthorizationCodeTokens(String idToken, String accessToken, String refreshToken, Long accessTokenExpiresIn) { + this.idToken = idToken; + this.accessToken = accessToken; + this.refreshToken = refreshToken; + this.accessTokenExpiresIn = accessTokenExpiresIn; + } + + /** + * Get the ID token + * + * @return ID token + */ public String getIdToken() { return idToken; } + /** + * Set the ID token + * + * @param idToken ID token + */ public void setIdToken(String idToken) { this.idToken = idToken; } + /** + * Get the access token + * + * @return the access token + */ public String getAccessToken() { return accessToken; } + /** + * Set the access token + * + * @param accessToken the access token + */ public void setAccessToken(String accessToken) { this.accessToken = accessToken; } + /** + * Get the refresh token + * + * @return refresh token + */ public String getRefreshToken() { return refreshToken; } + /** + * Set the refresh token + * + * @param refreshToken refresh token + */ public void setRefreshToken(String refreshToken) { this.refreshToken = refreshToken; } + + /** + * Get the access token expires_in value in seconds. + * It is relative to the time the access token is issued at. + * + * @return access token expires_in value in seconds. + */ + public Long getAccessTokenExpiresIn() { + return accessTokenExpiresIn; + } + + /** + * Set the access token expires_in value in seconds. + * It is relative to the time the access token is issued at. + * This property is only checked when an authorization code flow grant completes and does not have to be persisted.. + * + * @param accessTokenExpiresIn access token expires_in value in seconds. + */ + public void setAccessTokenExpiresIn(Long accessTokenExpiresIn) { + this.accessTokenExpiresIn = accessTokenExpiresIn; + } } 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 09a04df282a3c..301293de4d47a 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 @@ -316,12 +316,16 @@ public void setTrustStorePassword(String trustStorePassword) { /** * Allow inlining UserInfo in IdToken instead of caching it in the token cache. - * This property is only checked when an internal IdToken is generated when Oauth2 providers do not return IdToken. + * This property is only checked when an internal IdToken is generated when OAuth2 providers do not return IdToken. * Inlining UserInfo in the generated IdToken allows to store it in the session cookie and avoids introducing a cached * state. + *

+ * Inlining UserInfo in the generated IdToken is enabled if the session cookie is encrypted + * and the UserInfo cache is not enabled or caching UserInfo is disabled for the current tenant + * with the {@link #allowUserInfoCache} property set to `false`. */ - @ConfigItem(defaultValue = "false") - public boolean cacheUserInfoInIdtoken = false; + @ConfigItem + public Optional cacheUserInfoInIdtoken = Optional.empty(); @ConfigGroup public static class Logout { @@ -1975,12 +1979,12 @@ public void setAllowUserInfoCache(boolean allowUserInfoCache) { this.allowUserInfoCache = allowUserInfoCache; } - public boolean isCacheUserInfoInIdtoken() { + public Optional isCacheUserInfoInIdtoken() { return cacheUserInfoInIdtoken; } public void setCacheUserInfoInIdtoken(boolean cacheUserInfoInIdtoken) { - this.cacheUserInfoInIdtoken = cacheUserInfoInIdtoken; + this.cacheUserInfoInIdtoken = Optional.of(cacheUserInfoInIdtoken); } public IntrospectionCredentials getIntrospectionCredentials() { diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/TokenCertificateValidator.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/TokenCertificateValidator.java new file mode 100644 index 0000000000000..f6914aedfd2cc --- /dev/null +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/TokenCertificateValidator.java @@ -0,0 +1,25 @@ +package io.quarkus.oidc; + +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.List; + +/** + * TokenCertificateValidator can be used to verify X509 certificate chain + * that is inlined in the JWT token as a 'x5c' header value. + * + * Use {@link TenantFeature} qualifier to bind this validator to specific OIDC tenants. + */ +public interface TokenCertificateValidator { + /** + * Validate X509 certificate chain + * + * @param oidcConfig current OIDC tenant configuration. + * @param chain the certificate chain. The first element in the list is a leaf certificate, the last element - the root + * certificate. + * @param tokenClaims the decoded JWT token claims in JSON format. If necessary, implementations can convert it to JSON + * object. + * @throws {@link CertificateException} if the certificate chain validation has failed. + */ + void validate(OidcTenantConfig oidcConfig, List chain, String tokenClaims) throws CertificateException; +} 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 index 133be33ae688c..d8d1999f0d20f 100644 --- 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 @@ -11,24 +11,32 @@ import org.jose4j.jwx.JsonWebStructure; import org.jose4j.lang.UnresolvableKeyException; -import io.quarkus.oidc.OidcTenantConfig.CertificateChain; +import io.quarkus.oidc.OidcTenantConfig; +import io.quarkus.oidc.TokenCertificateValidator; import io.quarkus.runtime.configuration.ConfigurationException; import io.quarkus.security.runtime.X509IdentityProvider; import io.vertx.ext.auth.impl.CertificateHelper; public class CertChainPublicKeyResolver implements RefreshableVerificationKeyResolver { private static final Logger LOG = Logger.getLogger(OidcProvider.class); + final OidcTenantConfig oidcConfig; final Set thumbprints; final Optional expectedLeafCertificateName; + final List certificateValidators; - public CertChainPublicKeyResolver(CertificateChain chain) { - if (chain.getTrustStorePassword().isEmpty()) { + public CertChainPublicKeyResolver(OidcTenantConfig oidcConfig) { + this.oidcConfig = oidcConfig; + if (oidcConfig.certificateChain.getTrustStorePassword().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.getTrustStorePassword().get(), chain.trustStoreCertAlias, chain.getTrustStoreFileType()); - this.expectedLeafCertificateName = chain.leafCertificateName; + this.thumbprints = TrustStoreUtils.getTrustedCertificateThumbprints( + oidcConfig.certificateChain.trustStoreFile.get(), + oidcConfig.certificateChain.getTrustStorePassword().get(), + oidcConfig.certificateChain.trustStoreCertAlias, + oidcConfig.certificateChain.getTrustStoreFileType()); + this.expectedLeafCertificateName = oidcConfig.certificateChain.leafCertificateName; + this.certificateValidators = TenantFeatureFinder.find(oidcConfig, TokenCertificateValidator.class); } @Override @@ -45,34 +53,52 @@ public Key resolveKey(JsonWebSignature jws, List nestingContex LOG.debug("Token 'x5c' certificate chain is empty"); return null; } + + // General certificate chain validation + //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()); + } + + // Always do the root certificate thumbprint check LOG.debug("Checking a thumbprint of the root chain certificate"); String rootThumbprint = TrustStoreUtils.calculateThumprint(chain.get(chain.size() - 1)); if (!thumbprints.contains(rootThumbprint)) { LOG.error("Thumprint of the root chain certificate is invalid"); throw new UnresolvableKeyException("Thumprint of the root chain certificate is invalid"); } - if (expectedLeafCertificateName.isEmpty()) { - LOG.debug("Checking a thumbprint of the leaf chain certificate"); - String thumbprint = TrustStoreUtils.calculateThumprint(chain.get(0)); - if (!thumbprints.contains(thumbprint)) { - LOG.error("Thumprint of the leaf chain certificate is invalid"); - throw new UnresolvableKeyException("Thumprint of the leaf chain certificate is invalid"); + + // Run custom validators if any + if (!certificateValidators.isEmpty()) { + LOG.debug("Running custom TokenCertificateValidators"); + for (TokenCertificateValidator validator : certificateValidators) { + validator.validate(oidcConfig, chain, jws.getUnverifiedPayload()); } - } else { + } + + // Finally, check the leaf certificate if required + if (!expectedLeafCertificateName.isEmpty()) { + // Compare the leaf certificate common name against the configured value String leafCertificateName = X509IdentityProvider.getCommonName(chain.get(0).getSubjectX500Principal()); if (!expectedLeafCertificateName.get().equals(leafCertificateName)) { LOG.errorf("Wrong leaf certificate common name: %s", leafCertificateName); throw new UnresolvableKeyException("Wrong leaf certificate common name"); } + } else if (certificateValidators.isEmpty()) { + // No custom validators are registered and no leaf certificate CN is configured + // Check that the truststore contains a leaf certificate thumbprint + LOG.debug("Checking a thumbprint of the leaf chain certificate"); + String thumbprint = TrustStoreUtils.calculateThumprint(chain.get(0)); + if (!thumbprints.contains(thumbprint)) { + LOG.error("Thumprint of the leaf chain certificate is invalid"); + throw new UnresolvableKeyException("Thumprint of the leaf chain certificate 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 (UnresolvableKeyException ex) { throw ex; diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java index bd4981b412a7a..161fbcd67e884 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java @@ -761,7 +761,8 @@ public Uni apply(final AuthorizationCodeTokens tokens, final T LOG.errorf("ID token is not available in the authorization code grant response"); return Uni.createFrom().failure(new AuthenticationCompletionException()); } else { - tokens.setIdToken(generateInternalIdToken(configContext.oidcConfig, null, null)); + tokens.setIdToken(generateInternalIdToken(configContext.oidcConfig, null, null, + tokens.getAccessTokenExpiresIn())); internalIdToken = true; } } else { @@ -785,10 +786,11 @@ public Uni apply(final AuthorizationCodeTokens tokens, final T .call(new Function>() { @Override public Uni apply(SecurityIdentity identity) { - if (internalIdToken && configContext.oidcConfig.allowUserInfoCache - && configContext.oidcConfig.cacheUserInfoInIdtoken) { + if (internalIdToken + && OidcUtils.cacheUserInfoInIdToken(resolver, configContext.oidcConfig)) { tokens.setIdToken(generateInternalIdToken(configContext.oidcConfig, - identity.getAttribute(OidcUtils.USER_INFO_ATTRIBUTE), null)); + identity.getAttribute(OidcUtils.USER_INFO_ATTRIBUTE), null, + tokens.getAccessTokenExpiresIn())); } return processSuccessfulAuthentication(context, configContext, tokens, idToken, identity); @@ -890,7 +892,8 @@ private CodeAuthenticationStateBean getCodeAuthenticationBean(String[] parsedSta return null; } - private String generateInternalIdToken(OidcTenantConfig oidcConfig, UserInfo userInfo, String currentIdToken) { + private String generateInternalIdToken(OidcTenantConfig oidcConfig, UserInfo userInfo, String currentIdToken, + Long accessTokenExpiresInSecs) { JwtClaimsBuilder builder = Jwt.claims(); if (currentIdToken != null) { AbstractJsonObjectResponse currentIdTokenJson = new AbstractJsonObjectResponse( @@ -908,6 +911,8 @@ private String generateInternalIdToken(OidcTenantConfig oidcConfig, UserInfo use } if (oidcConfig.authentication.internalIdTokenLifespan.isPresent()) { builder.expiresIn(oidcConfig.authentication.internalIdTokenLifespan.get().getSeconds()); + } else if (accessTokenExpiresInSecs != null) { + builder.expiresIn(accessTokenExpiresInSecs); } builder.audience(oidcConfig.getClientId().get()); return builder.jws().header(INTERNAL_IDTOKEN_HEADER, true) @@ -936,7 +941,7 @@ public Uni apply(Void t) { if (configContext.oidcConfig.token.lifespanGrace.isPresent()) { maxAge += configContext.oidcConfig.token.lifespanGrace.getAsInt(); } - if (configContext.oidcConfig.token.refreshExpired) { + if (configContext.oidcConfig.token.refreshExpired && tokens.getRefreshToken() != null) { maxAge += configContext.oidcConfig.authentication.sessionAgeExtension.getSeconds(); } final long sessionMaxAge = maxAge; @@ -1247,7 +1252,8 @@ public AuthorizationCodeTokens apply(AuthorizationCodeTokens tokens) { tokens.setIdToken(currentIdToken); } } else { - tokens.setIdToken(generateInternalIdToken(configContext.oidcConfig, null, currentIdToken)); + tokens.setIdToken(generateInternalIdToken(configContext.oidcConfig, null, currentIdToken, + tokens.getAccessTokenExpiresIn())); } } 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 1052eba4f3595..d679739d539b8 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 @@ -237,14 +237,12 @@ UserInfoCache getUserInfoCache() { } private Uni getDynamicTenantConfig(RoutingContext context) { + if (isTenantSetByAnnotation(context, context.get(OidcUtils.TENANT_ID_ATTRIBUTE))) { + return Uni.createFrom().nullItem(); + } if (tenantConfigResolver.isResolvable()) { Uni oidcConfig = context.get(CURRENT_DYNAMIC_TENANT_CONFIG); if (oidcConfig == null) { - - if (isTenantSetByAnnotation(context, context.get(OidcUtils.TENANT_ID_ATTRIBUTE))) { - return Uni.createFrom().nullItem(); - } - oidcConfig = tenantConfigResolver.get().resolve(context, blockingRequestContext); if (oidcConfig == null) { //shouldn't happen, but guard against it anyway 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 dbb2adeb2af49..a2a2d85a2ab96 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 @@ -39,7 +39,7 @@ public DynamicVerificationKeyResolver(OidcProviderClient client, OidcTenantConfi 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); + chainResolverFallback = new CertChainPublicKeyResolver(config); } else { chainResolverFallback = null; } diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcIdentityProvider.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcIdentityProvider.java index f180b11454bcd..e903255d343e7 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 @@ -583,7 +583,7 @@ private static Uni validateTokenWithoutOidcServer(TokenAuthent private Uni getUserInfoUni(Map requestData, TokenAuthenticationRequest request, TenantConfigContext resolvedContext) { - if (isInternalIdToken(request) && resolvedContext.oidcConfig.cacheUserInfoInIdtoken) { + if (isInternalIdToken(request) && OidcUtils.cacheUserInfoInIdToken(tenantResolver, resolvedContext.oidcConfig)) { JsonObject userInfo = OidcUtils.decodeJwtContent(request.getToken().getToken()) .getJsonObject(OidcUtils.USER_INFO_ATTRIBUTE); if (userInfo != null) { @@ -615,7 +615,7 @@ public Uni get() { private Uni newUserInfoUni(TenantConfigContext resolvedContext, String accessToken) { Uni userInfoUni = resolvedContext.provider.getUserInfo(accessToken); if (tenantResolver.getUserInfoCache() == null || !resolvedContext.oidcConfig.allowUserInfoCache - || resolvedContext.oidcConfig.cacheUserInfoInIdtoken) { + || OidcUtils.cacheUserInfoInIdToken(tenantResolver, resolvedContext.oidcConfig)) { return userInfoUni; } else { return userInfoUni.call(new Function>() { 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 54c96cbff24e1..a3a826541673c 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 @@ -4,14 +4,12 @@ import java.nio.charset.StandardCharsets; import java.security.Key; import java.time.Duration; -import java.util.ArrayList; import java.util.Base64; import java.util.List; import java.util.Map; import java.util.function.BiFunction; import java.util.function.Function; -import jakarta.enterprise.inject.Default; import jakarta.json.JsonObject; import org.eclipse.microprofile.jwt.Claims; @@ -32,14 +30,11 @@ import org.jose4j.lang.InvalidAlgorithmException; import org.jose4j.lang.UnresolvableKeyException; -import io.quarkus.arc.Arc; import io.quarkus.logging.Log; import io.quarkus.oidc.AuthorizationCodeTokens; 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.TenantFeature.TenantFeatureLiteral; import io.quarkus.oidc.TokenCustomizer; import io.quarkus.oidc.TokenIntrospection; import io.quarkus.oidc.UserInfo; @@ -84,8 +79,8 @@ public class OidcProvider implements Closeable { final AlgorithmConstraints requiredAlgorithmConstraints; public OidcProvider(OidcProviderClient client, OidcTenantConfig oidcConfig, JsonWebKeySet jwks, Key tokenDecryptionKey) { - this(client, oidcConfig, jwks, TokenCustomizerFinder.find(oidcConfig), tokenDecryptionKey, - getCustomValidators(oidcConfig)); + this(client, oidcConfig, jwks, TenantFeatureFinder.find(oidcConfig), tokenDecryptionKey, + TenantFeatureFinder.find(oidcConfig, Validator.class)); } public OidcProvider(OidcProviderClient client, OidcTenantConfig oidcConfig, JsonWebKeySet jwks, @@ -94,10 +89,9 @@ public OidcProvider(OidcProviderClient client, OidcTenantConfig oidcConfig, Json this.oidcConfig = oidcConfig; this.tokenCustomizer = tokenCustomizer; if (jwks != null) { - this.asymmetricKeyResolver = new JsonWebKeyResolver(jwks, oidcConfig.token.forcedJwkRefreshInterval, - oidcConfig.certificateChain); + this.asymmetricKeyResolver = new JsonWebKeyResolver(jwks, oidcConfig.token.forcedJwkRefreshInterval); } else if (oidcConfig != null && oidcConfig.certificateChain.trustStoreFile.isPresent()) { - this.asymmetricKeyResolver = new CertChainPublicKeyResolver(oidcConfig.certificateChain); + this.asymmetricKeyResolver = new CertChainPublicKeyResolver(oidcConfig); } else { this.asymmetricKeyResolver = null; } @@ -112,22 +106,17 @@ public OidcProvider(OidcProviderClient client, OidcTenantConfig oidcConfig, Json this.requiredClaims = checkRequiredClaimsProp(); this.tokenDecryptionKey = tokenDecryptionKey; this.requiredAlgorithmConstraints = checkSignatureAlgorithm(); - - if (customValidators != null && !customValidators.isEmpty()) { - this.customValidators = customValidators; - } else { - this.customValidators = null; - } + this.customValidators = customValidators == null ? List.of() : customValidators; } public OidcProvider(String publicKeyEnc, OidcTenantConfig oidcConfig, Key tokenDecryptionKey) { this.client = null; this.oidcConfig = oidcConfig; - this.tokenCustomizer = TokenCustomizerFinder.find(oidcConfig); + this.tokenCustomizer = TenantFeatureFinder.find(oidcConfig); if (publicKeyEnc != null) { this.asymmetricKeyResolver = new LocalPublicKeyResolver(publicKeyEnc); } else if (oidcConfig.certificateChain.trustStoreFile.isPresent()) { - this.asymmetricKeyResolver = new CertChainPublicKeyResolver(oidcConfig.certificateChain); + this.asymmetricKeyResolver = new CertChainPublicKeyResolver(oidcConfig); } else { throw new IllegalStateException("Neither public key nor certificate chain verification modes are enabled"); } @@ -137,7 +126,7 @@ public OidcProvider(String publicKeyEnc, OidcTenantConfig oidcConfig, Key tokenD this.requiredClaims = checkRequiredClaimsProp(); this.tokenDecryptionKey = tokenDecryptionKey; this.requiredAlgorithmConstraints = checkSignatureAlgorithm(); - this.customValidators = getCustomValidators(oidcConfig); + this.customValidators = TenantFeatureFinder.find(oidcConfig, Validator.class); } private AlgorithmConstraints checkSignatureAlgorithm() { @@ -223,10 +212,8 @@ private TokenVerificationResult verifyJwtTokenInternal(String token, builder.registerValidator(new CustomClaimsValidator(Map.of(OidcConstants.NONCE, nonce))); } - if (customValidators != null) { - for (Validator customValidator : customValidators) { - builder.registerValidator(customValidator); - } + for (Validator customValidator : customValidators) { + builder.registerValidator(customValidator); } if (issuedAtRequired) { @@ -438,11 +425,11 @@ private class JsonWebKeyResolver implements RefreshableVerificationKeyResolver { volatile long forcedJwksRefreshIntervalMilliSecs; final CertChainPublicKeyResolver chainResolverFallback; - JsonWebKeyResolver(JsonWebKeySet jwks, Duration forcedJwksRefreshInterval, CertificateChain chain) { + JsonWebKeyResolver(JsonWebKeySet jwks, Duration forcedJwksRefreshInterval) { this.jwks = jwks; this.forcedJwksRefreshIntervalMilliSecs = forcedJwksRefreshInterval.toMillis(); - if (chain.trustStoreFile.isPresent()) { - chainResolverFallback = new CertChainPublicKeyResolver(chain); + if (oidcConfig.certificateChain.trustStoreFile.isPresent()) { + chainResolverFallback = new CertChainPublicKeyResolver(oidcConfig); } else { chainResolverFallback = null; } @@ -618,24 +605,4 @@ public String validate(JwtContext jwtContext) throws MalformedClaimException { } } - private static List getCustomValidators(OidcTenantConfig oidcTenantConfig) { - if (oidcTenantConfig != null && oidcTenantConfig.tenantId.isPresent()) { - var tenantsValidators = new ArrayList(); - for (var instance : Arc.container().listAll(Validator.class, Default.Literal.INSTANCE)) { - if (instance.isAvailable()) { - tenantsValidators.add(instance.get()); - } - } - for (var instance : Arc.container().listAll(Validator.class, - TenantFeatureLiteral.of(oidcTenantConfig.tenantId.get()))) { - if (instance.isAvailable()) { - tenantsValidators.add(instance.get()); - } - } - if (!tenantsValidators.isEmpty()) { - return List.copyOf(tenantsValidators); - } - } - return null; - } } 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 0879f649943bd..0a01e4b8adf9b 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 @@ -211,7 +211,14 @@ private AuthorizationCodeTokens getAuthorizationCodeTokens(HttpResponse final String idToken = json.getString(OidcConstants.ID_TOKEN_VALUE); final String accessToken = json.getString(OidcConstants.ACCESS_TOKEN_VALUE); final String refreshToken = json.getString(OidcConstants.REFRESH_TOKEN_VALUE); - return new AuthorizationCodeTokens(idToken, accessToken, refreshToken); + Long tokenExpiresIn = null; + Object tokenExpiresInObj = json.getValue(OidcConstants.EXPIRES_IN); + if (tokenExpiresInObj != null) { + tokenExpiresIn = tokenExpiresInObj instanceof Number ? ((Number) tokenExpiresInObj).longValue() + : Long.parseLong(tokenExpiresInObj.toString()); + } + + return new AuthorizationCodeTokens(idToken, accessToken, refreshToken, tokenExpiresIn); } private UserInfo getUserInfo(HttpResponse resp) { diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcUtils.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcUtils.java index 1a6d6abd97e80..d5c5d730a745e 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcUtils.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcUtils.java @@ -774,4 +774,16 @@ public static String getTenantIdFromCookie(String cookiePrefix, String cookieNam } } } + + public static boolean cacheUserInfoInIdToken(DefaultTenantConfigResolver resolver, OidcTenantConfig oidcConfig) { + + if (resolver.getUserInfoCache() != null && oidcConfig.allowUserInfoCache) { + return false; + } + if (oidcConfig.cacheUserInfoInIdtoken.isPresent()) { + return oidcConfig.cacheUserInfoInIdtoken.get(); + } + return resolver.getTokenStateManager() instanceof DefaultTokenStateManager + && oidcConfig.tokenStateManager.encryptionRequired; + } } diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/TokenCustomizerFinder.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/TenantFeatureFinder.java similarity index 53% rename from extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/TokenCustomizerFinder.java rename to extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/TenantFeatureFinder.java index d09633054b5fa..11a918f6dd5ba 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/TokenCustomizerFinder.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/TenantFeatureFinder.java @@ -1,16 +1,22 @@ package io.quarkus.oidc.runtime; +import java.util.ArrayList; +import java.util.List; + +import jakarta.enterprise.inject.Default; + import io.quarkus.arc.Arc; import io.quarkus.arc.ArcContainer; import io.quarkus.arc.InstanceHandle; import io.quarkus.oidc.OIDCException; import io.quarkus.oidc.OidcTenantConfig; import io.quarkus.oidc.TenantFeature; +import io.quarkus.oidc.TenantFeature.TenantFeatureLiteral; import io.quarkus.oidc.TokenCustomizer; -public class TokenCustomizerFinder { +public class TenantFeatureFinder { - private TokenCustomizerFinder() { + private TenantFeatureFinder() { } @@ -37,4 +43,24 @@ public static TokenCustomizer find(OidcTenantConfig oidcConfig) { return null; } + public static List find(OidcTenantConfig oidcTenantConfig, Class tenantFeatureClass) { + if (oidcTenantConfig != null && oidcTenantConfig.tenantId.isPresent()) { + var tenantsValidators = new ArrayList(); + for (var instance : Arc.container().listAll(tenantFeatureClass, Default.Literal.INSTANCE)) { + if (instance.isAvailable()) { + tenantsValidators.add(instance.get()); + } + } + for (var instance : Arc.container().listAll(tenantFeatureClass, + TenantFeatureLiteral.of(oidcTenantConfig.tenantId.get()))) { + if (instance.isAvailable()) { + tenantsValidators.add(instance.get()); + } + } + if (!tenantsValidators.isEmpty()) { + return List.copyOf(tenantsValidators); + } + } + return List.of(); + } } diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/OpenTelemetryRecorder.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/OpenTelemetryRecorder.java index 6598d2a15f0ac..0967428012cb2 100644 --- a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/OpenTelemetryRecorder.java +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/OpenTelemetryRecorder.java @@ -20,6 +20,8 @@ import io.quarkus.arc.SyntheticCreationalContext; import io.quarkus.opentelemetry.runtime.config.runtime.OTelRuntimeConfig; import io.quarkus.runtime.annotations.Recorder; +import io.quarkus.runtime.annotations.RuntimeInit; +import io.quarkus.runtime.annotations.StaticInit; import io.quarkus.runtime.configuration.DurationConverter; import io.smallrye.config.ConfigValue; import io.smallrye.config.SmallRyeConfig; @@ -30,23 +32,23 @@ public class OpenTelemetryRecorder { public static final String OPEN_TELEMETRY_DRIVER = "io.opentelemetry.instrumentation.jdbc.OpenTelemetryDriver"; - /* STATIC INIT */ + @StaticInit public void resetGlobalOpenTelemetryForDevMode() { GlobalOpenTelemetry.resetForTest(); GlobalEventEmitterProvider.resetForTest(); } - /* RUNTIME INIT */ + @RuntimeInit public void eagerlyCreateContextStorage() { ContextStorage.get(); } - /* RUNTIME INIT */ + @RuntimeInit public void storeVertxOnContextStorage(Supplier vertx) { QuarkusContextStorage.vertx = vertx.get(); } - /* RUNTIME INIT */ + @RuntimeInit public Function, OpenTelemetry> opentelemetryBean( OTelRuntimeConfig oTelRuntimeConfig) { return new Function<>() { diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/TracerRecorder.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/TracerRecorder.java index a0d5486ba0a3d..393aa22568fb1 100644 --- a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/TracerRecorder.java +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/TracerRecorder.java @@ -9,6 +9,7 @@ import io.opentelemetry.semconv.ResourceAttributes; import io.quarkus.arc.runtime.BeanContainer; import io.quarkus.runtime.annotations.Recorder; +import io.quarkus.runtime.annotations.StaticInit; @Recorder public class TracerRecorder { @@ -16,7 +17,7 @@ public class TracerRecorder { public static final Set dropNonApplicationUriTargets = new HashSet<>(); public static final Set dropStaticResourceTargets = new HashSet<>(); - /* STATIC INIT */ + @StaticInit public void setAttributes( BeanContainer beanContainer, String quarkusVersion, @@ -35,7 +36,7 @@ public void setAttributes( .getAttributes()); } - /* STATIC INIT */ + @StaticInit public void setupSampler( List dropNonApplicationUris, List dropStaticResources) { diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/InstrumentationRecorder.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/InstrumentationRecorder.java index debb921c7350b..11873ab929225 100644 --- a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/InstrumentationRecorder.java +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/InstrumentationRecorder.java @@ -19,6 +19,7 @@ import io.quarkus.opentelemetry.runtime.tracing.intrumentation.vertx.SqlClientInstrumenterVertxTracer; import io.quarkus.runtime.RuntimeValue; import io.quarkus.runtime.annotations.Recorder; +import io.quarkus.runtime.annotations.RuntimeInit; import io.vertx.core.VertxOptions; import io.vertx.core.metrics.MetricsOptions; import io.vertx.core.tracing.TracingOptions; @@ -34,7 +35,7 @@ public InstrumentationRecorder(RuntimeValue config) { this.config = config; } - /* RUNTIME INIT */ + @RuntimeInit public Consumer getVertxTracingOptions() { TracingOptions tracingOptions = new TracingOptions() .setFactory(FACTORY); @@ -42,6 +43,7 @@ public Consumer getVertxTracingOptions() { } /* RUNTIME INIT */ + @RuntimeInit public void setupVertxTracer(BeanContainer beanContainer, boolean sqlClientAvailable, boolean redisClientAvailable, final String semconvStability) { OpenTelemetry openTelemetry = beanContainer.beanInstance(OpenTelemetry.class); diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleMethodBuildItem.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleMethodBuildItem.java index 3509d439e5821..56809719f7b0a 100644 --- a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleMethodBuildItem.java +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleMethodBuildItem.java @@ -3,11 +3,12 @@ import org.jboss.jandex.MethodInfo; import io.quarkus.builder.item.MultiBuildItem; +import io.quarkus.qute.deployment.TemplatesAnalysisBuildItem.TemplateAnalysis; /** * Represents a message bundle method. *

- * Note that templates that contain no expressions don't need to be validated. + * Note that templates that contain no expressions/sections don't need to be validated. */ public final class MessageBundleMethodBuildItem extends MultiBuildItem { @@ -36,14 +37,27 @@ public String getKey() { return key; } + /** + * + * @return the template id or {@code null} if there is no need to use qute; i.e. no expression/section found + */ public String getTemplateId() { return templateId; } + /** + * For example, there is no corresponding method for generated enum constant message keys. + * + * @return the method or {@code null} if there is no corresponding method declared on the message bundle interface + */ public MethodInfo getMethod() { return method; } + public boolean hasMethod() { + return method != null; + } + public String getTemplate() { return template; } @@ -65,4 +79,19 @@ public boolean isDefaultBundle() { return isDefaultBundle; } + /** + * + * @return the path + * @see TemplateAnalysis#path + */ + public String getPathForAnalysis() { + if (method != null) { + return method.declaringClass().name() + "#" + method.name(); + } + if (templateId != null) { + return templateId; + } + return bundleName + "_" + key; + } + } 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 a78a3331d11d7..3b45e64f29e94 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 @@ -38,6 +38,7 @@ import org.jboss.jandex.ClassInfo; import org.jboss.jandex.ClassInfo.NestingType; 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; @@ -85,6 +86,8 @@ import io.quarkus.qute.Namespaces; import io.quarkus.qute.Resolver; import io.quarkus.qute.SectionHelperFactory; +import io.quarkus.qute.TemplateException; +import io.quarkus.qute.TemplateInstance; import io.quarkus.qute.deployment.QuteProcessor.JavaMemberLookupConfig; import io.quarkus.qute.deployment.QuteProcessor.MatchResult; import io.quarkus.qute.deployment.TemplatesAnalysisBuildItem.TemplateAnalysis; @@ -205,7 +208,7 @@ List processBundles(BeanArchiveIndexBuildItem beanArchiv Map localeToMergeCandidate = new HashMap<>(); for (Path messageFile : messageFiles) { String fileName = messageFile.getFileName().toString(); - if (fileName.startsWith(name)) { + if (bundleNameMatchesFileName(fileName, name)) { final String locale; int postfixIdx = fileName.indexOf('.'); if (postfixIdx == name.length()) { @@ -266,7 +269,7 @@ List processBundles(BeanArchiveIndexBuildItem beanArchiv // Generate implementations // name -> impl class Map generatedImplementations = generateImplementations(bundles, generatedClasses, - messageTemplateMethods); + messageTemplateMethods, index); // Register synthetic beans for (MessageBundleBuildItem bundle : bundles) { @@ -315,6 +318,30 @@ List processBundles(BeanArchiveIndexBuildItem beanArchiv return bundles; } + static boolean bundleNameMatchesFileName(String fileName, String name) { + int fileSeparatorIdx = fileName.indexOf('.'); + // Remove file extension if exists + if (fileSeparatorIdx > -1) { + fileName = fileName.substring(0, fileSeparatorIdx); + } + // Split the filename and the bundle name by underscores + String[] fileNameParts = fileName.split("_"); + String[] nameParts = name.split("_"); + + if (fileNameParts.length < nameParts.length) { + return false; + } + + // Compare each part of the filename with the corresponding part of the bundle name + for (int i = 0; i < nameParts.length; i++) { + if (!fileNameParts[i].equals(nameParts[i])) { + return false; + } + } + + return true; + } + @Record(value = STATIC_INIT) @BuildStep void initBundleContext(MessageBundleRecorder recorder, @@ -369,8 +396,11 @@ void validateMessageBundleMethods(TemplatesAnalysisBuildItem templatesAnalysis, if (messageBundleMethod != null) { // All top-level expressions without a namespace should be mapped to a param Set usedParamNames = new HashSet<>(); - Set paramNames = IntStream.range(0, messageBundleMethod.getMethod().parametersCount()) - .mapToObj(idx -> getParameterName(messageBundleMethod.getMethod(), idx)).collect(Collectors.toSet()); + Set paramNames = messageBundleMethod.hasMethod() + ? IntStream.range(0, messageBundleMethod.getMethod().parametersCount()) + .mapToObj(idx -> getParameterName(messageBundleMethod.getMethod(), idx)) + .collect(Collectors.toSet()) + : Set.of(); for (Expression expression : analysis.expressions) { validateExpression(incorrectExpressions, messageBundleMethod, expression, paramNames, usedParamNames, globals); @@ -407,9 +437,8 @@ private void validateExpression(BuildProducer inco // Expression has no type info or type info that does not match a method parameter // expressions that have incorrectExpressions.produce(new IncorrectExpressionBuildItem(expression.toOriginalString(), - name + " is not a parameter of the message bundle method " - + messageBundleMethod.getMethod().declaringClass().name() + "#" - + messageBundleMethod.getMethod().name() + "()", + name + " is not a parameter of the message bundle method: " + + messageBundleMethod.getPathForAnalysis(), expression.getOrigin())); } else { usedParamNames.add(name); @@ -544,6 +573,10 @@ public String apply(String id) { MethodInfo method = methods.get(methodPart.getName()); if (method == null) { + if (methods.containsKey(methodPart.getName())) { + // Skip validation - enum constant key + continue; + } if (!methodPart.isVirtualMethod() || methodPart.asVirtualMethod().getParameters().isEmpty()) { // The method template may contain no expressions method = defaultBundleInterface.method(methodPart.getName()); @@ -666,7 +699,8 @@ void generateExamplePropertiesFiles(List messageBu private Map generateImplementations(List bundles, BuildProducer generatedClasses, - BuildProducer messageTemplateMethods) throws IOException { + BuildProducer messageTemplateMethods, + IndexView index) throws IOException { Map generatedTypes = new HashMap<>(); @@ -677,29 +711,33 @@ private Map generateImplementations(List // take message templates not specified by Message#value from corresponding localized file Map defaultKeyToMap = getLocalizedFileKeyToTemplate(bundle, bundleInterface, - bundle.getDefaultLocale(), bundleInterface.methods(), null); + bundle.getDefaultLocale(), bundleInterface.methods(), null, index); MergeClassInfoWrapper bundleInterfaceWrapper = new MergeClassInfoWrapper(bundleInterface, null, null); + // Generate implementation for the default bundle interface String bundleImpl = generateImplementation(bundle, null, null, bundleInterfaceWrapper, - defaultClassOutput, messageTemplateMethods, defaultKeyToMap, null); + defaultClassOutput, messageTemplateMethods, defaultKeyToMap, null, index); generatedTypes.put(bundleInterface.name().toString(), bundleImpl); + + // Generate imeplementation for each localized interface for (Entry entry : bundle.getLocalizedInterfaces().entrySet()) { ClassInfo localizedInterface = entry.getValue(); // take message templates not specified by Message#value from corresponding localized file Map keyToMap = getLocalizedFileKeyToTemplate(bundle, bundleInterface, entry.getKey(), - localizedInterface.methods(), localizedInterface); + localizedInterface.methods(), localizedInterface, index); MergeClassInfoWrapper localizedInterfaceWrapper = new MergeClassInfoWrapper(localizedInterface, bundleInterface, keyToMap); generatedTypes.put(entry.getValue().name().toString(), generateImplementation(bundle, bundleInterface, bundleImpl, localizedInterfaceWrapper, - defaultClassOutput, messageTemplateMethods, keyToMap, null)); + defaultClassOutput, messageTemplateMethods, keyToMap, null, index)); } + // Generate implementation for each localized file for (Entry entry : bundle.getLocalizedFiles().entrySet()) { Path localizedFile = entry.getValue(); - var keyToTemplate = parseKeyToTemplateFromLocalizedFile(bundleInterface, localizedFile); + var keyToTemplate = parseKeyToTemplateFromLocalizedFile(bundleInterface, localizedFile, index); String locale = entry.getKey(); ClassOutput localeAwareGizmoAdaptor = new GeneratedClassGizmoAdaptor(generatedClasses, @@ -715,19 +753,19 @@ public String apply(String className) { })); generatedTypes.put(localizedFile.toString(), generateImplementation(bundle, bundleInterface, bundleImpl, new SimpleClassInfoWrapper(bundleInterface), - localeAwareGizmoAdaptor, messageTemplateMethods, keyToTemplate, locale)); + localeAwareGizmoAdaptor, messageTemplateMethods, keyToTemplate, locale, index)); } } return generatedTypes; } private Map getLocalizedFileKeyToTemplate(MessageBundleBuildItem bundle, - ClassInfo bundleInterface, String locale, List methods, ClassInfo localizedInterface) + ClassInfo bundleInterface, String locale, List methods, ClassInfo localizedInterface, IndexView index) throws IOException { Path localizedFile = bundle.getMergeCandidates().get(locale); if (localizedFile != null) { - Map keyToTemplate = parseKeyToTemplateFromLocalizedFile(bundleInterface, localizedFile); + Map keyToTemplate = parseKeyToTemplateFromLocalizedFile(bundleInterface, localizedFile, index); if (!keyToTemplate.isEmpty()) { // keep message templates if value wasn't provided by Message#value @@ -761,12 +799,17 @@ private Map getLocalizedFileKeyToTemplate(MessageBundleBuildItem } private Map parseKeyToTemplateFromLocalizedFile(ClassInfo bundleInterface, - Path localizedFile) throws IOException { + Path localizedFile, IndexView index) throws IOException { Map keyToTemplate = new HashMap<>(); for (ListIterator it = Files.readAllLines(localizedFile).listIterator(); it.hasNext();) { String line = it.next(); - if (line.startsWith("#") || line.isBlank()) { - // Comments and blank lines are skipped + if (line.isBlank()) { + // Blank lines are skipped + continue; + } + line = line.strip(); + if (line.startsWith("#")) { + // Comments are skipped continue; } int eqIdx = line.indexOf('='); @@ -775,7 +818,7 @@ private Map parseKeyToTemplateFromLocalizedFile(ClassInfo bundle "Missing key/value separator\n\t- file: " + localizedFile + "\n\t- line " + it.previousIndex()); } String key = line.substring(0, eqIdx).strip(); - if (!hasMessageBundleMethod(bundleInterface, key)) { + if (!hasMessageBundleMethod(bundleInterface, key) && !isEnumConstantMessageKey(key, index, bundleInterface)) { throw new MessageBundleException( "Message bundle method " + key + "() not found on: " + bundleInterface + "\n\t- file: " + localizedFile + "\n\t- line " + it.previousIndex()); @@ -793,6 +836,42 @@ private Map parseKeyToTemplateFromLocalizedFile(ClassInfo bundle return keyToTemplate; } + /** + * + * @param key + * @param bundleInterface + * @return {@code true} if the given key represents an enum constant message key, such as {@code myEnum_CONSTANT1} + * @see #toEnumConstantKey(String, String) + */ + boolean isEnumConstantMessageKey(String key, IndexView index, ClassInfo bundleInterface) { + if (key.isBlank()) { + return false; + } + int lastIdx = key.lastIndexOf("_"); + if (lastIdx != -1 && lastIdx != key.length()) { + String methodName = key.substring(0, lastIdx); + String constant = key.substring(lastIdx + 1, key.length()); + MethodInfo method = messageBundleMethod(bundleInterface, methodName); + if (method != null && method.parametersCount() == 1) { + Type paramType = method.parameterType(0); + if (paramType.kind() == org.jboss.jandex.Type.Kind.CLASS) { + ClassInfo maybeEnum = index.getClassByName(paramType.name()); + if (maybeEnum != null && maybeEnum.isEnum()) { + if (maybeEnum.fields().stream() + .filter(FieldInfo::isEnumConstant) + .map(FieldInfo::name) + .anyMatch(constant::equals)) { + return true; + } + throw new MessageBundleException( + String.format("%s is not an enum constant of %: %s", constant, maybeEnum, key)); + } + } + } + } + return false; + } + private void constructLine(StringBuilder builder, Iterator it) { if (it.hasNext()) { String nextLine = adaptLine(it.next()); @@ -810,19 +889,22 @@ private String adaptLine(String line) { } private boolean hasMessageBundleMethod(ClassInfo bundleInterface, String name) { + return messageBundleMethod(bundleInterface, name) != null; + } + + private MethodInfo messageBundleMethod(ClassInfo bundleInterface, String name) { for (MethodInfo method : bundleInterface.methods()) { if (method.name().equals(name)) { - return true; + return method; } } - return false; + return null; } private String generateImplementation(MessageBundleBuildItem bundle, ClassInfo defaultBundleInterface, - String defaultBundleImpl, - ClassInfoWrapper bundleInterfaceWrapper, ClassOutput classOutput, + String defaultBundleImpl, ClassInfoWrapper bundleInterfaceWrapper, ClassOutput classOutput, BuildProducer messageTemplateMethods, - Map messageTemplates, String locale) { + Map messageTemplates, String locale, IndexView index) { ClassInfo bundleInterface = bundleInterfaceWrapper.getClassInfo(); LOG.debugf("Generate bundle implementation for %s", bundleInterface); @@ -855,7 +937,7 @@ private String generateImplementation(MessageBundleBuildItem bundle, ClassInfo d ClassCreator bundleCreator = builder.build(); // key -> method - Map keyMap = new LinkedHashMap<>(); + Map keyMap = new LinkedHashMap<>(); List methods = new ArrayList<>(bundleInterfaceWrapper.methods()); // Sort methods methods.sort(Comparator.comparing(MethodInfo::name).thenComparing(Comparator.comparing(MethodInfo::toString))); @@ -898,7 +980,7 @@ private String generateImplementation(MessageBundleBuildItem bundle, ClassInfo d if (keyMap.containsKey(key)) { throw new MessageBundleException(String.format("Duplicate key [%s] found on %s", key, bundleInterface)); } - keyMap.put(key, method); + keyMap.put(key, new SimpleMessageMethod(method)); String messageTemplate = messageTemplates.get(method.name()); if (messageTemplate == null) { @@ -911,6 +993,50 @@ private String generateImplementation(MessageBundleBuildItem bundle, ClassInfo d method.parameterTypes().toArray(new Type[] {}))).annotation(Names.MESSAGE)); } + // We need some special handling for enum message bundle methods + // A message bundle method that accepts an enum and has no message template receives a generated template: + // {#when enumParamName} + // {#is CONSTANT1}{msg:org_acme_MyEnum_CONSTANT1} + // {#is CONSTANT2}{msg:org_acme_MyEnum_CONSTANT2} + // ... + // {/when} + // Furthermore, a special message method is generated for each enum constant + if (messageTemplate == null && method.parametersCount() == 1) { + Type paramType = method.parameterType(0); + if (paramType.kind() == org.jboss.jandex.Type.Kind.CLASS) { + ClassInfo maybeEnum = index.getClassByName(paramType.name()); + if (maybeEnum != null && maybeEnum.isEnum()) { + StringBuilder generatedMessageTemplate = new StringBuilder("{#when ") + .append(getParameterName(method, 0)) + .append("}"); + Set enumConstants = maybeEnum.fields().stream().filter(FieldInfo::isEnumConstant) + .map(FieldInfo::name).collect(Collectors.toSet()); + for (String enumConstant : enumConstants) { + // org_acme_MyEnum_CONSTANT1 + String enumConstantKey = toEnumConstantKey(method.name(), enumConstant); + String enumConstantTemplate = messageTemplates.get(enumConstantKey); + if (enumConstantTemplate == null) { + throw new TemplateException( + String.format("Enum constant message not found in bundle [%s] for key: %s", + bundleName + (locale != null ? "_" + locale : ""), enumConstantKey)); + } + generatedMessageTemplate.append("{#is ") + .append(enumConstant) + .append("}{") + .append(bundle.getName()) + .append(":") + .append(enumConstantKey) + .append("}"); + generateEnumConstantMessageMethod(bundleCreator, bundleName, locale, bundleInterface, + defaultBundleInterface, enumConstantKey, keyMap, enumConstantTemplate, + messageTemplateMethods); + } + generatedMessageTemplate.append("{/when}"); + messageTemplate = generatedMessageTemplate.toString(); + } + } + } + if (messageTemplate == null) { throw new MessageBundleException( String.format("Message template for key [%s] is missing for default locale [%s]", key, @@ -919,6 +1045,7 @@ private String generateImplementation(MessageBundleBuildItem bundle, ClassInfo d String templateId = null; if (messageTemplate.contains("}")) { + // Qute is needed - at least one expression/section found if (defaultBundleInterface != null) { if (locale == null) { AnnotationInstance localizedAnnotation = bundleInterface.declaredAnnotation(Names.LOCALIZED); @@ -946,6 +1073,12 @@ private String generateImplementation(MessageBundleBuildItem bundle, ClassInfo d // Create a template instance ResultHandle templateInstance = bundleMethod .invokeInterfaceMethod(io.quarkus.qute.deployment.Descriptors.TEMPLATE_INSTANCE, template); + if (locale != null) { + bundleMethod.invokeInterfaceMethod( + MethodDescriptor.ofMethod(TemplateInstance.class, "setLocale", TemplateInstance.class, + String.class), + templateInstance, bundleMethod.load(locale)); + } List paramTypes = method.parameterTypes(); if (!paramTypes.isEmpty()) { // Set data @@ -973,6 +1106,62 @@ private String generateImplementation(MessageBundleBuildItem bundle, ClassInfo d return generatedName.replace('/', '.'); } + private String toEnumConstantKey(String methodName, String enumConstant) { + return methodName + "_" + enumConstant; + } + + private void generateEnumConstantMessageMethod(ClassCreator bundleCreator, String bundleName, String locale, + ClassInfo bundleInterface, ClassInfo defaultBundleInterface, String enumConstantKey, + Map keyMap, String messageTemplate, + BuildProducer messageTemplateMethods) { + String templateId = null; + if (messageTemplate.contains("}")) { + if (defaultBundleInterface != null) { + if (locale == null) { + AnnotationInstance localizedAnnotation = bundleInterface + .declaredAnnotation(Names.LOCALIZED); + locale = localizedAnnotation.value().asString(); + } + templateId = bundleName + "_" + locale + "_" + enumConstantKey; + } else { + templateId = bundleName + "_" + enumConstantKey; + } + } + + MessageBundleMethodBuildItem messageBundleMethod = new MessageBundleMethodBuildItem(bundleName, enumConstantKey, + templateId, null, messageTemplate, + defaultBundleInterface == null); + messageTemplateMethods.produce(messageBundleMethod); + + MethodCreator enumConstantMethod = bundleCreator.getMethodCreator(enumConstantKey, + String.class); + + if (!messageBundleMethod.isValidatable()) { + // No expression/tag - no need to use qute + enumConstantMethod.returnValue(enumConstantMethod.load(messageTemplate)); + } else { + // Obtain the template, e.g. msg_org_acme_MyEnum_CONSTANT1 + ResultHandle template = enumConstantMethod.invokeStaticMethod( + io.quarkus.qute.deployment.Descriptors.BUNDLES_GET_TEMPLATE, + enumConstantMethod.load(templateId)); + // Create a template instance + ResultHandle templateInstance = enumConstantMethod + .invokeInterfaceMethod(io.quarkus.qute.deployment.Descriptors.TEMPLATE_INSTANCE, template); + if (locale != null) { + enumConstantMethod.invokeInterfaceMethod( + MethodDescriptor.ofMethod(TemplateInstance.class, "setLocale", TemplateInstance.class, + String.class), + templateInstance, enumConstantMethod.load(locale)); + } + // Render the template + enumConstantMethod.returnValue(enumConstantMethod.invokeInterfaceMethod( + io.quarkus.qute.deployment.Descriptors.TEMPLATE_INSTANCE_RENDER, templateInstance)); + } + + keyMap.put(enumConstantKey, + new EnumConstantMessageMethod(enumConstantMethod.getMethodDescriptor())); + } + /** * @return {@link Message#value()} if value was provided */ @@ -1006,7 +1195,7 @@ static String getParameterName(MethodInfo method, int position) { return name; } - private void implementResolve(String defaultBundleImpl, ClassCreator bundleCreator, Map keyMap) { + private void implementResolve(String defaultBundleImpl, ClassCreator bundleCreator, Map keyMap) { MethodCreator resolve = bundleCreator.getMethodCreator("resolve", CompletionStage.class, EvalContext.class); String resolveMethodPrefix = bundleCreator.getClassName().contains("/") ? bundleCreator.getClassName().substring(bundleCreator.getClassName().lastIndexOf('/') + 1) @@ -1077,7 +1266,7 @@ private void implementResolve(String defaultBundleImpl, ClassCreator bundleCreat int resolveIndex = 0; MethodCreator resolveGroup = null; - for (Entry entry : keyMap.entrySet()) { + for (Entry entry : keyMap.entrySet()) { if (resolveGroup == null || groupIndex++ >= groupLimit) { groupIndex = 0; String resolveMethodName = resolveMethodPrefix + "_resolve_" + resolveIndex++; @@ -1118,16 +1307,18 @@ private void implementResolve(String defaultBundleImpl, ClassCreator bundleCreat } } - private void addMessageMethod(MethodCreator resolve, String key, MethodInfo method, ResultHandle name, + private void addMessageMethod(MethodCreator resolve, String key, MessageMethod method, ResultHandle name, ResultHandle evaluatedParams, ResultHandle ret, String bundleClass) { List methodParams = method.parameterTypes(); BytecodeCreator matched = resolve.ifTrue(Gizmo.equals(resolve, resolve.load(key), name)) .trueBranch(); - if (method.parameterTypes().isEmpty()) { + if (methodParams.isEmpty()) { matched.invokeVirtualMethod(Descriptors.COMPLETABLE_FUTURE_COMPLETE, ret, - matched.invokeInterfaceMethod(method, matched.getThis())); + method.isMessageBundleInterfaceMethod() + ? matched.invokeInterfaceMethod(method.descriptor(), matched.getThis()) + : matched.invokeVirtualMethod(method.descriptor(), matched.getThis())); matched.returnValue(ret); } else { // The CompletionStage upon which we invoke whenComplete() @@ -1171,7 +1362,9 @@ private void addMessageMethod(MethodCreator resolve, String key, MethodInfo meth exception.getCaughtException()); tryCatch.assign(invokeRet, - tryCatch.invokeInterfaceMethod(MethodDescriptor.of(method), whenThis, paramsHandle)); + method.isMessageBundleInterfaceMethod() + ? tryCatch.invokeInterfaceMethod(method.descriptor(), whenThis, paramsHandle) + : tryCatch.invokeVirtualMethod(method.descriptor(), whenThis, paramsHandle)); tryCatch.invokeVirtualMethod(Descriptors.COMPLETABLE_FUTURE_COMPLETE, whenRet, invokeRet); // CompletableFuture.completeExceptionally(Throwable) @@ -1395,4 +1588,61 @@ public final MethodInfo method(String name, Type... parameters) { return classInfo.method(name, parameters); } } + + interface MessageMethod { + + List parameterTypes(); + + MethodDescriptor descriptor(); + + default boolean isMessageBundleInterfaceMethod() { + return true; + } + + } + + static class SimpleMessageMethod implements MessageMethod { + + final MethodInfo method; + + SimpleMessageMethod(MethodInfo method) { + this.method = method; + } + + @Override + public List parameterTypes() { + return method.parameterTypes(); + } + + @Override + public MethodDescriptor descriptor() { + return MethodDescriptor.of(method); + } + + } + + static class EnumConstantMessageMethod implements MessageMethod { + + final MethodDescriptor descriptor; + + EnumConstantMessageMethod(MethodDescriptor descriptor) { + this.descriptor = descriptor; + } + + @Override + public List parameterTypes() { + return List.of(); + } + + @Override + public MethodDescriptor descriptor() { + return descriptor; + } + + @Override + public boolean isMessageBundleInterfaceMethod() { + return false; + } + + } } 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 917c311f6aee7..7353f30506eaa 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 @@ -8,7 +8,6 @@ import static java.util.function.Predicate.not; import static java.util.stream.Collectors.toMap; -import java.io.File; import java.io.IOException; import java.io.Reader; import java.io.StringReader; @@ -715,10 +714,12 @@ public void beforeParsing(ParserHelper parserHelper) { MessageBundleMethodBuildItem messageBundleMethod = messageBundleMethodsMap.get(templateId); if (messageBundleMethod != null) { MethodInfo method = messageBundleMethod.getMethod(); - for (ListIterator it = method.parameterTypes().listIterator(); it.hasNext();) { - Type paramType = it.next(); - String name = MessageBundleProcessor.getParameterName(method, it.previousIndex()); - parserHelper.addParameter(name, getCheckedTemplateParameterTypeName(paramType)); + if (method != null) { + for (ListIterator it = method.parameterTypes().listIterator(); it.hasNext();) { + Type paramType = it.next(); + String name = MessageBundleProcessor.getParameterName(method, it.previousIndex()); + parserHelper.addParameter(name, getCheckedTemplateParameterTypeName(paramType)); + } } } } @@ -760,9 +761,7 @@ public void beforeParsing(ParserHelper parserHelper) { for (MessageBundleMethodBuildItem messageBundleMethod : messageBundleMethods) { Template template = dummyEngine.parse(messageBundleMethod.getTemplate(), null, messageBundleMethod.getTemplateId()); analysis.add(new TemplateAnalysis(messageBundleMethod.getTemplateId(), template.getGeneratedId(), - template.getExpressions(), template.getParameterDeclarations(), - messageBundleMethod.getMethod().declaringClass().name() + "#" + messageBundleMethod.getMethod().name() - + "()", + template.getExpressions(), template.getParameterDeclarations(), messageBundleMethod.getPathForAnalysis(), template.getFragmentIds())); } @@ -2149,15 +2148,17 @@ public boolean test(String path) { } for (Path resolvedPath : artifact.getResolvedPaths()) { if (Files.isDirectory(resolvedPath)) { - scanPath(resolvedPath, resolvedPath, config, templateRoots, watchedPaths, templatePaths, + scanRootPath(resolvedPath, config, templateRoots, watchedPaths, templatePaths, nativeImageResources); } else { try (FileSystem artifactFs = ZipUtils.newFileSystem(resolvedPath)) { + // Iterate over template roots, such as "templates", and collect the included templates for (String templateRoot : templateRoots) { Path artifactBasePath = artifactFs.getPath(templateRoot); if (Files.exists(artifactBasePath)) { - LOGGER.debugf("Found extension templates in: %s", resolvedPath); - scan(artifactBasePath, artifactBasePath, templateRoot + "/", watchedPaths, templatePaths, + LOGGER.debugf("Found template root in extension artifact: %s", resolvedPath); + scanDirectory(artifactBasePath, artifactBasePath, templateRoot + "/", watchedPaths, + templatePaths, nativeImageResources, config); } @@ -2173,13 +2174,20 @@ public boolean test(String path) { for (Path root : tree.getRoots()) { // Note that we cannot use ApplicationArchive.getChildPath(String) here because we would not be able to detect // a wrong directory name on case-insensitive file systems - scanPath(root, root, config, templateRoots, watchedPaths, templatePaths, nativeImageResources); + scanRootPath(root, config, templateRoots, watchedPaths, templatePaths, nativeImageResources); } }); } } - private void scanPath(Path rootPath, Path path, QuteConfig config, TemplateRootsBuildItem templateRoots, + private void scanRootPath(Path rootPath, QuteConfig config, TemplateRootsBuildItem templateRoots, + BuildProducer watchedPaths, + BuildProducer templatePaths, + BuildProducer nativeImageResources) { + scanRootPath(rootPath, rootPath, config, templateRoots, watchedPaths, templatePaths, nativeImageResources); + } + + private void scanRootPath(Path rootPath, Path path, QuteConfig config, TemplateRootsBuildItem templateRoots, BuildProducer watchedPaths, BuildProducer templatePaths, BuildProducer nativeImageResources) { @@ -2193,15 +2201,15 @@ private void scanPath(Path rootPath, Path path, QuteConfig config, TemplateRoots // "/io", "/META-INF", "/templates", "/web", etc. Path relativePath = rootPath.relativize(file); if (templateRoots.isRoot(relativePath)) { - LOGGER.debugf("Found templates dir: %s", file); - // The base path is an OS-specific path relative to the template root - String basePath = relativePath.toString() + File.separatorChar; - scan(file, file, basePath, watchedPaths, templatePaths, + LOGGER.debugf("Found templates root dir: %s", file); + // The base path is an OS-specific template root path relative to the scanned root path + String basePath = relativePath.toString() + relativePath.getFileSystem().getSeparator(); + scanDirectory(file, file, basePath, watchedPaths, templatePaths, nativeImageResources, config); } else if (templateRoots.maybeRoot(relativePath)) { // Scan the path recursively because the template root may be nested, for example "/web/public" - scanPath(rootPath, file, config, templateRoots, watchedPaths, templatePaths, nativeImageResources); + scanRootPath(rootPath, file, config, templateRoots, watchedPaths, templatePaths, nativeImageResources); } } } @@ -3384,33 +3392,54 @@ public static String getName(InjectionPointInfo injectionPoint) { throw new IllegalArgumentException(); } + /** + * + * @param templatePaths + * @param watchedPaths + * @param nativeImageResources + * @param osSpecificResourcePath The OS-specific resource path, i.e. templates\nested\foo.html + * @param templatePath The path relative to the template root; using the {@code /} path separator + * @param originalPath + * @param config + */ private static void produceTemplateBuildItems(BuildProducer templatePaths, BuildProducer watchedPaths, - BuildProducer nativeImageResources, String basePath, String filePath, + BuildProducer nativeImageResources, String osSpecificResourcePath, + String templatePath, Path originalPath, QuteConfig config) { - if (filePath.isEmpty()) { + if (templatePath.isEmpty()) { return; } - // OS-specific full path, i.e. templates\foo.html - String osSpecificPath = basePath + filePath; // OS-agnostic full path, i.e. templates/foo.html - String osAgnosticPath = osSpecificPath; - if (File.separatorChar != '/') { - osAgnosticPath = osAgnosticPath.replace(File.separatorChar, '/'); - } - LOGGER.debugf("Produce template build items [filePath: %s, fullPath: %s, originalPath: %s", filePath, osSpecificPath, + String osAgnosticResourcePath = toOsAgnosticPath(osSpecificResourcePath, originalPath.getFileSystem()); + LOGGER.debugf("Produce template build items [templatePath: %s, osSpecificResourcePath: %s, originalPath: %s", + templatePath, + osSpecificResourcePath, originalPath); boolean restartNeeded = true; if (config.devMode.noRestartTemplates.isPresent()) { - restartNeeded = !config.devMode.noRestartTemplates.get().matcher(osAgnosticPath).matches(); + restartNeeded = !config.devMode.noRestartTemplates.get().matcher(osAgnosticResourcePath).matches(); } - watchedPaths.produce(new HotDeploymentWatchedFileBuildItem(osAgnosticPath, restartNeeded)); - nativeImageResources.produce(new NativeImageResourceBuildItem(osSpecificPath)); + watchedPaths.produce(new HotDeploymentWatchedFileBuildItem(osAgnosticResourcePath, restartNeeded)); + nativeImageResources.produce(new NativeImageResourceBuildItem(osSpecificResourcePath)); templatePaths.produce( - new TemplatePathBuildItem(filePath, originalPath, readTemplateContent(originalPath, config.defaultCharset))); + new TemplatePathBuildItem(templatePath, originalPath, + readTemplateContent(originalPath, config.defaultCharset))); } - private void scan(Path root, Path directory, String basePath, BuildProducer watchedPaths, + /** + * + * @param root + * @param directory + * @param basePath OS-specific template root path relative to the scanned root path, e.g. {@code templates/} + * @param watchedPaths + * @param templatePaths + * @param nativeImageResources + * @param config + * @throws IOException + */ + private void scanDirectory(Path root, Path directory, String basePath, + BuildProducer watchedPaths, BuildProducer templatePaths, BuildProducer nativeImageResources, QuteConfig config) @@ -3431,24 +3460,36 @@ private void scan(Path root, Path directory, String basePath, BuildProducer> excludes) { for (Predicate exclude : excludes) { if (exclude.test(check)) { diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/MessageBundleProcessorTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/MessageBundleProcessorTest.java new file mode 100644 index 0000000000000..4850f984ef46c --- /dev/null +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/MessageBundleProcessorTest.java @@ -0,0 +1,26 @@ +package io.quarkus.qute.deployment; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +class MessageBundleProcessorTest { + + @Test + void bundleNameMatchesFileName() { + assertTrue(MessageBundleProcessor.bundleNameMatchesFileName("messages.properties", "messages")); + assertTrue(MessageBundleProcessor.bundleNameMatchesFileName("started.properties", "started")); + assertTrue(MessageBundleProcessor.bundleNameMatchesFileName("startedValidation.properties", "startedValidation")); + assertTrue(MessageBundleProcessor.bundleNameMatchesFileName("EmailBundles_startedValidation.properties", + "EmailBundles_startedValidation")); + assertTrue(MessageBundleProcessor.bundleNameMatchesFileName("EmailBundles_startedValidation_pt_BR.properties", + "EmailBundles_startedValidation")); + + assertFalse(MessageBundleProcessor.bundleNameMatchesFileName("startedValidation.properties", "started")); + assertFalse(MessageBundleProcessor.bundleNameMatchesFileName("EmailBundles_startedValidation.properties", + "EmailBundles_started")); + assertFalse(MessageBundleProcessor.bundleNameMatchesFileName("EmailBundles_startedValidation_pt_BR.properties", + "EmailBundles_started")); + } +} 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 b5b0388ecca85..35674762c495a 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 @@ -1,6 +1,10 @@ package io.quarkus.qute.deployment.globals; 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.concurrent.atomic.AtomicBoolean; import jakarta.inject.Inject; @@ -8,9 +12,11 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; +import io.quarkus.qute.Engine; import io.quarkus.qute.Qute; import io.quarkus.qute.Template; import io.quarkus.qute.TemplateGlobal; +import io.quarkus.qute.TemplateInstance; import io.quarkus.test.QuarkusUnitTest; public class TemplateGlobalTest { @@ -23,11 +29,19 @@ public class TemplateGlobalTest { "Hello {currentUser}|{global:currentUser}! Your name is {_name}|{global:_name}. You're {age}|{global:age} years old."), "templates/hello.txt")); + @Inject + Engine engine; + @Inject Template hello; @Test public void testTemplateData() { + TemplateInstance instance = engine.parse("Hello {age}!").instance(); + assertFalse(Globals.AGE_USED.get()); + assertEquals("Hello 40!", instance.render()); + assertTrue(Globals.AGE_USED.get()); + 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( @@ -45,11 +59,14 @@ public void testTemplateData() { public static class Globals { + static final AtomicBoolean AGE_USED = new AtomicBoolean(); + @TemplateGlobal(name = "currentUser") static String user = "Fu"; @TemplateGlobal static int age() { + AGE_USED.set(true); return user.equals("Fu") ? 40 : 20; } diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/EmailBundles.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/EmailBundles.java new file mode 100644 index 0000000000000..2f7c89a8d0c19 --- /dev/null +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/EmailBundles.java @@ -0,0 +1,45 @@ +package io.quarkus.qute.deployment.i18n; + +import io.quarkus.qute.i18n.Message; +import io.quarkus.qute.i18n.MessageBundle; + +public class EmailBundles { + @MessageBundle + interface started { + @Message + String started(String id, String filename); + + @Message + String documentAccessUrl(String url); + + @Message + String nextNotification(); + + @Message + String signingProcessStart(String id, String filename); + + @Message + String subject(String customer, String filename); + + @Message + String signForValidation(); + } + + @MessageBundle + interface startedValidator { + @Message + String started(String id, String filename); + + @Message + String turnEmailWillBeSent(); + + @Message + String youMayAlreadyAccessDocument(); + + @Message + String subject(String customer, String filename); + + @Message + String signForValidation(); + } +} diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleDefaultedNameTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleDefaultedNameTest.java index c171a5f3e1a22..1db4062826838 100644 --- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleDefaultedNameTest.java +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleDefaultedNameTest.java @@ -2,8 +2,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals; -import java.util.Locale; - import jakarta.inject.Inject; import org.jboss.shrinkwrap.api.asset.StringAsset; @@ -13,7 +11,6 @@ import io.quarkus.qute.Engine; import io.quarkus.qute.i18n.Message; import io.quarkus.qute.i18n.MessageBundle; -import io.quarkus.qute.i18n.MessageBundles; import io.quarkus.test.QuarkusUnitTest; public class MessageBundleDefaultedNameTest { @@ -43,8 +40,7 @@ public class MessageBundleDefaultedNameTest { public void testBundles() { assertEquals("Hello world!", Controller.Templates.index("world").render()); - assertEquals("Ahoj svete!", Controller.Templates.index("svete") - .setAttribute(MessageBundles.ATTRIBUTE_LOCALE, Locale.forLanguageTag("cs")).render()); + assertEquals("Ahoj svete!", Controller.Templates.index("svete").setLocale("cs").render()); assertEquals("Hello world!", engine.getTemplate("app").render()); assertEquals("Hello alpha!", engine.getTemplate("alpha").render()); diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleEnumTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleEnumTest.java new file mode 100644 index 0000000000000..8ac3a9e739810 --- /dev/null +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleEnumTest.java @@ -0,0 +1,74 @@ +package io.quarkus.qute.deployment.i18n; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import jakarta.inject.Inject; + +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.qute.Template; +import io.quarkus.qute.i18n.Message; +import io.quarkus.qute.i18n.MessageBundle; +import io.quarkus.test.QuarkusUnitTest; + +public class MessageBundleEnumTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(Messages.class, MyEnum.class) + .addAsResource("messages/enu.properties") + .addAsResource("messages/enu_cs.properties") + .addAsResource(new StringAsset( + "{enu:myEnum(MyEnum:ON)}::{enu:myEnum(MyEnum:OFF)}::{enu:myEnum(MyEnum:UNDEFINED)}::" + + "{enu:shortEnum(MyEnum:ON)}::{enu:shortEnum(MyEnum:OFF)}::{enu:shortEnum(MyEnum:UNDEFINED)}::" + + "{enu:foo(MyEnum:ON)}::{enu:foo(MyEnum:OFF)}::{enu:foo(MyEnum:UNDEFINED)}::" + + "{enu:locFileOverride(MyEnum:ON)}::{enu:locFileOverride(MyEnum:OFF)}::{enu:locFileOverride(MyEnum:UNDEFINED)}"), + "templates/foo.html")); + + @Inject + Template foo; + + @Test + public void testMessages() { + assertEquals("On::Off::Undefined::1::0::U::+::-::_::on::off::undefined", foo.render()); + assertEquals("Zapnuto::Vypnuto::Nedefinováno::1::0::N::+::-::_::zap::vyp::nedef", + foo.instance().setLocale("cs").render()); + } + + @MessageBundle(value = "enu", locale = "en") + public interface Messages { + + // Replaced with: + // @Message("{#when myEnum}" + // + "{#is ON}{enu:myEnum_ON}" + // + "{#is OFF}{enu:myEnum_OFF}" + // + "{#is UNDEFINED}{enu:myEnum_UNDEFINED}" + // + "{/when}") + @Message + String myEnum(MyEnum myEnum); + + // Replaced with: + // @Message("{#when myEnum}" + // + "{#is ON}{enu:shortEnum_ON}" + // + "{#is OFF}{enu:shortEnum_OFF}" + // + "{#is UNDEFINED}{enu:shortEnum_UNDEFINED}" + // + "{/when}") + @Message + String shortEnum(MyEnum myEnum); + + @Message("{#when myEnum}" + + "{#is ON}+" + + "{#is OFF}-" + + "{#else}_" + + "{/when}") + String foo(MyEnum myEnum); + + @Message + String locFileOverride(MyEnum myEnum); + + } + +} diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleLocaleTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleLocaleTest.java index 2131cc87cac11..31d2bdcf22a25 100644 --- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleLocaleTest.java +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleLocaleTest.java @@ -2,8 +2,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals; -import java.util.Locale; - import jakarta.inject.Inject; import org.jboss.shrinkwrap.api.asset.StringAsset; @@ -13,7 +11,6 @@ import io.quarkus.qute.Template; import io.quarkus.qute.i18n.Message; import io.quarkus.qute.i18n.MessageBundle; -import io.quarkus.qute.i18n.MessageBundles; import io.quarkus.test.QuarkusUnitTest; public class MessageBundleLocaleTest { @@ -31,8 +28,7 @@ public class MessageBundleLocaleTest { @Test public void testResolvers() { - assertEquals("Ahoj svete!", - foo.instance().setAttribute(MessageBundles.ATTRIBUTE_LOCALE, Locale.forLanguageTag("cs")).render()); + assertEquals("Ahoj svete!", foo.instance().setLocale("cs").render()); } @MessageBundle(locale = "cs") diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleLogicalLineTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleLogicalLineTest.java index cd6d3f735c280..fcc4f14a9c414 100644 --- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleLogicalLineTest.java +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleLogicalLineTest.java @@ -3,8 +3,6 @@ import static io.quarkus.qute.i18n.MessageBundle.DEFAULT_NAME; import static org.junit.jupiter.api.Assertions.assertEquals; -import java.util.Locale; - import jakarta.inject.Inject; import org.jboss.shrinkwrap.api.asset.StringAsset; @@ -14,7 +12,6 @@ import io.quarkus.qute.Template; import io.quarkus.qute.i18n.Message; import io.quarkus.qute.i18n.MessageBundle; -import io.quarkus.qute.i18n.MessageBundles; import io.quarkus.test.QuarkusUnitTest; public class MessageBundleLogicalLineTest { @@ -22,10 +19,10 @@ public class MessageBundleLogicalLineTest { @RegisterExtension static final QuarkusUnitTest config = new QuarkusUnitTest() .withApplicationRoot((jar) -> jar - .addClasses(Messages.class) + .addClasses(Messages.class, MyEnum.class) .addAsResource("messages/msg_cs.properties") .addAsResource(new StringAsset( - "{msg:hello('Edgar')} {msg:helloNextLine('Edgar')} ::{msg:fruits}"), + "{msg:hello('Edgar')}::{msg:helloNextLine('Edgar')}::{msg:fruits}::{msg:myEnum(MyEnum:OFF)}"), "templates/foo.html")); @Inject @@ -33,10 +30,10 @@ public class MessageBundleLogicalLineTest { @Test public void testResolvers() { - assertEquals("Hello Edgar! Hello \n Edgar! ::apple, banana, pear, watermelon, kiwi, mango", + assertEquals("Hello Edgar!::Hello \n Edgar!::apple, banana, pear, watermelon, kiwi, mango::Off", foo.render()); - assertEquals("Ahoj Edgar a dobrý den! Ahoj \n Edgar! ::apple, banana, pear, watermelon, kiwi, mango", - foo.instance().setAttribute(MessageBundles.ATTRIBUTE_LOCALE, Locale.forLanguageTag("cs")).render()); + assertEquals("Ahoj Edgar a dobrý den!::Ahoj \n Edgar!::jablko, banan, hruska, meloun, kiwi, mango::Vypnuto", + foo.instance().setLocale("cs").render()); } @MessageBundle(value = DEFAULT_NAME, locale = "en") @@ -50,6 +47,14 @@ public interface Messages { @Message("apple, banana, pear, watermelon, kiwi, mango") String fruits(); + + @Message("{#when myEnum}" + + "{#is ON}On" + + "{#is OFF}Off" + + "{#else}Undefined" + + "{/when}") + String myEnum(MyEnum myEnum); + } } diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleNameCollisionTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleNameCollisionTest.java new file mode 100644 index 0000000000000..6c1d85c11b4c5 --- /dev/null +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleNameCollisionTest.java @@ -0,0 +1,38 @@ +package io.quarkus.qute.deployment.i18n; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.qute.Engine; +import io.quarkus.qute.i18n.MessageBundles; +import io.quarkus.test.QuarkusUnitTest; + +public class MessageBundleNameCollisionTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .overrideConfigKey("quarkus.default-locale", "en_US") + .withApplicationRoot((jar) -> jar + .addClasses(EmailBundles.class) + .addAsResource("messages/EmailBundles_started.properties") + .addAsResource("messages/EmailBundles_started_en.properties") + .addAsResource("messages/EmailBundles_startedValidator.properties") + .addAsResource("messages/EmailBundles_startedValidator_en.properties")); + + @Inject + Engine engine; + + @Test + public void testBundleMethodIsFound() { + EmailBundles.startedValidator startedValidator = MessageBundles.get(EmailBundles.startedValidator.class); + assertEquals("You will be notified with another email when it is your turn to sign.", + startedValidator.turnEmailWillBeSent()); + assertEquals("You will be notified with another email when it is your turn to sign.", + engine.parse("{EmailBundles_startedValidator:turnEmailWillBeSent()}").render()); + } + +} diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleTest.java index 3b998b0a02af6..c9349a722dd84 100644 --- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleTest.java +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleTest.java @@ -83,8 +83,8 @@ public void testResolvers() { foo.instance().render()); assertEquals("Hello world! Ahoj Jachym! Hello you guys! Hello alpha! Hello! Hello foo from alpha!", foo.instance().setAttribute(MessageBundles.ATTRIBUTE_LOCALE, Locale.forLanguageTag("cs")).render()); - assertEquals("Hallo Welt! Hallo Jachym! Hello you guys! Hello alpha! Hello! Hello foo from alpha!", - foo.instance().setAttribute(MessageBundles.ATTRIBUTE_LOCALE, Locale.GERMAN).render()); + assertEquals("Hallo Welt! Hallo Jachym! Hallo you guys! Hello alpha! Hello! Hello foo from alpha!", + foo.instance().setLocale(Locale.GERMAN).render()); assertEquals("Dot test!", engine.parse("{msg:['dot.test']}").render()); assertEquals("Hello world! Hello Malachi Constant!", engine.getTemplate("dynamic").data("key", "hello_fullname").data("surname", "Constant").render()); diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MyEnum.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MyEnum.java new file mode 100644 index 0000000000000..7e26e81d95345 --- /dev/null +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MyEnum.java @@ -0,0 +1,10 @@ +package io.quarkus.qute.deployment.i18n; + +import io.quarkus.qute.TemplateEnum; + +@TemplateEnum +public enum MyEnum { + ON, + OFF, + UNDEFINED +} \ No newline at end of file diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/templateroot/AdditionalTemplateRootTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/templateroot/AdditionalTemplateRootTest.java index 7e26be68d7834..9095a01599387 100644 --- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/templateroot/AdditionalTemplateRootTest.java +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/templateroot/AdditionalTemplateRootTest.java @@ -28,6 +28,7 @@ public class AdditionalTemplateRootTest { static final QuarkusUnitTest config = new QuarkusUnitTest() .withApplicationRoot(root -> root .addAsResource(new StringAsset("Hi {name}!"), "templates/hi.txt") + .addAsResource(new StringAsset("Hoho {name}!"), "templates/nested/hoho.txt") .addAsResource(new StringAsset("Hello {name}!"), "web/public/hello.txt")) .addBuildChainCustomizer(buildCustomizer()); @@ -52,11 +53,13 @@ public void execute(BuildContext context) { if (item.getResources().contains("web/public/hello.txt") || item.getResources().contains("web\\public\\hello.txt") || item.getResources().contains("templates/hi.txt") - || item.getResources().contains("templates\\hi.txt")) { + || item.getResources().contains("templates\\hi.txt") + || item.getResources().contains("templates/nested/hoho.txt") + || item.getResources().contains("templates\\nested\\hoho.txt")) { found++; } } - if (found != 2) { + if (found != 3) { throw new IllegalStateException(items.stream().flatMap(i -> i.getResources().stream()) .collect(Collectors.toList()).toString()); } @@ -79,6 +82,7 @@ public void execute(BuildContext context) { public void testTemplate() { assertEquals("Hi M!", engine.getTemplate("hi").data("name", "M").render()); assertEquals("Hello M!", hello.data("name", "M").render()); + assertEquals("Hoho M!", engine.getTemplate("nested/hoho").data("name", "M").render()); } } diff --git a/extensions/qute/deployment/src/test/resources/messages/EmailBundles_started.properties b/extensions/qute/deployment/src/test/resources/messages/EmailBundles_started.properties new file mode 100644 index 0000000000000..007c4b2517dc8 --- /dev/null +++ b/extensions/qute/deployment/src/test/resources/messages/EmailBundles_started.properties @@ -0,0 +1,6 @@ +started=In this process you will sign the document to validate it. +signingProcessStart=you have started a signing process {id} for document "{filename}". +nextNotification=You will be notified with another email when it is your signing turn. +documentAccessUrl=You may access the document in the following link: +subject=Signing process initiated by {customer} for file {filename}. +signForValidation=In this process you will sign the document to validate it. \ No newline at end of file diff --git a/extensions/qute/deployment/src/test/resources/messages/EmailBundles_startedValidator.properties b/extensions/qute/deployment/src/test/resources/messages/EmailBundles_startedValidator.properties new file mode 100644 index 0000000000000..88f05a121ad14 --- /dev/null +++ b/extensions/qute/deployment/src/test/resources/messages/EmailBundles_startedValidator.properties @@ -0,0 +1,5 @@ +signForValidation=In this process you will sign the document to validate it. +started=has started a signing process {id} for the document "{filename}". +subject=Signing process initiated by {customer} for file {filename}. +turnEmailWillBeSent=You will be notified with another email when it is your turn to sign. +youMayAlreadyAccessDocument=You can access the document at the following link: diff --git a/extensions/qute/deployment/src/test/resources/messages/EmailBundles_startedValidator_en.properties b/extensions/qute/deployment/src/test/resources/messages/EmailBundles_startedValidator_en.properties new file mode 100644 index 0000000000000..79d65ea85fb73 --- /dev/null +++ b/extensions/qute/deployment/src/test/resources/messages/EmailBundles_startedValidator_en.properties @@ -0,0 +1,5 @@ +signForValidation=In this process you will sign the document to validate it. +started=has started a signing process {id} for the document "{filename}". +subject=Signing process initiated by {customer} for file {filename}. +turnEmailWillBeSent=You will be notified with another email when it is your turn to sign. +youMayAlreadyAccessDocument=You can access the document at the following link: \ No newline at end of file diff --git a/extensions/qute/deployment/src/test/resources/messages/EmailBundles_started_en.properties b/extensions/qute/deployment/src/test/resources/messages/EmailBundles_started_en.properties new file mode 100644 index 0000000000000..007c4b2517dc8 --- /dev/null +++ b/extensions/qute/deployment/src/test/resources/messages/EmailBundles_started_en.properties @@ -0,0 +1,6 @@ +started=In this process you will sign the document to validate it. +signingProcessStart=you have started a signing process {id} for document "{filename}". +nextNotification=You will be notified with another email when it is your signing turn. +documentAccessUrl=You may access the document in the following link: +subject=Signing process initiated by {customer} for file {filename}. +signForValidation=In this process you will sign the document to validate it. \ No newline at end of file diff --git a/extensions/qute/deployment/src/test/resources/messages/enu.properties b/extensions/qute/deployment/src/test/resources/messages/enu.properties new file mode 100644 index 0000000000000..072f933eb0881 --- /dev/null +++ b/extensions/qute/deployment/src/test/resources/messages/enu.properties @@ -0,0 +1,13 @@ +myEnum_ON=On +myEnum_OFF=Off +myEnum_UNDEFINED=Undefined + +shortEnum_ON=1 +shortEnum_OFF=0 +shortEnum_UNDEFINED=U + +locFileOverride={#when myEnum}\ + {#is ON}on\ + {#is OFF}off\ + {#else}undefined\ + {/when} \ No newline at end of file diff --git a/extensions/qute/deployment/src/test/resources/messages/enu_cs.properties b/extensions/qute/deployment/src/test/resources/messages/enu_cs.properties new file mode 100644 index 0000000000000..e3f5c0a2ae6de --- /dev/null +++ b/extensions/qute/deployment/src/test/resources/messages/enu_cs.properties @@ -0,0 +1,13 @@ +myEnum_ON=Zapnuto +myEnum_OFF=Vypnuto +myEnum_UNDEFINED=Nedefinováno + +shortEnum_ON=1 +shortEnum_OFF=0 +shortEnum_UNDEFINED=N + +locFileOverride={#when myEnum}\ + {#is ON}zap\ + {#is OFF}vyp\ + {#else}nedef\ + {/when} \ No newline at end of file diff --git a/extensions/qute/deployment/src/test/resources/messages/msg_cs.properties b/extensions/qute/deployment/src/test/resources/messages/msg_cs.properties index 4b54f8bf586b8..e322d21914f7d 100644 --- a/extensions/qute/deployment/src/test/resources/messages/msg_cs.properties +++ b/extensions/qute/deployment/src/test/resources/messages/msg_cs.properties @@ -3,7 +3,14 @@ hello=Ahoj \ dobrý den! helloNextLine=Ahoj \n {name}! - fruits = apple, banana, pear, \ - watermelon, \ + fruits = jablko, banan, hruska, \ + meloun, \ kiwi, mango + + # This is an example how to localize an enum value +myEnum={#when myEnum}\ + {#is ON}Zapnuto\ + {#is OFF}Vypnuto\ + {#else}Nedefinovano\ + {/when} \ No newline at end of file diff --git a/extensions/qute/runtime/src/main/java/io/quarkus/qute/i18n/Message.java b/extensions/qute/runtime/src/main/java/io/quarkus/qute/i18n/Message.java index 8f4c68664af85..93c5fbe6b1327 100644 --- a/extensions/qute/runtime/src/main/java/io/quarkus/qute/i18n/Message.java +++ b/extensions/qute/runtime/src/main/java/io/quarkus/qute/i18n/Message.java @@ -14,7 +14,8 @@ * {@link MessageBundle#defaultKey()}. *

* The {@link #value()} defines the template of a message. The method parameters can be used in this template. All the message - * templates are validated at build time. + * templates are validated at build time. If there is no template defined the template from a localized file is taken. In case + * the value is not provided at all the build fails. *

* Note that any method declared on a message bundle interface is consireded a message bundle method. If not annotated with this * annotation then the defaulted values are used for the key and template. @@ -22,6 +23,30 @@ * All message bundle methods must return {@link String}. If a message bundle method does not return string then the build * fails. * + *

Enums

+ * There is a convenient way to localize enums. + *

+ * If there is a message bundle method that accepts a single parameter of an enum type and has no message template defined then + * it + * receives a generated template: + * + *

+ * {#when enumParamName}
+ *     {#is CONSTANT1}{msg:methodName_CONSTANT1}
+ *     {#is CONSTANT2}{msg:methodName_CONSTANT2}
+ * {/when}
+ * 
+ * + * Furthermore, a special message method is generated for each enum constant. Finally, each localized file must contain keys and + * values for all constant message keys: + * + *
+ * methodName_CONSTANT1=Value 1
+ * methodName_CONSTANT2=Value 2
+ * 
+ * + * In a template, an enum constant can be localized with a message bundle method {@code msg:methodName(enumConstant)}. + * * @see MessageBundle */ @Retention(RUNTIME) @@ -69,6 +94,8 @@ * This value has higher priority over a message template specified in a localized file, and it's * considered a good practice to specify it. In case the value is not provided and there is no * match in the localized file too, the build fails. + *

+ * There is a convenient way to localize enums. See the javadoc of {@link Message}. * * @return the message template */ diff --git a/extensions/qute/runtime/src/main/java/io/quarkus/qute/i18n/MessageBundles.java b/extensions/qute/runtime/src/main/java/io/quarkus/qute/i18n/MessageBundles.java index b460ac5b144d8..6191405d631fc 100644 --- a/extensions/qute/runtime/src/main/java/io/quarkus/qute/i18n/MessageBundles.java +++ b/extensions/qute/runtime/src/main/java/io/quarkus/qute/i18n/MessageBundles.java @@ -28,7 +28,7 @@ public final class MessageBundles { - public static final String ATTRIBUTE_LOCALE = "locale"; + public static final String ATTRIBUTE_LOCALE = TemplateInstance.LOCALE; public static final String DEFAULT_LOCALE = "<>"; private static final Logger LOGGER = Logger.getLogger(MessageBundles.class); diff --git a/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/TemplateProducer.java b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/TemplateProducer.java index 1a7c6256a1664..6cd776eee8a40 100644 --- a/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/TemplateProducer.java +++ b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/TemplateProducer.java @@ -353,7 +353,8 @@ public Template getTemplate() { private TemplateInstance templateInstance() { TemplateInstance instance = template().instance(); if (dataMap != null) { - dataMap.forEach(instance::data); + dataMap.forEachData(instance::data); + dataMap.forEachComputedData(instance::computedData); } else if (data != null) { instance.data(data); } diff --git a/extensions/redis-client/deployment/src/main/java/io/quarkus/redis/deployment/client/DevServicesRedisProcessor.java b/extensions/redis-client/deployment/src/main/java/io/quarkus/redis/deployment/client/DevServicesRedisProcessor.java index 495d53622852e..5f7f898c18643 100644 --- a/extensions/redis-client/deployment/src/main/java/io/quarkus/redis/deployment/client/DevServicesRedisProcessor.java +++ b/extensions/redis-client/deployment/src/main/java/io/quarkus/redis/deployment/client/DevServicesRedisProcessor.java @@ -100,10 +100,12 @@ public List startRedisContainers(LaunchModeBuildItem try { for (Entry entry : currentDevServicesConfiguration.entrySet()) { String connectionName = entry.getKey(); + boolean useSharedNetwork = DevServicesSharedNetworkBuildItem.isSharedNetworkRequired(devServicesConfig, + devServicesSharedNetworkBuildItem); RunningDevService devService = startContainer(dockerStatusBuildItem, connectionName, entry.getValue().devservices(), launchMode.getLaunchMode(), - !devServicesSharedNetworkBuildItem.isEmpty(), devServicesConfig.timeout); + useSharedNetwork, devServicesConfig.timeout); if (devService == null) { continue; } diff --git a/extensions/resteasy-classic/resteasy-client/runtime/src/main/java/io/quarkus/restclient/runtime/QuarkusRestClientBuilder.java b/extensions/resteasy-classic/resteasy-client/runtime/src/main/java/io/quarkus/restclient/runtime/QuarkusRestClientBuilder.java index ead9b44944922..7ecc462c52d12 100644 --- a/extensions/resteasy-classic/resteasy-client/runtime/src/main/java/io/quarkus/restclient/runtime/QuarkusRestClientBuilder.java +++ b/extensions/resteasy-classic/resteasy-client/runtime/src/main/java/io/quarkus/restclient/runtime/QuarkusRestClientBuilder.java @@ -17,11 +17,9 @@ import java.net.URI; import java.net.URISyntaxException; import java.net.URL; -import java.security.AccessController; import java.security.KeyManagementException; import java.security.KeyStore; import java.security.NoSuchAlgorithmException; -import java.security.PrivilegedAction; import java.security.SecureRandom; import java.util.ArrayList; import java.util.Arrays; @@ -397,7 +395,7 @@ public T build(Class aClass) throws IllegalStateException, RestClientDefi * @return list of proxy hosts */ private List getProxyHostsAsRegex() { - String noProxyHostsSysProps = getSystemProperty("http.nonProxyHosts", null); + String noProxyHostsSysProps = System.getProperty("http.nonProxyHosts", null); if (noProxyHostsSysProps == null) { noProxyHostsSysProps = "localhost|127.*|[::1]"; } else { @@ -414,7 +412,7 @@ private List getProxyHostsAsRegex() { */ private boolean useURLConnection() { if (useURLConnection == null) { - String defaultToURLConnection = getSystemProperty( + String defaultToURLConnection = System.getProperty( "org.jboss.resteasy.microprofile.defaultToURLConnectionHttpClient", "false"); useURLConnection = defaultToURLConnection.equalsIgnoreCase("true"); } @@ -820,13 +818,6 @@ private static BeanManager getBeanManager() { } } - private String getSystemProperty(String key, String def) { - if (System.getSecurityManager() == null) { - return System.getProperty(key, def); - } - return AccessController.doPrivileged((PrivilegedAction) () -> System.getProperty(key, def)); - } - private final MpClientBuilderImpl builderDelegate; private final ConfigurationWrapper configurationWrapper; diff --git a/extensions/resteasy-reactive/rest-client-jaxrs/deployment/src/main/java/io/quarkus/jaxrs/client/reactive/deployment/JaxrsClientReactiveProcessor.java b/extensions/resteasy-reactive/rest-client-jaxrs/deployment/src/main/java/io/quarkus/jaxrs/client/reactive/deployment/JaxrsClientReactiveProcessor.java index 6ff286f6416f1..9529ffff88426 100644 --- a/extensions/resteasy-reactive/rest-client-jaxrs/deployment/src/main/java/io/quarkus/jaxrs/client/reactive/deployment/JaxrsClientReactiveProcessor.java +++ b/extensions/resteasy-reactive/rest-client-jaxrs/deployment/src/main/java/io/quarkus/jaxrs/client/reactive/deployment/JaxrsClientReactiveProcessor.java @@ -121,6 +121,7 @@ import org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames; import org.jboss.resteasy.reactive.common.processor.scanning.ResourceScanningResult; import org.jboss.resteasy.reactive.multipart.FileDownload; +import org.jboss.resteasy.reactive.multipart.FileUpload; import io.quarkus.arc.deployment.AdditionalBeanBuildItem; import io.quarkus.arc.deployment.BeanArchiveIndexBuildItem; @@ -190,6 +191,7 @@ public class JaxrsClientReactiveProcessor { private static final String PATH_SIGNATURE = "L" + java.nio.file.Path.class.getName().replace('.', '/') + ";"; private static final String BUFFER_SIGNATURE = "L" + Buffer.class.getName().replace('.', '/') + ";"; private static final String BYTE_ARRAY_SIGNATURE = "[B"; + private static final String FILE_UPLOAD_SIGNATURE = "L" + FileUpload.class.getName().replace('.', '/') + ";"; private static final Logger log = Logger.getLogger(JaxrsClientReactiveProcessor.class); @@ -201,6 +203,8 @@ public class JaxrsClientReactiveProcessor { String.class, Object.class); private static final MethodDescriptor MULTIVALUED_MAP_ADD = MethodDescriptor.ofMethod(MultivaluedMap.class, "add", void.class, Object.class, Object.class); + private static final MethodDescriptor MULTIVALUED_MAP_ADD_ALL = MethodDescriptor.ofMethod(MultivaluedMap.class, "addAll", + void.class, Object.class, Object[].class); private static final MethodDescriptor PATH_GET_FILENAME = MethodDescriptor.ofMethod(Path.class, "getFileName", Path.class); private static final MethodDescriptor OBJECT_TO_STRING = MethodDescriptor.ofMethod(Object.class, "toString", String.class); @@ -956,7 +960,7 @@ A more full example of generated client (with sub-resource) can is at the bottom Supplier beanParamDescriptorsField = classContext .getLazyBeanParameterDescriptors(beanParam.type); - formParams = addBeanParamData(jandexMethod, methodCreator, handleBeanParamMethod, + formParams = addBeanParamData(jandexMethod, paramIdx, methodCreator, handleBeanParamMethod, invocationBuilderRef, classContext, beanParam.getItems(), methodCreator.getMethodParam(paramIdx), methodTarget, index, restClientInterface.getClassName(), @@ -1024,8 +1028,9 @@ A more full example of generated client (with sub-resource) can is at the bottom } else if (param.parameterType == ParameterType.FORM) { formParams = createFormDataIfAbsent(methodCreator, formParams, multipart); // NOTE: don't use type here, because we're not going through the collection converters and stuff - addFormParam(methodCreator, param.name, methodCreator.getMethodParam(paramIdx), param.declaredType, - param.signature, + Type parameterType = jandexMethod.parameterType(paramIdx); + addFormParam(methodCreator, param.name, methodCreator.getMethodParam(paramIdx), + parameterType, param.declaredType, param.signature, index, restClientInterface.getClassName(), methodCreator.getThis(), formParams, getGenericTypeFromArray(methodCreator, methodGenericParametersField, paramIdx), getAnnotationsFromArray(methodCreator, methodParamAnnotationsField, paramIdx), @@ -1173,6 +1178,7 @@ private boolean isMultipartRequiringType(String signature, String partType) { || signature.equals(BUFFER_SIGNATURE) || signature.equals(BYTE_ARRAY_SIGNATURE) || signature.equals(MULTI_BYTE_SIGNATURE) + || signature.equals(FILE_UPLOAD_SIGNATURE) || partType != null); } @@ -1459,7 +1465,7 @@ private void handleSubResourceMethod(List AssignableResultHandle invocationBuilderRef = handleBeanParamMethod .createVariable(Invocation.Builder.class); handleBeanParamMethod.assign(invocationBuilderRef, handleBeanParamMethod.getMethodParam(0)); - formParams = addBeanParamData(jandexMethod, subMethodCreator, handleBeanParamMethod, + formParams = addBeanParamData(jandexMethod, methodIndex, subMethodCreator, handleBeanParamMethod, invocationBuilderRef, subContext, beanParam.getItems(), paramValue, methodTarget, index, interfaceClass.name().toString(), @@ -1586,7 +1592,7 @@ private void handleSubResourceMethod(List AssignableResultHandle invocationBuilderRef = handleBeanParamMethod .createVariable(Invocation.Builder.class); handleBeanParamMethod.assign(invocationBuilderRef, handleBeanParamMethod.getMethodParam(0)); - formParams = addBeanParamData(jandexMethod, subMethodCreator, handleBeanParamMethod, + formParams = addBeanParamData(jandexMethod, methodIndex, subMethodCreator, handleBeanParamMethod, invocationBuilderRef, subContext, beanParam.getItems(), subMethodCreator.getMethodParam(paramIdx), methodTarget, index, interfaceClass.name().toString(), @@ -1771,8 +1777,8 @@ private AssignableResultHandle createRestClientField(String name, ClassCreator c } private void handleMultipartField(String formParamName, String partType, String partFilename, - String type, - String parameterGenericType, ResultHandle fieldValue, AssignableResultHandle multipartForm, + String type, String parameterSignature, + ResultHandle fieldValue, AssignableResultHandle multipartForm, BytecodeCreator methodCreator, ResultHandle client, String restClientInterfaceClassName, ResultHandle parameterAnnotations, ResultHandle genericType, String errorLocation) { @@ -1790,6 +1796,8 @@ private void handleMultipartField(String formParamName, String partType, String } else if (type.equals(Path.class.getName())) { // and so is path addFile(ifValueNotNull, multipartForm, formParamName, partType, partFilename, fieldValue); + } else if (type.equals(FileUpload.class.getName())) { + addFileUpload(fieldValue, multipartForm, methodCreator); } else if (type.equals(InputStream.class.getName())) { // and so is path addInputStream(ifValueNotNull, multipartForm, formParamName, partType, partFilename, fieldValue, type); @@ -1806,7 +1814,7 @@ private void handleMultipartField(String formParamName, String partType, String MethodDescriptor.ofMethod(Buffer.class, "buffer", Buffer.class, byte[].class), fieldValue); addBuffer(ifValueNotNull, multipartForm, formParamName, partType, partFilename, buffer, errorLocation); - } else if (parameterGenericType.equals(MULTI_BYTE_SIGNATURE)) { + } else if (parameterSignature.equals(MULTI_BYTE_SIGNATURE)) { addMultiAsFile(ifValueNotNull, multipartForm, formParamName, partType, partFilename, fieldValue, errorLocation); } else if (partType != null) { if (partFilename != null) { @@ -1885,6 +1893,15 @@ private void addFile(BytecodeCreator methodCreator, AssignableResultHandle multi } } + private void addFileUpload(ResultHandle fieldValue, AssignableResultHandle multipartForm, + BytecodeCreator methodCreator) { + // MultipartForm#fileUpload(FileUpload fileUpload); + methodCreator.invokeVirtualMethod( + MethodDescriptor.ofMethod(ClientMultipartForm.class, "fileUpload", + ClientMultipartForm.class, FileUpload.class), + multipartForm, fieldValue); + } + private ResultHandle primitiveToString(BytecodeCreator methodCreator, ResultHandle fieldValue, FieldInfo field) { PrimitiveType primitiveType = field.type().asPrimitiveType(); switch (primitiveType.primitive()) { @@ -2407,7 +2424,7 @@ private Optional getJavaMethod(ClassInfo interfaceClass, ResourceMet } private AssignableResultHandle addBeanParamData(MethodInfo jandexMethod, - BytecodeCreator methodCreator, + int paramIndex, BytecodeCreator methodCreator, // Invocation.Builder executePut$$enrichInvocationBuilder${noOfBeanParam}(Invocation.Builder) BytecodeCreator invocationBuilderEnricher, AssignableResultHandle invocationBuilder, @@ -2429,7 +2446,8 @@ private AssignableResultHandle addBeanParamData(MethodInfo jandexMethod, formParams = createFormDataIfAbsent(methodCreator, formParams, multipart); } - addSubBeanParamData(jandexMethod, methodCreator, invocationBuilderEnricher, invocationBuilder, classContext, + addSubBeanParamData(jandexMethod, paramIndex, methodCreator, invocationBuilderEnricher, invocationBuilder, + classContext, beanParamItems, param, target, index, restClientInterfaceClassName, client, invocationEnricherClient, formParams, descriptorsField, multipart, beanParamClass); @@ -2437,7 +2455,7 @@ private AssignableResultHandle addBeanParamData(MethodInfo jandexMethod, return formParams; } - private void addSubBeanParamData(MethodInfo jandexMethod, BytecodeCreator methodCreator, + private void addSubBeanParamData(MethodInfo jandexMethod, int paramIndex, BytecodeCreator methodCreator, // Invocation.Builder executePut$$enrichInvocationBuilder${noOfBeanParam}(Invocation.Builder) BytecodeCreator invocationBuilderEnricher, AssignableResultHandle invocationBuilder, @@ -2475,7 +2493,7 @@ private void addSubBeanParamData(MethodInfo jandexMethod, BytecodeCreator method ResultHandle beanParamElementHandle = beanParamItem.extract(creator, param); Supplier newBeanParamDescriptorField = classContext .getLazyBeanParameterDescriptors(beanParamItem.className()); - addSubBeanParamData(jandexMethod, creator, invoEnricher, invocationBuilder, classContext, + addSubBeanParamData(jandexMethod, paramIndex, creator, invoEnricher, invocationBuilder, classContext, beanParamItem.items(), beanParamElementHandle, target, index, restClientInterfaceClassName, client, invocationEnricherClient, formParams, newBeanParamDescriptorField, multipart, @@ -2520,7 +2538,9 @@ private void addSubBeanParamData(MethodInfo jandexMethod, BytecodeCreator method case FORM_PARAM: FormParamItem formParam = (FormParamItem) item; addFormParam(creator, formParam.getFormParamName(), formParam.extract(creator, param), - formParam.getParamType(), formParam.getParamSignature(), restClientInterfaceClassName, client, + jandexMethod.parameterType(paramIndex), formParam.getParamType(), formParam.getParamSignature(), + index, + restClientInterfaceClassName, client, formParams, getGenericTypeFromParameter(creator, beanParamDescriptorField, item.fieldName()), getAnnotationsFromParameter(creator, beanParamDescriptorField, item.fieldName()), @@ -2786,25 +2806,53 @@ private void addPathParam(BytecodeCreator methodCreator, AssignableResultHandle methodCreator.load(paramName), handle)); } - private void addFormParam(BytecodeCreator methodCreator, String paramName, ResultHandle formParamHandle, - String parameterType, String parameterGenericType, + private void addFormParam(BytecodeCreator methodCreator, + String paramName, + ResultHandle formParamHandle, + Type parameterType, + String parameterTypeStr, + String parameterSignature, + IndexView index, String restClientInterfaceClassName, ResultHandle client, AssignableResultHandle formParams, ResultHandle genericType, ResultHandle parameterAnnotations, boolean multipart, String partType, String partFilename, String errorLocation) { if (multipart) { - handleMultipartField(paramName, partType, partFilename, parameterType, parameterGenericType, formParamHandle, + handleMultipartField(paramName, partType, partFilename, parameterTypeStr, parameterSignature, formParamHandle, formParams, methodCreator, client, restClientInterfaceClassName, parameterAnnotations, genericType, errorLocation); } else { - BytecodeCreator notNullValue = methodCreator.ifNull(formParamHandle).falseBranch(); - ResultHandle convertedFormParam = convertParamToString(notNullValue, client, formParamHandle, parameterType, - genericType, parameterAnnotations); - BytecodeCreator parameterIsStringBranch = checkStringParam(notNullValue, convertedFormParam, - restClientInterfaceClassName, errorLocation); - parameterIsStringBranch.invokeInterfaceMethod(MULTIVALUED_MAP_ADD, formParams, - notNullValue.load(paramName), convertedFormParam); + BytecodeCreator creator = methodCreator.ifNull(formParamHandle).falseBranch(); + if (isCollection(parameterType, index)) { + String componentType = null; + if (parameterType.kind() == PARAMETERIZED_TYPE) { + Type paramType = parameterType.asParameterizedType().arguments().get(0); + if ((paramType.kind() == CLASS) || (paramType.kind() == PARAMETERIZED_TYPE)) { + componentType = paramType.name().toString(); + } + } + if (componentType == null) { + componentType = DotNames.OBJECT.toString(); + } + ResultHandle paramArray = creator.invokeStaticMethod( + MethodDescriptor.ofMethod(ToObjectArray.class, "collection", Object[].class, Collection.class), + formParamHandle); + ResultHandle convertedParamArray = creator.invokeVirtualMethod( + MethodDescriptor.ofMethod(RestClientBase.class, "convertParamArray", Object[].class, Object[].class, + Class.class, java.lang.reflect.Type.class, Annotation[].class), + client, paramArray, creator.loadClassFromTCCL(componentType), genericType, creator.newArray( + Annotation.class, 0)); + creator.invokeInterfaceMethod(MULTIVALUED_MAP_ADD_ALL, formParams, + creator.load(paramName), convertedParamArray); + } else { + ResultHandle convertedFormParam = convertParamToString(creator, client, formParamHandle, parameterTypeStr, + genericType, parameterAnnotations); + BytecodeCreator parameterIsStringBranch = checkStringParam(creator, convertedFormParam, + restClientInterfaceClassName, errorLocation); + parameterIsStringBranch.invokeInterfaceMethod(MULTIVALUED_MAP_ADD, formParams, + creator.load(paramName), convertedFormParam); + } } } diff --git a/extensions/resteasy-reactive/rest-client-jaxrs/kotlin/pom.xml b/extensions/resteasy-reactive/rest-client-jaxrs/kotlin/pom.xml index d26c7918ee8e5..b7a7ce82ce837 100644 --- a/extensions/resteasy-reactive/rest-client-jaxrs/kotlin/pom.xml +++ b/extensions/resteasy-reactive/rest-client-jaxrs/kotlin/pom.xml @@ -11,7 +11,7 @@ quarkus-rest-client-jaxrs-kotlin Quarkus - REST - Kotlin - Provides Kotlin support for RESTEasy Reactive + Provides Kotlin support for Quarkus REST diff --git a/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/FormListTest.java b/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/FormListTest.java new file mode 100644 index 0000000000000..b05f4c58b8031 --- /dev/null +++ b/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/FormListTest.java @@ -0,0 +1,51 @@ +package io.quarkus.rest.client.reactive; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.net.URI; +import java.util.List; + +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; + +import org.eclipse.microprofile.rest.client.RestClientBuilder; +import org.jboss.resteasy.reactive.RestForm; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.test.common.http.TestHTTPResource; + +public class FormListTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar.addClasses(VoidReturnTypeTest.Resource.class)); + + @TestHTTPResource + URI baseUri; + + @Test + void testHeadersWithSubresource() { + Client client = RestClientBuilder.newBuilder().baseUri(baseUri).build(Client.class); + + assertThat(client.call(List.of("first", "second", "third"))).isEqualTo("first-second-third"); + assertThat(client.call(List.of("first"))).isEqualTo("first"); + } + + @Path("/test") + public static class Resource { + + @POST + public String response(@RestForm List input) { + return String.join("-", input); + } + } + + @Path("/test") + public interface Client { + + @POST + String call(@RestForm List input); + } +} diff --git a/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/multipart/MultipartDetectionTest.java b/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/multipart/MultipartDetectionTest.java index ab0a0675b8de3..b3fd11d635c78 100644 --- a/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/multipart/MultipartDetectionTest.java +++ b/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/multipart/MultipartDetectionTest.java @@ -81,6 +81,39 @@ void shouldCallImplicitEndpoints() throws IOException { .isEqualTo(file.getName() + " file Hello"); assertThat(client.postMultipartEntityImplicit(file.getName(), person)) .isEqualTo(file.getName() + " Stef:Epardaud"); + + assertThat(client.postMultipartImplicitFileUpload("Foo", new FileUpload() { + @Override + public String name() { + return "file"; + } + + @Override + public java.nio.file.Path filePath() { + return file.toPath(); + } + + @Override + public String fileName() { + return file.getName(); + } + + @Override + public long size() { + return -1; + } + + @Override + public String contentType() { + return "application/octet-stream"; + } + + @Override + public String charSet() { + return ""; + } + })) + .isEqualTo("Foo " + file.getName() + " Hello"); } @Path("form") @@ -142,6 +175,10 @@ String postMultipartEntityImplicit(@RestForm String name, @Consumes(MediaType.MULTIPART_FORM_DATA) String postMultipartExplicit(@RestForm String name, @RestForm File file); + @Path("multipart") + @POST + String postMultipartImplicitFileUpload(@RestForm String name, @RestForm FileUpload file); + @Path("urlencoded") @POST String postUrlencodedImplicit(@RestForm String name); diff --git a/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/multipart/MultipartFilenameTest.java b/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/multipart/MultipartFilenameTest.java index 2a70e77f31ff7..69bec01287d09 100644 --- a/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/multipart/MultipartFilenameTest.java +++ b/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/multipart/MultipartFilenameTest.java @@ -60,6 +60,49 @@ void shouldPassOriginalFileName() throws IOException { assertThat(client.postMultipart(form)).isEqualTo(file.getName()); } + @Test + void shouldWorkWithFileUpload() throws IOException { + Client client = RestClientBuilder.newBuilder().baseUri(baseUri).build(Client.class); + + File file = File.createTempFile("MultipartTest", ".txt"); + file.deleteOnExit(); + + ClientFormUsingFileUpload form = new ClientFormUsingFileUpload(); + form.file = new FileUpload() { + + @Override + public String name() { + return "myFile"; + } + + @Override + public java.nio.file.Path filePath() { + return file.toPath(); + } + + @Override + public String fileName() { + return file.getName(); + } + + @Override + public long size() { + return 0; + } + + @Override + public String contentType() { + return "application/octet-stream"; + } + + @Override + public String charSet() { + return ""; + } + }; + assertThat(client.postMultipartFileUpload(form)).isEqualTo(file.getName()); + } + @Test void shouldUseFileNameFromAnnotation() throws IOException { Client client = RestClientBuilder.newBuilder().baseUri(baseUri).build(Client.class); @@ -244,6 +287,10 @@ public interface Client { @Consumes(MediaType.MULTIPART_FORM_DATA) String postMultipart(@MultipartForm ClientForm clientForm); + @POST + @Consumes(MediaType.MULTIPART_FORM_DATA) + String postMultipartFileUpload(ClientFormUsingFileUpload clientForm); + @POST @Consumes(MediaType.MULTIPART_FORM_DATA) String postMultipartWithPartFilename(@MultipartForm ClientFormUsingFile clientForm); @@ -324,6 +371,11 @@ public static class ClientForm { public File file; } + public static class ClientFormUsingFileUpload { + @RestForm + public FileUpload file; + } + public static class ClientFormUsingFile { @FormParam("myFile") @PartType(APPLICATION_OCTET_STREAM) diff --git a/extensions/resteasy-reactive/rest-client/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientBuilderImpl.java b/extensions/resteasy-reactive/rest-client/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientBuilderImpl.java index daa221114c341..f5c68a2a5d4c8 100644 --- a/extensions/resteasy-reactive/rest-client/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientBuilderImpl.java +++ b/extensions/resteasy-reactive/rest-client/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientBuilderImpl.java @@ -203,7 +203,7 @@ public RestClientBuilderImpl userAgent(String userAgent) { @Override public RestClientBuilderImpl executorService(ExecutorService executor) { throw new IllegalArgumentException("Specifying executor service is not supported. " + - "The underlying call in RestEasy Reactive is non-blocking, " + + "The underlying call is non-blocking, " + "there is no reason to offload the call to a separate thread pool."); } diff --git a/extensions/resteasy-reactive/rest-common/runtime/pom.xml b/extensions/resteasy-reactive/rest-common/runtime/pom.xml index beae4962c358c..68c5bf066f308 100644 --- a/extensions/resteasy-reactive/rest-common/runtime/pom.xml +++ b/extensions/resteasy-reactive/rest-common/runtime/pom.xml @@ -11,7 +11,7 @@ quarkus-rest-common Quarkus - REST - Common - Runtime - Common runtime parts of Quarkus RESTEasy Reactive + Common runtime parts of Quarkus REST diff --git a/extensions/resteasy-reactive/rest-jackson-common/runtime/pom.xml b/extensions/resteasy-reactive/rest-jackson-common/runtime/pom.xml index 33eb3ae141aa7..23aac235818da 100644 --- a/extensions/resteasy-reactive/rest-jackson-common/runtime/pom.xml +++ b/extensions/resteasy-reactive/rest-jackson-common/runtime/pom.xml @@ -10,7 +10,7 @@ quarkus-rest-jackson-common Quarkus - REST - Jackson Common Bits - Runtime - Common classes for Jackson serialization support for RESTEasy Reactive + Common classes for Jackson serialization support for Quarkus REST diff --git a/extensions/resteasy-reactive/rest-jackson/runtime/pom.xml b/extensions/resteasy-reactive/rest-jackson/runtime/pom.xml index e68d4979923eb..1afb96cef2987 100644 --- a/extensions/resteasy-reactive/rest-jackson/runtime/pom.xml +++ b/extensions/resteasy-reactive/rest-jackson/runtime/pom.xml @@ -11,7 +11,7 @@ quarkus-rest-jackson Quarkus - REST - Jackson - Runtime - 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 + Jackson serialization support for Quarkus REST. This extension is not compatible with the quarkus-resteasy extension, or any of the extensions that depend on it diff --git a/extensions/resteasy-reactive/rest-jaxb/runtime/pom.xml b/extensions/resteasy-reactive/rest-jaxb/runtime/pom.xml index ba0ac4db7fa59..f5dd192a153c8 100644 --- a/extensions/resteasy-reactive/rest-jaxb/runtime/pom.xml +++ b/extensions/resteasy-reactive/rest-jaxb/runtime/pom.xml @@ -11,7 +11,7 @@ quarkus-rest-jaxb Quarkus - REST - JAXB - Runtime - JAXB serialization support for RESTEasy Reactive. This extension is not compatible with the quarkus-resteasy extension, or any of the extensions that depend on it. + JAXB serialization support for Quarkus REST. This extension is not compatible with the quarkus-resteasy extension, or any of the extensions that depend on it. diff --git a/extensions/resteasy-reactive/rest-jsonb-common/runtime/pom.xml b/extensions/resteasy-reactive/rest-jsonb-common/runtime/pom.xml index d6b533f0c4485..edfb5996b4747 100644 --- a/extensions/resteasy-reactive/rest-jsonb-common/runtime/pom.xml +++ b/extensions/resteasy-reactive/rest-jsonb-common/runtime/pom.xml @@ -11,7 +11,7 @@ quarkus-rest-jsonb-common Quarkus - REST - JSON-B Common Bits - Runtime - Common classes for JSON-B serialization support for RESTEasy Reactive + Common classes for JSON-B serialization support for Quarkus REST diff --git a/extensions/resteasy-reactive/rest-jsonb/runtime/pom.xml b/extensions/resteasy-reactive/rest-jsonb/runtime/pom.xml index 54cab04e8a631..3ee70588046e5 100644 --- a/extensions/resteasy-reactive/rest-jsonb/runtime/pom.xml +++ b/extensions/resteasy-reactive/rest-jsonb/runtime/pom.xml @@ -11,7 +11,7 @@ quarkus-rest-jsonb Quarkus - REST - JSON-B - Runtime - 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. + JSON-B serialization support for Quarkus REST. This extension is not compatible with the quarkus-resteasy extension, or any of the extensions that depend on it. diff --git a/extensions/resteasy-reactive/rest-kotlin-serialization/runtime/pom.xml b/extensions/resteasy-reactive/rest-kotlin-serialization/runtime/pom.xml index 32039114e3033..0a8e4172b1737 100644 --- a/extensions/resteasy-reactive/rest-kotlin-serialization/runtime/pom.xml +++ b/extensions/resteasy-reactive/rest-kotlin-serialization/runtime/pom.xml @@ -12,7 +12,7 @@ quarkus-rest-kotlin-serialization Quarkus - REST - Kotlin Serialization - Runtime - Kotlin Serialization support for RESTEasy Reactive. This extension is not compatible with the quarkus-resteasy extension, or any of the extensions that depend on it. + Kotlin Serialization support for Quarkus REST. This extension is not compatible with the quarkus-resteasy extension, or any of the extensions that depend on it. diff --git a/extensions/resteasy-reactive/rest-kotlin/runtime/pom.xml b/extensions/resteasy-reactive/rest-kotlin/runtime/pom.xml index 2d82c14aa260a..1e5b9c11486dd 100644 --- a/extensions/resteasy-reactive/rest-kotlin/runtime/pom.xml +++ b/extensions/resteasy-reactive/rest-kotlin/runtime/pom.xml @@ -11,7 +11,7 @@ quarkus-rest-kotlin Quarkus - REST - Kotlin - Runtime - Provides Kotlin support for RESTEasy Reactive + Provides Kotlin support for Quarkus REST diff --git a/extensions/resteasy-reactive/rest-links/runtime/pom.xml b/extensions/resteasy-reactive/rest-links/runtime/pom.xml index df85370552d28..6e75b222e6590 100644 --- a/extensions/resteasy-reactive/rest-links/runtime/pom.xml +++ b/extensions/resteasy-reactive/rest-links/runtime/pom.xml @@ -11,7 +11,7 @@ quarkus-rest-links Quarkus - REST - Links - Runtime - Web Links support for RESTEasy Reactive. Inject web links into response HTTP headers by annotating your endpoint resources. + Web Links support for Quarkus REST. Inject web links into response HTTP headers by annotating your endpoint resources. diff --git a/extensions/resteasy-reactive/rest-qute/deployment/src/main/java/io/quarkus/resteasy/reactive/qute/deployment/RestQuteConfig.java b/extensions/resteasy-reactive/rest-qute/deployment/src/main/java/io/quarkus/resteasy/reactive/qute/deployment/RestQuteConfig.java new file mode 100644 index 0000000000000..840de4a2bee99 --- /dev/null +++ b/extensions/resteasy-reactive/rest-qute/deployment/src/main/java/io/quarkus/resteasy/reactive/qute/deployment/RestQuteConfig.java @@ -0,0 +1,22 @@ +package io.quarkus.resteasy.reactive.qute.deployment; + +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.annotations.ConfigRoot; +import io.smallrye.config.ConfigMapping; +import io.smallrye.config.WithDefault; + +@ConfigMapping(prefix = "quarkus.rest.qute") +@ConfigRoot(phase = ConfigPhase.BUILD_TIME) +public interface RestQuteConfig { + + /** + * If set to {@code true} then the {@link io.quarkus.qute.TemplateInstance} is registered as a non-blocking return type for + * JAX-RS resource methods. + * + * @deprecated This config item will be removed at some time after Quarkus 3.16 + */ + @Deprecated(forRemoval = true, since = "3.10") + @WithDefault("false") + boolean templateInstanceNonBlockingType(); + +} diff --git a/extensions/resteasy-reactive/rest-qute/deployment/src/main/java/io/quarkus/resteasy/reactive/qute/deployment/ResteasyReactiveQuteProcessor.java b/extensions/resteasy-reactive/rest-qute/deployment/src/main/java/io/quarkus/resteasy/reactive/qute/deployment/ResteasyReactiveQuteProcessor.java index 59d8213be71a3..137c39476022c 100644 --- a/extensions/resteasy-reactive/rest-qute/deployment/src/main/java/io/quarkus/resteasy/reactive/qute/deployment/ResteasyReactiveQuteProcessor.java +++ b/extensions/resteasy-reactive/rest-qute/deployment/src/main/java/io/quarkus/resteasy/reactive/qute/deployment/ResteasyReactiveQuteProcessor.java @@ -18,6 +18,7 @@ import org.jboss.resteasy.reactive.server.processor.scanning.MethodScanner; import io.quarkus.deployment.Feature; +import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.builditem.FeatureBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveHierarchyIgnoreWarningBuildItem; @@ -49,8 +50,10 @@ ReflectiveHierarchyIgnoreWarningBuildItem ignoreReflectiveWarning() { } @BuildStep - NonBlockingReturnTypeBuildItem nonBlockingTemplateInstance() { - return new NonBlockingReturnTypeBuildItem(TEMPLATE_INSTANCE); + void nonBlockingTemplateInstance(RestQuteConfig config, BuildProducer nonBlockingType) { + if (config.templateInstanceNonBlockingType()) { + nonBlockingType.produce(new NonBlockingReturnTypeBuildItem(TEMPLATE_INSTANCE)); + } } @BuildStep diff --git a/extensions/resteasy-reactive/rest-qute/deployment/src/test/java/io/quarkus/resteasy/reactive/qute/deployment/TemplateInstanceNonBlockingEnabledTest.java b/extensions/resteasy-reactive/rest-qute/deployment/src/test/java/io/quarkus/resteasy/reactive/qute/deployment/TemplateInstanceNonBlockingEnabledTest.java new file mode 100644 index 0000000000000..d127fa10da991 --- /dev/null +++ b/extensions/resteasy-reactive/rest-qute/deployment/src/test/java/io/quarkus/resteasy/reactive/qute/deployment/TemplateInstanceNonBlockingEnabledTest.java @@ -0,0 +1,48 @@ +package io.quarkus.resteasy.reactive.qute.deployment; + +import static io.restassured.RestAssured.when; + +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +import org.hamcrest.Matchers; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.qute.Template; +import io.quarkus.qute.TemplateInstance; +import io.quarkus.runtime.BlockingOperationControl; +import io.quarkus.test.QuarkusUnitTest; + +public class TemplateInstanceNonBlockingEnabledTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(TestResource.class) + .addAsResource(new StringAsset("quarkus.rest.qute.template-instance-non-blocking-type=true"), + "application.properties") + .addAsResource(new StringAsset("Blocking allowed: {blockingAllowed}"), "templates/item.txt")); + + @Test + public void test() { + when().get("/test").then().statusCode(200).body(Matchers.is("Blocking allowed: false")); + } + + @Path("test") + public static class TestResource { + + @Inject + Template item; + + @GET + @Produces(MediaType.TEXT_PLAIN) + public TemplateInstance get() { + return item.data("blockingAllowed", BlockingOperationControl.isBlockingAllowed()); + } + } +} diff --git a/extensions/resteasy-reactive/rest-qute/deployment/src/test/java/io/quarkus/resteasy/reactive/qute/deployment/TemplateInstanceNonBlockingTest.java b/extensions/resteasy-reactive/rest-qute/deployment/src/test/java/io/quarkus/resteasy/reactive/qute/deployment/TemplateInstanceNonBlockingTest.java index d1fb7c35fc321..80711c150c370 100644 --- a/extensions/resteasy-reactive/rest-qute/deployment/src/test/java/io/quarkus/resteasy/reactive/qute/deployment/TemplateInstanceNonBlockingTest.java +++ b/extensions/resteasy-reactive/rest-qute/deployment/src/test/java/io/quarkus/resteasy/reactive/qute/deployment/TemplateInstanceNonBlockingTest.java @@ -1,7 +1,6 @@ package io.quarkus.resteasy.reactive.qute.deployment; import static io.restassured.RestAssured.when; -import static org.hamcrest.Matchers.is; import jakarta.inject.Inject; import jakarta.ws.rs.GET; @@ -29,7 +28,7 @@ public class TemplateInstanceNonBlockingTest { @Test public void test() { - when().get("/test").then().statusCode(200).body(Matchers.is("Blocking allowed: false")); + when().get("/test").then().statusCode(200).body(Matchers.is("Blocking allowed: true")); } @Path("test") diff --git a/extensions/resteasy-reactive/rest-qute/runtime/pom.xml b/extensions/resteasy-reactive/rest-qute/runtime/pom.xml index 93be27f7c66de..74f6acd3b62bd 100644 --- a/extensions/resteasy-reactive/rest-qute/runtime/pom.xml +++ b/extensions/resteasy-reactive/rest-qute/runtime/pom.xml @@ -11,7 +11,7 @@ quarkus-rest-qute Quarkus - REST - Qute - Runtime - Qute integration for RESTEasy Reactive. This extension is not compatible with the quarkus-resteasy extension, or any of the extensions that depend on it. + Qute integration for Quarkus REST. This extension is not compatible with the quarkus-resteasy extension, or any of the extensions that depend on it. diff --git a/extensions/resteasy-reactive/rest-servlet/runtime/pom.xml b/extensions/resteasy-reactive/rest-servlet/runtime/pom.xml index 0019722bc48dc..b5e15d17a14d9 100644 --- a/extensions/resteasy-reactive/rest-servlet/runtime/pom.xml +++ b/extensions/resteasy-reactive/rest-servlet/runtime/pom.xml @@ -11,7 +11,7 @@ quarkus-rest-servlet Quarkus - REST Servlet - Runtime - Servlet support for Quarkus RESTEasy Reactive + Servlet support for Quarkus REST diff --git a/extensions/resteasy-reactive/rest/deployment/src/main/resources/dev-ui/qwc-resteasy-reactive-endpoint-scores.js b/extensions/resteasy-reactive/rest/deployment/src/main/resources/dev-ui/qwc-resteasy-reactive-endpoint-scores.js index 01f7caddbc1b5..758bd983acace 100644 --- a/extensions/resteasy-reactive/rest/deployment/src/main/resources/dev-ui/qwc-resteasy-reactive-endpoint-scores.js +++ b/extensions/resteasy-reactive/rest/deployment/src/main/resources/dev-ui/qwc-resteasy-reactive-endpoint-scores.js @@ -1,5 +1,5 @@ -import { QwcHotReloadElement, html, css} from 'qwc-hot-reload-element'; -import { JsonRpc } from 'jsonrpc'; +import {css, html, QwcHotReloadElement} from 'qwc-hot-reload-element'; +import {JsonRpc} from 'jsonrpc'; import '@vaadin/details'; import '@vaadin/horizontal-layout'; @@ -103,7 +103,7 @@ export class QwcResteasyReactiveEndpointScores extends QwcHotReloadElement { } return html` + linkText="Learn how to write REST Services with Quarkus REST"> `; } diff --git a/extensions/resteasy-reactive/rest/spi-deployment/src/main/java/io/quarkus/resteasy/reactive/server/spi/NonBlockingReturnTypeBuildItem.java b/extensions/resteasy-reactive/rest/spi-deployment/src/main/java/io/quarkus/resteasy/reactive/server/spi/NonBlockingReturnTypeBuildItem.java index 7faa3c03802a0..41f9c9fc409dd 100644 --- a/extensions/resteasy-reactive/rest/spi-deployment/src/main/java/io/quarkus/resteasy/reactive/server/spi/NonBlockingReturnTypeBuildItem.java +++ b/extensions/resteasy-reactive/rest/spi-deployment/src/main/java/io/quarkus/resteasy/reactive/server/spi/NonBlockingReturnTypeBuildItem.java @@ -6,7 +6,10 @@ /** * Register a type as non-blocking by default when used as a return type of JAX-RS Resource + * + * @deprecated This build item will be removed at some time after Quarkus 3.16 */ +@Deprecated(forRemoval = true, since = "3.10") public final class NonBlockingReturnTypeBuildItem extends MultiBuildItem { private final DotName type; diff --git a/extensions/schema-registry/devservice/deployment/src/main/java/io/quarkus/apicurio/registry/devservice/DevServicesApicurioRegistryProcessor.java b/extensions/schema-registry/devservice/deployment/src/main/java/io/quarkus/apicurio/registry/devservice/DevServicesApicurioRegistryProcessor.java index 0ca85d928ad65..b80ab5eb86163 100644 --- a/extensions/schema-registry/devservice/deployment/src/main/java/io/quarkus/apicurio/registry/devservice/DevServicesApicurioRegistryProcessor.java +++ b/extensions/schema-registry/devservice/deployment/src/main/java/io/quarkus/apicurio/registry/devservice/DevServicesApicurioRegistryProcessor.java @@ -79,8 +79,10 @@ public DevServicesResultBuildItem startApicurioRegistryDevService(LaunchModeBuil (launchMode.isTest() ? "(test) " : "") + "Apicurio Registry Dev Services Starting:", consoleInstalledBuildItem, loggingSetupBuildItem); try { + boolean useSharedNetwork = DevServicesSharedNetworkBuildItem.isSharedNetworkRequired(devServicesConfig, + devServicesSharedNetworkBuildItem); devService = startApicurioRegistry(dockerStatusBuildItem, configuration, launchMode, - !devServicesSharedNetworkBuildItem.isEmpty(), devServicesConfig.timeout); + useSharedNetwork, devServicesConfig.timeout); compressor.close(); } catch (Throwable t) { compressor.closeAndDumpCaptured(); diff --git a/extensions/smallrye-context-propagation/deployment/src/main/java/io/quarkus/smallrye/context/deployment/ContextPropagationInitializedBuildItem.java b/extensions/smallrye-context-propagation/deployment/src/main/java/io/quarkus/smallrye/context/deployment/ContextPropagationInitializedBuildItem.java new file mode 100644 index 0000000000000..534494892a3a4 --- /dev/null +++ b/extensions/smallrye-context-propagation/deployment/src/main/java/io/quarkus/smallrye/context/deployment/ContextPropagationInitializedBuildItem.java @@ -0,0 +1,11 @@ +package io.quarkus.smallrye.context.deployment; + +import io.quarkus.builder.item.SimpleBuildItem; + +/** + * Marker build item for build ordering. Signifies that CP is set up + * and ready for use. + */ +public final class ContextPropagationInitializedBuildItem extends SimpleBuildItem { + +} diff --git a/extensions/smallrye-context-propagation/deployment/src/main/java/io/quarkus/smallrye/context/deployment/SmallRyeContextPropagationProcessor.java b/extensions/smallrye-context-propagation/deployment/src/main/java/io/quarkus/smallrye/context/deployment/SmallRyeContextPropagationProcessor.java index f923fc6dcaf70..341986949891a 100644 --- a/extensions/smallrye-context-propagation/deployment/src/main/java/io/quarkus/smallrye/context/deployment/SmallRyeContextPropagationProcessor.java +++ b/extensions/smallrye-context-propagation/deployment/src/main/java/io/quarkus/smallrye/context/deployment/SmallRyeContextPropagationProcessor.java @@ -96,6 +96,7 @@ void buildStatic(SmallRyeContextPropagationRecorder recorder, List cpInitializedBuildItem, BuildProducer feature, BuildProducer syntheticBeans) { feature.produce(new FeatureBuildItem(Feature.SMALLRYE_CONTEXT_PROPAGATION)); @@ -111,6 +112,8 @@ void build(SmallRyeContextPropagationRecorder recorder, .unremovable() .supplier(recorder.initializeManagedExecutor(executorBuildItem.getExecutorProxy())) .setRuntimeInit().done()); + + cpInitializedBuildItem.produce(new ContextPropagationInitializedBuildItem()); } // transform IPs for ManagedExecutor/ThreadContext that use config annotation and don't yet have @NamedInstance diff --git a/extensions/smallrye-context-propagation/runtime/src/main/java/io/quarkus/smallrye/context/runtime/QuarkusContextManagerProvider.java b/extensions/smallrye-context-propagation/runtime/src/main/java/io/quarkus/smallrye/context/runtime/QuarkusContextManagerProvider.java new file mode 100644 index 0000000000000..9f334e8c3fc7c --- /dev/null +++ b/extensions/smallrye-context-propagation/runtime/src/main/java/io/quarkus/smallrye/context/runtime/QuarkusContextManagerProvider.java @@ -0,0 +1,42 @@ +package io.quarkus.smallrye.context.runtime; + +import org.eclipse.microprofile.context.spi.ContextManager; + +import io.smallrye.context.SmallRyeContextManager; +import io.smallrye.context.SmallRyeContextManagerProvider; + +/** + * Quarkus doesn't need one manager per CL, we only have the one + */ +public class QuarkusContextManagerProvider extends SmallRyeContextManagerProvider { + + private SmallRyeContextManager contextManager; + + @Override + public SmallRyeContextManager getContextManager(ClassLoader classLoader) { + return contextManager; + } + + @Override + public SmallRyeContextManager getContextManager() { + return contextManager; + } + + @Override + public ContextManager findContextManager(ClassLoader classLoader) { + return contextManager; + } + + @Override + public void registerContextManager(ContextManager manager, ClassLoader classLoader) { + if (manager instanceof SmallRyeContextManager == false) { + throw new IllegalArgumentException("Only instances of SmallRyeContextManager are supported: " + manager); + } + contextManager = (SmallRyeContextManager) manager; + } + + @Override + public void releaseContextManager(ContextManager manager) { + contextManager = null; + } +} diff --git a/extensions/smallrye-context-propagation/runtime/src/main/java/io/quarkus/smallrye/context/runtime/SmallRyeContextPropagationRecorder.java b/extensions/smallrye-context-propagation/runtime/src/main/java/io/quarkus/smallrye/context/runtime/SmallRyeContextPropagationRecorder.java index b1a40ce4a00c7..4955a2bbf54b7 100644 --- a/extensions/smallrye-context-propagation/runtime/src/main/java/io/quarkus/smallrye/context/runtime/SmallRyeContextPropagationRecorder.java +++ b/extensions/smallrye-context-propagation/runtime/src/main/java/io/quarkus/smallrye/context/runtime/SmallRyeContextPropagationRecorder.java @@ -1,11 +1,18 @@ package io.quarkus.smallrye.context.runtime; +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.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import java.util.function.Supplier; import org.eclipse.microprofile.context.ManagedExecutor; import org.eclipse.microprofile.context.ThreadContext; +import org.eclipse.microprofile.context.spi.ContextManager.Builder; import org.eclipse.microprofile.context.spi.ContextManagerExtension; import org.eclipse.microprofile.context.spi.ContextManagerProvider; import org.eclipse.microprofile.context.spi.ThreadContextProvider; @@ -14,7 +21,6 @@ import io.quarkus.runtime.ShutdownContext; import io.quarkus.runtime.annotations.Recorder; import io.smallrye.context.SmallRyeContextManager; -import io.smallrye.context.SmallRyeContextManagerProvider; import io.smallrye.context.SmallRyeManagedExecutor; import io.smallrye.context.SmallRyeThreadContext; @@ -24,6 +30,92 @@ @Recorder public class SmallRyeContextPropagationRecorder { + private static final ExecutorService NOPE_EXECUTOR_SERVICE = new ExecutorService() { + + @Override + public void execute(Runnable command) { + nope(); + } + + @Override + public void shutdown() { + nope(); + } + + @Override + public List shutdownNow() { + nope(); + return null; + } + + @Override + public boolean isShutdown() { + nope(); + return false; + } + + @Override + public boolean isTerminated() { + nope(); + return false; + } + + @Override + public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException { + nope(); + return false; + } + + @Override + public Future submit(Callable task) { + nope(); + return null; + } + + @Override + public Future submit(Runnable task, T result) { + nope(); + return null; + } + + @Override + public Future submit(Runnable task) { + nope(); + return null; + } + + @Override + public List> invokeAll(Collection> tasks) throws InterruptedException { + nope(); + return null; + } + + @Override + public List> invokeAll(Collection> tasks, long timeout, TimeUnit unit) + throws InterruptedException { + nope(); + return null; + } + + @Override + public T invokeAny(Collection> tasks) + throws InterruptedException, ExecutionException { + nope(); + return null; + } + + @Override + public T invokeAny(Collection> tasks, long timeout, TimeUnit unit) + throws InterruptedException, ExecutionException, TimeoutException { + nope(); + return null; + } + + private void nope() { + throw new RuntimeException( + "Trying to invoke ContextPropagation on a partially-configured ContextManager instance. You should wait until runtime init is done. You can do that by consuming the ContextPropagationBuildItem."); + } + }; private static SmallRyeContextManager.Builder builder; public void configureStaticInit(List discoveredProviders, @@ -31,7 +123,7 @@ public void configureStaticInit(List discoveredProviders, // build the manager at static init time // in the live-reload mode, the provider instance may be already set in the previous start if (ContextManagerProvider.INSTANCE.get() == null) { - ContextManagerProvider contextManagerProvider = new SmallRyeContextManagerProvider(); + ContextManagerProvider contextManagerProvider = new QuarkusContextManagerProvider(); ContextManagerProvider.register(contextManagerProvider); } @@ -40,6 +132,16 @@ public void configureStaticInit(List discoveredProviders, .getContextManagerBuilder(); builder.withThreadContextProviders(discoveredProviders.toArray(new ThreadContextProvider[0])); builder.withContextManagerExtensions(discoveredExtensions.toArray(new ContextManagerExtension[0])); + + // During boot, if anyone is using CP, they will get no propagation and an error if they try to use + // the executor. This is (so far) only for spring-cloud-config-client which uses Vert.x via Mutiny + // to load config before we're ready for runtime init + SmallRyeContextManager.Builder noContextBuilder = (SmallRyeContextManager.Builder) ContextManagerProvider.instance() + .getContextManagerBuilder(); + noContextBuilder.withThreadContextProviders(new ThreadContextProvider[0]); + noContextBuilder.withContextManagerExtensions(new ContextManagerExtension[0]); + noContextBuilder.withDefaultExecutorService(NOPE_EXECUTOR_SERVICE); + ContextManagerProvider.instance().registerContextManager(noContextBuilder.build(), null /* not used */); } public void configureRuntime(ExecutorService executorService, ShutdownContext shutdownContext) { @@ -59,7 +161,7 @@ public void run() { } }); //Avoid leaking the classloader: - this.builder = null; + SmallRyeContextPropagationRecorder.builder = null; } public Supplier initializeManagedExecutor(ExecutorService executorService) { diff --git a/extensions/smallrye-fault-tolerance/deployment/src/main/java/io/quarkus/smallrye/faulttolerance/deployment/DotNames.java b/extensions/smallrye-fault-tolerance/deployment/src/main/java/io/quarkus/smallrye/faulttolerance/deployment/DotNames.java index 9a95de94793a6..c18bc940a2535 100644 --- a/extensions/smallrye-fault-tolerance/deployment/src/main/java/io/quarkus/smallrye/faulttolerance/deployment/DotNames.java +++ b/extensions/smallrye-fault-tolerance/deployment/src/main/java/io/quarkus/smallrye/faulttolerance/deployment/DotNames.java @@ -22,6 +22,7 @@ import io.smallrye.faulttolerance.api.ExponentialBackoff; import io.smallrye.faulttolerance.api.FibonacciBackoff; import io.smallrye.faulttolerance.api.RateLimit; +import io.smallrye.faulttolerance.api.RetryWhen; public final class DotNames { public static final DotName OBJECT = DotName.createSimple(Object.class); @@ -52,8 +53,9 @@ public final class DotNames { public static final DotName FIBONACCI_BACKOFF = DotName.createSimple(FibonacciBackoff.class); public static final DotName CUSTOM_BACKOFF = DotName.createSimple(CustomBackoff.class); public static final DotName CUSTOM_BACKOFF_STRATEGY = DotName.createSimple(CustomBackoffStrategy.class); + public static final DotName RETRY_WHEN = DotName.createSimple(RetryWhen.class); - // certain SmallRye annotations (@CircuitBreakerName, @[Non]Blocking, @*Backoff) alone do _not_ trigger + // certain SmallRye annotations (@CircuitBreakerName, @[Non]Blocking, @*Backoff, @RetryWhen) alone do _not_ trigger // the fault tolerance interceptor, only in combination with other fault tolerance annotations public static final Set FT_ANNOTATIONS = Set.of(APPLY_FAULT_TOLERANCE, ASYNCHRONOUS, ASYNCHRONOUS_NON_BLOCKING, BULKHEAD, CIRCUIT_BREAKER, FALLBACK, RATE_LIMIT, RETRY, TIMEOUT); diff --git a/extensions/smallrye-fault-tolerance/deployment/src/main/java/io/quarkus/smallrye/faulttolerance/deployment/FaultToleranceScanner.java b/extensions/smallrye-fault-tolerance/deployment/src/main/java/io/quarkus/smallrye/faulttolerance/deployment/FaultToleranceScanner.java index bca9ef3b1e576..f4105a2f2d516 100644 --- a/extensions/smallrye-fault-tolerance/deployment/src/main/java/io/quarkus/smallrye/faulttolerance/deployment/FaultToleranceScanner.java +++ b/extensions/smallrye-fault-tolerance/deployment/src/main/java/io/quarkus/smallrye/faulttolerance/deployment/FaultToleranceScanner.java @@ -31,6 +31,7 @@ import io.smallrye.faulttolerance.api.ExponentialBackoff; import io.smallrye.faulttolerance.api.FibonacciBackoff; import io.smallrye.faulttolerance.api.RateLimit; +import io.smallrye.faulttolerance.api.RetryWhen; import io.smallrye.faulttolerance.autoconfig.FaultToleranceMethod; import io.smallrye.faulttolerance.autoconfig.MethodDescriptor; @@ -80,7 +81,7 @@ boolean hasFTAnnotations(ClassInfo clazz) { void forEachMethod(ClassInfo clazz, Consumer action) { for (MethodInfo method : clazz.methods()) { if (method.name().startsWith("<")) { - // constructors and static inititalizers can't be intercepted + // constructors and static initializers can't be intercepted continue; } if (method.isSynthetic()) { @@ -134,6 +135,7 @@ FaultToleranceMethod createFaultToleranceMethod(ClassInfo beanClass, MethodInfo result.customBackoff = getAnnotation(CustomBackoff.class, method, beanClass, annotationsPresentDirectly); result.exponentialBackoff = getAnnotation(ExponentialBackoff.class, method, beanClass, annotationsPresentDirectly); result.fibonacciBackoff = getAnnotation(FibonacciBackoff.class, method, beanClass, annotationsPresentDirectly); + result.retryWhen = getAnnotation(RetryWhen.class, method, beanClass, annotationsPresentDirectly); result.annotationsPresentDirectly = annotationsPresentDirectly; diff --git a/extensions/smallrye-fault-tolerance/deployment/src/main/java/io/quarkus/smallrye/faulttolerance/deployment/SmallRyeFaultToleranceProcessor.java b/extensions/smallrye-fault-tolerance/deployment/src/main/java/io/quarkus/smallrye/faulttolerance/deployment/SmallRyeFaultToleranceProcessor.java index 345c8808dc85f..6bd18bf866aff 100644 --- a/extensions/smallrye-fault-tolerance/deployment/src/main/java/io/quarkus/smallrye/faulttolerance/deployment/SmallRyeFaultToleranceProcessor.java +++ b/extensions/smallrye-fault-tolerance/deployment/src/main/java/io/quarkus/smallrye/faulttolerance/deployment/SmallRyeFaultToleranceProcessor.java @@ -50,6 +50,7 @@ import io.quarkus.deployment.builditem.SystemPropertyBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveMethodBuildItem; +import io.quarkus.deployment.builditem.nativeimage.RuntimeInitializedClassBuildItem; import io.quarkus.deployment.builditem.nativeimage.ServiceProviderBuildItem; import io.quarkus.deployment.metrics.MetricsCapabilityBuildItem; import io.quarkus.deployment.recording.RecorderContext; @@ -87,7 +88,8 @@ public void build(BuildProducer annotationsTran CombinedIndexBuildItem combinedIndexBuildItem, BuildProducer reflectiveClass, BuildProducer reflectiveMethod, - BuildProducer config) { + BuildProducer config, + BuildProducer runtimeInitializedClassBuildItems) { feature.produce(new FeatureBuildItem(Feature.SMALLRYE_FAULT_TOLERANCE)); @@ -95,6 +97,8 @@ public void build(BuildProducer annotationsTran ContextPropagationRequestContextControllerProvider.class.getName())); serviceProvider.produce(new ServiceProviderBuildItem(RunnableWrapper.class.getName(), ContextPropagationRunnableWrapper.class.getName())); + // make sure this is initialised at runtime, otherwise it will get a non-initialised ContextPropagationManager + runtimeInitializedClassBuildItems.produce(new RuntimeInitializedClassBuildItem(RunnableWrapper.class.getName())); IndexView index = combinedIndexBuildItem.getIndex(); @@ -153,6 +157,15 @@ public void build(BuildProducer annotationsTran for (ClassInfo strategy : index.getAllKnownImplementors(DotNames.CUSTOM_BACKOFF_STRATEGY)) { reflectiveClass.produce(ReflectiveClassBuildItem.builder(strategy.name().toString()).methods().build()); } + // Add reflective access to retry predicates + for (AnnotationInstance annotation : index.getAnnotations(DotNames.RETRY_WHEN)) { + for (String memberName : List.of("result", "exception")) { + AnnotationValue member = annotation.value(memberName); + if (member != null) { + reflectiveClass.produce(ReflectiveClassBuildItem.builder(member.asClass().name().toString()).build()); + } + } + } for (DotName annotation : DotNames.FT_ANNOTATIONS) { reflectiveClass.produce(ReflectiveClassBuildItem.builder(annotation.toString()).methods().build()); @@ -350,8 +363,6 @@ void processFaultToleranceAnnotations(SmallRyeFaultToleranceRecorder recorder, } } - // since annotation transformations are applied lazily, we can't know - // all transformed `@*Backoff`s and have to rely on Jandex here for (DotName backoffAnnotation : DotNames.BACKOFF_ANNOTATIONS) { for (AnnotationInstance it : index.getAnnotations(backoffAnnotation)) { if (!annotationStore.hasAnnotation(it.target(), DotNames.RETRY)) { @@ -360,6 +371,11 @@ void processFaultToleranceAnnotations(SmallRyeFaultToleranceRecorder recorder, } } } + for (AnnotationInstance it : index.getAnnotations(DotNames.RETRY_WHEN)) { + if (!annotationStore.hasAnnotation(it.target(), DotNames.RETRY)) { + exceptions.add(new DefinitionException("@RetryWhen present on '" + it.target() + "', but @Retry is missing")); + } + } if (!exceptions.isEmpty()) { errors.produce(new ValidationPhaseBuildItem.ValidationErrorBuildItem(exceptions)); diff --git a/extensions/smallrye-fault-tolerance/deployment/src/main/resources/dev-ui/qwc-fault-tolerance-methods.js b/extensions/smallrye-fault-tolerance/deployment/src/main/resources/dev-ui/qwc-fault-tolerance-methods.js index 30dfbb9ab9cf9..f45e180b7259b 100644 --- a/extensions/smallrye-fault-tolerance/deployment/src/main/resources/dev-ui/qwc-fault-tolerance-methods.js +++ b/extensions/smallrye-fault-tolerance/deployment/src/main/resources/dev-ui/qwc-fault-tolerance-methods.js @@ -85,6 +85,7 @@ export class QwcFaultToleranceMethods extends LitElement { ${guardedMethod.ExponentialBackoff ? this._renderExponentialBackoff(guardedMethod.ExponentialBackoff) : html``} ${guardedMethod.FibonacciBackoff ? this._renderFibonacciBackoff(guardedMethod.FibonacciBackoff) : html``} ${guardedMethod.CustomBackoff ? this._renderCustomBackoff(guardedMethod.CustomBackoff) : html``} + ${guardedMethod.RetryWhen ? this._renderRetryWhen(guardedMethod.RetryWhen) : html``} ${guardedMethod.Timeout ? this._renderTimeout(guardedMethod.Timeout) : html``} `; @@ -179,6 +180,15 @@ export class QwcFaultToleranceMethods extends LitElement { `; } + _renderRetryWhen(retryWhen) { + return html` + + ↪ + @RetryWhen(result = ${retryWhen.result}, exception = ${retryWhen.exception}) + + `; + } + _renderTimeout(timeout) { return html` @Timeout(${timeout.value} ${timeout.valueUnit}) diff --git a/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/asynchronous/additional/AsyncNonBlockingService.java b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/asynchronous/additional/AsyncNonBlockingService.java index 143bc0ab6992c..44d2ce505936f 100644 --- a/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/asynchronous/additional/AsyncNonBlockingService.java +++ b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/asynchronous/additional/AsyncNonBlockingService.java @@ -1,7 +1,7 @@ package io.quarkus.smallrye.faulttolerance.test.asynchronous.additional; -import static io.smallrye.faulttolerance.core.util.CompletionStages.failedFuture; import static java.util.concurrent.CompletableFuture.completedFuture; +import static java.util.concurrent.CompletableFuture.failedFuture; import java.util.List; import java.util.concurrent.CompletionStage; diff --git a/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/asynchronous/additional/BlockingService.java b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/asynchronous/additional/BlockingService.java index 079497679b66c..117ed7ab47e5b 100644 --- a/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/asynchronous/additional/BlockingService.java +++ b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/asynchronous/additional/BlockingService.java @@ -1,7 +1,7 @@ package io.quarkus.smallrye.faulttolerance.test.asynchronous.additional; -import static io.smallrye.faulttolerance.core.util.CompletionStages.failedFuture; import static java.util.concurrent.CompletableFuture.completedFuture; +import static java.util.concurrent.CompletableFuture.failedFuture; import java.util.List; import java.util.concurrent.CompletionStage; diff --git a/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/asynchronous/additional/NonblockingService.java b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/asynchronous/additional/NonblockingService.java index 49e7895249086..5e8b403c44dc1 100644 --- a/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/asynchronous/additional/NonblockingService.java +++ b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/asynchronous/additional/NonblockingService.java @@ -1,7 +1,7 @@ package io.quarkus.smallrye.faulttolerance.test.asynchronous.additional; -import static io.smallrye.faulttolerance.core.util.CompletionStages.failedFuture; import static java.util.concurrent.CompletableFuture.completedFuture; +import static java.util.concurrent.CompletableFuture.failedFuture; import java.util.List; import java.util.concurrent.CompletionStage; diff --git a/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/asynchronous/noncompat/NoncompatNonblockingService.java b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/asynchronous/noncompat/NoncompatNonblockingService.java index 9088f96ffe61f..d86232d589f8e 100644 --- a/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/asynchronous/noncompat/NoncompatNonblockingService.java +++ b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/asynchronous/noncompat/NoncompatNonblockingService.java @@ -1,7 +1,7 @@ package io.quarkus.smallrye.faulttolerance.test.asynchronous.noncompat; -import static io.smallrye.faulttolerance.core.util.CompletionStages.failedFuture; import static java.util.concurrent.CompletableFuture.completedFuture; +import static java.util.concurrent.CompletableFuture.failedFuture; import java.util.List; import java.util.concurrent.CompletionStage; diff --git a/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/ratelimit/RateLimitTest.java b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/ratelimit/RateLimitTest.java index 27e24c8a764ba..9f487d684ab4e 100644 --- a/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/ratelimit/RateLimitTest.java +++ b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/ratelimit/RateLimitTest.java @@ -2,6 +2,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import jakarta.inject.Inject; @@ -26,6 +27,7 @@ public void test() { assertEquals(3, rateLimit.hello()); assertEquals(4, rateLimit.hello()); assertEquals(5, rateLimit.hello()); - assertThrows(RateLimitException.class, () -> rateLimit.hello()); + RateLimitException rateLimitException = assertThrows(RateLimitException.class, () -> rateLimit.hello()); + assertTrue(rateLimitException.getRetryAfterMillis() > 0); } } diff --git a/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/retry/when/IsIllegalArgumentException.java b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/retry/when/IsIllegalArgumentException.java new file mode 100644 index 0000000000000..db978033f0c39 --- /dev/null +++ b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/retry/when/IsIllegalArgumentException.java @@ -0,0 +1,10 @@ +package io.quarkus.smallrye.faulttolerance.test.retry.when; + +import java.util.function.Predicate; + +public class IsIllegalArgumentException implements Predicate { + @Override + public boolean test(Throwable throwable) { + return throwable instanceof IllegalArgumentException; + } +} diff --git a/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/retry/when/IsNull.java b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/retry/when/IsNull.java new file mode 100644 index 0000000000000..4aa61be73387e --- /dev/null +++ b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/retry/when/IsNull.java @@ -0,0 +1,10 @@ +package io.quarkus.smallrye.faulttolerance.test.retry.when; + +import java.util.function.Predicate; + +public class IsNull implements Predicate { + @Override + public boolean test(Object o) { + return o == null; + } +} diff --git a/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/retry/when/RetryOnAndRetryWhenExceptionService.java b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/retry/when/RetryOnAndRetryWhenExceptionService.java new file mode 100644 index 0000000000000..bacc0907a2cf0 --- /dev/null +++ b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/retry/when/RetryOnAndRetryWhenExceptionService.java @@ -0,0 +1,15 @@ +package io.quarkus.smallrye.faulttolerance.test.retry.when; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.faulttolerance.Retry; + +import io.smallrye.faulttolerance.api.RetryWhen; + +@ApplicationScoped +public class RetryOnAndRetryWhenExceptionService { + @Retry(retryOn = IllegalStateException.class) + @RetryWhen(exception = IsIllegalArgumentException.class) + public void hello() { + } +} diff --git a/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/retry/when/RetryOnAndRetryWhenExceptionTest.java b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/retry/when/RetryOnAndRetryWhenExceptionTest.java new file mode 100644 index 0000000000000..efed54c502657 --- /dev/null +++ b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/retry/when/RetryOnAndRetryWhenExceptionTest.java @@ -0,0 +1,27 @@ +package io.quarkus.smallrye.faulttolerance.test.retry.when; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import jakarta.enterprise.inject.spi.DeploymentException; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class RetryOnAndRetryWhenExceptionTest { + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(RetryOnAndRetryWhenExceptionService.class, IsIllegalArgumentException.class)) + .assertException(e -> { + assertEquals(DeploymentException.class, e.getClass()); + assertTrue(e.getMessage().contains("Invalid @RetryWhen.exception")); + assertTrue(e.getMessage().contains("must not be combined with @Retry.retryOn")); + }); + + @Test + public void test() { + } +} diff --git a/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/retry/when/RetryOnClassRetryWhenOnMethodService.java b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/retry/when/RetryOnClassRetryWhenOnMethodService.java new file mode 100644 index 0000000000000..1527fe421d866 --- /dev/null +++ b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/retry/when/RetryOnClassRetryWhenOnMethodService.java @@ -0,0 +1,16 @@ +package io.quarkus.smallrye.faulttolerance.test.retry.when; + +import jakarta.enterprise.context.Dependent; + +import org.eclipse.microprofile.faulttolerance.Retry; + +import io.smallrye.faulttolerance.api.RetryWhen; + +@Dependent +@Retry +public class RetryOnClassRetryWhenOnMethodService { + @RetryWhen + public void hello() { + throw new IllegalArgumentException(); + } +} diff --git a/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/retry/when/RetryOnClassRetryWhenOnMethodTest.java b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/retry/when/RetryOnClassRetryWhenOnMethodTest.java new file mode 100644 index 0000000000000..3058e8bff2196 --- /dev/null +++ b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/retry/when/RetryOnClassRetryWhenOnMethodTest.java @@ -0,0 +1,27 @@ +package io.quarkus.smallrye.faulttolerance.test.retry.when; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import jakarta.enterprise.inject.spi.DefinitionException; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class RetryOnClassRetryWhenOnMethodTest { + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(RetryOnClassRetryWhenOnMethodService.class)) + .assertException(e -> { + assertEquals(DefinitionException.class, e.getClass()); + assertTrue(e.getMessage().contains("@RetryWhen present")); + assertTrue(e.getMessage().contains("@Retry is missing")); + }); + + @Test + public void test() { + } +} diff --git a/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/retry/when/RetryOnMethodRetryWhenOnClassService.java b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/retry/when/RetryOnMethodRetryWhenOnClassService.java new file mode 100644 index 0000000000000..bb1141d02fff6 --- /dev/null +++ b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/retry/when/RetryOnMethodRetryWhenOnClassService.java @@ -0,0 +1,16 @@ +package io.quarkus.smallrye.faulttolerance.test.retry.when; + +import jakarta.enterprise.context.Dependent; + +import org.eclipse.microprofile.faulttolerance.Retry; + +import io.smallrye.faulttolerance.api.RetryWhen; + +@Dependent +@RetryWhen +public class RetryOnMethodRetryWhenOnClassService { + @Retry + public void hello() { + throw new IllegalArgumentException(); + } +} diff --git a/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/retry/when/RetryOnMethodRetryWhenOnClassTest.java b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/retry/when/RetryOnMethodRetryWhenOnClassTest.java new file mode 100644 index 0000000000000..feabd09a9d954 --- /dev/null +++ b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/retry/when/RetryOnMethodRetryWhenOnClassTest.java @@ -0,0 +1,27 @@ +package io.quarkus.smallrye.faulttolerance.test.retry.when; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import jakarta.enterprise.inject.spi.DefinitionException; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class RetryOnMethodRetryWhenOnClassTest { + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(RetryOnMethodRetryWhenOnClassService.class)) + .assertException(e -> { + assertEquals(DefinitionException.class, e.getClass()); + assertTrue(e.getMessage().contains("@RetryWhen present")); + assertTrue(e.getMessage().contains("@Retry is missing")); + }); + + @Test + public void test() { + } +} diff --git a/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/retry/when/RetryWhenResultAndExceptionService.java b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/retry/when/RetryWhenResultAndExceptionService.java new file mode 100644 index 0000000000000..a8b0a7756e7af --- /dev/null +++ b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/retry/when/RetryWhenResultAndExceptionService.java @@ -0,0 +1,31 @@ +package io.quarkus.smallrye.faulttolerance.test.retry.when; + +import java.util.concurrent.atomic.AtomicInteger; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.faulttolerance.Retry; + +import io.smallrye.faulttolerance.api.RetryWhen; + +@ApplicationScoped +public class RetryWhenResultAndExceptionService { + private final AtomicInteger attempts = new AtomicInteger(); + + @Retry + @RetryWhen(result = IsNull.class, exception = IsIllegalArgumentException.class) + public String hello() { + int current = attempts.incrementAndGet(); + if (current == 1) { + return null; + } else if (current == 2) { + throw new IllegalArgumentException(); + } else { + return "hello"; + } + } + + public AtomicInteger getAttempts() { + return attempts; + } +} diff --git a/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/retry/when/RetryWhenResultAndExceptionTest.java b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/retry/when/RetryWhenResultAndExceptionTest.java new file mode 100644 index 0000000000000..024c09293151a --- /dev/null +++ b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/retry/when/RetryWhenResultAndExceptionTest.java @@ -0,0 +1,26 @@ +package io.quarkus.smallrye.faulttolerance.test.retry.when; + +import static org.assertj.core.api.Assertions.assertThat; + +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class RetryWhenResultAndExceptionTest { + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(RetryWhenResultAndExceptionService.class, IsNull.class, IsIllegalArgumentException.class)); + + @Inject + RetryWhenResultAndExceptionService service; + + @Test + public void test() { + assertThat(service.hello()).isEqualTo("hello"); + assertThat(service.getAttempts()).hasValue(3); + } +} diff --git a/extensions/smallrye-fault-tolerance/runtime/src/main/java/io/quarkus/smallrye/faulttolerance/runtime/SmallRyeFaultToleranceRecorder.java b/extensions/smallrye-fault-tolerance/runtime/src/main/java/io/quarkus/smallrye/faulttolerance/runtime/SmallRyeFaultToleranceRecorder.java index f1481dc59cfde..78824953316c2 100644 --- a/extensions/smallrye-fault-tolerance/runtime/src/main/java/io/quarkus/smallrye/faulttolerance/runtime/SmallRyeFaultToleranceRecorder.java +++ b/extensions/smallrye-fault-tolerance/runtime/src/main/java/io/quarkus/smallrye/faulttolerance/runtime/SmallRyeFaultToleranceRecorder.java @@ -41,7 +41,7 @@ public void createFaultToleranceOperation(List ftMethods) if (error instanceof DeploymentException) { throw (DeploymentException) error; } else { - throw new DeploymentException(allExceptions.get(0)); + throw new DeploymentException(error); } } else { StringBuilder message = new StringBuilder("Found " + allExceptions.size() + " deployment problems: "); diff --git a/extensions/smallrye-fault-tolerance/runtime/src/main/java/io/quarkus/smallrye/faulttolerance/runtime/devui/FaultToleranceJsonRpcService.java b/extensions/smallrye-fault-tolerance/runtime/src/main/java/io/quarkus/smallrye/faulttolerance/runtime/devui/FaultToleranceJsonRpcService.java index 77014a969da4e..4362f1509f2e2 100644 --- a/extensions/smallrye-fault-tolerance/runtime/src/main/java/io/quarkus/smallrye/faulttolerance/runtime/devui/FaultToleranceJsonRpcService.java +++ b/extensions/smallrye-fault-tolerance/runtime/src/main/java/io/quarkus/smallrye/faulttolerance/runtime/devui/FaultToleranceJsonRpcService.java @@ -22,6 +22,7 @@ import io.smallrye.faulttolerance.api.ExponentialBackoff; import io.smallrye.faulttolerance.api.FibonacciBackoff; import io.smallrye.faulttolerance.api.RateLimit; +import io.smallrye.faulttolerance.api.RetryWhen; import io.smallrye.faulttolerance.config.FaultToleranceOperation; import io.vertx.core.json.JsonArray; import io.vertx.core.json.JsonObject; @@ -127,6 +128,11 @@ private JsonObject convert(FaultToleranceOperation operation) { result.put(CustomBackoff.class.getSimpleName(), new JsonObject() .put("value", operation.getCustomBackoff().value().getName())); } + if (operation.hasRetryWhen()) { + result.put(RetryWhen.class.getSimpleName(), new JsonObject() + .put("result", operation.getRetryWhen().result().getName()) + .put("exception", operation.getRetryWhen().exception().getName())); + } if (operation.hasTimeout()) { result.put(Timeout.class.getSimpleName(), new JsonObject() .put("value", operation.getTimeout().value()) diff --git a/extensions/smallrye-graphql/deployment/src/test/java/io/quarkus/smallrye/graphql/deployment/CompletionStageTest.java b/extensions/smallrye-graphql/deployment/src/test/java/io/quarkus/smallrye/graphql/deployment/CompletionStageTest.java index ea4a4b78b6c89..2a1c7912ac44d 100644 --- a/extensions/smallrye-graphql/deployment/src/test/java/io/quarkus/smallrye/graphql/deployment/CompletionStageTest.java +++ b/extensions/smallrye-graphql/deployment/src/test/java/io/quarkus/smallrye/graphql/deployment/CompletionStageTest.java @@ -45,7 +45,7 @@ public void testSourcePost() { " title\n" + " published\n" + " buyLink\n" + - " authors {\n" + + " asyncAuthors {\n" + " name\n" + " bornName\n" + " }\n" + @@ -87,7 +87,7 @@ public CompletionStage getBuyLink(@Source Book book) { return CompletableFuture.supplyAsync(() -> String.format(AMAZON_SEARCH_FORMAT, title)); } - public CompletionStage>> getAuthors(@Source List books) { + public CompletionStage>> getAsyncAuthors(@Source List books) { List> authorsOfAllBooks = new ArrayList<>(); for (Book book : books) { List authors = new ArrayList<>(); diff --git a/extensions/smallrye-graphql/deployment/src/test/java/io/quarkus/smallrye/graphql/deployment/UniTest.java b/extensions/smallrye-graphql/deployment/src/test/java/io/quarkus/smallrye/graphql/deployment/UniTest.java index 6375602409431..3685c05c51f1a 100644 --- a/extensions/smallrye-graphql/deployment/src/test/java/io/quarkus/smallrye/graphql/deployment/UniTest.java +++ b/extensions/smallrye-graphql/deployment/src/test/java/io/quarkus/smallrye/graphql/deployment/UniTest.java @@ -44,7 +44,7 @@ public void testSourcePost() { " title\n" + " published\n" + " buyLink\n" + - " authors {\n" + + " asyncAuthors {\n" + " name\n" + " bornName\n" + " }\n" + @@ -81,7 +81,7 @@ public Uni getBuyLink(@Source Book book) { return Uni.createFrom().item(() -> String.format(AMAZON_SEARCH_FORMAT, title)); } - public Uni>> getAuthors(@Source List books) { + public Uni>> getAsyncAuthors(@Source List books) { List> authorsOfAllBooks = new ArrayList<>(); for (Book book : books) { List authors = new ArrayList<>(); diff --git a/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/spi/QuarkusClassloadingService.java b/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/spi/QuarkusClassloadingService.java index 8fc89fdeb99c4..359f9c4417aaa 100644 --- a/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/spi/QuarkusClassloadingService.java +++ b/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/spi/QuarkusClassloadingService.java @@ -1,9 +1,5 @@ package io.quarkus.smallrye.graphql.runtime.spi; -import java.security.AccessController; -import java.security.PrivilegedActionException; -import java.security.PrivilegedExceptionAction; - import graphql.schema.PropertyDataFetcherHelper; import io.smallrye.graphql.execution.Classes; import io.smallrye.graphql.spi.ClassloadingService; @@ -38,12 +34,10 @@ public Class loadClass(String className) { if (Classes.isPrimitive(className)) { return Classes.getPrimativeClassType(className); } else { - return AccessController.doPrivileged((PrivilegedExceptionAction>) () -> { - ClassLoader cl = classLoader == null ? Thread.currentThread().getContextClassLoader() : classLoader; - return loadClass(className, cl); - }); + ClassLoader cl = classLoader == null ? Thread.currentThread().getContextClassLoader() : classLoader; + return loadClass(className, cl); } - } catch (PrivilegedActionException | ClassNotFoundException pae) { + } catch (ClassNotFoundException pae) { throw new RuntimeException("Can not load class [" + className + "]", pae); } } diff --git a/extensions/smallrye-jwt/runtime/src/main/java/io/quarkus/smallrye/jwt/runtime/auth/RawOptionalClaimCreator.java b/extensions/smallrye-jwt/runtime/src/main/java/io/quarkus/smallrye/jwt/runtime/auth/RawOptionalClaimCreator.java index f9f174a283900..7237c7008c4fb 100644 --- a/extensions/smallrye-jwt/runtime/src/main/java/io/quarkus/smallrye/jwt/runtime/auth/RawOptionalClaimCreator.java +++ b/extensions/smallrye-jwt/runtime/src/main/java/io/quarkus/smallrye/jwt/runtime/auth/RawOptionalClaimCreator.java @@ -15,7 +15,7 @@ public class RawOptionalClaimCreator implements BeanCreator> { @Override public Optional create(CreationalContext> creationalContext, Map params) { - InjectionPoint injectionPoint = InjectionPointProvider.get(); + InjectionPoint injectionPoint = InjectionPointProvider.getCurrent(creationalContext); if (injectionPoint == null) { throw new IllegalStateException("No current injection point found"); } 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 be5e12ed76f10..53f3f86b0fe17 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 @@ -617,11 +617,14 @@ private OASFilter getAutoServerFilter(SmallRyeOpenApiConfig config, boolean defa } private Map> getRolesAllowedMethodReferences(OpenApiFilteredIndexViewBuildItem indexViewBuildItem) { + IndexView index = indexViewBuildItem.getIndex(); return SecurityConstants.ROLES_ALLOWED .stream() - .map(indexViewBuildItem.getIndex()::getAnnotations) + .map(index::getAnnotations) .flatMap(Collection::stream) - .flatMap(SmallRyeOpenApiProcessor::getMethods) + .flatMap((t) -> { + return getMethods(t, index); + }) .collect(Collectors.toMap( e -> JandexUtil.createUniqueMethodReference(e.getKey().declaringClass(), e.getKey()), e -> List.of(e.getValue().value().asStringArray()), @@ -636,26 +639,35 @@ private Map> getRolesAllowedMethodReferences(OpenApiFiltere private List getPermissionsAllowedMethodReferences( OpenApiFilteredIndexViewBuildItem indexViewBuildItem) { - return indexViewBuildItem.getIndex() + + FilteredIndexView index = indexViewBuildItem.getIndex(); + + return index .getAnnotations(DotName.createSimple(PermissionsAllowed.class)) .stream() - .flatMap(SmallRyeOpenApiProcessor::getMethods) + .flatMap((t) -> { + return getMethods(t, index); + }) .map(e -> JandexUtil.createUniqueMethodReference(e.getKey().declaringClass(), e.getKey())) .distinct() .toList(); } private List getAuthenticatedMethodReferences(OpenApiFilteredIndexViewBuildItem indexViewBuildItem) { - return indexViewBuildItem.getIndex() + IndexView index = indexViewBuildItem.getIndex(); + return index .getAnnotations(DotName.createSimple(Authenticated.class.getName())) .stream() - .flatMap(SmallRyeOpenApiProcessor::getMethods) + .flatMap((t) -> { + return getMethods(t, index); + }) .map(e -> JandexUtil.createUniqueMethodReference(e.getKey().declaringClass(), e.getKey())) .distinct() .toList(); } - private static Stream> getMethods(AnnotationInstance annotation) { + private static Stream> getMethods(AnnotationInstance annotation, + IndexView index) { if (annotation.target().kind() == Kind.METHOD) { MethodInfo method = annotation.target().asMethod(); @@ -664,8 +676,8 @@ private static Stream> getMethods(Anno } } else if (annotation.target().kind() == Kind.CLASS) { ClassInfo classInfo = annotation.target().asClass(); - - return classInfo.methods() + List methods = getMethods(classInfo, index); + return methods .stream() // drop methods that specify the annotation directly .filter(method -> !method.hasDeclaredAnnotation(annotation.name())) @@ -784,6 +796,24 @@ private static boolean isOpenAPIEndpoint(MethodInfo method) { return false; } + private static List getMethods(ClassInfo declaringClass, IndexView index) { + + List methods = new ArrayList<>(); + methods.addAll(declaringClass.methods()); + + // Check if the method overrides a method from an interface + for (Type interfaceType : declaringClass.interfaceTypes()) { + ClassInfo interfaceClass = index.getClassByName(interfaceType.name()); + if (interfaceClass != null) { + for (MethodInfo interfaceMethod : interfaceClass.methods()) { + methods.add(interfaceMethod); + } + } + } + return methods; + + } + private static Set getAllOpenAPIEndpoints() { Set httpAnnotations = new HashSet<>(); httpAnnotations.addAll(JaxRsConstants.HTTP_METHODS); diff --git a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/ApplicationContext.java b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/ApplicationContext.java new file mode 100644 index 0000000000000..65bd400713f92 --- /dev/null +++ b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/ApplicationContext.java @@ -0,0 +1,11 @@ +package io.quarkus.smallrye.openapi.test.jaxrs; + +import jakarta.ws.rs.ApplicationPath; +import jakarta.ws.rs.core.Application; + +import org.eclipse.microprofile.openapi.annotations.security.SecurityScheme; + +@ApplicationPath("/") +@SecurityScheme(ref = "oidc_auth") +public class ApplicationContext extends Application { +} diff --git a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/AutoSecurityRolesAllowedWithInterfaceTestCase.java b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/AutoSecurityRolesAllowedWithInterfaceTestCase.java new file mode 100644 index 0000000000000..086456e1f0757 --- /dev/null +++ b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/AutoSecurityRolesAllowedWithInterfaceTestCase.java @@ -0,0 +1,53 @@ +package io.quarkus.smallrye.openapi.test.jaxrs; + +import static org.hamcrest.Matchers.aMapWithSize; +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.emptyIterable; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.equalToObject; +import static org.hamcrest.Matchers.hasEntry; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.iterableWithSize; + +import org.hamcrest.Matcher; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; + +class AutoSecurityRolesAllowedWithInterfaceTestCase { + + @RegisterExtension + static QuarkusUnitTest runner = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(ApplicationContext.class, + FooAPI.class, FooResource.class)); + + static Matcher> schemeArray(String schemeName) { + return allOf( + iterableWithSize(1), + hasItem(allOf( + aMapWithSize(1), + hasEntry(equalTo(schemeName), emptyIterable())))); + } + + @Test + void testAutoSecurityRequirement() { + + var oidcAuth = schemeArray("oidc_auth"); + + RestAssured.given() + .header("Accept", "application/json") + .when() + .get("/q/openapi") + .then() + .log().body() + .and() + .body("components.securitySchemes.oidc_auth.$ref", equalToObject("#/components/securitySchemes/oidc_auth")) + .and() + .body("paths.'/secured/foo'.get.security", oidcAuth); + + } + +} diff --git a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/FooAPI.java b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/FooAPI.java new file mode 100644 index 0000000000000..0f1866d03a3cb --- /dev/null +++ b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/FooAPI.java @@ -0,0 +1,14 @@ +package io.quarkus.smallrye.openapi.test.jaxrs; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +@Path("/secured/foo") +public interface FooAPI { + @GET + @Produces(MediaType.APPLICATION_JSON) + Response getFoo(); +} \ No newline at end of file diff --git a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/FooResource.java b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/FooResource.java new file mode 100644 index 0000000000000..90709291bf68e --- /dev/null +++ b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/FooResource.java @@ -0,0 +1,12 @@ +package io.quarkus.smallrye.openapi.test.jaxrs; + +import jakarta.annotation.security.RolesAllowed; +import jakarta.ws.rs.core.Response; + +@RolesAllowed("RoleXY") +public class FooResource implements FooAPI { + @Override + public Response getFoo() { + return Response.ok("ok").build(); + } +} diff --git a/extensions/spring-data-jpa/deployment/src/main/java/io/quarkus/spring/data/deployment/SqlFileConfigBuilderCustomizer.java b/extensions/spring-data-jpa/deployment/src/main/java/io/quarkus/spring/data/deployment/SqlFileConfigBuilderCustomizer.java new file mode 100644 index 0000000000000..b296ecb9053fe --- /dev/null +++ b/extensions/spring-data-jpa/deployment/src/main/java/io/quarkus/spring/data/deployment/SqlFileConfigBuilderCustomizer.java @@ -0,0 +1,36 @@ +package io.quarkus.spring.data.deployment; + +import java.net.URL; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import io.smallrye.config.PropertiesConfigSource; +import io.smallrye.config.SmallRyeConfigBuilder; +import io.smallrye.config.SmallRyeConfigBuilderCustomizer; + +public class SqlFileConfigBuilderCustomizer implements SmallRyeConfigBuilderCustomizer { + @Override + public void configBuilder(SmallRyeConfigBuilder builder) { + List supportedSqlFiles = List.of("import.sql", "data.sql"); + List sqlFilesThatExist = new ArrayList<>(); + + for (String sqlFile : supportedSqlFiles) { + URL resource = Thread.currentThread().getContextClassLoader().getResource(sqlFile); + // we only check for files that are part of the application itself, + // this is done as to follow what the HibernateOrmProcessor does + if ((resource != null) && !resource.getProtocol().equals("jar")) { + sqlFilesThatExist.add(sqlFile); + } + } + + // use a priority of 50 to make sure that this is overridable by any of the standard methods + if (!sqlFilesThatExist.isEmpty()) { + builder.withSources( + new PropertiesConfigSource( + Map.of("quarkus.hibernate-orm.sql-load-script", String.join(",", sqlFilesThatExist)), + "quarkus-spring-data-jpa", 50)); + } + + } +} diff --git a/extensions/spring-data-jpa/deployment/src/main/resources/META-INF/services/io.smallrye.config.SmallRyeConfigBuilderCustomizer b/extensions/spring-data-jpa/deployment/src/main/resources/META-INF/services/io.smallrye.config.SmallRyeConfigBuilderCustomizer new file mode 100644 index 0000000000000..5de637f10302e --- /dev/null +++ b/extensions/spring-data-jpa/deployment/src/main/resources/META-INF/services/io.smallrye.config.SmallRyeConfigBuilderCustomizer @@ -0,0 +1 @@ +io.quarkus.spring.data.deployment.SqlFileConfigBuilderCustomizer diff --git a/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/BothImportAndDataSqlTest.java b/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/BothImportAndDataSqlTest.java new file mode 100644 index 0000000000000..6a8a6db69b249 --- /dev/null +++ b/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/BothImportAndDataSqlTest.java @@ -0,0 +1,33 @@ +package io.quarkus.spring.data.deployment; + +import static org.assertj.core.api.Assertions.assertThat; + +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; + +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.test.QuarkusUnitTest; + +public class BothImportAndDataSqlTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest().setArchiveProducer( + () -> ShrinkWrap.create(JavaArchive.class) + .addAsResource("users1.sql", "data.sql") + .addAsResource("users2.sql", "import.sql") + .addClasses(User.class, LoginEvent.class, UserRepository.class)) + .withConfigurationResource("application.properties"); + + @Inject + UserRepository repo; + + @Test + @Transactional + public void test() { + assertThat(repo.count()).isEqualTo(2); + } +} diff --git a/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/ModifyingQueryWithFlushAndClearUsingDataSqlTest.java b/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/ModifyingQueryWithFlushAndClearUsingDataSqlTest.java new file mode 100644 index 0000000000000..d30f2370e34b9 --- /dev/null +++ b/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/ModifyingQueryWithFlushAndClearUsingDataSqlTest.java @@ -0,0 +1,119 @@ +package io.quarkus.spring.data.deployment; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.ZonedDateTime; +import java.util.Optional; + +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class ModifyingQueryWithFlushAndClearUsingDataSqlTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest().setArchiveProducer( + () -> ShrinkWrap.create(JavaArchive.class) + .addAsResource("import_users.sql", "data.sql") + .addClasses(User.class, LoginEvent.class, UserRepository.class)) + .withConfigurationResource("application.properties"); + + @Inject + UserRepository repo; + + @BeforeEach + @Transactional + public void setUp() { + final User user = getUser("JOHN"); + user.setLoginCounter(0); + repo.save(user); + } + + @Test + @Transactional + public void testNoAutoClear() { + getUser("JOHN"); // read user to attach it to entity manager + + repo.incrementLoginCounterPlain("JOHN"); + + final User userAfterIncrement = getUser("JOHN"); // we get the cached entity + // the read doesn't re-read the incremented counter and is therefore equal to the old value + assertThat(userAfterIncrement.getLoginCounter()).isEqualTo(0); + } + + @Test + @Transactional + public void testAutoClear() { + getUser("JOHN"); // read user to attach it to entity manager + + repo.incrementLoginCounterAutoClear("JOHN"); + + final User userAfterIncrement = getUser("JOHN"); + assertThat(userAfterIncrement.getLoginCounter()).isEqualTo(1); + } + + @Test + @Transactional + public void testNoAutoFlush() { + final User user = getUser("JOHN"); + createLoginEvent(user); + + repo.processLoginEventsPlain(); + + final User verifyUser = getUser("JOHN"); + // processLoginEvents did not see the new login event + final boolean allProcessed = verifyUser.getLoginEvents().stream() + .allMatch(loginEvent -> loginEvent.isProcessed()); + assertThat(allProcessed).describedAs("all LoginEvents are marked as processed").isFalse(); + } + + @Test + @Transactional + public void testAutoFlush() { + final User user = getUser("JOHN"); + createLoginEvent(user); + + repo.processLoginEventsPlainAutoClearAndFlush(); + + final User verifyUser = getUser("JOHN"); + final boolean allProcessed = verifyUser.getLoginEvents().stream() + .allMatch(loginEvent -> loginEvent.isProcessed()); + assertThat(allProcessed).describedAs("all LoginEvents are marked as processed").isTrue(); + } + + @Test + @Transactional + public void testNamedQueryOnEntities() { + User user = repo.getUserByFullNameUsingNamedQuery("John Doe"); + assertThat(user).isNotNull(); + } + + @Test + @Transactional + public void testNamedQueriesOnEntities() { + User user = repo.getUserByFullNameUsingNamedQueries("John Doe"); + assertThat(user).isNotNull(); + } + + private LoginEvent createLoginEvent(User user) { + final LoginEvent loginEvent = new LoginEvent(); + loginEvent.setUser(user); + loginEvent.setZonedDateTime(ZonedDateTime.now()); + user.addEvent(loginEvent); + return loginEvent; + } + + private User getUser(String userId) { + final Optional user = repo.findById(userId); + assertThat(user).describedAs("user <%s>", userId).isPresent(); + return user.get(); + } + +} diff --git a/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/devmode/RepositoryReloadWithDataSqlTest.java b/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/devmode/RepositoryReloadWithDataSqlTest.java new file mode 100644 index 0000000000000..c6c0cf2b0385a --- /dev/null +++ b/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/devmode/RepositoryReloadWithDataSqlTest.java @@ -0,0 +1,39 @@ +package io.quarkus.spring.data.devmode; + +import static org.hamcrest.CoreMatchers.containsString; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusDevModeTest; +import io.restassured.RestAssured; + +public class RepositoryReloadWithDataSqlTest { + + @RegisterExtension + static QuarkusDevModeTest TEST = new QuarkusDevModeTest() + .withApplicationRoot((jar) -> jar + .addAsResource("application.properties") + .addAsResource("import_books.sql", "data.sql") + .addClasses(Book.class, BookRepository.class, BookResource.class)); + + @Test + public void testRepositoryIsReloaded() { + RestAssured.get("/book").then() + .statusCode(200) + .body(containsString("Strangers"), containsString("Ascent"), containsString("Everything")); + + TEST.modifySourceFile("BookRepository.java", s -> s.replace("// ", + "java.util.Optional findById(Integer id);")); + + TEST.modifySourceFile("BookResource.java", s -> s.replace("// ", + "@GET @Path(\"/{id}\") @Produces(MediaType.APPLICATION_JSON)\n" + + " public java.util.Optional findById(@jakarta.ws.rs.PathParam(\"id\") Integer id) {\n" + + " return bookRepository.findById(id);\n" + + " }")); + + RestAssured.get("/book/1").then() + .statusCode(200) + .body(containsString("Strangers")); + } +} diff --git a/extensions/spring-data-jpa/deployment/src/test/resources/users1.sql b/extensions/spring-data-jpa/deployment/src/test/resources/users1.sql new file mode 100644 index 0000000000000..d04eefd2895af --- /dev/null +++ b/extensions/spring-data-jpa/deployment/src/test/resources/users1.sql @@ -0,0 +1 @@ +INSERT INTO user_(userid, fullname, logincounter, active) VALUES ('JOHN', 'John Doe', 0, true); diff --git a/extensions/spring-data-jpa/deployment/src/test/resources/users2.sql b/extensions/spring-data-jpa/deployment/src/test/resources/users2.sql new file mode 100644 index 0000000000000..8de8f2dd7c9eb --- /dev/null +++ b/extensions/spring-data-jpa/deployment/src/test/resources/users2.sql @@ -0,0 +1 @@ +INSERT INTO user_(userid, fullname, logincounter, active) VALUES ('JANE', 'Jane Doe', 1, true); diff --git a/extensions/spring-web/resteasy-reactive/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/spring-web/resteasy-reactive/runtime/src/main/resources/META-INF/quarkus-extension.yaml index 2ecdd23b44ad4..1db221c7f1981 100644 --- a/extensions/spring-web/resteasy-reactive/runtime/src/main/resources/META-INF/quarkus-extension.yaml +++ b/extensions/spring-web/resteasy-reactive/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -1,5 +1,5 @@ --- artifact: ${project.groupId}:${project.artifactId}:${project.version} -name: "Spring Web RESTEasy Reactive" +name: "Spring Web REST" metadata: unlisted: true 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 a79eb04049dbd..6e86f73d93de0 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 @@ -3,6 +3,8 @@ import static io.quarkus.arc.processor.DotNames.APPLICATION_SCOPED; import static io.quarkus.arc.processor.DotNames.DEFAULT_BEAN; import static io.quarkus.arc.processor.DotNames.SINGLETON; +import static io.quarkus.vertx.http.runtime.security.HttpAuthenticator.BASIC_AUTH_ANNOTATION_DETECTED; +import static io.quarkus.vertx.http.runtime.security.HttpAuthenticator.TEST_IF_BASIC_AUTH_IMPLICITLY_REQUIRED; import static java.util.stream.Collectors.toMap; import java.lang.reflect.Modifier; @@ -32,15 +34,19 @@ import io.quarkus.arc.deployment.AdditionalBeanBuildItem; import io.quarkus.arc.deployment.AnnotationsTransformerBuildItem; +import io.quarkus.arc.deployment.BeanRegistrationPhaseBuildItem; import io.quarkus.arc.deployment.SyntheticBeanBuildItem; import io.quarkus.arc.processor.AnnotationsTransformer; +import io.quarkus.arc.processor.BeanInfo; import io.quarkus.deployment.Capabilities; import io.quarkus.deployment.Capability; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.annotations.ExecutionTime; import io.quarkus.deployment.annotations.Record; +import io.quarkus.deployment.builditem.ApplicationIndexBuildItem; import io.quarkus.deployment.builditem.CombinedIndexBuildItem; +import io.quarkus.deployment.builditem.SystemPropertyBuildItem; import io.quarkus.runtime.RuntimeValue; import io.quarkus.runtime.configuration.ConfigurationException; import io.quarkus.security.spi.AdditionalSecuredMethodsBuildItem; @@ -71,6 +77,7 @@ public class HttpSecurityProcessor { private static final DotName AUTH_MECHANISM_NAME = DotName.createSimple(HttpAuthenticationMechanism.class); private static final DotName BASIC_AUTH_MECH_NAME = DotName.createSimple(BasicAuthenticationMechanism.class); + private static final DotName BASIC_AUTH_ANNOTATION_NAME = DotName.createSimple(BasicAuthentication.class); @Record(ExecutionTime.STATIC_INIT) @BuildStep @@ -127,13 +134,46 @@ void setMtlsCertificateRoleProperties( } } + @BuildStep(onlyIf = IsApplicationBasicAuthRequired.class) + void detectBasicAuthImplicitlyRequired(HttpBuildTimeConfig buildTimeConfig, + BeanRegistrationPhaseBuildItem beanRegistrationPhaseBuildItem, ApplicationIndexBuildItem applicationIndexBuildItem, + BuildProducer systemPropertyProducer, + List eagerSecurityInterceptorBindings) { + if (makeBasicAuthMechDefaultBean(buildTimeConfig)) { + var appIndex = applicationIndexBuildItem.getIndex(); + boolean noCustomAuthMechanismsDetected = beanRegistrationPhaseBuildItem + .getContext() + .beans() + .filter(b -> b.hasType(AUTH_MECHANISM_NAME)) + .filter(BeanInfo::isClassBean) + .filter(b -> appIndex.getClassByName(b.getBeanClass()) != null) + .isEmpty(); + // we can't decide whether custom mechanisms support basic auth or not + if (noCustomAuthMechanismsDetected) { + systemPropertyProducer + .produce(new SystemPropertyBuildItem(TEST_IF_BASIC_AUTH_IMPLICITLY_REQUIRED, Boolean.TRUE.toString())); + if (!eagerSecurityInterceptorBindings.isEmpty()) { + boolean basicAuthAnnotationUsed = eagerSecurityInterceptorBindings + .stream() + .map(EagerSecurityInterceptorBindingBuildItem::getAnnotationBindings) + .flatMap(Arrays::stream) + .anyMatch(BASIC_AUTH_ANNOTATION_NAME::equals); + // @BasicAuthentication is used, hence the basic authentication is required + if (basicAuthAnnotationUsed) { + systemPropertyProducer + .produce(new SystemPropertyBuildItem(BASIC_AUTH_ANNOTATION_DETECTED, Boolean.TRUE.toString())); + } + } + } + } + } + @BuildStep(onlyIf = IsApplicationBasicAuthRequired.class) AdditionalBeanBuildItem initBasicAuth(HttpBuildTimeConfig buildTimeConfig, BuildProducer annotationsTransformerProducer, BuildProducer securityInformationProducer) { - if (!buildTimeConfig.auth.form.enabled && !isMtlsClientAuthenticationEnabled(buildTimeConfig) - && !buildTimeConfig.auth.basic.orElse(false)) { + if (makeBasicAuthMechDefaultBean(buildTimeConfig)) { //if not explicitly enabled we make this a default bean, so it is the fallback if nothing else is defined annotationsTransformerProducer.produce(new AnnotationsTransformerBuildItem(AnnotationsTransformer .appliedToClass() @@ -148,7 +188,12 @@ AdditionalBeanBuildItem initBasicAuth(HttpBuildTimeConfig buildTimeConfig, return AdditionalBeanBuildItem.builder().setUnremovable().addBeanClass(BasicAuthenticationMechanism.class).build(); } - public static boolean applicationBasicAuthRequired(HttpBuildTimeConfig buildTimeConfig, + private static boolean makeBasicAuthMechDefaultBean(HttpBuildTimeConfig buildTimeConfig) { + return !buildTimeConfig.auth.form.enabled && !isMtlsClientAuthenticationEnabled(buildTimeConfig) + && !buildTimeConfig.auth.basic.orElse(false); + } + + private static boolean applicationBasicAuthRequired(HttpBuildTimeConfig buildTimeConfig, ManagementInterfaceBuildTimeConfig managementInterfaceBuildTimeConfig) { //basic auth explicitly disabled if (buildTimeConfig.auth.basic.isPresent() && !buildTimeConfig.auth.basic.get()) { diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/StaticResourcesProcessor.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/StaticResourcesProcessor.java index d80af2bca4b0e..4371475cfb77e 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/StaticResourcesProcessor.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/StaticResourcesProcessor.java @@ -2,13 +2,7 @@ import static io.quarkus.deployment.annotations.ExecutionTime.RUNTIME_INIT; -import java.io.IOException; -import java.io.UncheckedIOException; -import java.nio.file.FileVisitResult; import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.SimpleFileVisitor; -import java.nio.file.attribute.BasicFileAttributes; import java.util.ArrayList; import java.util.HashSet; import java.util.List; @@ -16,17 +10,18 @@ import java.util.Set; import io.quarkus.arc.deployment.BeanContainerBuildItem; +import io.quarkus.bootstrap.classloading.ClassPathElement; import io.quarkus.bootstrap.classloading.QuarkusClassLoader; -import io.quarkus.deployment.ApplicationArchive; import io.quarkus.deployment.Capabilities; import io.quarkus.deployment.Capability; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.annotations.Record; -import io.quarkus.deployment.builditem.ApplicationArchivesBuildItem; import io.quarkus.deployment.builditem.nativeimage.NativeImageResourceBuildItem; import io.quarkus.deployment.pkg.steps.NativeOrNativeSourcesBuild; -import io.quarkus.runtime.util.ClassPathUtils; +import io.quarkus.paths.FilteredPathTree; +import io.quarkus.paths.PathFilter; +import io.quarkus.paths.PathVisitor; import io.quarkus.vertx.core.deployment.CoreVertxBuildItem; import io.quarkus.vertx.http.deployment.spi.AdditionalStaticResourceBuildItem; import io.quarkus.vertx.http.deployment.spi.StaticResourcesBuildItem; @@ -38,14 +33,14 @@ public class StaticResourcesProcessor { @BuildStep - void collectStaticResources(Capabilities capabilities, ApplicationArchivesBuildItem applicationArchivesBuildItem, + void collectStaticResources(Capabilities capabilities, List additionalStaticResources, - BuildProducer staticResources) throws Exception { + BuildProducer staticResources) { if (capabilities.isPresent(Capability.SERVLET)) { // Servlet container handles static resources return; } - Set paths = getClasspathResources(applicationArchivesBuildItem); + Set paths = getClasspathResources(); for (AdditionalStaticResourceBuildItem bi : additionalStaticResources) { paths.add(new StaticResourcesBuildItem.Entry(bi.getPath(), bi.isDirectory())); } @@ -75,62 +70,63 @@ public void nativeImageResource(Optional staticResourc // TODO: do we perhaps want to register the whole directory? continue; } + String metaInfResourcesPath = StaticResourcesRecorder.META_INF_RESOURCES + entry.getPath(); metaInfResources.add(metaInfResourcesPath); } producer.produce(new NativeImageResourceBuildItem(metaInfResources)); + + // register all directories under META-INF/resources for reflection in order to enable + // the serving of index.html in arbitrarily nested directories + final Set collectedDirs = new HashSet<>(); + visitRuntimeMetaInfResources(visit -> { + if (Files.isDirectory(visit.getPath())) { + final String relativePath = visit.getRelativePath("/"); + if (collectedDirs.add(relativePath)) { + producer.produce(new NativeImageResourceBuildItem(relativePath)); + } + } + }); } } /** * Find all static file resources that are available from classpath. * - * @param applicationArchivesBuildItem * @return the set of static resources - * @throws Exception */ - private Set getClasspathResources(ApplicationArchivesBuildItem applicationArchivesBuildItem) - throws Exception { + private Set getClasspathResources() { Set knownPaths = new HashSet<>(); - - ClassPathUtils.consumeAsPaths(StaticResourcesRecorder.META_INF_RESOURCES, resource -> { - collectKnownPaths(resource, knownPaths); + visitRuntimeMetaInfResources(visit -> { + if (!Files.isDirectory(visit.getPath())) { + knownPaths.add(new StaticResourcesBuildItem.Entry( + visit.getRelativePath("/").substring(StaticResourcesRecorder.META_INF_RESOURCES.length()), + false)); + } }); - - for (ApplicationArchive i : applicationArchivesBuildItem.getAllApplicationArchives()) { - i.accept(tree -> { - Path resource = tree.getPath(StaticResourcesRecorder.META_INF_RESOURCES); - if (resource != null && Files.exists(resource)) { - collectKnownPaths(resource, knownPaths); - } - }); - } - return knownPaths; } - private void collectKnownPaths(Path resource, Set knownPaths) { - try { - Files.walkFileTree(resource, new SimpleFileVisitor() { - @Override - public FileVisitResult visitFile(Path p, BasicFileAttributes attrs) - throws IOException { - String file = resource.relativize(p).toString(); - // Windows has a backslash - file = file.replace('\\', '/'); - if (!file.startsWith("/")) { - file = "/" + file; - } - - if (QuarkusClassLoader - .isResourcePresentAtRuntime(StaticResourcesRecorder.META_INF_RESOURCES + file)) { - knownPaths.add(new StaticResourcesBuildItem.Entry(file, false)); - } - return FileVisitResult.CONTINUE; + /** + * Visits all {@code META-INF/resources} directories and their content found on the runtime classpath + * + * @param visitor visitor implementation + */ + private static void visitRuntimeMetaInfResources(PathVisitor visitor) { + final List elements = QuarkusClassLoader.getElements(StaticResourcesRecorder.META_INF_RESOURCES, + false); + if (!elements.isEmpty()) { + final PathFilter filter = PathFilter.forIncludes(List.of( + StaticResourcesRecorder.META_INF_RESOURCES + "/**", + StaticResourcesRecorder.META_INF_RESOURCES)); + for (var element : elements) { + if (element.isRuntime()) { + element.apply(tree -> { + new FilteredPathTree(tree, filter).walk(visitor); + return null; + }); } - }); - } catch (IOException e) { - throw new UncheckedIOException(e); + } } } } diff --git a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/controller/router-controller.js b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/controller/router-controller.js index a70a2a0efd2e9..3fd5f879c6e6d 100644 --- a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/controller/router-controller.js +++ b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/controller/router-controller.js @@ -176,7 +176,6 @@ export class RouterController { addRoute(path, component, name, page, defaultRoute = false) { path = this.getPageUrlFor(page); - var currentSelection = window.location.pathname; const search = new URLSearchParams(window.location.search); if (!this.isExistingPath(path)) { RouterController.pageMap.set(path, page); diff --git a/extensions/vertx-http/dev-ui-spi/src/main/java/io/quarkus/devui/spi/page/Page.java b/extensions/vertx-http/dev-ui-spi/src/main/java/io/quarkus/devui/spi/page/Page.java index 247b2986f1c14..e7cb77009afbf 100644 --- a/extensions/vertx-http/dev-ui-spi/src/main/java/io/quarkus/devui/spi/page/Page.java +++ b/extensions/vertx-http/dev-ui-spi/src/main/java/io/quarkus/devui/spi/page/Page.java @@ -1,5 +1,8 @@ package io.quarkus.devui.spi.page; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.util.Map; /** @@ -61,6 +64,12 @@ protected Page(String icon, public String getId() { String id = this.title.toLowerCase().replaceAll(SPACE, DASH); + try { + id = URLEncoder.encode(id, StandardCharsets.UTF_8.toString()); + } catch (UnsupportedEncodingException ex) { + throw new RuntimeException(ex); + } + if (!this.isInternal() && this.namespace != null) { // This is extension pages in Dev UI id = this.namespace.toLowerCase() + SLASH + id; diff --git a/extensions/vertx-http/dev-ui-spi/src/main/java/io/quarkus/devui/spi/page/PageBuilder.java b/extensions/vertx-http/dev-ui-spi/src/main/java/io/quarkus/devui/spi/page/PageBuilder.java index 0e77a50ca6b73..823fd87570a83 100644 --- a/extensions/vertx-http/dev-ui-spi/src/main/java/io/quarkus/devui/spi/page/PageBuilder.java +++ b/extensions/vertx-http/dev-ui-spi/src/main/java/io/quarkus/devui/spi/page/PageBuilder.java @@ -42,7 +42,10 @@ public T title(String title) { @SuppressWarnings("unchecked") public T staticLabel(String staticLabel) { - this.staticLabel = staticLabel; + if (this.staticLabel == null) { + this.staticLabel = ""; + } + this.staticLabel = this.staticLabel + " " + staticLabel; return (T) this; } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/AuthConfig.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/AuthConfig.java index e4ff3dec29715..79bccaec08a5c 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/AuthConfig.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/AuthConfig.java @@ -14,7 +14,8 @@ public class AuthConfig { /** * If basic auth should be enabled. If both basic and form auth is enabled then basic auth will be enabled in silent mode. * - * If no authentication mechanisms are configured basic auth is the default. + * The basic auth is enabled by default if no authentication mechanisms are configured or Quarkus can safely + * determine that basic authentication is required. */ @ConfigItem public Optional basic; diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/StaticResourcesConfig.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/StaticResourcesConfig.java index 94ff031402d81..a2c650a813966 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/StaticResourcesConfig.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/StaticResourcesConfig.java @@ -1,5 +1,6 @@ package io.quarkus.vertx.http.runtime; +import java.nio.charset.Charset; import java.time.Duration; import io.quarkus.runtime.annotations.ConfigGroup; @@ -50,4 +51,10 @@ public class StaticResourcesConfig { @ConfigItem(defaultValue = "10000") public int maxCacheSize; + /** + * Content encoding for text related files + */ + @ConfigItem(defaultValue = "UTF-8") + public Charset contentEncoding; + } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/StaticResourcesRecorder.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/StaticResourcesRecorder.java index 7738d9bfe34cd..2ef933b783d1c 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/StaticResourcesRecorder.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/StaticResourcesRecorder.java @@ -49,7 +49,7 @@ public Consumer start(Set knownPaths) { for (Path resourcePath : hotDeploymentResourcePaths) { String root = resourcePath.toAbsolutePath().toString(); StaticHandler staticHandler = StaticHandler.create(FileSystemAccess.ROOT, root) - .setDefaultContentEncoding("UTF-8") + .setDefaultContentEncoding(config.contentEncoding.name()) .setCachingEnabled(false) .setIndexPage(config.indexPage) .setIncludeHidden(config.includeHidden) diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpAuthenticator.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpAuthenticator.java index 84eb2f49368a4..87b39a7cdcd3b 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpAuthenticator.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpAuthenticator.java @@ -23,16 +23,20 @@ import org.jboss.logging.Logger; import io.netty.handler.codec.http.HttpResponseStatus; +import io.quarkus.arc.Arc; import io.quarkus.security.AuthenticationFailedException; import io.quarkus.security.identity.IdentityProvider; import io.quarkus.security.identity.IdentityProviderManager; import io.quarkus.security.identity.SecurityIdentity; import io.quarkus.security.identity.request.AnonymousAuthenticationRequest; import io.quarkus.security.identity.request.AuthenticationRequest; +import io.quarkus.security.identity.request.UsernamePasswordAuthenticationRequest; import io.quarkus.security.spi.runtime.AuthenticationFailureEvent; import io.quarkus.security.spi.runtime.AuthenticationSuccessEvent; import io.quarkus.security.spi.runtime.SecurityEventHelper; import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig; +import io.quarkus.vertx.http.runtime.HttpConfiguration; +import io.quarkus.vertx.http.runtime.security.annotation.BasicAuthentication; import io.smallrye.mutiny.Uni; import io.vertx.ext.web.RoutingContext; @@ -41,6 +45,24 @@ */ @Singleton public class HttpAuthenticator { + /** + * Special handling for the basic authentication mechanism, for user convenience, we add the mechanism when: + * - not explicitly disabled or enabled + * - is default bean and not programmatically looked up because there are other authentication mechanisms + * - no custom auth mechanism is defined because then, we can't tell if user didn't provide custom impl. + * - there is a provider that supports it (if not, we inform user via the log) + *

+ * Presence of this system property means that we need to test whether: + * - there are HTTP Permissions using explicitly this mechanism + * - or {@link io.quarkus.vertx.http.runtime.security.annotation.BasicAuthentication} + */ + public static final String TEST_IF_BASIC_AUTH_IMPLICITLY_REQUIRED = "io.quarkus.security.http.test-if-basic-auth-implicitly-required"; + /** + * Whether {@link io.quarkus.vertx.http.runtime.security.annotation.BasicAuthentication} has been detected, + * which means that user needs to use basic authentication. + * Only set when detected and {@link HttpAuthenticator#TEST_IF_BASIC_AUTH_IMPLICITLY_REQUIRED} is true. + */ + public static final String BASIC_AUTH_ANNOTATION_DETECTED = "io.quarkus.security.http.basic-authentication-annotation-detected"; private static final Logger log = Logger.getLogger(HttpAuthenticator.class); /** * Added to a {@link RoutingContext} as selected authentication mechanism. @@ -106,6 +128,7 @@ public HttpAuthenticator(IdentityProviderManager identityProviderManager, """.formatted(mechanism.getClass().getName(), mechanism.getCredentialTypes())); } } + addBasicAuthMechanismIfImplicitlyRequired(httpAuthenticationMechanism, mechanisms, providers); if (mechanisms.isEmpty()) { this.mechanisms = new HttpAuthenticationMechanism[] { new NoAuthenticationMechanism() }; } else { @@ -377,6 +400,42 @@ public void accept(HttpCredentialTransport t) { }); } + private static void addBasicAuthMechanismIfImplicitlyRequired( + Instance httpAuthenticationMechanism, + List mechanisms, Instance> providers) { + if (!Boolean.getBoolean(TEST_IF_BASIC_AUTH_IMPLICITLY_REQUIRED) || isBasicAuthNotRequired()) { + return; + } + + var basicAuthMechInstance = httpAuthenticationMechanism.select(BasicAuthenticationMechanism.class); + if (basicAuthMechInstance.isResolvable() && !mechanisms.contains(basicAuthMechInstance.get())) { + for (IdentityProvider i : providers) { + if (UsernamePasswordAuthenticationRequest.class.equals(i.getRequestType())) { + mechanisms.add(basicAuthMechInstance.get()); + return; + } + } + log.debug(""" + BasicAuthenticationMechanism has been enabled because no custom authentication mechanism has been detected + and basic authentication is required either by the HTTP Security Policy or '@BasicAuthentication', but + there is no IdentityProvider based on username and password. Please use one of supported extensions. + For more information, go to the https://quarkus.io/guides/security-basic-authentication-howto. + """); + } + } + + private static boolean isBasicAuthNotRequired() { + if (Boolean.getBoolean(BASIC_AUTH_ANNOTATION_DETECTED)) { + return false; + } + for (var policy : Arc.container().instance(HttpConfiguration.class).get().auth.permissions.values()) { + if (BasicAuthentication.AUTH_MECHANISM_SCHEME.equals(policy.authMechanism.orElse(null))) { + return false; + } + } + return true; + } + static class NoAuthenticationMechanism implements HttpAuthenticationMechanism { @Override @@ -397,8 +456,8 @@ public Set> getCredentialTypes() { } @Override - public HttpCredentialTransport getCredentialTransport() { - return null; + public Uni getCredentialTransport(RoutingContext context) { + return Uni.createFrom().nullItem(); } } diff --git a/extensions/webjars-locator/deployment/pom.xml b/extensions/webjars-locator/deployment/pom.xml index d2e2c75ca3f28..c7e61954ff64b 100644 --- a/extensions/webjars-locator/deployment/pom.xml +++ b/extensions/webjars-locator/deployment/pom.xml @@ -16,6 +16,7 @@ 2.24.0 1.13.0 + 4.5.2 @@ -31,6 +32,10 @@ io.quarkus quarkus-webjars-locator + + io.mvnpm + importmap + @@ -63,6 +68,13 @@ 3.0.6 test + + org.mvnpm + bootstrap + ${mvnpm.bootstrap.version} + test + + io.quarkus quarkus-resteasy-deployment @@ -90,6 +102,7 @@ ${webjar.jquery-ui.version} ${webjar.momentjs.version} + ${mvnpm.bootstrap.version} diff --git a/extensions/webjars-locator/deployment/src/main/java/io/quarkus/webjar/locator/deployment/ImportMapBuildItem.java b/extensions/webjars-locator/deployment/src/main/java/io/quarkus/webjar/locator/deployment/ImportMapBuildItem.java new file mode 100644 index 0000000000000..9b951366ab031 --- /dev/null +++ b/extensions/webjars-locator/deployment/src/main/java/io/quarkus/webjar/locator/deployment/ImportMapBuildItem.java @@ -0,0 +1,15 @@ +package io.quarkus.webjar.locator.deployment; + +import io.quarkus.builder.item.SimpleBuildItem; + +public final class ImportMapBuildItem extends SimpleBuildItem { + private final String importmap; + + public ImportMapBuildItem(String importmap) { + this.importmap = importmap; + } + + public String getImportMap() { + return this.importmap; + } +} diff --git a/extensions/webjars-locator/deployment/src/main/java/io/quarkus/webjar/locator/deployment/WebJarLocatorConfig.java b/extensions/webjars-locator/deployment/src/main/java/io/quarkus/webjar/locator/deployment/WebJarLocatorConfig.java new file mode 100644 index 0000000000000..2676e483b2c79 --- /dev/null +++ b/extensions/webjars-locator/deployment/src/main/java/io/quarkus/webjar/locator/deployment/WebJarLocatorConfig.java @@ -0,0 +1,26 @@ +package io.quarkus.webjar.locator.deployment; + +import java.util.Map; + +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConfigRoot; + +/** + * Build time configuration for WebJar Locator. + */ +@ConfigRoot +public class WebJarLocatorConfig { + + /** + * If the version reroute is enabled. + */ + @ConfigItem(defaultValue = "true") + public boolean versionReroute; + + /** + * User defined import mappings + */ + @ConfigItem + public Map importMappings; + +} diff --git a/extensions/webjars-locator/deployment/src/main/java/io/quarkus/webjar/locator/deployment/WebJarLocatorStandaloneBuildStep.java b/extensions/webjars-locator/deployment/src/main/java/io/quarkus/webjar/locator/deployment/WebJarLocatorStandaloneBuildStep.java index 95dd06ed48fb7..bf3af18e035a1 100644 --- a/extensions/webjars-locator/deployment/src/main/java/io/quarkus/webjar/locator/deployment/WebJarLocatorStandaloneBuildStep.java +++ b/extensions/webjars-locator/deployment/src/main/java/io/quarkus/webjar/locator/deployment/WebJarLocatorStandaloneBuildStep.java @@ -2,16 +2,20 @@ import java.io.IOException; import java.io.UncheckedIOException; +import java.net.MalformedURLException; +import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; -import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.stream.Stream; import org.jboss.logging.Logger; +import io.mvnpm.importmap.Aggregator; import io.quarkus.bootstrap.classloading.ClassPathElement; import io.quarkus.bootstrap.classloading.QuarkusClassLoader; import io.quarkus.deployment.Feature; @@ -26,75 +30,142 @@ import io.quarkus.vertx.http.deployment.RouteBuildItem; import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig; import io.quarkus.webjar.locator.runtime.WebJarLocatorRecorder; +import io.vertx.core.Handler; +import io.vertx.ext.web.RoutingContext; public class WebJarLocatorStandaloneBuildStep { private static final String WEBJARS_PREFIX = "META-INF/resources/webjars"; + private static final String WEBJARS_NAME = "webjars"; + + private static final String MVNPM_PREFIX = "META-INF/resources/_static"; + private static final String MVNPM_NAME = "mvnpm"; + private static final Logger log = Logger.getLogger(WebJarLocatorStandaloneBuildStep.class.getName()); @BuildStep @Record(ExecutionTime.RUNTIME_INIT) public void findWebjarsAndCreateHandler( + WebJarLocatorConfig config, HttpBuildTimeConfig httpConfig, BuildProducer feature, BuildProducer routes, + BuildProducer im, CurateOutcomeBuildItem curateOutcome, WebJarLocatorRecorder recorder) throws Exception { - final List providers = QuarkusClassLoader.getElements(WEBJARS_PREFIX, false); - Map webjarNameToVersionMap = Collections.emptyMap(); + LibInfo webjarsLibInfo = getLibInfo(curateOutcome, WEBJARS_PREFIX, WEBJARS_NAME); + LibInfo mvnpmNameLibInfo = getLibInfo(curateOutcome, MVNPM_PREFIX, MVNPM_NAME); + + if (webjarsLibInfo != null || mvnpmNameLibInfo != null) { + feature.produce(new FeatureBuildItem(Feature.WEBJARS_LOCATOR)); + + if (webjarsLibInfo != null) { + if (config.versionReroute) { + Handler handler = recorder.getHandler(getRootPath(httpConfig, "webjars"), + webjarsLibInfo.nameVersionMap); + routes.produce(RouteBuildItem.builder().route("/webjars/*").handler(handler).build()); + } + } else { + log.warn( + "No WebJars were found in the project. Requests to the /webjars/ path will always return 404 (Not Found)"); + } + if (mvnpmNameLibInfo != null) { + if (config.versionReroute) { + Handler handler = recorder.getHandler(getRootPath(httpConfig, "_static"), + mvnpmNameLibInfo.nameVersionMap); + routes.produce(RouteBuildItem.builder().route("/_static/*").handler(handler).build()); + } + // Also create a importmap endpoint + Aggregator aggregator = new Aggregator(mvnpmNameLibInfo.jars); + if (!config.importMappings.isEmpty()) { + aggregator.addMappings(config.importMappings); + } + + String importMap = aggregator.aggregateAsJson(false); + im.produce(new ImportMapBuildItem(importMap)); + String path = getRootPath(httpConfig, IMPORTMAP_ROOT) + IMPORTMAP_FILENAME; + Handler importMapHandler = recorder.getImportMapHandler(path, + importMap); + routes.produce( + RouteBuildItem.builder().route("/" + IMPORTMAP_ROOT + "/" + IMPORTMAP_FILENAME) + .handler(importMapHandler).build()); + } else { + log.warn( + "No Mvnpm jars were found in the project. Requests to the /_static/ path will always return 404 (Not Found)"); + } + } + } + + private LibInfo getLibInfo(CurateOutcomeBuildItem curateOutcome, String prefix, String name) { + + final List providers = QuarkusClassLoader.getElements(prefix, false); if (!providers.isEmpty()) { - final Map webJarKeys = new HashMap<>(providers.size()); + final Map keys = new HashMap<>(providers.size()); for (ClassPathElement provider : providers) { - if (provider.getDependencyKey() == null || !provider.isRuntime()) { - log.warn("webjars content found in " + provider.getRoot() - + " won't be available. Please, report this issue."); - } else { - webJarKeys.put(provider.getDependencyKey(), provider); + if (provider.getDependencyKey() != null && provider.isRuntime()) { + keys.put(provider.getDependencyKey(), provider); } } - if (!webJarKeys.isEmpty()) { - final Map webjarMap = new HashMap<>(webJarKeys.size()); + if (!keys.isEmpty()) { + final Map map = new HashMap<>(keys.size()); + final Set jars = new HashSet<>(); for (ResolvedDependency dep : curateOutcome.getApplicationModel().getDependencies()) { if (!dep.isRuntimeCp()) { continue; } - final ClassPathElement provider = webJarKeys.get(dep.getKey()); + + final ClassPathElement provider = keys.get(dep.getKey()); if (provider == null) { continue; } provider.apply(tree -> { - final Path webjarsDir = tree.getPath(WEBJARS_PREFIX); + final Path dir = tree.getPath(prefix); final Path nameDir; - try (Stream webjarsDirPaths = Files.list(webjarsDir)) { - nameDir = webjarsDirPaths.filter(Files::isDirectory).findFirst().get(); + try (Stream dirPaths = Files.list(dir)) { + nameDir = dirPaths.filter(Files::isDirectory).findFirst().get(); } catch (IOException e) { throw new UncheckedIOException(e); } if (nameDir == null) { - log.warn("Failed to determine the name for webjars included in " + log.warn("Failed to determine the name for " + name + " included in " + tree.getOriginalTree().getRoots()); return null; } final String version = Files.isDirectory(nameDir.resolve(dep.getVersion())) ? dep.getVersion() : null; - webjarMap.put(nameDir.getFileName().toString(), version); + map.put(nameDir.getFileName().toString(), version); + try { + jars.add(dep.getResolvedPaths().getSinglePath().toUri().toURL()); + } catch (MalformedURLException ex) { + throw new RuntimeException(ex); + } return null; }); } - webjarNameToVersionMap = webjarMap; + + return new LibInfo(map, jars); } } + return null; + } - if (!webjarNameToVersionMap.isEmpty()) { - // The context path + the resources path - String rootPath = httpConfig.rootPath; - String webjarRootPath = (rootPath.endsWith("/")) ? rootPath + "webjars/" : rootPath + "/webjars/"; - feature.produce(new FeatureBuildItem(Feature.WEBJARS_LOCATOR)); - routes.produce( - RouteBuildItem.builder().route("/webjars/*") - .handler(recorder.getHandler(webjarRootPath, webjarNameToVersionMap)).build()); - } else { - log.warn("No WebJars were found in the project. Requests to the /webjars/ path will always return 404 (Not Found)"); + private String getRootPath(HttpBuildTimeConfig httpConfig, String path) { + // The context path + the resources path + String rootPath = httpConfig.rootPath; + return (rootPath.endsWith("/")) ? rootPath + path + "/" : rootPath + "/" + path + "/"; + } + + static class LibInfo { + Map nameVersionMap; + Set jars; + + LibInfo(Map nameVersionMap, Set jars) { + this.nameVersionMap = nameVersionMap; + this.jars = jars; } + } + + private static final String IMPORTMAP_ROOT = "_importmap"; + private static final String IMPORTMAP_FILENAME = "generated_importmap.js"; } diff --git a/extensions/webjars-locator/deployment/src/main/java/io/quarkus/webjar/locator/deployment/devui/WebJarLibrariesBuildItem.java b/extensions/webjars-locator/deployment/src/main/java/io/quarkus/webjar/locator/deployment/devui/WebJarLibrariesBuildItem.java index 87003d19c4908..709d0d21f88c0 100644 --- a/extensions/webjars-locator/deployment/src/main/java/io/quarkus/webjar/locator/deployment/devui/WebJarLibrariesBuildItem.java +++ b/extensions/webjars-locator/deployment/src/main/java/io/quarkus/webjar/locator/deployment/devui/WebJarLibrariesBuildItem.java @@ -2,17 +2,22 @@ import java.util.List; -import io.quarkus.builder.item.SimpleBuildItem; - -public final class WebJarLibrariesBuildItem extends SimpleBuildItem { +import io.quarkus.builder.item.MultiBuildItem; +public final class WebJarLibrariesBuildItem extends MultiBuildItem { + private final String provider; private final List webJarLibraries; - public WebJarLibrariesBuildItem(List webJarLibraries) { + public WebJarLibrariesBuildItem(String provider, List webJarLibraries) { + this.provider = provider; this.webJarLibraries = webJarLibraries; } public List getWebJarLibraries() { - return webJarLibraries; + return this.webJarLibraries; + } + + public String getProvider() { + return this.provider; } } diff --git a/extensions/webjars-locator/deployment/src/main/java/io/quarkus/webjar/locator/deployment/devui/WebJarLocatorDevModeApiProcessor.java b/extensions/webjars-locator/deployment/src/main/java/io/quarkus/webjar/locator/deployment/devui/WebJarLocatorDevModeApiProcessor.java index ee17abebef60c..cc2c688e56ea3 100644 --- a/extensions/webjars-locator/deployment/src/main/java/io/quarkus/webjar/locator/deployment/devui/WebJarLocatorDevModeApiProcessor.java +++ b/extensions/webjars-locator/deployment/src/main/java/io/quarkus/webjar/locator/deployment/devui/WebJarLocatorDevModeApiProcessor.java @@ -30,7 +30,10 @@ public class WebJarLocatorDevModeApiProcessor { - private static final String WEBJARS_PREFIX = "META-INF/resources/webjars"; + private static final String PREFIX = "META-INF/resources/"; + private static final String WEBJARS_PATH = "webjars"; + private static final String MVNPM_PATH = "_static"; + private static final Logger log = Logger.getLogger(WebJarLocatorDevModeApiProcessor.class.getName()); @BuildStep(onlyIf = IsDevelopment.class) @@ -39,8 +42,18 @@ public void findWebjarsAssets( CurateOutcomeBuildItem curateOutcome, BuildProducer webJarLibrariesProducer) { + final List webJarLibraries = getLibraries(httpConfig, curateOutcome, WEBJARS_PATH); + webJarLibrariesProducer.produce(new WebJarLibrariesBuildItem("webjars", webJarLibraries)); + + final List mvnpmLibraries = getLibraries(httpConfig, curateOutcome, MVNPM_PATH); + webJarLibrariesProducer.produce(new WebJarLibrariesBuildItem("mvnpm", mvnpmLibraries)); + + } + + private List getLibraries(HttpBuildTimeConfig httpConfig, + CurateOutcomeBuildItem curateOutcome, String path) { final List webJarLibraries = new ArrayList<>(); - final List providers = QuarkusClassLoader.getElements(WEBJARS_PREFIX, false); + final List providers = QuarkusClassLoader.getElements(PREFIX + path, false); if (!providers.isEmpty()) { // Map of webjar artifact keys to class path elements final Map webJarKeys = providers.stream() @@ -51,19 +64,21 @@ public void findWebjarsAssets( // The root path of the application final String rootPath = httpConfig.rootPath; // The root path of the webjars - final String webjarRootPath = (rootPath.endsWith("/")) ? rootPath + "webjars/" : rootPath + "/webjars/"; + final String webjarRootPath = (rootPath.endsWith("/")) ? rootPath + path + "/" : rootPath + "/" + path + "/"; // For each packaged webjar dependency, create a WebJarLibrary object curateOutcome.getApplicationModel().getDependencies().stream() - .map(dep -> createWebJarLibrary(dep, webjarRootPath, webJarKeys)) + .map(dep -> createWebJarLibrary(dep, webjarRootPath, webJarKeys, path)) .filter(Objects::nonNull).forEach(webJarLibraries::add); } } - webJarLibrariesProducer.produce(new WebJarLibrariesBuildItem(webJarLibraries)); + return webJarLibraries; } - private WebJarLibrary createWebJarLibrary(ResolvedDependency dep, String webjarRootPath, - Map webJarKeys) { + private WebJarLibrary createWebJarLibrary(ResolvedDependency dep, + String webjarRootPath, + Map webJarKeys, + String path) { // If the dependency is not a runtime class path dependency, return null if (!dep.isRuntimeCp()) { return null; @@ -74,7 +89,7 @@ private WebJarLibrary createWebJarLibrary(ResolvedDependency dep, String webjarR } final WebJarLibrary webJarLibrary = new WebJarLibrary(provider.getDependencyKey().getArtifactId()); provider.apply(tree -> { - final Path webjarsDir = tree.getPath(WEBJARS_PREFIX); + final Path webjarsDir = tree.getPath(PREFIX + path); final Path nameDir; try (Stream webjarsDirPaths = Files.list(webjarsDir)) { nameDir = webjarsDirPaths.filter(Files::isDirectory).findFirst().orElseThrow(() -> new IOException( diff --git a/extensions/webjars-locator/deployment/src/main/java/io/quarkus/webjar/locator/deployment/devui/WebJarLocatorDevUIProcessor.java b/extensions/webjars-locator/deployment/src/main/java/io/quarkus/webjar/locator/deployment/devui/WebJarLocatorDevUIProcessor.java index 9fb2440b5bdaa..c13cf38105351 100644 --- a/extensions/webjars-locator/deployment/src/main/java/io/quarkus/webjar/locator/deployment/devui/WebJarLocatorDevUIProcessor.java +++ b/extensions/webjars-locator/deployment/src/main/java/io/quarkus/webjar/locator/deployment/devui/WebJarLocatorDevUIProcessor.java @@ -1,22 +1,29 @@ package io.quarkus.webjar.locator.deployment.devui; +import java.util.ArrayList; import java.util.List; +import java.util.Optional; import io.quarkus.deployment.IsDevelopment; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.devui.spi.page.CardPageBuildItem; import io.quarkus.devui.spi.page.Page; +import io.quarkus.webjar.locator.deployment.ImportMapBuildItem; public class WebJarLocatorDevUIProcessor { @BuildStep(onlyIf = IsDevelopment.class) public void createPages(BuildProducer cardPageProducer, - WebJarLibrariesBuildItem webJarLibrariesBuildItem) { + List webJarLibrariesBuildItems, + Optional importMapBuildItem) { - CardPageBuildItem cardPageBuildItem = new CardPageBuildItem(); - List webJarLibraries = webJarLibrariesBuildItem.getWebJarLibraries(); + List webJarLibraries = new ArrayList<>(); + for (WebJarLibrariesBuildItem webJarLibrariesBuildItem : webJarLibrariesBuildItems) { + webJarLibraries.addAll(webJarLibrariesBuildItem.getWebJarLibraries()); + } + CardPageBuildItem cardPageBuildItem = new CardPageBuildItem(); if (!webJarLibraries.isEmpty()) { // WebJar Libraries cardPageBuildItem.addBuildTimeData("webJarLibraries", webJarLibraries); @@ -24,9 +31,21 @@ public void createPages(BuildProducer cardPageProducer, // WebJar Asset List cardPageBuildItem.addPage(Page.webComponentPageBuilder() .componentLink("qwc-webjar-locator-webjar-libraries.js") - .title("WebJar Libraries") + .title("Web libraries") .icon("font-awesome-solid:folder-tree") .staticLabel(String.valueOf(webJarLibraries.size()))); + + if (importMapBuildItem.isPresent()) { + cardPageBuildItem.addBuildTimeData("importMap", importMapBuildItem.get().getImportMap()); + + // ImportMap + cardPageBuildItem.addPage(Page.webComponentPageBuilder() + .componentLink("qwc-webjar-locator-importmap.js") + .title("Import Map") + .icon("font-awesome-solid:diagram-project")); + + } + } cardPageProducer.produce(cardPageBuildItem); diff --git a/extensions/webjars-locator/deployment/src/main/resources/dev-ui/qwc-webjar-locator-importmap.js b/extensions/webjars-locator/deployment/src/main/resources/dev-ui/qwc-webjar-locator-importmap.js new file mode 100644 index 0000000000000..fa732bb3ff901 --- /dev/null +++ b/extensions/webjars-locator/deployment/src/main/resources/dev-ui/qwc-webjar-locator-importmap.js @@ -0,0 +1,48 @@ +import {LitElement, html, css} from 'lit'; +import {importMap} from 'build-time-data'; + +import '@quarkus-webcomponents/codeblock'; + +export class QwcWebjarLocatorImportmap extends LitElement { + + static styles = css` + :host{ + display: flex; + flex-direction: column; + gap: 15px; + padding: 10px; + height: 100%; + } + `; + + static properties = { + _importMap: {type: String} + }; + + constructor() { + super(); + this._importMap = importMap; + } + + render() { + return html` + To use this in your app, add this to the head of your main html: +

+ + +
+ + Here is the generated import map: +
+ + +
+ `; + } +} + +customElements.define('qwc-webjar-locator-importmap', QwcWebjarLocatorImportmap) \ No newline at end of file diff --git a/extensions/webjars-locator/deployment/src/test/java/io/quarkus/webjar/locator/test/ImportMapTest.java b/extensions/webjars-locator/deployment/src/test/java/io/quarkus/webjar/locator/test/ImportMapTest.java new file mode 100644 index 0000000000000..02f01cb64b614 --- /dev/null +++ b/extensions/webjars-locator/deployment/src/test/java/io/quarkus/webjar/locator/test/ImportMapTest.java @@ -0,0 +1,34 @@ +package io.quarkus.webjar.locator.test; + +import static org.hamcrest.Matchers.containsString; + +import java.util.List; + +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.maven.dependency.Dependency; +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; + +public class ImportMapTest extends WebJarLocatorTestSupport { + private static final String META_INF_RESOURCES = "META-INF/resources/"; + + @RegisterExtension + static QuarkusUnitTest runner = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addAsResource(new StringAsset("Hello!"), META_INF_RESOURCES + "/index.html") + .addAsResource(new StringAsset("Test"), META_INF_RESOURCES + "/some/path/test.txt")) + .setForcedDependencies(List.of( + Dependency.of("org.mvnpm", "bootstrap", BOOTSTRAP_VERSION))); + + @Test + public void test() { + // Test normal files + RestAssured.get("/_importmap/generated_importmap.js").then() + .statusCode(200) + .body(containsString("\"bootstrap/\" : \"/_static/bootstrap/" + BOOTSTRAP_VERSION + "/dist/\"")); + + } +} diff --git a/extensions/webjars-locator/deployment/src/test/java/io/quarkus/webjar/locator/test/WebJarLocatorDevModeTest.java b/extensions/webjars-locator/deployment/src/test/java/io/quarkus/webjar/locator/test/WebJarLocatorDevModeTest.java index 7949be6326818..fad3ee7262948 100644 --- a/extensions/webjars-locator/deployment/src/test/java/io/quarkus/webjar/locator/test/WebJarLocatorDevModeTest.java +++ b/extensions/webjars-locator/deployment/src/test/java/io/quarkus/webjar/locator/test/WebJarLocatorDevModeTest.java @@ -46,12 +46,16 @@ public void testDevMode() { .statusCode(200); RestAssured.get("/webjars/momentjs/min/moment.min.js").then() .statusCode(200); + RestAssured.get("/_static/bootstrap/dist/js/bootstrap.min.js").then() + .statusCode(200); // Test using version in url of existing Web Jar RestAssured.get("/webjars/jquery-ui/" + JQUERY_UI_VERSION + "/jquery-ui.min.js").then() .statusCode(200); RestAssured.get("/webjars/momentjs/" + MOMENTJS_VERSION + "/min/moment.min.js").then() .statusCode(200); + RestAssured.get("/_static/bootstrap/" + BOOTSTRAP_VERSION + "/dist/js/bootstrap.min.js").then() + .statusCode(200); // Test non-existing Web Jar RestAssured.get("/webjars/bootstrap/js/bootstrap.min.js").then() @@ -60,6 +64,8 @@ public void testDevMode() { .statusCode(404); RestAssured.get("/webjars/momentjs/2.25.0/min/moment.min.js").then() .statusCode(404); + RestAssured.get("/_static/foundation-sites/6.8.1/dist/js/foundation.esm.js").then() + .statusCode(404); // Test webjar that does not have a version in the jar path RestAssured.get("/webjars/dcjs/dc.min.js").then() @@ -93,12 +99,16 @@ public void testDevMode() { .statusCode(200); RestAssured.get("/webjars/momentjs/min/moment.min.js").then() .statusCode(200); + RestAssured.get("/_static/bootstrap/dist/js/bootstrap.min.js").then() + .statusCode(200); // Test using version in url of existing Web Jar RestAssured.get("/webjars/jquery-ui/" + JQUERY_UI_VERSION + "/jquery-ui.min.js").then() .statusCode(200); RestAssured.get("/webjars/momentjs/" + MOMENTJS_VERSION + "/min/moment.min.js").then() .statusCode(200); + RestAssured.get("/_static/bootstrap/" + BOOTSTRAP_VERSION + "/dist/js/bootstrap.min.js").then() + .statusCode(200); // Test non-existing Web Jar RestAssured.get("/webjars/bootstrap/js/bootstrap.min.js").then() @@ -107,5 +117,7 @@ public void testDevMode() { .statusCode(404); RestAssured.get("/webjars/momentjs/2.25.0/min/moment.min.js").then() .statusCode(404); + RestAssured.get("/_static/foundation-sites/6.8.1/dist/js/foundation.esm.js").then() + .statusCode(404); } } diff --git a/extensions/webjars-locator/deployment/src/test/java/io/quarkus/webjar/locator/test/WebJarLocatorRootPathTest.java b/extensions/webjars-locator/deployment/src/test/java/io/quarkus/webjar/locator/test/WebJarLocatorRootPathTest.java index 26db27c282959..6efa49606eb43 100644 --- a/extensions/webjars-locator/deployment/src/test/java/io/quarkus/webjar/locator/test/WebJarLocatorRootPathTest.java +++ b/extensions/webjars-locator/deployment/src/test/java/io/quarkus/webjar/locator/test/WebJarLocatorRootPathTest.java @@ -21,8 +21,10 @@ public class WebJarLocatorRootPathTest extends WebJarLocatorTestSupport { .addAsResource(new StringAsset("Hello!"), META_INF_RESOURCES + "index.html") .addAsResource(new StringAsset("Test"), META_INF_RESOURCES + "some/path/test.txt")) .overrideConfigKey("quarkus.http.root-path", "/app") - .setForcedDependencies(List.of(Dependency.of("org.webjars", "jquery-ui", JQUERY_UI_VERSION), - Dependency.of("org.webjars", "momentjs", MOMENTJS_VERSION))); + .setForcedDependencies(List.of( + Dependency.of("org.webjars", "jquery-ui", JQUERY_UI_VERSION), + Dependency.of("org.webjars", "momentjs", MOMENTJS_VERSION), + Dependency.of("org.mvnpm", "bootstrap", BOOTSTRAP_VERSION))); @Test public void test() { @@ -43,12 +45,16 @@ public void test() { .statusCode(200); RestAssured.get("/webjars/momentjs/min/moment.min.js").then() .statusCode(200); + RestAssured.get("/_static/bootstrap/dist/js/bootstrap.min.js").then() + .statusCode(200); // Test using version in url of existing Web Jar RestAssured.get("/webjars/jquery-ui/" + JQUERY_UI_VERSION + "/jquery-ui.min.js").then() .statusCode(200); RestAssured.get("/webjars/momentjs/" + MOMENTJS_VERSION + "/min/moment.min.js").then() .statusCode(200); + RestAssured.get("/_static/bootstrap/" + BOOTSTRAP_VERSION + "/dist/js/bootstrap.min.js").then() + .statusCode(200); // Test non-existing Web Jar RestAssured.get("/webjars/bootstrap/js/bootstrap.min.js").then() @@ -57,6 +63,8 @@ public void test() { .statusCode(404); RestAssured.get("/webjars/momentjs/2.25.0/min/moment.min.js").then() .statusCode(404); + RestAssured.get("/_static/foundation-sites/6.8.1/dist/js/foundation.esm.js").then() + .statusCode(404); // Test webjar that does not have a version in the jar path RestAssured.get("/webjars/dcjs/dc.min.js").then() diff --git a/extensions/webjars-locator/deployment/src/test/java/io/quarkus/webjar/locator/test/WebJarLocatorTest.java b/extensions/webjars-locator/deployment/src/test/java/io/quarkus/webjar/locator/test/WebJarLocatorTest.java index 7453c71b3b306..715a2d042f187 100644 --- a/extensions/webjars-locator/deployment/src/test/java/io/quarkus/webjar/locator/test/WebJarLocatorTest.java +++ b/extensions/webjars-locator/deployment/src/test/java/io/quarkus/webjar/locator/test/WebJarLocatorTest.java @@ -22,7 +22,8 @@ public class WebJarLocatorTest extends WebJarLocatorTestSupport { .addAsResource(new StringAsset("Test"), META_INF_RESOURCES + "/some/path/test.txt")) .setForcedDependencies(List.of( Dependency.of("org.webjars", "jquery-ui", JQUERY_UI_VERSION), - Dependency.of("org.webjars", "momentjs", MOMENTJS_VERSION))); + Dependency.of("org.webjars", "momentjs", MOMENTJS_VERSION), + Dependency.of("org.mvnpm", "bootstrap", BOOTSTRAP_VERSION))); @Test public void test() { @@ -44,12 +45,16 @@ public void test() { .statusCode(200); RestAssured.get("/webjars/momentjs/min/moment.min.js").then() .statusCode(200); + RestAssured.get("/_static/bootstrap/dist/js/bootstrap.min.js").then() + .statusCode(200); // Test using version in url of existing Web Jar RestAssured.get("/webjars/jquery-ui/" + JQUERY_UI_VERSION + "/jquery-ui.min.js").then() .statusCode(200); RestAssured.get("/webjars/momentjs/" + MOMENTJS_VERSION + "/min/moment.min.js").then() .statusCode(200); + RestAssured.get("/_static/bootstrap/" + BOOTSTRAP_VERSION + "/dist/js/bootstrap.min.js").then() + .statusCode(200); // Test non-existing Web Jar RestAssured.get("/webjars/bootstrap/js/bootstrap.min.js").then() @@ -58,6 +63,8 @@ public void test() { .statusCode(404); RestAssured.get("/webjars/momentjs/2.25.0/min/moment.min.js").then() .statusCode(404); + RestAssured.get("/_static/foundation-sites/6.8.1/dist/js/foundation.esm.js").then() + .statusCode(404); // Test webjar that does not have a version in the jar path RestAssured.get("/webjars/dcjs/dc.min.js").then() diff --git a/extensions/webjars-locator/deployment/src/test/java/io/quarkus/webjar/locator/test/WebJarLocatorTestSupport.java b/extensions/webjars-locator/deployment/src/test/java/io/quarkus/webjar/locator/test/WebJarLocatorTestSupport.java index 83e84572f0b8e..0afe0cfae437c 100644 --- a/extensions/webjars-locator/deployment/src/test/java/io/quarkus/webjar/locator/test/WebJarLocatorTestSupport.java +++ b/extensions/webjars-locator/deployment/src/test/java/io/quarkus/webjar/locator/test/WebJarLocatorTestSupport.java @@ -4,4 +4,5 @@ class WebJarLocatorTestSupport { static final String JQUERY_UI_VERSION = System.getProperty("webjar.jquery-ui.version"); static final String MOMENTJS_VERSION = System.getProperty("webjar.momentjs.version"); + static final String BOOTSTRAP_VERSION = System.getProperty("mvnpm.bootstrap.version"); } diff --git a/extensions/webjars-locator/runtime/src/main/java/io/quarkus/webjar/locator/runtime/WebJarLocatorRecorder.java b/extensions/webjars-locator/runtime/src/main/java/io/quarkus/webjar/locator/runtime/WebJarLocatorRecorder.java index 813f4e920f830..55f22be5cf44a 100644 --- a/extensions/webjars-locator/runtime/src/main/java/io/quarkus/webjar/locator/runtime/WebJarLocatorRecorder.java +++ b/extensions/webjars-locator/runtime/src/main/java/io/quarkus/webjar/locator/runtime/WebJarLocatorRecorder.java @@ -4,6 +4,8 @@ import io.quarkus.runtime.annotations.Recorder; import io.vertx.core.Handler; +import io.vertx.core.http.HttpHeaders; +import io.vertx.core.http.HttpServerResponse; import io.vertx.ext.web.RoutingContext; @Recorder @@ -42,4 +44,27 @@ public Handler getHandler(String webjarsRootUrl, Map getImportMapHandler(String expectedPath, String importmap) { + return new Handler() { + @Override + public void handle(RoutingContext event) { + String path = event.normalizedPath(); + if (path.equals(expectedPath)) { + HttpServerResponse response = event.response(); + response.headers().set(HttpHeaders.CONTENT_TYPE, "text/javascript"); + response.end(JAVASCRIPT_CODE.formatted(importmap)); + } else { + // should not happen if route is set up correctly + event.next(); + } + } + }; + } + + private static final String JAVASCRIPT_CODE = """ + const im = document.createElement('script'); + im.type = 'importmap'; + im.textContent = JSON.stringify(%s); + document.currentScript.after(im); + """; } diff --git a/extensions/websockets-next/server/deployment/src/test/java/io/quarkus/websockets/next/test/errors/WriteErrorClosedConnectionTest.java b/extensions/websockets-next/server/deployment/src/test/java/io/quarkus/websockets/next/test/errors/WriteErrorClosedConnectionTest.java new file mode 100644 index 0000000000000..ec46f533c4564 --- /dev/null +++ b/extensions/websockets-next/server/deployment/src/test/java/io/quarkus/websockets/next/test/errors/WriteErrorClosedConnectionTest.java @@ -0,0 +1,70 @@ +package io.quarkus.websockets.next.test.errors; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.net.URI; +import java.time.Duration; +import java.util.concurrent.atomic.AtomicBoolean; + +import jakarta.inject.Inject; + +import org.awaitility.Awaitility; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.test.common.http.TestHTTPResource; +import io.quarkus.websockets.next.OnBinaryMessage; +import io.quarkus.websockets.next.OnError; +import io.quarkus.websockets.next.WebSocket; +import io.quarkus.websockets.next.WebSocketConnection; +import io.quarkus.websockets.next.test.utils.WSClient; +import io.smallrye.mutiny.Uni; +import io.vertx.core.Vertx; +import io.vertx.core.buffer.Buffer; + +public class WriteErrorClosedConnectionTest { + + @RegisterExtension + public static final QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot(root -> { + root.addClasses(Echo.class, WSClient.class); + }); + + @Inject + Vertx vertx; + + @TestHTTPResource("echo") + URI testUri; + + @Test + void testError() { + WSClient client = WSClient.create(vertx).connect(testUri); + client.sendAndAwait(Buffer.buffer("1")); + Awaitility.await().atMost(Duration.ofSeconds(5)).until(() -> client.isClosed()); + assertTrue(Echo.ERROR_HANDLER_CALLED.get()); + } + + @WebSocket(path = "/echo") + public static class Echo { + + static final AtomicBoolean ERROR_HANDLER_CALLED = new AtomicBoolean(); + + @OnBinaryMessage + Uni process(Buffer message, WebSocketConnection connection) { + // This should result in a failure because the connection is closed + // but we still try to write a binary message + return connection.close().replaceWith(message); + } + + @OnError + void runtimeProblem(Throwable t, WebSocketConnection connection) { + if (connection.isOpen()) { + throw new IllegalStateException(); + } + ERROR_HANDLER_CALLED.set(true); + } + + } + +} diff --git a/extensions/websockets-next/server/deployment/src/test/java/io/quarkus/websockets/next/test/maxmessagesize/MaxMessageSizeTest.java b/extensions/websockets-next/server/deployment/src/test/java/io/quarkus/websockets/next/test/maxmessagesize/MaxMessageSizeTest.java new file mode 100644 index 0000000000000..2ffe0778d69f7 --- /dev/null +++ b/extensions/websockets-next/server/deployment/src/test/java/io/quarkus/websockets/next/test/maxmessagesize/MaxMessageSizeTest.java @@ -0,0 +1,63 @@ +package io.quarkus.websockets.next.test.maxmessagesize; + +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.net.URI; +import java.util.concurrent.atomic.AtomicBoolean; + +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.test.common.http.TestHTTPResource; +import io.quarkus.websockets.next.OnError; +import io.quarkus.websockets.next.OnTextMessage; +import io.quarkus.websockets.next.WebSocket; +import io.quarkus.websockets.next.test.utils.WSClient; +import io.vertx.core.Vertx; + +public class MaxMessageSizeTest { + + @RegisterExtension + public static final QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot(root -> { + root.addClasses(Echo.class, WSClient.class); + }).overrideConfigKey("quarkus.websockets-next.max-message-size", "10"); + + @Inject + Vertx vertx; + + @TestHTTPResource("/echo") + URI echoUri; + + @Test + void testMaxMessageSize() { + WSClient client = WSClient.create(vertx).connect(echoUri); + String msg = "foo".repeat(10); + String reply = client.sendAndAwaitReply(msg).toString(); + assertNotEquals(msg, reply); + assertTrue(Echo.ISE_THROWN.get()); + } + + @WebSocket(path = "/echo") + public static class Echo { + + static final AtomicBoolean ISE_THROWN = new AtomicBoolean(); + + @OnTextMessage + String process(String message) { + return message; + } + + @OnError + String onError(IllegalStateException ise) { + ISE_THROWN.set(true); + return ise.getMessage(); + } + + } + +} diff --git a/extensions/websockets-next/server/deployment/src/test/java/io/quarkus/websockets/next/test/subprotocol/SubprotocolNotAvailableTest.java b/extensions/websockets-next/server/deployment/src/test/java/io/quarkus/websockets/next/test/subprotocol/SubprotocolNotAvailableTest.java index 9ef02fe878268..9a79b8d12fda0 100644 --- a/extensions/websockets-next/server/deployment/src/test/java/io/quarkus/websockets/next/test/subprotocol/SubprotocolNotAvailableTest.java +++ b/extensions/websockets-next/server/deployment/src/test/java/io/quarkus/websockets/next/test/subprotocol/SubprotocolNotAvailableTest.java @@ -5,11 +5,16 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import java.net.URI; +import java.time.Duration; import java.util.concurrent.CompletionException; import java.util.concurrent.atomic.AtomicBoolean; +import jakarta.enterprise.context.Destroyed; +import jakarta.enterprise.context.SessionScoped; +import jakarta.enterprise.event.Observes; import jakarta.inject.Inject; +import org.awaitility.Awaitility; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -43,18 +48,26 @@ void testConnectionRejected() { Throwable cause = e.getCause(); assertTrue(cause instanceof WebSocketClientHandshakeException); assertFalse(Endpoint.OPEN_CALLED.get()); + // Wait until the CDI singleton context is destroyed + // Otherwise the test app is shut down before the WebSocketSessionContext is ended properly + Awaitility.await().atMost(Duration.ofSeconds(5)).until(() -> Endpoint.SESSION_CONTEXT_DESTROYED.get()); } @WebSocket(path = "/endpoint") public static class Endpoint { static final AtomicBoolean OPEN_CALLED = new AtomicBoolean(); + static final AtomicBoolean SESSION_CONTEXT_DESTROYED = new AtomicBoolean(); @OnOpen void open() { OPEN_CALLED.set(true); } + static void sessionContextDestroyed(@Observes @Destroyed(SessionScoped.class) Object event) { + SESSION_CONTEXT_DESTROYED.set(true); + } + } } diff --git a/extensions/websockets-next/server/runtime/src/main/java/io/quarkus/websockets/next/WebSocketsRuntimeConfig.java b/extensions/websockets-next/server/runtime/src/main/java/io/quarkus/websockets/next/WebSocketsRuntimeConfig.java index ff38df72391d6..e1c76dc33dde3 100644 --- a/extensions/websockets-next/server/runtime/src/main/java/io/quarkus/websockets/next/WebSocketsRuntimeConfig.java +++ b/extensions/websockets-next/server/runtime/src/main/java/io/quarkus/websockets/next/WebSocketsRuntimeConfig.java @@ -1,12 +1,14 @@ package io.quarkus.websockets.next; -import java.time.Duration; import java.util.List; import java.util.Optional; +import java.util.OptionalInt; import io.quarkus.runtime.annotations.ConfigPhase; import io.quarkus.runtime.annotations.ConfigRoot; import io.smallrye.config.ConfigMapping; +import io.smallrye.config.WithDefault; +import io.vertx.core.http.HttpServerOptions; @ConfigMapping(prefix = "quarkus.websockets-next") @ConfigRoot(phase = ConfigPhase.RUN_TIME) @@ -14,16 +16,27 @@ public interface WebSocketsRuntimeConfig { /** * See The WebSocket Protocol - * - * @return the supported subprotocols */ Optional> supportedSubprotocols(); /** - * TODO Not implemented yet. - * - * The default timeout to complete processing of a message. + * Compression Extensions for WebSocket are supported by default. + *

+ * See also RFC 7692 */ - Optional timeout(); + @WithDefault("true") + boolean perMessageCompressionSupported(); + + /** + * The compression level must be a value between 0 and 9. The default value is + * {@value HttpServerOptions#DEFAULT_WEBSOCKET_COMPRESSION_LEVEL}. + */ + OptionalInt compressionLevel(); + + /** + * The maximum size of a message in bytes. The default values is + * {@value HttpServerOptions#DEFAULT_MAX_WEBSOCKET_MESSAGE_SIZE}. + */ + OptionalInt maxMessageSize(); } diff --git a/extensions/websockets-next/server/runtime/src/main/java/io/quarkus/websockets/next/runtime/ContextSupport.java b/extensions/websockets-next/server/runtime/src/main/java/io/quarkus/websockets/next/runtime/ContextSupport.java index 6edb66693f906..9e4b7a4e81525 100644 --- a/extensions/websockets-next/server/runtime/src/main/java/io/quarkus/websockets/next/runtime/ContextSupport.java +++ b/extensions/websockets-next/server/runtime/src/main/java/io/quarkus/websockets/next/runtime/ContextSupport.java @@ -69,7 +69,7 @@ void endSession() { } ContextState currentRequestContextState() { - return requestContext.getState(); + return requestContext.getStateIfActive(); } static Context createNewDuplicatedContext(Context context, WebSocketConnection connection) { diff --git a/extensions/websockets-next/server/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketHttpServerOptionsCustomizer.java b/extensions/websockets-next/server/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketHttpServerOptionsCustomizer.java index 5018b1aee2b35..5233fd4a1cc34 100644 --- a/extensions/websockets-next/server/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketHttpServerOptionsCustomizer.java +++ b/extensions/websockets-next/server/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketHttpServerOptionsCustomizer.java @@ -17,12 +17,23 @@ public class WebSocketHttpServerOptionsCustomizer implements HttpServerOptionsCu @Override public void customizeHttpServer(HttpServerOptions options) { - config.supportedSubprotocols().orElse(List.of()).forEach(options::addWebSocketSubProtocol); + customize(options); } @Override public void customizeHttpsServer(HttpServerOptions options) { + customize(options); + } + + private void customize(HttpServerOptions options) { config.supportedSubprotocols().orElse(List.of()).forEach(options::addWebSocketSubProtocol); + options.setPerMessageWebSocketCompressionSupported(config.perMessageCompressionSupported()); + if (config.compressionLevel().isPresent()) { + options.setWebSocketCompressionLevel(config.compressionLevel().getAsInt()); + } + if (config.maxMessageSize().isPresent()) { + options.setMaxWebSocketMessageSize(config.maxMessageSize().getAsInt()); + } } } diff --git a/extensions/websockets-next/server/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketServerRecorder.java b/extensions/websockets-next/server/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketServerRecorder.java index a0b98d13b1209..c53d15645b01d 100644 --- a/extensions/websockets-next/server/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketServerRecorder.java +++ b/extensions/websockets-next/server/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketServerRecorder.java @@ -234,6 +234,21 @@ public void handle(Void event) { }); } }); + + ws.exceptionHandler(new Handler() { + @Override + public void handle(Throwable t) { + ContextSupport.createNewDuplicatedContext(context, connection).runOnContext(new Handler() { + @Override + public void handle(Void event) { + endpoint.doOnError(t).subscribe().with( + v -> LOG.debugf("Error [%s] processed: %s", t.getClass(), connection), + t -> LOG.errorf(t, "Unhandled error occured: %s", t.toString(), + connection)); + } + }); + } + }); }); } }; diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanGenerator.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanGenerator.java index c9df9d7965a50..0e94f7df30f00 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanGenerator.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanGenerator.java @@ -1305,26 +1305,45 @@ private ResultHandle newInstanceHandle(BeanInfo bean, ClassCreator beanCreator, if (Modifier.isPrivate(constructor.flags())) { privateMembers.add(isApplicationClass, String.format("Bean constructor %s on %s", constructor, constructor.declaringClass().name())); - ResultHandle paramTypesArray = creator.newArray(Class.class, creator.load(providerHandles.size())); - ResultHandle argsArray = creator.newArray(Object.class, creator.load(providerHandles.size())); + int params = providerHandles.size(); + if (DecoratorGenerator.isAbstractDecoratorImpl(bean, providerTypeName)) { + params++; + } + ResultHandle paramTypesArray = creator.newArray(Class.class, creator.load(params)); + ResultHandle argsArray = creator.newArray(Object.class, creator.load(params)); for (int i = 0; i < injectionPoints.size(); i++) { creator.writeArrayValue(paramTypesArray, i, creator.loadClass(injectionPoints.get(i).getType().name().toString())); creator.writeArrayValue(argsArray, i, providerHandles.get(i)); } + if (DecoratorGenerator.isAbstractDecoratorImpl(bean, providerTypeName)) { + creator.writeArrayValue(paramTypesArray, params - 1, creator.loadClass(CreationalContext.class)); + creator.writeArrayValue(argsArray, params - 1, createMethod.getMethodParam(0)); + } registration.registerMethod(constructor); return creator.invokeStaticMethod(MethodDescriptors.REFLECTIONS_NEW_INSTANCE, creator.loadClass(constructor.declaringClass().name().toString()), paramTypesArray, argsArray); } else { // new SimpleBean(foo) - String[] paramTypes = new String[injectionPoints.size()]; + int params = injectionPoints.size(); + if (DecoratorGenerator.isAbstractDecoratorImpl(bean, providerTypeName)) { + params++; + } + String[] paramTypes = new String[params]; for (ListIterator iterator = injectionPoints.listIterator(); iterator.hasNext();) { InjectionPointInfo injectionPoint = iterator.next(); paramTypes[iterator.previousIndex()] = DescriptorUtils.typeToString(injectionPoint.getType()); } - return creator.newInstance(MethodDescriptor.ofConstructor(providerTypeName, paramTypes), - providerHandles.toArray(new ResultHandle[0])); + ResultHandle[] args = new ResultHandle[params]; + for (int i = 0; i < providerHandles.size(); i++) { + args[i] = providerHandles.get(i); + } + if (DecoratorGenerator.isAbstractDecoratorImpl(bean, providerTypeName)) { + paramTypes[params - 1] = CreationalContext.class.getName(); + args[params - 1] = createMethod.getMethodParam(0); + } + return creator.newInstance(MethodDescriptor.ofConstructor(providerTypeName, paramTypes), args); } } else { MethodInfo noArgsConstructor = bean.getTarget().get().asClass().method(Methods.INIT); @@ -1332,16 +1351,31 @@ private ResultHandle newInstanceHandle(BeanInfo bean, ClassCreator beanCreator, privateMembers.add(isApplicationClass, String.format("Bean constructor %s on %s", noArgsConstructor, noArgsConstructor.declaringClass().name())); - ResultHandle paramTypesArray = creator.newArray(Class.class, creator.load(0)); - ResultHandle argsArray = creator.newArray(Object.class, creator.load(0)); + ResultHandle paramTypesArray; + ResultHandle argsArray; + if (DecoratorGenerator.isAbstractDecoratorImpl(bean, providerTypeName)) { + paramTypesArray = creator.newArray(Class.class, 1); + argsArray = creator.newArray(Object.class, 1); + creator.writeArrayValue(paramTypesArray, 0, creator.loadClass(CreationalContext.class)); + creator.writeArrayValue(argsArray, 0, createMethod.getMethodParam(0)); + } else { + paramTypesArray = creator.newArray(Class.class, 0); + argsArray = creator.newArray(Object.class, 0); + } registration.registerMethod(noArgsConstructor); return creator.invokeStaticMethod(MethodDescriptors.REFLECTIONS_NEW_INSTANCE, creator.loadClass(noArgsConstructor.declaringClass().name().toString()), paramTypesArray, argsArray); } else { - // new SimpleBean() - return creator.newInstance(MethodDescriptor.ofConstructor(providerTypeName)); + if (DecoratorGenerator.isAbstractDecoratorImpl(bean, providerTypeName)) { + // new SimpleDecorator_Impl(ctx) + return creator.newInstance(MethodDescriptor.ofConstructor(providerTypeName, CreationalContext.class), + createMethod.getMethodParam(0)); + } else { + // new SimpleBean() + return creator.newInstance(MethodDescriptor.ofConstructor(providerTypeName)); + } } } } 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 48c92af011388..97a4bbd430776 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 @@ -5,6 +5,7 @@ import static org.objectweb.asm.Opcodes.ACC_PUBLIC; import java.lang.reflect.Type; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -16,6 +17,8 @@ import java.util.function.Predicate; import java.util.function.Supplier; +import jakarta.enterprise.context.spi.CreationalContext; + import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.ClassInfo; import org.jboss.jandex.DotName; @@ -157,6 +160,10 @@ static String createBaseName(ClassInfo decoratorClass) { return baseName; } + static boolean isAbstractDecoratorImpl(BeanInfo bean, String providerTypeName) { + return bean.isDecorator() && ((DecoratorInfo) bean).isAbstract() && providerTypeName.endsWith(ABSTRACT_IMPL_SUFFIX); + } + private String generateDecoratorImplementation(String baseName, String targetPackage, DecoratorInfo decorator, ClassInfo decoratorClass, ClassOutput classOutput) { // MyDecorator_Impl @@ -171,8 +178,13 @@ private String generateDecoratorImplementation(String baseName, String targetPac // Constructor MethodInfo decoratorConstructor = decoratorClass.firstMethod(Methods.INIT); + List decoratorConstructorParams = new ArrayList<>(); + for (org.jboss.jandex.Type parameterType : decoratorConstructor.parameterTypes()) { + decoratorConstructorParams.add(parameterType.name().toString()); + } + decoratorConstructorParams.add(CreationalContext.class.getName()); MethodCreator constructor = decoratorImplCreator.getMethodCreator(Methods.INIT, "V", - decoratorConstructor.parameterTypes().stream().map(it -> it.name().toString()).toArray()); + decoratorConstructorParams.toArray(new Object[0])); ResultHandle[] constructorArgs = new ResultHandle[decoratorConstructor.parametersCount()]; for (int i = 0; i < decoratorConstructor.parametersCount(); i++) { constructorArgs[i] = constructor.getMethodParam(i); @@ -181,7 +193,8 @@ private String generateDecoratorImplementation(String baseName, String targetPac constructor.invokeSpecialMethod(decoratorConstructor, constructor.getThis(), constructorArgs); // Set the delegate field constructor.writeInstanceField(delegateField.getFieldDescriptor(), constructor.getThis(), - constructor.invokeStaticMethod(MethodDescriptors.DECORATOR_DELEGATE_PROVIDER_GET)); + constructor.invokeStaticMethod(MethodDescriptors.DECORATOR_DELEGATE_PROVIDER_GET, + constructor.getMethodParam(decoratorConstructor.parametersCount()))); constructor.returnValue(null); // Find non-decorated methods from all decorated types 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 79b2f7af860b2..ebccb00134a45 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 @@ -287,14 +287,11 @@ public final class MethodDescriptors { public static final MethodDescriptor CLIENT_PROXIES_GET_DELEGATE = MethodDescriptor.ofMethod(ClientProxies.class, "getDelegate", Object.class, InjectableBean.class); - public static final MethodDescriptor DECORATOR_DELEGATE_PROVIDER_SET = MethodDescriptor - .ofMethod(DecoratorDelegateProvider.class, "set", Object.class, Object.class); + public static final MethodDescriptor DECORATOR_DELEGATE_PROVIDER_GET = MethodDescriptor.ofMethod( + DecoratorDelegateProvider.class, "getCurrent", Object.class, CreationalContext.class); - public static final MethodDescriptor DECORATOR_DELEGATE_PROVIDER_UNSET = MethodDescriptor - .ofMethod(DecoratorDelegateProvider.class, "unset", void.class); - - public static final MethodDescriptor DECORATOR_DELEGATE_PROVIDER_GET = MethodDescriptor - .ofMethod(DecoratorDelegateProvider.class, "get", Object.class); + public static final MethodDescriptor DECORATOR_DELEGATE_PROVIDER_SET = MethodDescriptor.ofMethod( + DecoratorDelegateProvider.class, "setCurrent", Object.class, CreationalContext.class, Object.class); public static final MethodDescriptor INSTANCES_LIST_OF = MethodDescriptor .ofMethod(Instances.class, "listOf", List.class, InjectableBean.class, Type.class, Type.class, diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/SubclassGenerator.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/SubclassGenerator.java index 1574f72bc58be..04b8bc44075ef 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/SubclassGenerator.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/SubclassGenerator.java @@ -817,12 +817,14 @@ && isDecorated(decoratedMethodDescriptors, methodDescriptor, resolvedMethodDescr } ResultHandle delegateSubclassInstance = subclassConstructor.newInstance(MethodDescriptor.ofConstructor( delegateSubclass.getClassName(), constructorParameterTypes.toArray(new String[0])), paramHandles); - subclassConstructor.invokeStaticMethod(MethodDescriptors.DECORATOR_DELEGATE_PROVIDER_SET, delegateSubclassInstance); + ResultHandle prev = subclassConstructor.invokeStaticMethod( + MethodDescriptors.DECORATOR_DELEGATE_PROVIDER_SET, creationalContext, delegateSubclassInstance); ResultHandle decoratorInstance = subclassConstructor.invokeInterfaceMethod( MethodDescriptors.INJECTABLE_REF_PROVIDER_GET, constructorMethodParam, creationalContext); // And unset the delegate IP afterwards - subclassConstructor.invokeStaticMethod(MethodDescriptors.DECORATOR_DELEGATE_PROVIDER_UNSET); + subclassConstructor.invokeStaticMethod( + MethodDescriptors.DECORATOR_DELEGATE_PROVIDER_SET, creationalContext, prev); decoratorToResultHandle.put(decorator.getIdentifier(), decoratorInstance); diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/ArcContainerImpl.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/ArcContainerImpl.java index 94d6c17e59d12..25e8e75258b5d 100644 --- a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/ArcContainerImpl.java +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/ArcContainerImpl.java @@ -553,14 +553,14 @@ static InstanceHandle beanInstanceHandle(InjectableBean bean, Creation } InjectionPoint prev = null; if (resetCurrentInjectionPoint) { - prev = InjectionPointProvider.set(CurrentInjectionPointProvider.EMPTY); + prev = InjectionPointProvider.setCurrent(creationalContext, CurrentInjectionPointProvider.EMPTY); } try { return new EagerInstanceHandle<>(bean, bean.get(creationalContext), creationalContext, parentContext, destroyLogic); } finally { if (resetCurrentInjectionPoint) { - InjectionPointProvider.set(prev); + InjectionPointProvider.setCurrent(creationalContext, prev); } } } else { diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/BeanManagerImpl.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/BeanManagerImpl.java index 72d4853101f83..9527ac27ae1c5 100644 --- a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/BeanManagerImpl.java +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/BeanManagerImpl.java @@ -62,12 +62,12 @@ public Object getReference(Bean bean, Type beanType, CreationalContext ctx if (bean instanceof InjectableBean && ctx instanceof CreationalContextImpl) { // there's no actual injection point or an `Instance` object, // the "current" injection point must be `null` - InjectionPoint prev = InjectionPointProvider.set(null); + InjectionPoint prev = InjectionPointProvider.setCurrent(ctx, null); try { return ArcContainerImpl.beanInstanceHandle((InjectableBean) bean, (CreationalContextImpl) ctx, false, null, true).get(); } finally { - InjectionPointProvider.set(prev); + InjectionPointProvider.setCurrent(ctx, prev); } } throw new IllegalArgumentException( @@ -86,12 +86,12 @@ public Object getInjectableReference(InjectionPoint ij, CreationalContext ctx throw new UnsatisfiedResolutionException(); } InjectableBean bean = (InjectableBean) resolve(beans); - InjectionPoint prev = InjectionPointProvider.set(ij); + InjectionPoint prev = InjectionPointProvider.setCurrent(ctx, ij); try { return ArcContainerImpl.beanInstanceHandle(bean, (CreationalContextImpl) ctx, false, null, true).get(); } finally { - InjectionPointProvider.set(prev); + InjectionPointProvider.setCurrent(ctx, prev); } } throw new IllegalArgumentException( diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/CreationalContextImpl.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/CreationalContextImpl.java index 5de156b6170d5..43b6c612db3c8 100644 --- a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/CreationalContextImpl.java +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/CreationalContextImpl.java @@ -7,6 +7,7 @@ import jakarta.enterprise.context.spi.Contextual; import jakarta.enterprise.context.spi.CreationalContext; +import jakarta.enterprise.inject.spi.InjectionPoint; import io.quarkus.arc.InjectableBean; import io.quarkus.arc.InjectableReferenceProvider; @@ -24,6 +25,9 @@ public class CreationalContextImpl implements CreationalContext, Function< private final CreationalContextImpl parent; private List> dependentInstances; + private InjectionPoint currentInjectionPoint; + private Object currentDecoratorDelegate; + public CreationalContextImpl(Contextual contextual) { this(contextual, null); } @@ -129,4 +133,50 @@ public static void addDependencyToParent(InjectableBean bean, I instance, } } + static InjectionPoint getCurrentInjectionPoint(CreationalContext ctx) { + CreationalContextImpl instance = unwrap(ctx); + while (instance != null) { + synchronized (instance) { + InjectionPoint result = instance.currentInjectionPoint; + if (result != null) { + return result; + } + } + instance = instance.parent; + } + return null; + } + + static InjectionPoint setCurrentInjectionPoint(CreationalContext ctx, InjectionPoint injectionPoint) { + CreationalContextImpl instance = unwrap(ctx); + synchronized (instance) { + InjectionPoint previous = instance.currentInjectionPoint; + instance.currentInjectionPoint = injectionPoint; + return previous; + } + } + + static Object getCurrentDecoratorDelegate(CreationalContext ctx) { + CreationalContextImpl instance = unwrap(ctx); + while (instance != null) { + synchronized (instance) { + Object result = instance.currentDecoratorDelegate; + if (result != null) { + return result; + } + } + instance = instance.parent; + } + return null; + } + + static Object setCurrentDecoratorDelegate(CreationalContext ctx, Object decoratorDelegate) { + CreationalContextImpl instance = unwrap(ctx); + synchronized (instance) { + Object previous = instance.currentDecoratorDelegate; + instance.currentDecoratorDelegate = decoratorDelegate; + return previous; + } + } + } diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/CurrentInjectionPointProvider.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/CurrentInjectionPointProvider.java index 6b3f020fc9481..76a368267c472 100644 --- a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/CurrentInjectionPointProvider.java +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/CurrentInjectionPointProvider.java @@ -43,11 +43,11 @@ public CurrentInjectionPointProvider(InjectableBean bean, Supplier creationalContext) { - InjectionPoint prev = InjectionPointProvider.set(injectionPoint); + InjectionPoint prev = InjectionPointProvider.setCurrent(creationalContext, injectionPoint); try { return delegateSupplier.get().get(creationalContext); } finally { - InjectionPointProvider.set(prev); + InjectionPointProvider.setCurrent(creationalContext, prev); } } diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/DecoratorDelegateProvider.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/DecoratorDelegateProvider.java index d7ef2fede1a11..f548a6087b6a4 100644 --- a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/DecoratorDelegateProvider.java +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/DecoratorDelegateProvider.java @@ -6,40 +6,24 @@ public class DecoratorDelegateProvider implements InjectableReferenceProvider { - private static final ThreadLocal CURRENT = new ThreadLocal<>(); - @Override public Object get(CreationalContext creationalContext) { - return CURRENT.get(); + return getCurrent(creationalContext); + } + + public static Object getCurrent(CreationalContext ctx) { + return CreationalContextImpl.getCurrentDecoratorDelegate(ctx); } /** - * Set the current delegate for a non-null parameter, remove the threadlocal for null parameter. + * Set the current delegate for a non-null parameter, or remove it for null parameter. * - * @param delegate * @return the previous delegate or {@code null} */ - public static Object set(Object delegate) { - if (delegate != null) { - Object prev = CURRENT.get(); - if (delegate.equals(prev)) { - return delegate; - } else { - CURRENT.set(delegate); - return prev; - } - } else { - CURRENT.remove(); - return null; - } - } - - public static void unset() { - set(null); - } - - public static Object get() { - return CURRENT.get(); + public static Object setCurrent(CreationalContext ctx, Object delegate) { + // it wouldn't be necessary to reset this, but we do that as a safeguard, + // to prevent accidental references from keeping these objects alive + return CreationalContextImpl.setCurrentDecoratorDelegate(ctx, delegate); } } diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/EventBean.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/EventBean.java index a67ea603cc11d..0b1a67c849e3a 100644 --- a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/EventBean.java +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/EventBean.java @@ -19,7 +19,7 @@ public Set getTypes() { @Override public Event get(CreationalContext> creationalContext) { // Obtain current IP to get the required type and qualifiers - InjectionPoint ip = InjectionPointProvider.get(); + InjectionPoint ip = InjectionPointProvider.getCurrent(creationalContext); return new EventImpl<>(ip.getType(), ip.getQualifiers(), ip); } diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/InjectionPointBean.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/InjectionPointBean.java index 0163206d6ca7c..7cb2c87660d91 100644 --- a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/InjectionPointBean.java +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/InjectionPointBean.java @@ -16,7 +16,7 @@ public Set getTypes() { @Override public InjectionPoint get(CreationalContext creationalContext) { - return InjectionPointProvider.get(); + return InjectionPointProvider.getCurrent(creationalContext); } @Override diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/InjectionPointProvider.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/InjectionPointProvider.java index 5dd28847b94b0..081d16751e608 100644 --- a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/InjectionPointProvider.java +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/InjectionPointProvider.java @@ -11,36 +11,24 @@ */ public class InjectionPointProvider implements InjectableReferenceProvider { - private static final ThreadLocal CURRENT = new ThreadLocal<>(); - @Override public InjectionPoint get(CreationalContext creationalContext) { - return CURRENT.get(); + return getCurrent(creationalContext); + } + + public static InjectionPoint getCurrent(CreationalContext ctx) { + return CreationalContextImpl.getCurrentInjectionPoint(ctx); } /** - * Set the current injection point for a non-null parameter, remove the threadlocal for null parameter. + * Set the current injection point for a non-null parameter, or remove it for null parameter. * - * @param injectionPoint * @return the previous injection point or {@code null} */ - static InjectionPoint set(InjectionPoint injectionPoint) { - if (injectionPoint != null) { - InjectionPoint prev = InjectionPointProvider.CURRENT.get(); - if (injectionPoint.equals(prev)) { - return injectionPoint; - } else { - InjectionPointProvider.CURRENT.set(injectionPoint); - return prev; - } - } else { - CURRENT.remove(); - return null; - } - } - - public static InjectionPoint get() { - return CURRENT.get(); + static InjectionPoint setCurrent(CreationalContext ctx, InjectionPoint ip) { + // it wouldn't be necessary to reset this, but we do that as a safeguard, + // to prevent accidental references from keeping these objects alive + return CreationalContextImpl.setCurrentInjectionPoint(ctx, ip); } } diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/InstanceBean.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/InstanceBean.java index 0fc33ce7e6c96..7580d82d48e39 100644 --- a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/InstanceBean.java +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/InstanceBean.java @@ -30,7 +30,7 @@ public Class getBeanClass() { @Override public Instance get(CreationalContext> creationalContext) { // Obtain current IP to get the required type and qualifiers - InjectionPoint ip = InjectionPointProvider.get(); + InjectionPoint ip = InjectionPointProvider.getCurrent(creationalContext); InstanceImpl> instance = InstanceImpl.forInjection((InjectableBean) ip.getBean(), ip.getType(), ip.getQualifiers(), (CreationalContextImpl) creationalContext, Collections.EMPTY_SET, ip.getMember(), 0, ip.isTransient()); diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/InstanceImpl.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/InstanceImpl.java index 4c84a064e46d4..2ce4864251c0a 100644 --- a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/InstanceImpl.java +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/InstanceImpl.java @@ -264,14 +264,14 @@ private InstanceHandle getHandle(InjectableBean bean) { public H get() { InjectionPoint prev = null; if (resetCurrentInjectionPoint) { - prev = InjectionPointProvider.set(new InjectionPointImpl(injectionPointType, requiredType, + prev = InjectionPointProvider.setCurrent(context, new InjectionPointImpl(injectionPointType, requiredType, requiredQualifiers, targetBean, annotations, javaMember, position, isTransient)); } try { return bean.get(context); } finally { if (resetCurrentInjectionPoint) { - InjectionPointProvider.set(prev); + InjectionPointProvider.setCurrent(context, prev); } } } @@ -317,7 +317,7 @@ private T getBeanInstance(InjectableBean bean) { CreationalContextImpl ctx = creationalContext.child(bean); InjectionPoint prev = null; if (resetCurrentInjectionPoint) { - prev = InjectionPointProvider.set(new InjectionPointImpl(injectionPointType, requiredType, + prev = InjectionPointProvider.setCurrent(ctx, new InjectionPointImpl(injectionPointType, requiredType, requiredQualifiers, targetBean, annotations, javaMember, position, isTransient)); } T instance; @@ -325,7 +325,7 @@ private T getBeanInstance(InjectableBean bean) { instance = bean.get(ctx); } finally { if (resetCurrentInjectionPoint) { - InjectionPointProvider.set(prev); + InjectionPointProvider.setCurrent(ctx, prev); } } return instance; diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/Instances.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/Instances.java index 66c7f7b1b8261..f65b917fd3f5d 100644 --- a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/Instances.java +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/Instances.java @@ -67,15 +67,14 @@ public static List listOf(InjectableBean targetBean, Type injectionPoi return Collections.emptyList(); } List list = new ArrayList<>(beans.size()); - InjectionPoint prev = InjectionPointProvider - .set(new InjectionPointImpl(injectionPointType, requiredType, requiredQualifiers, targetBean, - annotations, javaMember, position, isTransient)); + InjectionPoint prev = InjectionPointProvider.setCurrent(creationalContext, new InjectionPointImpl(injectionPointType, + requiredType, requiredQualifiers, targetBean, annotations, javaMember, position, isTransient)); try { for (InjectableBean bean : beans) { list.add(getBeanInstance(CreationalContextImpl.unwrap(creationalContext), (InjectableBean) bean)); } } finally { - InjectionPointProvider.set(prev); + InjectionPointProvider.setCurrent(creationalContext, prev); } return List.copyOf(list); @@ -126,12 +125,11 @@ private static InstanceHandle getHandle(CreationalContextImpl parent, @Override public T get() { - InjectionPoint prev = InjectionPointProvider - .set(injectionPointSupplier.get()); + InjectionPoint prev = InjectionPointProvider.setCurrent(ctx, injectionPointSupplier.get()); try { return bean.get(ctx); } finally { - InjectionPointProvider.set(prev); + InjectionPointProvider.setCurrent(ctx, prev); } } }, null); diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/ApplicationModelBuilder.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/ApplicationModelBuilder.java index 0dcc5c24da325..95784e1755be8 100644 --- a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/ApplicationModelBuilder.java +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/ApplicationModelBuilder.java @@ -10,6 +10,8 @@ import java.util.Map; import java.util.Properties; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedDeque; import org.jboss.logging.Logger; @@ -36,13 +38,13 @@ public class ApplicationModelBuilder { ResolvedDependency appArtifact; final Map dependencies = new LinkedHashMap<>(); - final Set parentFirstArtifacts = new HashSet<>(); - final Set runnerParentFirstArtifacts = new HashSet<>(); - final List excludedArtifacts = new ArrayList<>(); - final Map> excludedResources = new HashMap<>(0); - final Set lesserPriorityArtifacts = new HashSet<>(); - final Set reloadableWorkspaceModules = new HashSet<>(); - final List extensionCapabilities = new ArrayList<>(); + final Collection parentFirstArtifacts = new ConcurrentLinkedDeque<>(); + final Collection runnerParentFirstArtifacts = new ConcurrentLinkedDeque<>(); + final Collection excludedArtifacts = new ConcurrentLinkedDeque<>(); + final Map> excludedResources = new ConcurrentHashMap<>(); + final Collection lesserPriorityArtifacts = new ConcurrentLinkedDeque<>(); + final Collection reloadableWorkspaceModules = new ConcurrentLinkedDeque<>(); + final Collection extensionCapabilities = new ConcurrentLinkedDeque<>(); PlatformImports platformImports; final Map projectModules = new HashMap<>(); diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/DefaultApplicationModel.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/DefaultApplicationModel.java index d245c1cad796c..b03ebc134dbcb 100644 --- a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/DefaultApplicationModel.java +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/DefaultApplicationModel.java @@ -29,8 +29,8 @@ public DefaultApplicationModel(ApplicationModelBuilder builder) { this.appArtifact = builder.appArtifact; this.dependencies = builder.buildDependencies(); this.platformImports = builder.platformImports; - this.capabilityContracts = builder.extensionCapabilities; - this.localProjectArtifacts = builder.reloadableWorkspaceModules; + this.capabilityContracts = List.copyOf(builder.extensionCapabilities); + this.localProjectArtifacts = Set.copyOf(builder.reloadableWorkspaceModules); this.excludedResources = builder.excludedResources; } diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/util/PropertyUtils.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/util/PropertyUtils.java index 5a15d4b205b38..8751c22210ce4 100644 --- a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/util/PropertyUtils.java +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/util/PropertyUtils.java @@ -5,8 +5,6 @@ import java.io.Writer; import java.nio.file.Files; import java.nio.file.Path; -import java.security.AccessController; -import java.security.PrivilegedAction; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -40,32 +38,12 @@ public static String getUserHome() { public static String getProperty(final String name, String defValue) { assert name != null : "name is null"; - final SecurityManager sm = System.getSecurityManager(); - if (sm != null) { - return AccessController.doPrivileged(new PrivilegedAction() { - @Override - public String run() { - return System.getProperty(name, defValue); - } - }); - } else { - return System.getProperty(name, defValue); - } + return System.getProperty(name, defValue); } public static String getProperty(final String name) { assert name != null : "name is null"; - final SecurityManager sm = System.getSecurityManager(); - if (sm != null) { - return AccessController.doPrivileged(new PrivilegedAction() { - @Override - public String run() { - return System.getProperty(name); - } - }); - } else { - return System.getProperty(name); - } + return System.getProperty(name); } public static final Boolean getBooleanOrNull(String name) { diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/maven/dependency/DependencyFlags.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/maven/dependency/DependencyFlags.java index 641c677f562dd..cfc4b0539fbe9 100644 --- a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/maven/dependency/DependencyFlags.java +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/maven/dependency/DependencyFlags.java @@ -45,5 +45,4 @@ public interface DependencyFlags { */ int COMPILE_ONLY = 0b01000000000000; /* @formatter:on */ - } diff --git a/independent-projects/bootstrap/bom/pom.xml b/independent-projects/bootstrap/bom/pom.xml index 10504e6d084e3..5db6112b194ad 100644 --- a/independent-projects/bootstrap/bom/pom.xml +++ b/independent-projects/bootstrap/bom/pom.xml @@ -481,11 +481,6 @@ - - io.fabric8 - maven-model-helper - ${maven-model-helper.version} - io.smallrye.common diff --git a/independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/resolver/CollectDependenciesBase.java b/independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/resolver/CollectDependenciesBase.java index ade5086b19147..dc86cd8b16d82 100644 --- a/independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/resolver/CollectDependenciesBase.java +++ b/independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/resolver/CollectDependenciesBase.java @@ -49,8 +49,7 @@ public void testCollectedDependencies() throws Exception { } // stripping the resolved paths final List resolvedDeps = getTestResolver().resolveModel(root.toArtifact()).getDependencies() - .stream() - .map(d -> new ArtifactDependency(d)).collect(Collectors.toList()); + .stream().map(ArtifactDependency::new).collect(Collectors.toList()); assertEquals(new HashSet<>(expected), new HashSet<>(resolvedDeps)); } diff --git a/independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/resolver/ResolverSetupCleanup.java b/independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/resolver/ResolverSetupCleanup.java index 880ef8079b0bb..5a5d2666f48e5 100644 --- a/independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/resolver/ResolverSetupCleanup.java +++ b/independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/resolver/ResolverSetupCleanup.java @@ -18,6 +18,7 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import io.quarkus.bootstrap.resolver.maven.ApplicationDependencyModelResolver; import io.quarkus.bootstrap.resolver.maven.BootstrapMavenException; import io.quarkus.bootstrap.resolver.maven.MavenArtifactResolver; import io.quarkus.bootstrap.resolver.maven.workspace.LocalProject; @@ -148,6 +149,7 @@ protected boolean isBootstrapForTestMode() { protected BootstrapAppModelResolver newAppModelResolver(LocalProject currentProject) throws Exception { final BootstrapAppModelResolver appModelResolver = new BootstrapAppModelResolver(newArtifactResolver(currentProject)); + appModelResolver.setIncubatingModelResolver(ApplicationDependencyModelResolver.isIncubatingEnabled(null)); if (isBootstrapForTestMode()) { appModelResolver.setTest(true); } diff --git a/independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/resolver/TsArtifact.java b/independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/resolver/TsArtifact.java index 21004a9983db8..c13b3c844e6de 100644 --- a/independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/resolver/TsArtifact.java +++ b/independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/resolver/TsArtifact.java @@ -15,7 +15,6 @@ import io.quarkus.maven.dependency.ArtifactCoords; import io.quarkus.maven.dependency.ArtifactKey; import io.quarkus.maven.dependency.GACT; -import io.quarkus.maven.dependency.GACTV; /** * @@ -180,6 +179,10 @@ public TsArtifact addDependency(TsDependency dep) { return this; } + public TsArtifact addManagedDependency(TsArtifact a) { + return addManagedDependency(new TsDependency(a)); + } + public TsArtifact addManagedDependency(TsDependency dep) { if (managedDeps.isEmpty()) { managedDeps = new ArrayList<>(); @@ -239,9 +242,10 @@ public Model getPomModel() { } if (!managedDeps.isEmpty()) { - model.setDependencyManagement(new DependencyManagement()); + var dm = new DependencyManagement(); + model.setDependencyManagement(dm); for (TsDependency dep : managedDeps) { - model.getDependencyManagement().addDependency(dep.toPomDependency()); + dm.addDependency(dep.toPomDependency()); } } @@ -252,7 +256,7 @@ public Model getPomModel() { } public ArtifactCoords toArtifact() { - return new GACTV(groupId, artifactId, classifier, type, version); + return ArtifactCoords.of(groupId, artifactId, classifier, type, version); } /** diff --git a/independent-projects/bootstrap/maven-resolver/pom.xml b/independent-projects/bootstrap/maven-resolver/pom.xml index b6c21737f4c89..97ce31340034c 100644 --- a/independent-projects/bootstrap/maven-resolver/pom.xml +++ b/independent-projects/bootstrap/maven-resolver/pom.xml @@ -125,10 +125,6 @@ - - io.fabric8 - maven-model-helper - org.junit.jupiter junit-jupiter diff --git a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/BootstrapAppModelResolver.java b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/BootstrapAppModelResolver.java index e7109757aa759..037ad003fea3f 100644 --- a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/BootstrapAppModelResolver.java +++ b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/BootstrapAppModelResolver.java @@ -34,8 +34,10 @@ import io.quarkus.bootstrap.BootstrapDependencyProcessingException; import io.quarkus.bootstrap.model.ApplicationModel; import io.quarkus.bootstrap.model.ApplicationModelBuilder; +import io.quarkus.bootstrap.resolver.maven.ApplicationDependencyModelResolver; import io.quarkus.bootstrap.resolver.maven.ApplicationDependencyTreeResolver; import io.quarkus.bootstrap.resolver.maven.BootstrapMavenException; +import io.quarkus.bootstrap.resolver.maven.DependencyLoggingConfig; import io.quarkus.bootstrap.resolver.maven.MavenArtifactResolver; import io.quarkus.bootstrap.util.DependencyUtils; import io.quarkus.bootstrap.workspace.ArtifactSources; @@ -55,17 +57,34 @@ public class BootstrapAppModelResolver implements AppModelResolver { protected final MavenArtifactResolver mvn; - protected Consumer buildTreeConsumer; + private DependencyLoggingConfig depLogConfig; protected boolean devmode; protected boolean test; private boolean collectReloadableDeps = true; + private boolean incubatingModelResolver; public BootstrapAppModelResolver(MavenArtifactResolver mvn) { this.mvn = mvn; } + /** + * Temporary method that will be removed once the incubating implementation becomes the default. + * + * @return this application model resolver + */ + public BootstrapAppModelResolver setIncubatingModelResolver(boolean incubatingModelResolver) { + this.incubatingModelResolver = incubatingModelResolver; + return this; + } + public void setBuildTreeLogger(Consumer buildTreeConsumer) { - this.buildTreeConsumer = buildTreeConsumer; + if (buildTreeConsumer != null) { + depLogConfig = DependencyLoggingConfig.builder().setMessageConsumer(buildTreeConsumer).build(); + } + } + + public void setDepLogConfig(DependencyLoggingConfig depLogConfig) { + this.depLogConfig = depLogConfig; } /** @@ -328,13 +347,33 @@ private ApplicationModel buildAppModel(ResolvedDependency appArtifact, } var collectRtDepsRequest = MavenArtifactResolver.newCollectRequest(artifact, directDeps, managedDeps, List.of(), repos); try { - ApplicationDependencyTreeResolver.newInstance() - .setArtifactResolver(mvn) - .setApplicationModelBuilder(appBuilder) - .setCollectReloadableModules(collectReloadableDeps && reloadableModules.isEmpty()) - .setCollectCompileOnly(filteredProvidedDeps) - .setBuildTreeConsumer(buildTreeConsumer) - .resolve(collectRtDepsRequest); + long start = 0; + boolean logTime = false; + if (logTime) { + start = System.currentTimeMillis(); + } + if (incubatingModelResolver) { + ApplicationDependencyModelResolver.newInstance() + .setArtifactResolver(mvn) + .setApplicationModelBuilder(appBuilder) + .setCollectReloadableModules(collectReloadableDeps && reloadableModules.isEmpty()) + .setCollectCompileOnly(filteredProvidedDeps) + .setDependencyLogging(depLogConfig) + .resolve(collectRtDepsRequest); + } else { + ApplicationDependencyTreeResolver.newInstance() + .setArtifactResolver(mvn) + .setApplicationModelBuilder(appBuilder) + .setCollectReloadableModules(collectReloadableDeps && reloadableModules.isEmpty()) + .setCollectCompileOnly(filteredProvidedDeps) + .setBuildTreeConsumer(depLogConfig == null ? null : depLogConfig.getMessageConsumer()) + .resolve(collectRtDepsRequest); + } + if (logTime) { + System.err.println( + "Application model resolved in " + (System.currentTimeMillis() - start) + "ms, incubating=" + + incubatingModelResolver); + } } catch (BootstrapDependencyProcessingException e) { throw new AppModelResolverException( "Failed to inject extension deployment dependencies for " + appArtifact.toCompactCoords(), e); diff --git a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/ApplicationDependencyModelResolver.java b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/ApplicationDependencyModelResolver.java new file mode 100644 index 0000000000000..9a3b9e1855b4e --- /dev/null +++ b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/ApplicationDependencyModelResolver.java @@ -0,0 +1,1257 @@ +package io.quarkus.bootstrap.resolver.maven; + +import static io.quarkus.bootstrap.util.DependencyUtils.getCoords; +import static io.quarkus.bootstrap.util.DependencyUtils.getKey; +import static io.quarkus.bootstrap.util.DependencyUtils.getWinner; +import static io.quarkus.bootstrap.util.DependencyUtils.hasWinner; +import static io.quarkus.bootstrap.util.DependencyUtils.newDependencyBuilder; +import static io.quarkus.bootstrap.util.DependencyUtils.toArtifact; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedDeque; +import java.util.function.BiConsumer; + +import org.eclipse.aether.DefaultRepositorySystemSession; +import org.eclipse.aether.RepositoryException; +import org.eclipse.aether.RepositorySystemSession; +import org.eclipse.aether.artifact.Artifact; +import org.eclipse.aether.collection.CollectRequest; +import org.eclipse.aether.collection.DependencyCollectionException; +import org.eclipse.aether.collection.DependencyGraphTransformationContext; +import org.eclipse.aether.collection.DependencySelector; +import org.eclipse.aether.graph.DefaultDependencyNode; +import org.eclipse.aether.graph.Dependency; +import org.eclipse.aether.graph.DependencyNode; +import org.eclipse.aether.graph.Exclusion; +import org.eclipse.aether.repository.RemoteRepository; +import org.eclipse.aether.resolution.ArtifactDescriptorResult; +import org.eclipse.aether.resolution.ArtifactRequest; +import org.eclipse.aether.resolution.ArtifactResolutionException; +import org.eclipse.aether.util.artifact.JavaScopes; +import org.eclipse.aether.util.graph.manager.DependencyManagerUtils; +import org.eclipse.aether.util.graph.selector.ExclusionDependencySelector; +import org.eclipse.aether.util.graph.transformer.ConflictIdSorter; +import org.eclipse.aether.util.graph.transformer.ConflictResolver; +import org.jboss.logging.Logger; + +import io.quarkus.bootstrap.BootstrapConstants; +import io.quarkus.bootstrap.BootstrapDependencyProcessingException; +import io.quarkus.bootstrap.model.ApplicationModelBuilder; +import io.quarkus.bootstrap.model.CapabilityContract; +import io.quarkus.bootstrap.model.PlatformImportsImpl; +import io.quarkus.bootstrap.resolver.AppModelResolverException; +import io.quarkus.bootstrap.util.BootstrapUtils; +import io.quarkus.bootstrap.util.DependencyUtils; +import io.quarkus.bootstrap.workspace.WorkspaceModule; +import io.quarkus.maven.dependency.ArtifactCoords; +import io.quarkus.maven.dependency.ArtifactKey; +import io.quarkus.maven.dependency.DependencyFlags; +import io.quarkus.maven.dependency.ResolvedDependencyBuilder; +import io.quarkus.paths.PathTree; + +public class ApplicationDependencyModelResolver { + + private static final Logger log = Logger.getLogger(ApplicationDependencyModelResolver.class); + + private static final String QUARKUS_RUNTIME_ARTIFACT = "quarkus.runtime"; + private static final String QUARKUS_EXTENSION_DEPENDENCY = "quarkus.ext"; + + private static final String INCUBATING_MODEL_RESOLVER = "quarkus.bootstrap.incubating-model-resolver"; + + /* @formatter:off */ + private static final byte COLLECT_TOP_EXTENSION_RUNTIME_NODES = 0b001; + private static final byte COLLECT_DIRECT_DEPS = 0b010; + private static final byte COLLECT_RELOADABLE_MODULES = 0b100; + /* @formatter:on */ + + private static final Artifact[] NO_ARTIFACTS = new Artifact[0]; + + /** + * Temporary method that will be removed once this implementation becomes the default. + * + * @return true if this implementation is enabled + */ + public static boolean isIncubatingEnabled(Properties projectProperties) { + var value = System.getProperty(INCUBATING_MODEL_RESOLVER); + if (value == null && projectProperties != null) { + value = String.valueOf(projectProperties.get(INCUBATING_MODEL_RESOLVER)); + } + return Boolean.parseBoolean(value); + } + + public static ApplicationDependencyModelResolver newInstance() { + return new ApplicationDependencyModelResolver(); + } + + private final ExtensionInfo EXT_INFO_NONE = new ExtensionInfo(); + + private final List topExtensionDeps = new ArrayList<>(); + private final Map allExtensions = new ConcurrentHashMap<>(); + private List conditionalDepsToProcess = new ArrayList<>(); + + private final Map> artifactDeps = new HashMap<>(); + + private final Collection errors = new ConcurrentLinkedDeque<>(); + + private MavenArtifactResolver resolver; + private List managedDeps; + private ApplicationModelBuilder appBuilder; + private boolean collectReloadableModules; + private DependencyLoggingConfig depLogging; + private List collectCompileOnly; + + public ApplicationDependencyModelResolver setArtifactResolver(MavenArtifactResolver resolver) { + this.resolver = resolver; + return this; + } + + public ApplicationDependencyModelResolver setApplicationModelBuilder(ApplicationModelBuilder appBuilder) { + this.appBuilder = appBuilder; + return this; + } + + public ApplicationDependencyModelResolver setCollectReloadableModules(boolean collectReloadableModules) { + this.collectReloadableModules = collectReloadableModules; + return this; + } + + public ApplicationDependencyModelResolver setDependencyLogging(DependencyLoggingConfig depLogging) { + this.depLogging = depLogging; + return this; + } + + /** + * In addition to resolving dependencies for the build classpath, also resolve these compile-only dependencies + * and add them to the application model as {@link DependencyFlags#COMPILE_ONLY}. + * + * @param collectCompileOnly compile-only dependencies to add to the model + * @return self + */ + public ApplicationDependencyModelResolver setCollectCompileOnly(List collectCompileOnly) { + this.collectCompileOnly = collectCompileOnly; + return this; + } + + public void resolve(CollectRequest collectRtDepsRequest) throws AppModelResolverException { + this.managedDeps = collectRtDepsRequest.getManagedDependencies(); + // managed dependencies will be a bit augmented with every added extension, so let's load the properties early + collectPlatformProperties(); + this.managedDeps = managedDeps.isEmpty() ? new ArrayList<>() : managedDeps; + + DependencyNode root = resolveRuntimeDeps(collectRtDepsRequest); + processRuntimeDeps(root); + final List activatedConditionalDeps = activateConditionalDeps(); + + // resolve and inject deployment dependency branches for the top (first met) runtime extension nodes + injectDeployment(activatedConditionalDeps); + root = normalize(resolver.getSession(), root); + processDeploymentDeps(root); + + for (var d : appBuilder.getDependencies()) { + if (!d.isFlagSet(DependencyFlags.RELOADABLE) && !d.isFlagSet(DependencyFlags.VISITED)) { + clearReloadableFlag(d); + } + } + + for (var d : appBuilder.getDependencies()) { + d.clearFlag(DependencyFlags.VISITED); + if (d.isFlagSet(DependencyFlags.RELOADABLE)) { + appBuilder.addReloadableWorkspaceModule(d.getKey()); + } + appBuilder.addDependency(d); + } + + collectCompileOnly(collectRtDepsRequest, root); + } + + private List activateConditionalDeps() { + if (conditionalDepsToProcess.isEmpty()) { + return List.of(); + } + var activatedConditionalDeps = new ArrayList(); + boolean checkDependencyConditions = true; + while (!conditionalDepsToProcess.isEmpty() && checkDependencyConditions) { + checkDependencyConditions = false; + var unsatisfiedConditionalDeps = conditionalDepsToProcess; + conditionalDepsToProcess = new ArrayList<>(); + for (ConditionalDependency cd : unsatisfiedConditionalDeps) { + if (cd.isSatisfied()) { + cd.activate(); + activatedConditionalDeps.add(cd); + // if a dependency was activated, the remaining not satisfied conditions should be checked again + checkDependencyConditions = true; + } else { + conditionalDepsToProcess.add(cd); + } + } + } + return activatedConditionalDeps; + } + + private void processDeploymentDeps(DependencyNode root) { + var app = new AppDep(root); + var futures = new ArrayList>(); + app.scheduleChildVisits(futures, AppDep::scheduleDeploymentVisit); + CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); + if (logErrors()) { + throw new RuntimeException( + "Failed to process Quarkus application deployment dependencies, please see the errors logged above for more details."); + } + for (var d : app.children) { + d.addToModel(); + } + + if (depLogging != null) { + new AppDepLogger().log(app); + } + } + + private boolean logErrors() { + if (!errors.isEmpty()) { + log.error("The following errors were encountered while processing Quarkus application dependencies:"); + var i = 1; + for (var error : errors) { + log.error(i++ + ")", error); + } + return true; + } + return false; + } + + private void injectDeployment(List activatedConditionalDeps) { + final List> futures = new ArrayList<>(topExtensionDeps.size() + + activatedConditionalDeps.size()); + for (ExtensionDependency extDep : topExtensionDeps) { + futures.add(CompletableFuture.supplyAsync(() -> { + var resolvedDep = appBuilder.getDependency(getKey(extDep.info.deploymentArtifact)); + if (resolvedDep == null) { + try { + extDep.collectDeploymentDeps(); + return () -> extDep.injectDeploymentNode(null); + } catch (BootstrapDependencyProcessingException e) { + errors.add(e); + } + } else { + // if resolvedDep isn't null, it means the deployment artifact is on the runtime classpath + // in which case we also clear the reloadable flag on it, in case it's coming from the workspace + resolvedDep.clearFlag(DependencyFlags.RELOADABLE); + } + return null; + })); + } + // non-conditional deployment branches should be added before the activated conditional ones to have consistent + // dependency graph structures + CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); + if (errors.isEmpty() && !activatedConditionalDeps.isEmpty()) { + for (ConditionalDependency cd : activatedConditionalDeps) { + futures.add(CompletableFuture.supplyAsync(() -> { + var resolvedDep = appBuilder.getDependency(getKey(cd.appDep.ext.info.deploymentArtifact)); + if (resolvedDep == null) { + var extDep = cd.getExtensionDependency(); + try { + extDep.collectDeploymentDeps(); + return () -> extDep.injectDeploymentNode(cd.appDep.ext.getParentDeploymentNode()); + } catch (BootstrapDependencyProcessingException e) { + errors.add(e); + } + } else { + // if resolvedDep isn't null, it means the deployment artifact is on the runtime classpath + // in which case we also clear the reloadable flag on it, in case it's coming from the workspace + resolvedDep.clearFlag(DependencyFlags.RELOADABLE); + } + return null; + })); + } + } + CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); + if (logErrors()) { + throw new RuntimeException( + "Failed to process Quarkus application deployment dependencies, please see the errors logged above for more details."); + } + + for (var future : futures) { + var ext = future.getNow(null); + if (ext != null) { + ext.run(); + } + } + } + + /** + * Resolves and adds compile-only dependencies to the application model with the {@link DependencyFlags#COMPILE_ONLY} flag. + * Compile-only dependencies are resolved as direct dependencies of the root with all the previously resolved dependencies + * enforced as version constraints to make sure compile-only dependencies do not override runtime dependencies of the final + * application. + * + * @param collectRtDepsRequest original runtime dependencies collection request + * @param root the root node of the Quarkus build time dependency tree + * @throws BootstrapMavenException in case of a failure + */ + private void collectCompileOnly(CollectRequest collectRtDepsRequest, DependencyNode root) throws BootstrapMavenException { + if (collectCompileOnly.isEmpty()) { + return; + } + // add all the build time dependencies as version constraints + var depStack = new ArrayDeque>(); + var children = root.getChildren(); + while (children != null) { + for (DependencyNode node : children) { + managedDeps.add(node.getDependency()); + if (!node.getChildren().isEmpty()) { + depStack.add(node.getChildren()); + } + } + children = depStack.poll(); + } + final CollectRequest request = new CollectRequest() + .setDependencies(collectCompileOnly) + .setManagedDependencies(managedDeps) + .setRepositories(collectRtDepsRequest.getRepositories()); + if (collectRtDepsRequest.getRoot() != null) { + request.setRoot(collectRtDepsRequest.getRoot()); + } else { + request.setRootArtifact(collectRtDepsRequest.getRootArtifact()); + } + + try { + root = resolver.getSystem().collectDependencies(resolver.getSession(), request).getRoot(); + } catch (DependencyCollectionException e) { + throw new BootstrapDependencyProcessingException( + "Failed to collect compile-only dependencies of " + root.getArtifact(), e); + } + children = root.getChildren(); + int flags = DependencyFlags.DIRECT | DependencyFlags.COMPILE_ONLY; + while (children != null) { + for (DependencyNode node : children) { + if (hasWinner(node)) { + continue; + } + var extInfo = getExtensionInfoOrNull(node.getArtifact(), node.getRepositories()); + var dep = appBuilder.getDependency(getKey(node.getArtifact())); + if (dep == null) { + dep = newDependencyBuilder(node, resolver).setFlags(flags); + if (extInfo != null) { + dep.setFlags(DependencyFlags.RUNTIME_EXTENSION_ARTIFACT); + if (dep.isFlagSet(DependencyFlags.DIRECT)) { + dep.setFlags(DependencyFlags.TOP_LEVEL_RUNTIME_EXTENSION_ARTIFACT); + } + } + appBuilder.addDependency(dep); + } else { + dep.setFlags(DependencyFlags.COMPILE_ONLY); + } + if (!node.getChildren().isEmpty()) { + depStack.add(node.getChildren()); + } + } + flags = DependencyFlags.COMPILE_ONLY; + children = depStack.poll(); + } + } + + private void collectPlatformProperties() throws AppModelResolverException { + final PlatformImportsImpl platformReleases = new PlatformImportsImpl(); + for (Dependency d : managedDeps) { + final Artifact artifact = d.getArtifact(); + final String extension = artifact.getExtension(); + final String artifactId = artifact.getArtifactId(); + if ("json".equals(extension) + && artifactId.endsWith(BootstrapConstants.PLATFORM_DESCRIPTOR_ARTIFACT_ID_SUFFIX)) { + platformReleases.addPlatformDescriptor(artifact.getGroupId(), artifactId, artifact.getClassifier(), extension, + artifact.getVersion()); + } else if ("properties".equals(extension) + && artifactId.endsWith(BootstrapConstants.PLATFORM_PROPERTIES_ARTIFACT_ID_SUFFIX)) { + platformReleases.addPlatformProperties(artifact.getGroupId(), artifactId, artifact.getClassifier(), extension, + artifact.getVersion(), resolver.resolve(artifact).getArtifact().getFile().toPath()); + } + } + appBuilder.setPlatformImports(platformReleases); + } + + private void clearReloadableFlag(ResolvedDependencyBuilder dep) { + final Set deps = artifactDeps.get(dep.getArtifactCoords()); + if (deps == null || deps.isEmpty()) { + return; + } + for (ArtifactKey key : deps) { + final ResolvedDependencyBuilder child = appBuilder.getDependency(key); + if (child == null || child.isFlagSet(DependencyFlags.VISITED)) { + continue; + } + child.setFlags(DependencyFlags.VISITED); + child.clearFlag(DependencyFlags.RELOADABLE); + clearReloadableFlag(child); + } + } + + private DependencyNode normalize(RepositorySystemSession session, DependencyNode root) throws AppModelResolverException { + final DependencyGraphTransformationContext context = new SimpleDependencyGraphTransformationContext(session); + try { + // resolves version conflicts + root = new ConflictIdSorter().transformGraph(root, context); + return session.getDependencyGraphTransformer().transformGraph(root, context); + } catch (RepositoryException e) { + throw new AppModelResolverException("Failed to resolve dependency graph conflicts", e); + } + } + + private DependencyNode resolveRuntimeDeps(CollectRequest request) + throws AppModelResolverException { + boolean verbose = true; //Boolean.getBoolean("quarkus.bootstrap.verbose-model-resolver"); + if (verbose) { + var session = resolver.getSession(); + final DefaultRepositorySystemSession mutableSession = new DefaultRepositorySystemSession(resolver.getSession()); + mutableSession.setConfigProperty(ConflictResolver.CONFIG_PROP_VERBOSE, true); + mutableSession.setConfigProperty(DependencyManagerUtils.CONFIG_PROP_VERBOSE, true); + session = mutableSession; + + var ctx = new BootstrapMavenContext(BootstrapMavenContext.config() + .setRepositorySystem(resolver.getSystem()) + .setRepositorySystemSession(session) + .setRemoteRepositories(resolver.getRepositories()) + .setRemoteRepositoryManager(resolver.getRemoteRepositoryManager()) + .setCurrentProject(resolver.getMavenContext().getCurrentProject()) + .setWorkspaceDiscovery(collectReloadableModules)); + resolver = new MavenArtifactResolver(ctx); + } + try { + return resolver.getSystem().collectDependencies(resolver.getSession(), request).getRoot(); + } catch (DependencyCollectionException e) { + final Artifact a = request.getRoot() == null ? request.getRootArtifact() : request.getRoot().getArtifact(); + throw new BootstrapMavenException("Failed to resolve dependencies for " + a, e); + } + } + + private boolean isRuntimeArtifact(ArtifactKey key) { + final ResolvedDependencyBuilder dep = appBuilder.getDependency(key); + return dep != null && dep.isFlagSet(DependencyFlags.RUNTIME_CP); + } + + private void processRuntimeDeps(DependencyNode root) { + final AppDep app = new AppDep(root); + app.walkingFlags = COLLECT_TOP_EXTENSION_RUNTIME_NODES | COLLECT_DIRECT_DEPS; + if (collectReloadableModules) { + app.walkingFlags |= COLLECT_RELOADABLE_MODULES; + } + + var futures = new ArrayList>(); + app.scheduleChildVisits(futures, AppDep::scheduleRuntimeVisit); + CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); + if (logErrors()) { + throw new RuntimeException( + "Failed to process Quarkus application runtime dependencies, please see the errors logged above for more details."); + } + app.setChildFlags(); + } + + private class AppDep { + final AppDep parent; + final DependencyNode node; + ExtensionDependency ext; + byte walkingFlags; + ResolvedDependencyBuilder resolvedDep; + final List children; + + AppDep(DependencyNode node) { + this.parent = null; + this.node = node; + this.children = new ArrayList<>(node.getChildren().size()); + } + + AppDep(AppDep parent, DependencyNode node) { + this.parent = parent; + this.node = node; + this.children = new ArrayList<>(node.getChildren().size()); + } + + void addToModel() { + for (var child : children) { + child.addToModel(); + } + // this node is added after its children to stay compatible with the legacy impl + if (resolvedDep != null) { + appBuilder.addDependency(resolvedDep); + } + } + + void scheduleDeploymentVisit(List> futures) { + futures.add(CompletableFuture.runAsync(() -> { + try { + visitDeploymentDependency(); + } catch (Throwable e) { + errors.add(e); + } + })); + scheduleChildVisits(futures, AppDep::scheduleDeploymentVisit); + } + + void visitDeploymentDependency() { + var dep = appBuilder.getDependency(getKey(node.getArtifact())); + if (dep == null) { + try { + resolvedDep = newDependencyBuilder(node, resolver).setFlags(DependencyFlags.DEPLOYMENT_CP); + } catch (BootstrapMavenException e) { + throw new RuntimeException(e); + } + } + } + + void scheduleRuntimeVisit(List> futures) { + futures.add(CompletableFuture.runAsync(() -> { + try { + visitRuntimeDependency(); + } catch (Throwable t) { + errors.add(t); + } + })); + scheduleChildVisits(futures, AppDep::scheduleRuntimeVisit); + } + + void visitRuntimeDependency() { + Artifact artifact = node.getArtifact(); + final ArtifactKey key = getKey(artifact); + if (resolvedDep == null) { + resolvedDep = appBuilder.getDependency(key); + } + + try { + var ext = getExtensionDependencyOrNull(); + if (resolvedDep == null) { + WorkspaceModule module = null; + if (resolver.getProjectModuleResolver() != null) { + module = resolver.getProjectModuleResolver().getProjectModule(artifact.getGroupId(), + artifact.getArtifactId(), artifact.getVersion()); + } + resolvedDep = DependencyUtils.toAppArtifact(getResolvedArtifact(), module) + .setOptional(node.getDependency().isOptional()) + .setScope(node.getDependency().getScope()) + .setRuntimeCp() + .setDeploymentCp(); + if (JavaScopes.PROVIDED.equals(resolvedDep.getScope())) { + resolvedDep.setFlags(DependencyFlags.COMPILE_ONLY); + } + if (ext != null) { + resolvedDep.setRuntimeExtensionArtifact(); + collectConditionalDependencies(); + } + } + } catch (DeploymentInjectionException e) { + throw e; + } catch (Exception t) { + throw new DeploymentInjectionException("Failed to inject extension deployment dependencies", t); + } + } + + void scheduleChildVisits(List> futures, + BiConsumer>> childVisitor) { + var childNodes = node.getChildren(); + List filtered = null; + var depKeys = artifactDeps.computeIfAbsent(getCoords(node.getArtifact()), key -> new HashSet<>(childNodes.size())); + for (int i = 0; i < childNodes.size(); ++i) { + var childNode = childNodes.get(i); + var winner = getWinner(childNode); + if (winner == null) { + depKeys.add(getKey(childNode.getArtifact())); + var child = new AppDep(this, childNode); + children.add(child); + if (filtered != null) { + filtered.add(childNode); + } + } else { + depKeys.add(getKey(winner.getArtifact())); + if (filtered == null) { + filtered = new ArrayList<>(childNodes.size()); + for (int j = 0; j < i; ++j) { + filtered.add(childNodes.get(j)); + } + } + } + } + if (filtered != null) { + node.setChildren(filtered); + } + for (var child : children) { + childVisitor.accept(child, futures); + } + } + + void setChildFlags() { + for (var c : children) { + c.setFlags(walkingFlags); + } + } + + void setFlags(byte walkingFlags) { + + if (ext != null) { + var parentExtDep = parent; + while (parentExtDep != null) { + if (parentExtDep.ext != null) { + parentExtDep.ext.addExtensionDependency(ext); + break; + } + parentExtDep = parentExtDep.parent; + } + ext.info.ensureActivated(); + } + + if (appBuilder.getDependency(resolvedDep.getKey()) == null) { + appBuilder.addDependency(resolvedDep); + if (ext != null) { + managedDeps.add(new Dependency(ext.info.deploymentArtifact, JavaScopes.COMPILE)); + } + } + this.walkingFlags = walkingFlags; + + resolvedDep.setDirect(isWalkingFlagOn(COLLECT_DIRECT_DEPS)); + if (ext != null && isWalkingFlagOn(COLLECT_TOP_EXTENSION_RUNTIME_NODES)) { + resolvedDep.setFlags(DependencyFlags.TOP_LEVEL_RUNTIME_EXTENSION_ARTIFACT); + clearWalkingFlag(COLLECT_TOP_EXTENSION_RUNTIME_NODES); + topExtensionDeps.add(ext); + } + if (isWalkingFlagOn(COLLECT_RELOADABLE_MODULES)) { + if (resolvedDep.getWorkspaceModule() != null + && !resolvedDep.isFlagSet(DependencyFlags.RUNTIME_EXTENSION_ARTIFACT)) { + resolvedDep.setReloadable(); + } else { + clearWalkingFlag(COLLECT_RELOADABLE_MODULES); + } + } + + clearWalkingFlag(COLLECT_DIRECT_DEPS); + + setChildFlags(); + } + + private ExtensionDependency getExtensionDependencyOrNull() + throws BootstrapDependencyProcessingException { + if (ext != null) { + return ext; + } + ext = ExtensionDependency.get(node); + if (ext == null) { + final ExtensionInfo extInfo = getExtensionInfoOrNull(node.getArtifact(), node.getRepositories()); + if (extInfo != null) { + ext = new ExtensionDependency(extInfo, node, collectExclusions()); + } + } + return ext; + } + + private Collection collectExclusions() { + if (parent == null) { + return List.of(); + } + Collection exclusions = null; + var next = this; + while (next != null) { + if (next.ext != null) { + if (exclusions == null) { + return next.ext.exclusions; + } + exclusions.addAll(next.ext.exclusions); + return exclusions; + } + var nextExcl = next.node.getDependency() == null ? null : next.node.getDependency().getExclusions(); + if (nextExcl != null && !nextExcl.isEmpty()) { + if (exclusions == null) { + exclusions = new ArrayList<>(nextExcl); + } + } + next = next.parent; + } + return exclusions == null ? List.of() : exclusions; + } + + Artifact getResolvedArtifact() { + var result = node.getArtifact(); + if (result.getFile() == null) { + result = resolve(result, node.getRepositories()); + node.setArtifact(result); + } + return result; + } + + private boolean isWalkingFlagOn(byte flag) { + return (walkingFlags & flag) > 0; + } + + private void clearWalkingFlag(byte flag) { + if ((walkingFlags & flag) > 0) { + walkingFlags ^= flag; + } + } + + private void collectConditionalDependencies() + throws BootstrapDependencyProcessingException { + if (ext.info.conditionalDeps.length == 0 || ext.conditionalDepsQueued) { + return; + } + ext.conditionalDepsQueued = true; + + final DependencySelector selector = ext.exclusions == null ? null + : new ExclusionDependencySelector(ext.exclusions); + for (Artifact conditionalArtifact : ext.info.conditionalDeps) { + if (selector != null && !selector.selectDependency(new Dependency(conditionalArtifact, JavaScopes.RUNTIME))) { + continue; + } + final ExtensionInfo conditionalInfo = getExtensionInfoOrNull(conditionalArtifact, + ext.runtimeNode.getRepositories()); + if (conditionalInfo == null) { + log.warn(ext.info.runtimeArtifact + " declares a conditional dependency on " + conditionalArtifact + + " that is not a Quarkus extension and will be ignored"); + continue; + } + if (conditionalInfo.activated) { + continue; + } + final ConditionalDependency conditionalDep = new ConditionalDependency(conditionalInfo, this); + conditionalDepsToProcess.add(conditionalDep); + conditionalDep.appDep.collectConditionalDependencies(); + } + } + } + + private ExtensionInfo getExtensionInfoOrNull(Artifact artifact, List repos) + throws BootstrapDependencyProcessingException { + if (!artifact.getExtension().equals(ArtifactCoords.TYPE_JAR)) { + return null; + } + final ArtifactKey extKey = getKey(artifact); + ExtensionInfo ext = allExtensions.get(extKey); + if (ext != null) { + return ext == EXT_INFO_NONE ? null : ext; + } + artifact = resolve(artifact, repos); + final Path path = artifact.getFile().toPath(); + final Properties descriptor = PathTree.ofDirectoryOrArchive(path).apply(BootstrapConstants.DESCRIPTOR_PATH, visit -> { + if (visit == null) { + return null; + } + try { + return readDescriptor(visit.getPath()); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }); + if (descriptor == null) { + allExtensions.put(extKey, EXT_INFO_NONE); + return null; + } + ext = new ExtensionInfo(artifact, descriptor); + allExtensions.put(extKey, ext); + return ext; + } + + private DependencyNode collectDependencies(Artifact artifact, Collection exclusions, + List repos) { + DependencyNode root; + try { + root = resolver.getSystem() + .collectDependencies(resolver.getSession(), getCollectRequest(artifact, exclusions, repos)) + .getRoot(); + } catch (DependencyCollectionException e) { + throw new DeploymentInjectionException("Failed to collect dependencies for " + artifact, e); + } + if (root.getChildren().size() != 1) { + throw new DeploymentInjectionException("Only one child expected but got " + root.getChildren()); + } + return root.getChildren().get(0); + } + + private CollectRequest getCollectRequest(Artifact artifact, Collection exclusions, + List repos) { + final ArtifactDescriptorResult descr; + try { + descr = resolver.resolveDescriptor(artifact, repos); + } catch (BootstrapMavenException e) { + throw new DeploymentInjectionException("Failed to resolve descriptor for " + artifact, e); + } + final List allConstraints = new ArrayList<>( + managedDeps.size() + descr.getManagedDependencies().size()); + allConstraints.addAll(managedDeps); + allConstraints.addAll(descr.getManagedDependencies()); + return new CollectRequest() + .setManagedDependencies(allConstraints) + .setRepositories(repos) + .setRootArtifact(artifact) + .setDependencies(List.of(new Dependency(artifact, JavaScopes.COMPILE, false, exclusions))); + } + + private Artifact resolve(Artifact artifact, List repos) { + if (artifact.getFile() != null) { + return artifact; + } + try { + return resolver.getSystem().resolveArtifact(resolver.getSession(), + new ArtifactRequest() + .setArtifact(artifact) + .setRepositories(repos)) + .getArtifact(); + } catch (ArtifactResolutionException e) { + throw new DeploymentInjectionException("Failed to resolve artifact " + artifact, e); + } + } + + private static Properties readDescriptor(Path path) throws IOException { + final Properties rtProps = new Properties(); + try (BufferedReader reader = Files.newBufferedReader(path)) { + rtProps.load(reader); + } + return rtProps; + } + + private class ExtensionInfo { + + final Artifact runtimeArtifact; + final Properties props; + final Artifact deploymentArtifact; + final Artifact[] conditionalDeps; + final ArtifactKey[] dependencyCondition; + boolean activated; + + private ExtensionInfo() { + runtimeArtifact = null; + props = null; + deploymentArtifact = null; + conditionalDeps = null; + dependencyCondition = null; + } + + ExtensionInfo(Artifact runtimeArtifact, Properties props) throws BootstrapDependencyProcessingException { + this.runtimeArtifact = runtimeArtifact; + this.props = props; + + String value = props.getProperty(BootstrapConstants.PROP_DEPLOYMENT_ARTIFACT); + if (value == null) { + throw new BootstrapDependencyProcessingException("Extension descriptor from " + runtimeArtifact + + " does not include " + BootstrapConstants.PROP_DEPLOYMENT_ARTIFACT); + } + Artifact deploymentArtifact = toArtifact(value); + if (deploymentArtifact.getVersion() == null || deploymentArtifact.getVersion().isEmpty()) { + deploymentArtifact = deploymentArtifact.setVersion(runtimeArtifact.getVersion()); + } + this.deploymentArtifact = deploymentArtifact; + + value = props.getProperty(BootstrapConstants.CONDITIONAL_DEPENDENCIES); + if (value != null) { + final String[] deps = BootstrapUtils.splitByWhitespace(value); + conditionalDeps = new Artifact[deps.length]; + for (int i = 0; i < deps.length; ++i) { + try { + conditionalDeps[i] = toArtifact(deps[i]); + } catch (Exception e) { + throw new BootstrapDependencyProcessingException( + "Failed to parse conditional dependencies configuration of " + runtimeArtifact, e); + } + } + } else { + conditionalDeps = NO_ARTIFACTS; + } + + dependencyCondition = BootstrapUtils + .parseDependencyCondition(props.getProperty(BootstrapConstants.DEPENDENCY_CONDITION)); + } + + void ensureActivated() { + if (activated) { + return; + } + activated = true; + appBuilder.handleExtensionProperties(props, runtimeArtifact.toString()); + + final String providesCapabilities = props.getProperty(BootstrapConstants.PROP_PROVIDES_CAPABILITIES); + final String requiresCapabilities = props.getProperty(BootstrapConstants.PROP_REQUIRES_CAPABILITIES); + if (providesCapabilities != null || requiresCapabilities != null) { + appBuilder.addExtensionCapabilities( + CapabilityContract.of(toCompactCoords(runtimeArtifact), providesCapabilities, requiresCapabilities)); + } + } + } + + private class ExtensionDependency { + + static ExtensionDependency get(DependencyNode node) { + return (ExtensionDependency) node.getData().get(QUARKUS_EXTENSION_DEPENDENCY); + } + + final ExtensionInfo info; + final DependencyNode runtimeNode; + final Collection exclusions; + boolean conditionalDepsQueued; + private List extDeps; + private DependencyNode deploymentNode; + private DependencyNode parentNode; + + ExtensionDependency(ExtensionInfo info, DependencyNode node, Collection exclusions) { + this.runtimeNode = node; + this.info = info; + this.exclusions = exclusions; + + @SuppressWarnings("unchecked") + final Map data = (Map) node.getData(); + if (data.isEmpty()) { + node.setData(QUARKUS_EXTENSION_DEPENDENCY, this); + } else if (data.put(QUARKUS_EXTENSION_DEPENDENCY, this) != null) { + throw new IllegalStateException( + "Dependency node " + node + " has already been associated with an extension dependency"); + } + } + + DependencyNode getParentDeploymentNode() { + if (parentNode == null) { + return null; + } + var ext = ExtensionDependency.get(parentNode); + if (ext == null) { + return null; + } + return ext.deploymentNode == null ? ext.parentNode : ext.deploymentNode; + } + + void addExtensionDependency(ExtensionDependency dep) { + if (extDeps == null) { + extDeps = new ArrayList<>(); + } + extDeps.add(dep); + } + + private void collectDeploymentDeps() + throws BootstrapDependencyProcessingException { + log.debugf("Collecting dependencies of %s", info.deploymentArtifact); + deploymentNode = collectDependencies(info.deploymentArtifact, exclusions, runtimeNode.getRepositories()); + if (deploymentNode.getChildren().isEmpty()) { + throw new BootstrapDependencyProcessingException( + "Failed to collect dependencies of " + deploymentNode.getArtifact() + + ": either its POM could not be resolved from the available Maven repositories " + + "or the artifact does not have any dependencies while at least a dependency on the runtime artifact " + + info.runtimeArtifact + " is expected"); + } + if (!replaceDirectDepBranch(deploymentNode, true)) { + throw new BootstrapDependencyProcessingException( + "Quarkus extension deployment artifact " + deploymentNode.getArtifact() + + " does not appear to depend on the corresponding runtime artifact " + + info.runtimeArtifact); + } + } + + private void injectDeploymentNode(DependencyNode parentDeploymentNode) { + if (parentDeploymentNode == null) { + runtimeNode.setData(QUARKUS_RUNTIME_ARTIFACT, runtimeNode.getArtifact()); + runtimeNode.setArtifact(deploymentNode.getArtifact()); + runtimeNode.setChildren(deploymentNode.getChildren()); + } else { + parentDeploymentNode.getChildren().add(deploymentNode); + } + } + + private boolean replaceDirectDepBranch(DependencyNode parentNode, boolean replaceRuntimeNode) { + int i = 0; + DependencyNode inserted = null; + var childNodes = parentNode.getChildren(); + while (i < childNodes.size()) { + var node = childNodes.get(i); + final Artifact a = node.getArtifact(); + if (a != null && !hasWinner(node) && isSameKey(info.runtimeArtifact, a)) { + // we are not comparing the version in the above condition because the runtime version + // may appear to be different from the deployment one and that's ok + // e.g. the version of the runtime artifact could be managed by a BOM + // but overridden by the user in the project config. The way the deployment deps + // are resolved here, the deployment version of the runtime artifact will be the one from the BOM. + if (replaceRuntimeNode) { + inserted = new DefaultDependencyNode(runtimeNode); + inserted.setChildren(runtimeNode.getChildren()); + childNodes.set(i, inserted); + } else { + inserted = runtimeNode; + } + if (this.deploymentNode == null && this.parentNode == null) { + this.parentNode = parentNode; + } + break; + } + ++i; + } + if (inserted == null) { + return false; + } + + if (extDeps != null) { + var depQueue = new ArrayList<>(childNodes); + var exts = new ArrayList<>(extDeps); + for (int j = 0; j < depQueue.size(); ++j) { + var depNode = depQueue.get(j); + if (hasWinner(depNode)) { + continue; + } + for (int k = 0; k < exts.size(); ++k) { + if (exts.get(k).replaceDirectDepBranch(depNode, replaceRuntimeNode && depNode != inserted)) { + exts.remove(k); + break; + } + } + if (exts.isEmpty()) { + break; + } + depQueue.addAll(depNode.getChildren()); + } + } + + return true; + } + } + + private class ConditionalDependency { + + final AppDep appDep; + private boolean activated; + + private ConditionalDependency(ExtensionInfo info, AppDep parent) { + final DefaultDependencyNode rtNode = new DefaultDependencyNode( + new Dependency(info.runtimeArtifact, JavaScopes.COMPILE)); + rtNode.setVersion(new BootstrapArtifactVersion(info.runtimeArtifact.getVersion())); + rtNode.setVersionConstraint(new BootstrapArtifactVersionConstraint( + new BootstrapArtifactVersion(info.runtimeArtifact.getVersion()))); + rtNode.setRepositories(parent.ext.runtimeNode.getRepositories()); + + appDep = new AppDep(parent, rtNode); + appDep.ext = new ExtensionDependency(info, rtNode, parent.ext.exclusions); + } + + ExtensionDependency getExtensionDependency() { + return appDep.ext; + } + + void activate() { + if (activated) { + return; + } + activated = true; + final ExtensionDependency extDep = getExtensionDependency(); + final DependencyNode originalNode = collectDependencies(appDep.ext.info.runtimeArtifact, extDep.exclusions, + extDep.runtimeNode.getRepositories()); + final DefaultDependencyNode rtNode = (DefaultDependencyNode) extDep.runtimeNode; + rtNode.setRepositories(originalNode.getRepositories()); + // if this node has conditional dependencies on its own, they may have been activated by this time + // in which case they would be included into its children + List currentChildren = rtNode.getChildren(); + if (currentChildren == null || currentChildren.isEmpty()) { + rtNode.setChildren(originalNode.getChildren()); + } else { + currentChildren.addAll(originalNode.getChildren()); + } + + appDep.walkingFlags = COLLECT_DIRECT_DEPS; + if (collectReloadableModules) { + appDep.walkingFlags |= COLLECT_RELOADABLE_MODULES; + } + var futures = new ArrayList>(); + appDep.scheduleRuntimeVisit(futures); + CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); + if (logErrors()) { + throw new RuntimeException( + "Failed to process Quarkus application conditional dependencies, please see the errors logged above for more details."); + } + + appDep.setFlags(appDep.walkingFlags); + + var parentExtDep = appDep.parent; + parentExtDep.children.add(appDep); + while (parentExtDep != null) { + if (parentExtDep.ext != null) { + parentExtDep.ext.addExtensionDependency(appDep.ext); + break; + } + parentExtDep = parentExtDep.parent; + } + appDep.ext.info.ensureActivated(); + + appDep.parent.ext.runtimeNode.getChildren().add(rtNode); + } + + boolean isSatisfied() { + if (appDep.ext.info.dependencyCondition == null) { + return true; + } + for (ArtifactKey key : appDep.ext.info.dependencyCondition) { + if (!isRuntimeArtifact(key)) { + return false; + } + } + return true; + } + } + + private static boolean isSameKey(Artifact a1, Artifact a2) { + return a2.getArtifactId().equals(a1.getArtifactId()) + && a2.getGroupId().equals(a1.getGroupId()) + && a2.getClassifier().equals(a1.getClassifier()) + && a2.getExtension().equals(a1.getExtension()); + } + + private static String toCompactCoords(Artifact a) { + final StringBuilder b = new StringBuilder(); + b.append(a.getGroupId()).append(':').append(a.getArtifactId()).append(':'); + if (!a.getClassifier().isEmpty()) { + b.append(a.getClassifier()).append(':'); + } + if (!ArtifactCoords.TYPE_JAR.equals(a.getExtension())) { + b.append(a.getExtension()).append(':'); + } + b.append(a.getVersion()); + return b.toString(); + } + + private class AppDepLogger { + + final List depth = new ArrayList<>(); + + private AppDepLogger() { + } + + void log(AppDep root) { + logInternal(root); + + final int childrenTotal = root.children.size(); + if (childrenTotal > 0) { + if (childrenTotal == 1) { + depth.add(false); + log(root.children.get(0)); + } else { + depth.add(true); + int i = 0; + while (i < childrenTotal) { + log(root.children.get(i++)); + if (i == childrenTotal - 1) { + depth.set(depth.size() - 1, false); + } + } + } + depth.remove(depth.size() - 1); + } + } + + private void logInternal(AppDep dep) { + var buf = new StringBuilder(); + if (!depth.isEmpty()) { + for (int i = 0; i < depth.size() - 1; ++i) { + if (depth.get(i)) { + //buf.append("| "); + buf.append('\u2502').append(" "); + } else { + buf.append(" "); + } + } + if (depth.get(depth.size() - 1)) { + //buf.append("|- "); + buf.append('\u251c').append('\u2500').append(' '); + } else { + //buf.append("\\- "); + buf.append('\u2514').append('\u2500').append(' '); + } + } + buf.append(dep.node.getArtifact()); + if (!depth.isEmpty()) { + appendFlags(buf, getResolvedDependency(getKey(dep.node.getArtifact()))); + } + depLogging.getMessageConsumer().accept(buf.toString()); + + if (depLogging.isGraph()) { + var depKeys = artifactDeps.get(getCoords(dep.node.getArtifact())); + if (depKeys != null && !depKeys.isEmpty() && depKeys.size() != dep.children.size()) { + final Map versions = new HashMap<>(dep.children.size()); + for (var c : dep.children) { + versions.put(getKey(c.node.getArtifact()), c.node.getArtifact().getVersion()); + } + var list = new ArrayList(depKeys.size() - dep.children.size()); + for (var key : depKeys) { + if (!versions.containsKey(key)) { + var d = getResolvedDependency(key); + var sb = new StringBuilder().append(d.toGACTVString()); + appendFlags(sb, d); + list.add(sb.append(" [+]").toString()); + } + } + Collections.sort(list); + for (int j = 0; j < list.size(); ++j) { + buf = new StringBuilder(); + if (!depth.isEmpty()) { + for (int i = 0; i < depth.size() - 1; ++i) { + if (depth.get(i)) { + //buf.append("| "); + buf.append('\u2502').append(" "); + } else { + buf.append(" "); + } + } + if (depth.get(depth.size() - 1)) { + //buf.append("| "); + buf.append('\u2502').append(" "); + } else { + buf.append(" "); + } + } + + if (j < list.size() - 1) { + //buf.append("|- "); + buf.append('\u251c').append('\u2500').append(' '); + } else if (dep.children.isEmpty()) { + //buf.append("\\- "); + buf.append('\u2514').append('\u2500').append(' '); + } else { + //buf.append("|- "); + buf.append('\u251c').append('\u2500').append(' '); + } + buf.append(list.get(j)); + depLogging.getMessageConsumer().accept(buf.toString()); + } + } + } + } + + private void appendFlags(StringBuilder sb, ResolvedDependencyBuilder d) { + sb.append(" (").append(d.getScope()); + if (d.isFlagSet(DependencyFlags.OPTIONAL)) { + sb.append(" optional"); + } + if (depLogging.isVerbose()) { + if (d.isFlagSet(DependencyFlags.RUNTIME_CP)) { + sb.append(", runtime classpath"); + } else { + sb.append(", build-time classpath"); + } + if (d.isFlagSet(DependencyFlags.RUNTIME_EXTENSION_ARTIFACT)) { + sb.append(", extension"); + } + if (d.isFlagSet(DependencyFlags.RELOADABLE)) { + sb.append(", reloadable"); + } + } + sb.append(')'); + } + + private ResolvedDependencyBuilder getResolvedDependency(ArtifactKey key) { + var d = appBuilder.getDependency(key); + if (d == null) { + throw new IllegalArgumentException(key + " is not found among application dependencies"); + } + return d; + } + } +} diff --git a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/ApplicationDependencyTreeResolver.java b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/ApplicationDependencyTreeResolver.java index 7a3fd7574fb5d..006cd4923d840 100644 --- a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/ApplicationDependencyTreeResolver.java +++ b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/ApplicationDependencyTreeResolver.java @@ -202,7 +202,6 @@ public void resolve(CollectRequest collectRtDepsRequest) throws AppModelResolver } } - // resolve and inject deployment dependency branches for the top (first met) runtime extension nodes for (ExtensionDependency extDep : topExtensionDeps) { injectDeploymentDependencies(extDep); } @@ -868,7 +867,7 @@ private ConditionalDependency(ExtensionInfo info, ExtensionDependency dependent) ExtensionDependency getExtensionDependency() { if (dependency == null) { final DefaultDependencyNode rtNode = new DefaultDependencyNode( - new Dependency(info.runtimeArtifact, JavaScopes.RUNTIME)); + new Dependency(info.runtimeArtifact, JavaScopes.COMPILE)); rtNode.setVersion(new BootstrapArtifactVersion(info.runtimeArtifact.getVersion())); rtNode.setVersionConstraint(new BootstrapArtifactVersionConstraint( new BootstrapArtifactVersion(info.runtimeArtifact.getVersion()))); 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 14d4f5ef80f2b..c489b9c958b6a 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 @@ -57,13 +57,7 @@ public List resolveArtifacts(RepositorySystemSession session, Collection requests) throws ArtifactResolutionException { return repoSystem.resolveArtifacts(session, requests); } - }, new VersionRangeResolver() { - @Override - public VersionRangeResult resolveVersionRange(RepositorySystemSession session, - VersionRangeRequest request) throws VersionRangeResolutionException { - return repoSystem.resolveVersionRange(session, request); - } - }, ctx.getRemoteRepositoryManager(), ctx.getRemoteRepositories()); + }, repoSystem::resolveVersionRange, ctx.getRemoteRepositoryManager(), ctx.getRemoteRepositories()); } private final RepositorySystemSession session; diff --git a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/DependencyLoggingConfig.java b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/DependencyLoggingConfig.java new file mode 100644 index 0000000000000..d9cb55946daac --- /dev/null +++ b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/DependencyLoggingConfig.java @@ -0,0 +1,65 @@ +package io.quarkus.bootstrap.resolver.maven; + +import java.util.function.Consumer; + +public class DependencyLoggingConfig { + + public static Builder builder() { + return new DependencyLoggingConfig().new Builder(); + } + + public class Builder { + + private boolean built; + + private Builder() { + } + + public Builder setGraph(boolean graph) { + if (!built) { + DependencyLoggingConfig.this.graph = graph; + } + return this; + } + + public Builder setVerbose(boolean verbose) { + if (!built) { + DependencyLoggingConfig.this.verbose = verbose; + } + return this; + } + + public Builder setMessageConsumer(Consumer msgConsumer) { + if (!built) { + DependencyLoggingConfig.this.msgConsumer = msgConsumer; + } + return this; + } + + public DependencyLoggingConfig build() { + if (!built) { + built = true; + if (msgConsumer == null) { + throw new IllegalArgumentException("msgConsumer has not been initialized"); + } + } + return DependencyLoggingConfig.this; + } + } + + private boolean verbose; + private boolean graph; + private Consumer msgConsumer; + + public boolean isGraph() { + return graph; + } + + public boolean isVerbose() { + return verbose; + } + + public Consumer getMessageConsumer() { + return msgConsumer; + } +} diff --git a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/workspace/ModelUtils.java b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/workspace/ModelUtils.java index 9786fbb004fc0..97f6c8f1456d4 100644 --- a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/workspace/ModelUtils.java +++ b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/workspace/ModelUtils.java @@ -1,9 +1,9 @@ package io.quarkus.bootstrap.resolver.maven.workspace; import java.io.BufferedReader; +import java.io.BufferedWriter; import java.io.IOException; import java.io.InputStream; -import java.io.UncheckedIOException; import java.nio.file.DirectoryStream; import java.nio.file.FileSystem; import java.nio.file.Files; @@ -16,9 +16,10 @@ import org.apache.maven.model.Model; import org.apache.maven.model.Parent; +import org.apache.maven.model.io.xpp3.MavenXpp3Reader; +import org.apache.maven.model.io.xpp3.MavenXpp3Writer; +import org.codehaus.plexus.util.xml.pull.XmlPullParserException; -import io.fabric8.maven.Maven; -import io.fabric8.maven.XMLFormat; import io.quarkus.bootstrap.util.PropertyUtils; import io.quarkus.fs.util.ZipUtils; import io.quarkus.maven.dependency.ArtifactCoords; @@ -231,30 +232,21 @@ private static Properties loadPomProps(Path appJar, Path artifactIdPath) throws } public static Model readModel(final Path pomXml) throws IOException { - try { - return Maven.readModel(pomXml); - } catch (UncheckedIOException e) { - throw e.getCause(); - } catch (RuntimeException e) { - throw new IOException("Failed to read model", e.getCause()); - } + return readModel(Files.newInputStream(pomXml)); } public static Model readModel(InputStream stream) throws IOException { try (InputStream is = stream) { - return Maven.readModel(is); - } catch (UncheckedIOException e) { - throw e.getCause(); - } catch (RuntimeException e) { - throw new IOException("Failed to read model", e.getCause()); + return new MavenXpp3Reader().read(stream); + } catch (XmlPullParserException e) { + throw new IOException("Failed to parse POM", e); } } public static void persistModel(Path pomFile, Model model) throws IOException { - try { - Maven.writeModel(model, pomFile, XMLFormat.builder().indent(" ").insertLineBreakBetweenMajorSections().build()); - } catch (UncheckedIOException e) { - throw e.getCause(); + final MavenXpp3Writer xpp3Writer = new MavenXpp3Writer(); + try (BufferedWriter pomFileWriter = Files.newBufferedWriter(pomFile)) { + xpp3Writer.write(pomFileWriter, 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 66998179e9e7c..cc5f42ddec40a 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 @@ -10,6 +10,7 @@ import org.eclipse.aether.artifact.DefaultArtifact; import org.eclipse.aether.graph.Dependency; import org.eclipse.aether.graph.DependencyNode; +import org.eclipse.aether.util.graph.transformer.ConflictResolver; import io.quarkus.bootstrap.resolver.maven.BootstrapMavenException; import io.quarkus.bootstrap.resolver.maven.MavenArtifactResolver; @@ -152,4 +153,13 @@ public static ResolvedDependencyBuilder toAppArtifact(Artifact artifact, Workspa .setVersion(artifact.getVersion()) .setResolvedPaths(artifact.getFile() == null ? PathList.empty() : PathList.of(artifact.getFile().toPath())); } + + public static boolean hasWinner(DependencyNode node) { + return node.getData().containsKey(ConflictResolver.NODE_DATA_WINNER) && node.getChildren().isEmpty(); + } + + public static DependencyNode getWinner(DependencyNode node) { + final DependencyNode winner = (DependencyNode) node.getData().get(ConflictResolver.NODE_DATA_WINNER); + return winner == null || !node.getChildren().isEmpty() ? null : winner; + } } diff --git a/independent-projects/bootstrap/pom.xml b/independent-projects/bootstrap/pom.xml index e6c49f43ba8c4..87bd3e65cc0fb 100644 --- a/independent-projects/bootstrap/pom.xml +++ b/independent-projects/bootstrap/pom.xml @@ -80,7 +80,6 @@ 0.1.3 2.23.0 1.9.0 - 36 bom diff --git a/independent-projects/bootstrap/runner/src/main/java/io/quarkus/bootstrap/runner/RunnerClassLoader.java b/independent-projects/bootstrap/runner/src/main/java/io/quarkus/bootstrap/runner/RunnerClassLoader.java index f0047050d8d62..62c02e8fe08d3 100644 --- a/independent-projects/bootstrap/runner/src/main/java/io/quarkus/bootstrap/runner/RunnerClassLoader.java +++ b/independent-projects/bootstrap/runner/src/main/java/io/quarkus/bootstrap/runner/RunnerClassLoader.java @@ -26,6 +26,10 @@ */ public final class RunnerClassLoader extends ClassLoader { + static { + registerAsParallelCapable(); + } + /** * A map of resources by dir name. Root dir/default package is represented by the empty string */ @@ -101,18 +105,55 @@ public Class loadClass(String name, boolean resolve) throws ClassNotFoundExce continue; } definePackage(packageName, resources); - try { - return defineClass(name, data, 0, data.length, resource.getProtectionDomain()); - } catch (LinkageError e) { - loaded = findLoadedClass(name); - if (loaded != null) { - return loaded; + return defineClass(name, data, resource); + } + } + return getParent().loadClass(name); + } + + private void definePackage(String pkgName, ClassLoadingResource[] resources) { + if ((pkgName != null) && getDefinedPackage(pkgName) == null) { + for (ClassLoadingResource classPathElement : resources) { + ManifestInfo mf = classPathElement.getManifestInfo(); + if (mf != null) { + try { + definePackage(pkgName, mf.getSpecTitle(), + mf.getSpecVersion(), + mf.getSpecVendor(), + mf.getImplTitle(), + mf.getImplVersion(), + mf.getImplVendor(), null); + } catch (IllegalArgumentException e) { + var loaded = getDefinedPackage(pkgName); + if (loaded == null) { + throw e; + } } + return; + } + } + try { + definePackage(pkgName, null, null, null, null, null, null, null); + } catch (IllegalArgumentException e) { + var loaded = getDefinedPackage(pkgName); + if (loaded == null) { throw e; } } } - return getParent().loadClass(name); + } + + private Class defineClass(String name, byte[] data, ClassLoadingResource resource) { + Class loaded; + try { + return defineClass(name, data, 0, data.length, resource.getProtectionDomain()); + } catch (LinkageError e) { + loaded = findLoadedClass(name); + if (loaded != null) { + return loaded; + } + throw e; + } } private void accessingResource(final ClassLoadingResource resource) { @@ -219,28 +260,6 @@ protected Enumeration findResources(String name) { return Collections.enumeration(urls); } - private void definePackage(String pkgName, ClassLoadingResource[] resources) { - if ((pkgName != null) && getPackage(pkgName) == null) { - synchronized (getClassLoadingLock(pkgName)) { - if (getPackage(pkgName) == null) { - for (ClassLoadingResource classPathElement : resources) { - ManifestInfo mf = classPathElement.getManifestInfo(); - if (mf != null) { - definePackage(pkgName, mf.getSpecTitle(), - mf.getSpecVersion(), - mf.getSpecVendor(), - mf.getImplTitle(), - mf.getImplVersion(), - mf.getImplVendor(), null); - return; - } - } - definePackage(pkgName, null, null, null, null, null, null, null); - } - } - } - } - private String getPackageNameFromClassName(String className) { final int index = className.lastIndexOf('.'); if (index == -1) { @@ -316,4 +335,23 @@ public void beforeCheckpoint(Context ctx) { public void afterRestore(Context ctx) { } } + + @Override + public boolean equals(Object o) { + //see comment in hashCode + return this == o; + } + + @Override + public int hashCode() { + //We can return a constant as we expect to have a single instance of these; + //this is useful to avoid triggering a call to the identity hashcode, + //which could be rather inefficient as there's good chances that some component + //will have inflated the monitor of this instance. + //A hash collision would be unfortunate but unexpected, and shouldn't be a problem + //as the equals implementation still does honour the identity contract . + //See also discussion on https://github.com/smallrye/smallrye-context-propagation/pull/443 + return 1; + } + } diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateInstance.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateInstance.java index 225cf8fe31215..5b5a2f1f9a42a 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateInstance.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateInstance.java @@ -1,7 +1,9 @@ package io.quarkus.qute; +import java.util.Locale; import java.util.concurrent.CompletionStage; import java.util.function.Consumer; +import java.util.function.Function; import io.smallrye.mutiny.Multi; import io.smallrye.mutiny.Uni; @@ -30,9 +32,14 @@ public interface TemplateInstance { */ String SELECTED_VARIANT = "selectedVariant"; + /** + * Attribute key - locale. + */ + String LOCALE = "locale"; + /** * Set the the root data object. Invocation of this method removes any data set previously by - * {@link #data(String, Object)}. + * {@link #data(String, Object)} and {@link #computedData(String, Function)}. * * @param data * @return @@ -42,8 +49,8 @@ default TemplateInstance data(Object data) { } /** - * Put the data in a map. The map will be used as the root context object during rendering. Invocation of this - * method removes the root data object previously set by {@link #data(Object)}. + * Put the data in a map. The map will be used as the root context object during rendering. Remove the root data object + * previously set by {@link #data(Object)}. * * @param key * @param data @@ -54,7 +61,21 @@ default TemplateInstance data(String key, Object data) { } /** + * Associates the specified mapping function with the specified key. The function is applied each time a value for the given + * key is requested. Also removes the root data object previously set by {@link #data(Object)}. + *

+ * If the key is already associated with a value using the {@link #data(String, Object)} method then the mapping function is + * never used. * + * @param key + * @param function + * @return self + */ + default TemplateInstance computedData(String key, Function function) { + throw new UnsupportedOperationException(); + } + + /** * @param key * @param value * @return self @@ -64,7 +85,6 @@ default TemplateInstance setAttribute(String key, Object value) { } /** - * * @param key * @return the attribute or null */ @@ -126,7 +146,6 @@ default CompletionStage consume(Consumer consumer) { } /** - * * @return the timeout * @see TemplateInstance#TIMEOUT */ @@ -135,7 +154,6 @@ default long getTimeout() { } /** - * * @return the original template */ default Template getTemplate() { @@ -143,7 +161,6 @@ default Template getTemplate() { } /** - * * @param id * @return the fragment or {@code null} * @see Template#getFragment(String) @@ -162,6 +179,38 @@ default TemplateInstance onRendered(Runnable action) { throw new UnsupportedOperationException(); } + /** + * Sets the {@code locale} attribute that can be used to localize parts of the template, i.e. to specify the locale for all + * message bundle expressions in the template. + * + * @param locale a language tag + * @return self + */ + default TemplateInstance setLocale(String locale) { + return setAttribute(LOCALE, Locale.forLanguageTag(locale)); + } + + /** + * Sets the {@code locale} attribute that can be used to localize parts of the template, i.e. to specify the locale for all + * message bundle expressions in the template. + * + * @param locale a {@link Locale} instance + * @return self + */ + default TemplateInstance setLocale(Locale locale) { + return setAttribute(LOCALE, locale); + } + + /** + * Sets the variant attribute that can be used to select a specific variant of the template. + * + * @param variant the variant + * @return self + */ + default TemplateInstance setVariant(Variant variant) { + return setAttribute(SELECTED_VARIANT, variant); + } + /** * This component can be used to initialize a template instance, i.e. the data and attributes. * diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateInstanceBase.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateInstanceBase.java index 98344d31f0f5a..b0fe17297597d 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateInstanceBase.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateInstanceBase.java @@ -3,8 +3,13 @@ import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.Function; import org.jboss.logging.Logger; @@ -16,7 +21,7 @@ public abstract class TemplateInstanceBase implements TemplateInstance { static final Map EMPTY_DATA_MAP = Collections.singletonMap(DATA_MAP_KEY, true); protected Object data; - protected Map dataMap; + protected DataMap dataMap; protected final Map attributes; protected List renderedActions; @@ -35,10 +40,19 @@ public TemplateInstance data(Object data) { public TemplateInstance data(String key, Object data) { this.data = null; if (dataMap == null) { - dataMap = new HashMap(); - dataMap.put(DATA_MAP_KEY, true); + dataMap = new DataMap(); } - dataMap.put(key, data); + dataMap.put(Objects.requireNonNull(key), data); + return this; + } + + @Override + public TemplateInstance computedData(String key, Function function) { + this.data = null; + if (dataMap == null) { + dataMap = new DataMap(); + } + dataMap.computed(Objects.requireNonNull(key), Objects.requireNonNull(function)); return this; } @@ -86,11 +100,71 @@ protected Object data() { return data; } if (dataMap != null) { - return Mapper.wrap(dataMap); + return dataMap; } return EMPTY_DATA_MAP; } protected abstract Engine engine(); + public static class DataMap implements Mapper { + + private final Map map = new HashMap<>(); + private Map> computations = null; + + void put(String key, Object value) { + map.put(key, value); + } + + void computed(String key, Function function) { + if (!map.containsKey(key)) { + if (computations == null) { + computations = new HashMap<>(); + } + computations.put(key, function); + } + } + + @Override + public Object get(String key) { + Object val = map.get(key); + if (val == null) { + if (key.equals(DATA_MAP_KEY)) { + return true; + } else if (computations != null) { + Function fun = computations.get(key); + if (fun != null) { + return fun.apply(key); + } + } + } + return val; + } + + @Override + public boolean appliesTo(String key) { + return map.containsKey(key) || (computations != null && computations.containsKey(key)); + } + + @Override + public Set mappedKeys() { + Set ret = new HashSet<>(map.keySet()); + if (computations != null) { + ret.addAll(computations.keySet()); + } + return ret; + } + + public void forEachData(BiConsumer action) { + map.forEach(action); + } + + public void forEachComputedData(BiConsumer> action) { + if (computations != null) { + computations.forEach(action); + } + } + + } + } diff --git a/independent-projects/qute/core/src/test/java/io/quarkus/qute/TemplateInstanceTest.java b/independent-projects/qute/core/src/test/java/io/quarkus/qute/TemplateInstanceTest.java index 8481acf73b882..1ee22f343b255 100644 --- a/independent-projects/qute/core/src/test/java/io/quarkus/qute/TemplateInstanceTest.java +++ b/independent-projects/qute/core/src/test/java/io/quarkus/qute/TemplateInstanceTest.java @@ -1,8 +1,10 @@ package io.quarkus.qute; 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.Locale; import java.util.concurrent.atomic.AtomicBoolean; import org.junit.jupiter.api.Test; @@ -41,4 +43,55 @@ public void testGetTemplate() { String generatedId = hello.getGeneratedId(); assertEquals(generatedId, hello.instance().getTemplate().getGeneratedId()); } + + @Test + public void testComputeData() { + Engine engine = Engine.builder().addDefaults().build(); + TemplateInstance instance = engine.parse("Hello {foo} and {bar}!").instance(); + AtomicBoolean barUsed = new AtomicBoolean(); + AtomicBoolean fooUsed = new AtomicBoolean(); + instance + .computedData("bar", key -> { + barUsed.set(true); + return key.length(); + }) + .data("bar", 30) + .computedData("foo", key -> { + fooUsed.set(true); + return key.toUpperCase(); + }); + assertFalse(fooUsed.get()); + assertEquals("Hello FOO and 30!", instance.render()); + assertTrue(fooUsed.get()); + assertFalse(barUsed.get()); + } + + @Test + public void testLocale() throws Exception { + Engine engine = Engine.builder().addDefaults() + .addValueResolver(ValueResolver.builder() + .applyToName("locale") + .resolveSync(ctx -> ctx.getAttribute(TemplateInstance.LOCALE)) + .build()) + .build(); + Template hello = engine.parse("Hello {locale}!"); + assertEquals("Hello fr!", hello.instance().setLocale(Locale.FRENCH).render()); + } + + @Test + public void testVariant() { + Engine engine = Engine.builder().addDefaults() + .addValueResolver(ValueResolver.builder() + .applyToName("variant") + .resolveSync(ctx -> ctx.getAttribute(TemplateInstance.SELECTED_VARIANT)) + .build()) + .addValueResolver(ValueResolver.builder() + .appliesTo(ctx -> ctx.getBase() instanceof Variant && ctx.getName().equals("contentType")) + .resolveSync(ctx -> ((Variant) ctx.getBase()).getContentType()) + .build()) + .build(); + Template hello = engine.parse("Hello {variant.contentType}!"); + String render = hello.instance().setVariant(Variant.forContentType(Variant.TEXT_HTML)).render(); + assertEquals("Hello text/html!", render); + } } diff --git a/independent-projects/qute/generator/src/main/java/io/quarkus/qute/generator/Descriptors.java b/independent-projects/qute/generator/src/main/java/io/quarkus/qute/generator/Descriptors.java index 45280fd0fd451..2736d81b39583 100644 --- a/independent-projects/qute/generator/src/main/java/io/quarkus/qute/generator/Descriptors.java +++ b/independent-projects/qute/generator/src/main/java/io/quarkus/qute/generator/Descriptors.java @@ -5,6 +5,7 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; import java.util.function.BiConsumer; +import java.util.function.Function; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -95,6 +96,9 @@ private Descriptors() { NotFound.class, EvalContext.class); public static final MethodDescriptor TEMPLATE_INSTANCE_DATA = MethodDescriptor.ofMethod(TemplateInstance.class, "data", TemplateInstance.class, String.class, Object.class); + public static final MethodDescriptor TEMPLATE_INSTANCE_COMPUTED_DATA = MethodDescriptor.ofMethod(TemplateInstance.class, + "computedData", + TemplateInstance.class, String.class, Function.class); public static final FieldDescriptor EVALUATED_PARAMS_STAGE = FieldDescriptor.of(EvaluatedParams.class, "stage", CompletionStage.class); 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 6d259cb04ee9d..02dfc4bee58d0 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 @@ -11,6 +11,7 @@ import java.util.Set; import java.util.concurrent.CompletionStage; import java.util.function.Consumer; +import java.util.function.Function; import org.jboss.jandex.AnnotationTarget; import org.jboss.jandex.ClassInfo; @@ -24,6 +25,7 @@ import io.quarkus.gizmo.ClassCreator; import io.quarkus.gizmo.ClassOutput; import io.quarkus.gizmo.FieldDescriptor; +import io.quarkus.gizmo.FunctionCreator; import io.quarkus.gizmo.MethodCreator; import io.quarkus.gizmo.MethodDescriptor; import io.quarkus.gizmo.ResultHandle; @@ -74,22 +76,27 @@ public void generate(ClassInfo declaringClass, Map tar for (Entry entry : targets.entrySet()) { ResultHandle name = accept.load(entry.getKey()); + FunctionCreator fun = accept.createFunction(Function.class); + BytecodeCreator funBytecode = fun.getBytecode(); ResultHandle global; switch (entry.getValue().kind()) { case FIELD: FieldInfo field = entry.getValue().asField(); validate(field); - global = accept.readStaticField(FieldDescriptor.of(field)); + global = funBytecode.readStaticField(FieldDescriptor.of(field)); break; case METHOD: MethodInfo method = entry.getValue().asMethod(); validate(method); - global = accept.invokeStaticMethod(MethodDescriptor.of(method)); + global = funBytecode.invokeStaticMethod(MethodDescriptor.of(method)); break; default: throw new IllegalStateException("Unsupported target: " + entry.getValue()); } - accept.invokeInterfaceMethod(Descriptors.TEMPLATE_INSTANCE_DATA, accept.getMethodParam(0), name, global); + funBytecode.returnValue(global); + // Global variables are computed lazily + accept.invokeInterfaceMethod(Descriptors.TEMPLATE_INSTANCE_COMPUTED_DATA, accept.getMethodParam(0), name, + fun.getInstance()); } accept.returnValue(null); diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/api/ClientMultipartForm.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/api/ClientMultipartForm.java index 7b7bc74d9c220..b2fcbb5852922 100644 --- a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/api/ClientMultipartForm.java +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/api/ClientMultipartForm.java @@ -7,6 +7,7 @@ import org.jboss.resteasy.reactive.client.impl.multipart.QuarkusMultipartForm; import org.jboss.resteasy.reactive.client.impl.multipart.QuarkusMultipartFormDataPart; +import org.jboss.resteasy.reactive.multipart.FileUpload; import io.smallrye.mutiny.Multi; import io.vertx.core.buffer.Buffer; @@ -86,4 +87,9 @@ public ClientMultipartForm multiAsTextFileUpload(String name, String filename, M return this; } + public ClientMultipartForm fileUpload(FileUpload fileUpload) { + binaryFileUpload(fileUpload.name(), fileUpload.fileName(), fileUpload.filePath().toString(), fileUpload.contentType()); + return this; + } + } diff --git a/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/multipart/FileDownload.java b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/multipart/FileDownload.java index 8a797e2766aee..25f48b6819786 100644 --- a/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/multipart/FileDownload.java +++ b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/multipart/FileDownload.java @@ -1,4 +1,9 @@ package org.jboss.resteasy.reactive.multipart; +/** + * Represent a file that should be pushed to the client. + *

+ * WARNING: This type is currently only supported on the server + */ public interface FileDownload extends FilePart { } diff --git a/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/multipart/FileUpload.java b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/multipart/FileUpload.java index 7576168d51aa6..b844cf4eab250 100644 --- a/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/multipart/FileUpload.java +++ b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/multipart/FileUpload.java @@ -2,6 +2,11 @@ import java.nio.file.Path; +/** + * Represent a file that has been uploaded. + *

+ * This type is usually used on server, but it is also supported in the REST Client. + */ public interface FileUpload extends FilePart { /** diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/handlers/VariableProducesHandler.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/handlers/VariableProducesHandler.java index 6c1e712350e67..77faad733c928 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/handlers/VariableProducesHandler.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/handlers/VariableProducesHandler.java @@ -44,8 +44,18 @@ public void handle(ResteasyReactiveRequestContext requestContext) throws Excepti //TODO? return; } - MediaType res = mediaTypeList.negotiateProduces(requestContext.serverRequest().getRequestHeader(HttpHeaders.ACCEPT)) - .getKey(); + MediaType res = null; + List accepts = requestContext.getHttpHeaders().getRequestHeader(HttpHeaders.ACCEPT); + for (String accept : accepts) { + res = mediaTypeList.negotiateProduces(accept).getKey(); + if (res != null) { + break; + } + } + if (res == null) { // fallback for some tests + res = mediaTypeList.negotiateProduces(requestContext.serverRequest().getRequestHeader(HttpHeaders.ACCEPT)) + .getKey(); + } if (res == null) { throw new WebApplicationException(Response .notAcceptable(Variant.mediaTypes(mediaTypeList.getSortedMediaTypes()).build()) diff --git a/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/matching/PreMatchAcceptInHeader.java b/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/matching/PreMatchAcceptInHeader.java deleted file mode 100644 index cfbd44a4795fc..0000000000000 --- a/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/matching/PreMatchAcceptInHeader.java +++ /dev/null @@ -1,124 +0,0 @@ -package org.jboss.resteasy.reactive.server.vertx.test.matching; - -import static io.restassured.RestAssured.given; -import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.equalTo; - -import java.util.function.Supplier; - -import jakarta.ws.rs.GET; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.Produces; -import jakarta.ws.rs.container.ContainerRequestContext; -import jakarta.ws.rs.container.ContainerRequestFilter; -import jakarta.ws.rs.container.PreMatching; -import jakarta.ws.rs.core.HttpHeaders; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.MultivaluedMap; -import jakarta.ws.rs.ext.Provider; - -import org.jboss.resteasy.reactive.server.vertx.test.framework.ResteasyReactiveUnitTest; -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; - -public class PreMatchAcceptInHeader { - - @RegisterExtension - static ResteasyReactiveUnitTest test = new ResteasyReactiveUnitTest() - .setArchiveProducer(new Supplier<>() { - @Override - public JavaArchive get() { - return ShrinkWrap.create(JavaArchive.class) - .addClass(PathSegmentTest.Resource.class); - } - }); - - @Test - void browserDefault() { - given().accept("text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8") - .when() - .get("test") - .then() - .statusCode(200) - .body(containsString("")); - } - - @Test - void text() { - given().accept("text/plain") - .when() - .get("test") - .then() - .statusCode(200) - .body(equalTo("test")); - } - - @Test - void html() { - given().accept("text/html") - .when() - .get("test") - .then() - .statusCode(200) - .body(equalTo("test")); - } - - @Test - void json() { - given().accept("application/json") - .when() - .get("test") - .then() - .statusCode(404); - } - - @Test - void setAcceptToTextInFilter() { - given().accept("application/json") - .header("x-set-accept-to-text", "true") - .when() - .get("test") - .then() - .statusCode(200) - .body(equalTo("test")); - } - - @Path("/test") - public static class Resource { - - @GET - @Produces(MediaType.TEXT_PLAIN) - public String text() { - return "text"; - } - - @GET - @Produces(MediaType.TEXT_HTML) - public String html() { - return """ - - - - - Hello World - - - """; - } - } - - @PreMatching - @Provider - public static class SetAcceptHeaderFilter implements ContainerRequestFilter { - - @Override - public void filter(ContainerRequestContext requestContext) { - MultivaluedMap headers = requestContext.getHeaders(); - if ("true".equals(headers.getFirst("x-set-accept-to-text"))) { - headers.putSingle(HttpHeaders.ACCEPT, MediaType.TEXT_PLAIN); - } - } - } -} diff --git a/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/matching/PreMatchAcceptInHeaderTest.java b/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/matching/PreMatchAcceptInHeaderTest.java new file mode 100644 index 0000000000000..16b31cd54b127 --- /dev/null +++ b/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/matching/PreMatchAcceptInHeaderTest.java @@ -0,0 +1,230 @@ +package org.jboss.resteasy.reactive.server.vertx.test.matching; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; + +import java.io.IOException; +import java.io.OutputStream; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.util.function.Supplier; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.container.ContainerRequestContext; +import jakarta.ws.rs.container.ContainerRequestFilter; +import jakarta.ws.rs.container.PreMatching; +import jakarta.ws.rs.core.HttpHeaders; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.MultivaluedMap; +import jakarta.ws.rs.ext.Provider; + +import org.jboss.resteasy.reactive.server.spi.ResteasyReactiveResourceInfo; +import org.jboss.resteasy.reactive.server.spi.ServerMessageBodyWriter; +import org.jboss.resteasy.reactive.server.spi.ServerRequestContext; +import org.jboss.resteasy.reactive.server.vertx.test.framework.ResteasyReactiveUnitTest; +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; + +public class PreMatchAcceptInHeaderTest { + + @RegisterExtension + static ResteasyReactiveUnitTest test = new ResteasyReactiveUnitTest() + .setArchiveProducer(new Supplier<>() { + @Override + public JavaArchive get() { + return ShrinkWrap.create(JavaArchive.class); + } + }); + + @Test + void browserDefault() { + given().accept("text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8") + .when() + .get("test") + .then() + .statusCode(200) + .body(containsString("")); + } + + @Test + void text() { + given().accept("text/plain") + .when() + .get("test") + .then() + .statusCode(200) + .body(equalTo("text")); + } + + @Test + void html() { + given().accept("text/html") + .when() + .get("test") + .then() + .statusCode(200) + .body(containsString("")); + } + + @Test + void json() { + given().accept("application/json") + .when() + .get("test") + .then() + .statusCode(406); + } + + @Test + void setAcceptToTextInFilter() { + given().accept("application/json") + .header("x-set-accept-to-text", "true") + .when() + .get("test") + .then() + .statusCode(200) + .body(equalTo("text")); + } + + @Test + void entityJsonWithoutAcceptToTextInFilter() { + given().accept("application/json") + .when() + .get("test/entity") + .then() + .statusCode(200) + .body(containsString("\"text\"")); + } + + @Test + void entityTextWithoutAcceptToTextInFilter() { + given().accept("text/plain") + .when() + .get("test/entity") + .then() + .statusCode(200) + .body(equalTo("text")); + } + + @Test + void entityTextWithAcceptToTextInFilter() { + given().accept("application/json") + .header("x-set-accept-to-text", "true") + .when() + .get("test/entity") + .then() + .statusCode(200) + .body(equalTo("text")); + } + + @Path("/test") + public static class Resource { + + @GET + @Produces(MediaType.TEXT_PLAIN) + public String text() { + return "text"; + } + + @GET + @Produces(MediaType.TEXT_HTML) + public String html() { + return """ + + + + + Hello World + + + """; + } + + @GET + @Path("entity") + @Produces({ MediaType.TEXT_PLAIN, MediaType.APPLICATION_JSON }) + public Entity entity() { + return new Entity("text"); + } + } + + public record Entity(String value) { + } + + @PreMatching + @Provider + public static class SetAcceptHeaderFilter implements ContainerRequestFilter { + + @Override + public void filter(ContainerRequestContext requestContext) { + MultivaluedMap headers = requestContext.getHeaders(); + if ("true".equals(headers.getFirst("x-set-accept-to-text"))) { + headers.putSingle(HttpHeaders.ACCEPT, MediaType.TEXT_PLAIN); + } + } + } + + @Provider + @Produces(MediaType.TEXT_PLAIN) + public static class DummyTextMessageBodyWriter implements ServerMessageBodyWriter { + + @Override + public boolean isWriteable(Class type, Type genericType, ResteasyReactiveResourceInfo target, + MediaType mediaType) { + return Entity.class.equals(type); + } + + @Override + public void writeResponse(Object o, Type genericType, ServerRequestContext context) + throws WebApplicationException, IOException { + context.serverResponse().end(((Entity) o).value()); + } + + @Override + public boolean isWriteable(Class type, Type genericType, Annotation[] annotations, MediaType mediaType) { + return Entity.class.equals(type); + } + + @Override + public void writeTo(Object o, Class type, Type genericType, Annotation[] annotations, MediaType mediaType, + MultivaluedMap httpHeaders, OutputStream entityStream) + throws IOException, WebApplicationException { + throw new IllegalStateException("should not be called"); + } + } + + @Provider + @Produces(MediaType.APPLICATION_JSON) + public static class DummyJsonMessageBodyWriter implements ServerMessageBodyWriter { + + @Override + public boolean isWriteable(Class type, Type genericType, ResteasyReactiveResourceInfo target, + MediaType mediaType) { + return Entity.class.equals(type); + } + + @Override + public void writeResponse(Object o, Type genericType, ServerRequestContext context) + throws WebApplicationException, IOException { + context.serverResponse().end("{\"value\":\"" + ((Entity) o).value() + "\"}"); + } + + @Override + public boolean isWriteable(Class type, Type genericType, Annotation[] annotations, MediaType mediaType) { + return Entity.class.equals(type); + } + + @Override + public void writeTo(Object o, Class type, Type genericType, Annotation[] annotations, MediaType mediaType, + MultivaluedMap httpHeaders, OutputStream entityStream) + throws IOException, WebApplicationException { + throw new IllegalStateException("should not be called"); + } + } +} diff --git a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/project/update/rewrite/QuarkusUpdateRecipe.java b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/project/update/rewrite/QuarkusUpdateRecipe.java index 2d6339b98ae9d..2881032b0fd5d 100644 --- a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/project/update/rewrite/QuarkusUpdateRecipe.java +++ b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/project/update/rewrite/QuarkusUpdateRecipe.java @@ -51,6 +51,10 @@ public QuarkusUpdateRecipe addRecipe(Map recipe) { if (!recipe.containsKey("name") || !(recipe.get("name") instanceof String)) { throw new IllegalArgumentException("Recipe name is required"); } + // some YAML documents might not be recipes. For instance, they could be categories. + if (!recipe.containsKey("type") || !recipe.get("type").toString().endsWith("/recipe")) { + return this; + } this.recipes.add(recipe); return this; } diff --git a/independent-projects/tools/devtools-testing/src/main/java/io/quarkus/devtools/testing/WrapperRunner.java b/independent-projects/tools/devtools-testing/src/main/java/io/quarkus/devtools/testing/WrapperRunner.java index 04f0791eddf93..3eb9d97174f10 100644 --- a/independent-projects/tools/devtools-testing/src/main/java/io/quarkus/devtools/testing/WrapperRunner.java +++ b/independent-projects/tools/devtools-testing/src/main/java/io/quarkus/devtools/testing/WrapperRunner.java @@ -23,7 +23,7 @@ public final class WrapperRunner { public enum Wrapper { GRADLE("gradlew", "gradlew.bat", new String[] { "--no-daemon", "build", "--info", "--stacktrace" }), - MAVEN("mvnw", "mvnw.cmd", new String[] { "package" }); + MAVEN("mvnw", "mvnw.cmd", new String[] { "-B", "package" }); private final String execUnix; private final String execWindows; @@ -51,7 +51,7 @@ public static Wrapper fromBuildtool(String buildtool) { case "gradle-kotlin-dsl": return GRADLE; default: - throw new IllegalStateException("No wrapper linked to buildtool: " + buildtool); + throw new IllegalStateException("No wrapper linked to build tool: " + buildtool); } } diff --git a/independent-projects/tools/registry-client/src/main/java/io/quarkus/registry/config/PropertiesUtil.java b/independent-projects/tools/registry-client/src/main/java/io/quarkus/registry/config/PropertiesUtil.java index 57125c8938306..2e4c52b4228f4 100644 --- a/independent-projects/tools/registry-client/src/main/java/io/quarkus/registry/config/PropertiesUtil.java +++ b/independent-projects/tools/registry-client/src/main/java/io/quarkus/registry/config/PropertiesUtil.java @@ -1,7 +1,5 @@ package io.quarkus.registry.config; -import java.security.AccessController; -import java.security.PrivilegedAction; import java.util.Locale; public class PropertiesUtil { @@ -26,32 +24,12 @@ public static String getUserHome() { public static String getProperty(final String name, String defValue) { assert name != null : "name is null"; - final SecurityManager sm = System.getSecurityManager(); - if (sm != null) { - return AccessController.doPrivileged(new PrivilegedAction() { - @Override - public String run() { - return System.getProperty(name, defValue); - } - }); - } else { - return System.getProperty(name, defValue); - } + return System.getProperty(name, defValue); } public static String getProperty(final String name) { assert name != null : "name is null"; - final SecurityManager sm = System.getSecurityManager(); - if (sm != null) { - return AccessController.doPrivileged(new PrivilegedAction() { - @Override - public String run() { - return System.getProperty(name); - } - }); - } else { - return System.getProperty(name); - } + return System.getProperty(name); } public static final Boolean getBooleanOrNull(String name) { diff --git a/integration-tests/amazon-lambda-http-resteasy-reactive/pom.xml b/integration-tests/amazon-lambda-http-resteasy-reactive/pom.xml index 121ac2b9b2806..1037b6d3d9329 100644 --- a/integration-tests/amazon-lambda-http-resteasy-reactive/pom.xml +++ b/integration-tests/amazon-lambda-http-resteasy-reactive/pom.xml @@ -11,7 +11,7 @@ quarkus-integration-test-amazon-lambda-http-resteasy-reactive Quarkus - Integration Tests - Amazon Lambda HTTP RESTEasy Reactive - Test with Resteasy Reactive and Amazon Lambda HTTP + Test with Quarkus REST and Amazon Lambda HTTP io.quarkus diff --git a/integration-tests/amazon-lambda-rest-resteasy-reactive/pom.xml b/integration-tests/amazon-lambda-rest-resteasy-reactive/pom.xml index f162cce9f6e1c..aa09da904da27 100644 --- a/integration-tests/amazon-lambda-rest-resteasy-reactive/pom.xml +++ b/integration-tests/amazon-lambda-rest-resteasy-reactive/pom.xml @@ -11,7 +11,7 @@ quarkus-integration-test-amazon-lambda-rest-resteasy-reactive Quarkus - Integration Tests - Amazon Lambda AWS Gateway REST API - Module that contains Amazon Lambda related tests for RESTEasy Reactive + Module that contains Amazon Lambda related tests for Quarkus REST io.quarkus diff --git a/integration-tests/cache/src/main/java/io/quarkus/it/cache/ExpensiveResource.java b/integration-tests/cache/src/main/java/io/quarkus/it/cache/ExpensiveResource.java index 8efdcf9abfdc0..84cdc5a3a75bd 100644 --- a/integration-tests/cache/src/main/java/io/quarkus/it/cache/ExpensiveResource.java +++ b/integration-tests/cache/src/main/java/io/quarkus/it/cache/ExpensiveResource.java @@ -13,11 +13,13 @@ @Path("/expensive-resource") public class ExpensiveResource { + public static final String EXPENSIVE_RESOURCE_CACHE_NAME = "expensiveResourceCache"; + private int invocations; @GET @Path("/{keyElement1}/{keyElement2}/{keyElement3}") - @CacheResult(cacheName = "expensiveResourceCache", lockTimeout = 5000) + @CacheResult(cacheName = EXPENSIVE_RESOURCE_CACHE_NAME, lockTimeout = 5000) public ExpensiveResponse getExpensiveResponse(@PathParam("keyElement1") @CacheKey String keyElement1, @PathParam("keyElement2") @CacheKey String keyElement2, @PathParam("keyElement3") @CacheKey String keyElement3, @QueryParam("foo") String foo) { diff --git a/integration-tests/cache/src/main/java/io/quarkus/it/cache/GetIfPresentResource.java b/integration-tests/cache/src/main/java/io/quarkus/it/cache/GetIfPresentResource.java new file mode 100644 index 0000000000000..14e9feb8082c4 --- /dev/null +++ b/integration-tests/cache/src/main/java/io/quarkus/it/cache/GetIfPresentResource.java @@ -0,0 +1,36 @@ +package io.quarkus.it.cache; + +import static java.util.concurrent.CompletableFuture.completedFuture; + +import java.util.concurrent.CompletionStage; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; + +import org.jboss.resteasy.reactive.RestPath; + +import io.quarkus.cache.Cache; +import io.quarkus.cache.CacheName; +import io.quarkus.cache.CaffeineCache; + +@Path("/get-if-present") +public class GetIfPresentResource { + + public static final String GET_IF_PRESENT_CACHE_NAME = "getIfPresentCache"; + + @CacheName(GET_IF_PRESENT_CACHE_NAME) + Cache cache; + + @GET + @Path("/{key}") + public CompletionStage getIfPresent(@RestPath String key) { + return cache.as(CaffeineCache.class).getIfPresent(key); + } + + @PUT + @Path("/{key}") + public void put(@RestPath String key, String value) { + cache.as(CaffeineCache.class).put(key, completedFuture(value)); + } +} diff --git a/integration-tests/cache/src/main/resources/application.properties b/integration-tests/cache/src/main/resources/application.properties index b94edbb983678..9f87c19434340 100644 --- a/integration-tests/cache/src/main/resources/application.properties +++ b/integration-tests/cache/src/main/resources/application.properties @@ -8,5 +8,6 @@ quarkus.cache.caffeine."forest".expire-after-write=10M quarkus.cache.caffeine."expensiveResourceCache".expire-after-write=10M quarkus.cache.caffeine."expensiveResourceCache".metrics-enabled=true +quarkus.cache.caffeine."getIfPresentCache".metrics-enabled=true io.quarkus.it.cache.SunriseRestClient/mp-rest/url=${test.url} diff --git a/integration-tests/cache/src/test/java/io/quarkus/it/cache/CacheTestCase.java b/integration-tests/cache/src/test/java/io/quarkus/it/cache/CacheTestCase.java index 48fc07d224685..64197281e8775 100644 --- a/integration-tests/cache/src/test/java/io/quarkus/it/cache/CacheTestCase.java +++ b/integration-tests/cache/src/test/java/io/quarkus/it/cache/CacheTestCase.java @@ -1,5 +1,8 @@ package io.quarkus.it.cache; +import static io.quarkus.it.cache.ExpensiveResource.EXPENSIVE_RESOURCE_CACHE_NAME; +import static io.quarkus.it.cache.GetIfPresentResource.GET_IF_PRESENT_CACHE_NAME; +import static io.restassured.RestAssured.given; import static io.restassured.RestAssured.when; import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -14,20 +17,65 @@ public class CacheTestCase { @Test - public void testCache() { + void testCache() { + assertMetrics(EXPENSIVE_RESOURCE_CACHE_NAME, 0, 0, 0); + runExpensiveRequest(); + assertMetrics(EXPENSIVE_RESOURCE_CACHE_NAME, 1, 1, 0); + runExpensiveRequest(); + assertMetrics(EXPENSIVE_RESOURCE_CACHE_NAME, 1, 1, 1); + runExpensiveRequest(); - when().get("/expensive-resource/invocations").then().statusCode(200).body(is("1")); + assertMetrics(EXPENSIVE_RESOURCE_CACHE_NAME, 1, 1, 2); - String metricsResponse = when().get("/q/metrics").then().extract().asString(); - assertTrue(metricsResponse.contains("cache_puts_total{cache=\"expensiveResourceCache\"} 1.0")); - assertTrue(metricsResponse.contains("cache_gets_total{cache=\"expensiveResourceCache\",result=\"miss\"} 1.0")); - assertTrue(metricsResponse.contains("cache_gets_total{cache=\"expensiveResourceCache\",result=\"hit\"} 2.0")); + when().get("/expensive-resource/invocations").then().statusCode(200).body(is("1")); } private void runExpensiveRequest() { when().get("/expensive-resource/I/love/Quarkus?foo=bar").then().statusCode(200).body("result", is("I love Quarkus too!")); } + + @Test + void testGetIfPresentMetrics() { + assertMetrics(GET_IF_PRESENT_CACHE_NAME, 0, 0, 0); + + String cacheKey = "foo"; + String cacheValue = "bar"; + + given().pathParam("key", cacheKey) + .when().get("/get-if-present/{key}") + .then().statusCode(204); + assertMetrics(GET_IF_PRESENT_CACHE_NAME, 0, 1, 0); + + given().pathParam("key", cacheKey) + .when().get("/get-if-present/{key}") + .then().statusCode(204); + assertMetrics(GET_IF_PRESENT_CACHE_NAME, 0, 2, 0); + + given().pathParam("key", cacheKey).body(cacheValue) + .when().put("/get-if-present/{key}") + .then().statusCode(204); + assertMetrics(GET_IF_PRESENT_CACHE_NAME, 1, 2, 0); + + given().pathParam("key", cacheKey) + .when().get("/get-if-present/{key}") + .then().statusCode(200).body(is(cacheValue)); + assertMetrics(GET_IF_PRESENT_CACHE_NAME, 1, 2, 1); + + given().pathParam("key", cacheKey) + .when().get("/get-if-present/{key}") + .then().statusCode(200).body(is(cacheValue)); + assertMetrics(GET_IF_PRESENT_CACHE_NAME, 1, 2, 2); + } + + private void assertMetrics(String cacheName, double expectedPuts, double expectedMisses, double expectedHits) { + String metricsResponse = when().get("/q/metrics").then().extract().asString(); + assertTrue(metricsResponse.contains(String.format("cache_puts_total{cache=\"%s\"} %.1f", cacheName, expectedPuts))); + assertTrue(metricsResponse + .contains(String.format("cache_gets_total{cache=\"%s\",result=\"miss\"} %.1f", cacheName, expectedMisses))); + assertTrue(metricsResponse + .contains(String.format("cache_gets_total{cache=\"%s\",result=\"hit\"} %.1f", cacheName, expectedHits))); + } } diff --git a/integration-tests/grpc-test-random-port/src/test/java/io/quarkus/grpc/examples/hello/RandomPortSeparateServerPlainIT.java b/integration-tests/grpc-test-random-port/src/test/java/io/quarkus/grpc/examples/hello/RandomPortSeparateServerPlainIT.java index 9e8da069b8d21..02b4a89c88323 100644 --- a/integration-tests/grpc-test-random-port/src/test/java/io/quarkus/grpc/examples/hello/RandomPortSeparateServerPlainIT.java +++ b/integration-tests/grpc-test-random-port/src/test/java/io/quarkus/grpc/examples/hello/RandomPortSeparateServerPlainIT.java @@ -1,9 +1,33 @@ package io.quarkus.grpc.examples.hello; +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +import com.google.common.net.HostAndPort; + +import examples.GreeterGrpc; +import examples.HelloRequest; +import io.grpc.netty.NettyChannelBuilder; import io.quarkus.test.junit.QuarkusIntegrationTest; import io.quarkus.test.junit.TestProfile; @QuarkusIntegrationTest @TestProfile(RandomPortSeparateServerPlainTestBase.Profile.class) class RandomPortSeparateServerPlainIT extends RandomPortSeparateServerPlainTestBase { + + @Test + void testWithNative() { + var channel = NettyChannelBuilder.forAddress("localhost", 9000).usePlaintext().build(); + var stub = GreeterGrpc.newBlockingStub(channel); + HelloRequest request = HelloRequest.newBuilder().setName("neo").build(); + var resp = stub.sayHello(request); + assertThat(resp.getMessage()).startsWith("Hello neo"); + + int clientPort = HostAndPort.fromString(channel.authority()).getPort(); + assertThat(clientPort).isNotEqualTo(0); + assertThat(clientPort).isEqualTo(9000); + + channel.shutdownNow(); + } } diff --git a/integration-tests/grpc-test-random-port/src/test/java/io/quarkus/grpc/examples/hello/RandomPortSeparateServerTlsIT.java b/integration-tests/grpc-test-random-port/src/test/java/io/quarkus/grpc/examples/hello/RandomPortSeparateServerTlsIT.java deleted file mode 100644 index 51853a2854b11..0000000000000 --- a/integration-tests/grpc-test-random-port/src/test/java/io/quarkus/grpc/examples/hello/RandomPortSeparateServerTlsIT.java +++ /dev/null @@ -1,9 +0,0 @@ -package io.quarkus.grpc.examples.hello; - -import io.quarkus.test.junit.QuarkusIntegrationTest; -import io.quarkus.test.junit.TestProfile; - -@QuarkusIntegrationTest -@TestProfile(RandomPortSeparateServerTlsTestBase.Profile.class) -class RandomPortSeparateServerTlsIT extends RandomPortSeparateServerTlsTestBase { -} diff --git a/integration-tests/grpc-test-random-port/src/test/java/io/quarkus/grpc/examples/hello/RandomPortTestBase.java b/integration-tests/grpc-test-random-port/src/test/java/io/quarkus/grpc/examples/hello/RandomPortTestBase.java index 5fe588155b622..5fe0786811fba 100644 --- a/integration-tests/grpc-test-random-port/src/test/java/io/quarkus/grpc/examples/hello/RandomPortTestBase.java +++ b/integration-tests/grpc-test-random-port/src/test/java/io/quarkus/grpc/examples/hello/RandomPortTestBase.java @@ -12,6 +12,7 @@ import examples.MutinyGreeterGrpc.MutinyGreeterStub; import io.grpc.Channel; import io.quarkus.grpc.GrpcClient; +import io.quarkus.test.junit.DisabledOnIntegrationTest; abstract class RandomPortTestBase { @GrpcClient("hello") @@ -21,6 +22,7 @@ abstract class RandomPortTestBase { Channel channel; @Test + @DisabledOnIntegrationTest void testRandomPort() { assertSoftly(softly -> { HelloRequest request = HelloRequest.newBuilder().setName("neo").build(); diff --git a/integration-tests/grpc-test-random-port/src/test/java/io/quarkus/grpc/examples/hello/RandomPortVertxServerPlainIT.java b/integration-tests/grpc-test-random-port/src/test/java/io/quarkus/grpc/examples/hello/RandomPortVertxServerPlainIT.java index bef37a3a9e053..701b6b0e85216 100644 --- a/integration-tests/grpc-test-random-port/src/test/java/io/quarkus/grpc/examples/hello/RandomPortVertxServerPlainIT.java +++ b/integration-tests/grpc-test-random-port/src/test/java/io/quarkus/grpc/examples/hello/RandomPortVertxServerPlainIT.java @@ -1,9 +1,33 @@ package io.quarkus.grpc.examples.hello; +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +import com.google.common.net.HostAndPort; + +import examples.GreeterGrpc; +import examples.HelloRequest; +import io.grpc.netty.NettyChannelBuilder; import io.quarkus.test.junit.QuarkusIntegrationTest; import io.quarkus.test.junit.TestProfile; @QuarkusIntegrationTest @TestProfile(RandomPortVertxServerPlainTestBase.Profile.class) class RandomPortVertxServerPlainIT extends RandomPortVertxServerPlainTestBase { + + @Test + void testWithNative() { + var channel = NettyChannelBuilder.forAddress("localhost", 8081).usePlaintext().build(); + var stub = GreeterGrpc.newBlockingStub(channel); + HelloRequest request = HelloRequest.newBuilder().setName("neo").build(); + var resp = stub.sayHello(request); + assertThat(resp.getMessage()).startsWith("Hello neo"); + + int clientPort = HostAndPort.fromString(channel.authority()).getPort(); + assertThat(clientPort).isNotEqualTo(0); + assertThat(clientPort).isEqualTo(8081); + + channel.shutdownNow(); + } } diff --git a/integration-tests/grpc-test-random-port/src/test/java/io/quarkus/grpc/examples/hello/RandomPortVertxServerTlsIT.java b/integration-tests/grpc-test-random-port/src/test/java/io/quarkus/grpc/examples/hello/RandomPortVertxServerTlsIT.java deleted file mode 100644 index 632306895da84..0000000000000 --- a/integration-tests/grpc-test-random-port/src/test/java/io/quarkus/grpc/examples/hello/RandomPortVertxServerTlsIT.java +++ /dev/null @@ -1,9 +0,0 @@ -package io.quarkus.grpc.examples.hello; - -import io.quarkus.test.junit.QuarkusIntegrationTest; -import io.quarkus.test.junit.TestProfile; - -@QuarkusIntegrationTest -@TestProfile(RandomPortVertxServerTlsTestBase.Profile.class) -class RandomPortVertxServerTlsIT extends RandomPortVertxServerTlsTestBase { -} diff --git a/integration-tests/hibernate-reactive-mssql/src/main/java/io/quarkus/it/hibernate/reactive/mssql/DialectEndpoint.java b/integration-tests/hibernate-reactive-mssql/src/main/java/io/quarkus/it/hibernate/reactive/mssql/DialectEndpoint.java index 85511daf4fef9..b3f51254a7fea 100644 --- a/integration-tests/hibernate-reactive-mssql/src/main/java/io/quarkus/it/hibernate/reactive/mssql/DialectEndpoint.java +++ b/integration-tests/hibernate-reactive-mssql/src/main/java/io/quarkus/it/hibernate/reactive/mssql/DialectEndpoint.java @@ -3,16 +3,17 @@ import java.io.IOException; import java.io.PrintWriter; -import org.hibernate.SessionFactory; -import org.hibernate.engine.spi.SessionFactoryImplementor; - -import io.quarkus.hibernate.orm.runtime.config.DialectVersions; 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 diff --git a/integration-tests/hibernate-reactive-mssql/src/main/java/io/quarkus/it/hibernate/reactive/mssql/HibernateReactiveMSSQLTestEndpoint.java b/integration-tests/hibernate-reactive-mssql/src/main/java/io/quarkus/it/hibernate/reactive/mssql/HibernateReactiveMSSQLTestEndpoint.java index ef8aa9634fae0..1127443b989b9 100644 --- a/integration-tests/hibernate-reactive-mssql/src/main/java/io/quarkus/it/hibernate/reactive/mssql/HibernateReactiveMSSQLTestEndpoint.java +++ b/integration-tests/hibernate-reactive-mssql/src/main/java/io/quarkus/it/hibernate/reactive/mssql/HibernateReactiveMSSQLTestEndpoint.java @@ -1,5 +1,9 @@ package io.quarkus.it.hibernate.reactive.mssql; +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + import org.hibernate.reactive.mutiny.Mutiny; import io.smallrye.mutiny.Uni; @@ -7,9 +11,6 @@ import io.vertx.mutiny.sqlclient.Row; import io.vertx.mutiny.sqlclient.RowSet; import io.vertx.mutiny.sqlclient.Tuple; -import jakarta.inject.Inject; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.Path; @Path("/tests") public class HibernateReactiveMSSQLTestEndpoint { diff --git a/integration-tests/hibernate-validator-resteasy-reactive/pom.xml b/integration-tests/hibernate-validator-resteasy-reactive/pom.xml index 530f503282a58..e97d2598faa5d 100644 --- a/integration-tests/hibernate-validator-resteasy-reactive/pom.xml +++ b/integration-tests/hibernate-validator-resteasy-reactive/pom.xml @@ -11,7 +11,7 @@ quarkus-integration-test-hibernate-validator-resteasy-reactive Quarkus - Integration Tests - Hibernate Validator - Module that contains Hibernate Validator/Bean Validation related tests using RESTEasy Reactive + Module that contains Hibernate Validator/Bean Validation related tests using Quarkus REST diff --git a/integration-tests/liquibase/src/main/java/io/quarkus/it/liquibase/LiquibaseFunctionalityResource.java b/integration-tests/liquibase/src/main/java/io/quarkus/it/liquibase/LiquibaseFunctionalityResource.java index b71310c69e38c..501fbdb58a82f 100644 --- a/integration-tests/liquibase/src/main/java/io/quarkus/it/liquibase/LiquibaseFunctionalityResource.java +++ b/integration-tests/liquibase/src/main/java/io/quarkus/it/liquibase/LiquibaseFunctionalityResource.java @@ -1,5 +1,8 @@ package io.quarkus.it.liquibase; +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.Statement; import java.util.List; import java.util.Objects; import java.util.stream.Collectors; @@ -9,6 +12,9 @@ import jakarta.ws.rs.Path; import jakarta.ws.rs.WebApplicationException; +import io.agroal.api.AgroalDataSource; +import io.quarkus.agroal.DataSource; +import io.quarkus.liquibase.LiquibaseDataSource; import io.quarkus.liquibase.LiquibaseFactory; import liquibase.Liquibase; import liquibase.changelog.ChangeSet; @@ -21,6 +27,14 @@ public class LiquibaseFunctionalityResource { @Inject LiquibaseFactory liquibaseFactory; + @Inject + @LiquibaseDataSource("second") + LiquibaseFactory liquibaseSecondFactory; + + @Inject + @DataSource("second") + AgroalDataSource secondDataSource; + @GET @Path("update") public String doUpdateAuto() { @@ -32,6 +46,7 @@ public String doUpdateAuto() { liquibaseFactory.createLabels()); List changeSets = Objects.requireNonNull(status, "ChangeSetStatus is null! Database update was not applied"); + return changeSets.stream() .filter(ChangeSetStatus::getPreviouslyRan) .map(ChangeSetStatus::getChangeSet) @@ -42,6 +57,31 @@ public String doUpdateAuto() { } } + @GET + @Path("updateWithDedicatedUser") + public String updateWithDedicatedUser() { + try (Liquibase liquibase = liquibaseSecondFactory.createLiquibase()) { + liquibase.update(liquibaseSecondFactory.createContexts(), liquibaseSecondFactory.createLabels()); + List status = liquibase.getChangeSetStatuses(liquibaseSecondFactory.createContexts(), + liquibaseSecondFactory.createLabels()); + List changeSets = Objects.requireNonNull(status, + "ChangeSetStatus is null! Database update was not applied"); + + try (Connection connection = secondDataSource.getConnection()) { + try (Statement s = connection.createStatement()) { + ResultSet rs = s.executeQuery("SELECT CREATEDBY FROM QUARKUS_TABLE WHERE ID = 1"); + if (rs.next()) { + return rs.getString("CREATEDBY"); + } + return null; + } + } + } catch (Exception ex) { + throw new WebApplicationException(ex.getMessage(), ex); + } + + } + private void assertCommandScopeResolvesProperly() { try { new CommandScope("dropAll"); @@ -49,5 +89,4 @@ private void assertCommandScopeResolvesProperly() { throw new RuntimeException("Unable to load 'dropAll' via Liquibase's CommandScope", e); } } - } diff --git a/integration-tests/liquibase/src/main/resources/application.properties b/integration-tests/liquibase/src/main/resources/application.properties index f2bc258ff694d..036f9cef122dc 100644 --- a/integration-tests/liquibase/src/main/resources/application.properties +++ b/integration-tests/liquibase/src/main/resources/application.properties @@ -2,7 +2,13 @@ quarkus.datasource.db-kind=h2 quarkus.datasource.username=sa quarkus.datasource.password=sa -quarkus.datasource.jdbc.url=jdbc:h2:tcp://localhost/mem:test_quarkus;DB_CLOSE_DELAY=-1 +quarkus.datasource.jdbc.url=jdbc:h2:mem:test;DB_CLOSE_DELAY=-1 + +# Second datasource +quarkus.datasource.second.db-kind=h2 +quarkus.datasource.second.username=readonly +quarkus.datasource.second.password=readonly +quarkus.datasource.second.jdbc.url=jdbc:h2:mem:second;INIT=RUNSCRIPT FROM 'src/main/resources/db/second/initdb.sql' # Liquibase config properties quarkus.liquibase.change-log=db/changeLog.xml @@ -11,6 +17,13 @@ quarkus.liquibase.migrate-at-start=false quarkus.liquibase.database-change-log-lock-table-name=changelog_lock quarkus.liquibase.database-change-log-table-name=changelog +# Config for second datasource with different user / password +quarkus.liquibase.second.username=admin +quarkus.liquibase.second.password=pass +quarkus.liquibase.second.change-log=db/second/changeLog.xml +quarkus.liquibase.second.clean-at-start=false +quarkus.liquibase.second.migrate-at-start=false + # Debug logging #quarkus.log.console.level=DEBUG #quarkus.log.category."liquibase".level=DEBUG diff --git a/integration-tests/liquibase/src/main/resources/db/second/changeLog.xml b/integration-tests/liquibase/src/main/resources/db/second/changeLog.xml new file mode 100644 index 0000000000000..8d79230fa4d32 --- /dev/null +++ b/integration-tests/liquibase/src/main/resources/db/second/changeLog.xml @@ -0,0 +1,9 @@ + + + + + \ No newline at end of file diff --git a/integration-tests/liquibase/src/main/resources/db/second/create-table.xml b/integration-tests/liquibase/src/main/resources/db/second/create-table.xml new file mode 100644 index 0000000000000..7878e39dd51fd --- /dev/null +++ b/integration-tests/liquibase/src/main/resources/db/second/create-table.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/integration-tests/liquibase/src/main/resources/db/second/initdb.sql b/integration-tests/liquibase/src/main/resources/db/second/initdb.sql new file mode 100644 index 0000000000000..f1f2c732613c6 --- /dev/null +++ b/integration-tests/liquibase/src/main/resources/db/second/initdb.sql @@ -0,0 +1,5 @@ +CREATE USER IF NOT EXISTS admin PASSWORD 'pass' ADMIN; +GRANT ALL ON SCHEMA PUBLIC TO admin; + +CREATE USER IF NOT EXISTS readonly PASSWORD 'readonly' ADMIN; +GRANT SELECT ON SCHEMA PUBLIC TO readonly; \ No newline at end of file diff --git a/integration-tests/liquibase/src/main/resources/db/second/insert-into-table.xml b/integration-tests/liquibase/src/main/resources/db/second/insert-into-table.xml new file mode 100644 index 0000000000000..60c8d153f099a --- /dev/null +++ b/integration-tests/liquibase/src/main/resources/db/second/insert-into-table.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/integration-tests/liquibase/src/test/java/io/quarkus/it/liquibase/LiquibaseFunctionalityTest.java b/integration-tests/liquibase/src/test/java/io/quarkus/it/liquibase/LiquibaseFunctionalityTest.java index 52246e1254d69..447537cdef628 100644 --- a/integration-tests/liquibase/src/test/java/io/quarkus/it/liquibase/LiquibaseFunctionalityTest.java +++ b/integration-tests/liquibase/src/test/java/io/quarkus/it/liquibase/LiquibaseFunctionalityTest.java @@ -18,6 +18,13 @@ public void testLiquibaseQuarkusFunctionality() { doTestLiquibaseQuarkusFunctionality(isIncludeAllExpectedToWork()); } + @Test + @DisplayName("Migrates a schema correctly using dedicated username and password from config properties") + public void testLiquibaseUsingDedicatedUsernameAndPassword() { + when().get("/liquibase/updateWithDedicatedUser").then().body(is( + "ADMIN")); + } + static void doTestLiquibaseQuarkusFunctionality(boolean isIncludeAllExpectedToWork) { when() .get("/liquibase/update") 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 39fd504562a81..262bc40ceab09 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 @@ -3,8 +3,6 @@ import static io.quarkus.maven.it.ApplicationNameAndVersionTestUtil.assertApplicationPropertiesSetCorrectly; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; -import static org.hamcrest.CoreMatchers.containsString; -import static org.hamcrest.CoreMatchers.equalTo; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; @@ -943,37 +941,6 @@ public void testThatExternalConfigOverridesConfigInJar() throws MavenInvocationE .until(() -> devModeClient.getHttpResponse("/app/hello/greeting").contains(uuid)); } - @Test - public void testThatNewResourcesAreServed() throws MavenInvocationException, IOException { - testDir = initProject("projects/classic", "projects/project-classic-run-resource-change"); - runAndCheck(); - - // Create a new resource - File source = new File(testDir, "src/main/resources/META-INF/resources/lorem.txt"); - FileUtils.write(source, - "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", - "UTF-8"); - await() - .pollDelay(100, TimeUnit.MILLISECONDS) - .atMost(TestUtils.getDefaultTimeout(), TimeUnit.MINUTES) - .until(() -> devModeClient.getHttpResponse("/lorem.txt"), containsString("Lorem ipsum")); - - // Update the resource - String uuid = UUID.randomUUID().toString(); - FileUtils.write(source, uuid, "UTF-8"); - await() - .pollDelay(100, TimeUnit.MILLISECONDS) - .atMost(TestUtils.getDefaultTimeout(), TimeUnit.MINUTES) - .until(() -> devModeClient.getHttpResponse("/lorem.txt"), equalTo(uuid)); - - // Delete the resource - source.delete(); - await() - .pollDelay(100, TimeUnit.MILLISECONDS) - .atMost(TestUtils.getDefaultTimeout(), TimeUnit.MINUTES) - .until(() -> devModeClient.getHttpResponse("/lorem.txt", 404)); - } - @Test public void testThatConfigFileDeletionsAreDetected() throws MavenInvocationException, IOException { testDir = initProject("projects/dev-mode-file-deletion"); diff --git a/integration-tests/maven/src/test/java/io/quarkus/maven/it/FlakyDevMojoIT.java b/integration-tests/maven/src/test/java/io/quarkus/maven/it/FlakyDevMojoIT.java new file mode 100644 index 0000000000000..a2b4beaacf714 --- /dev/null +++ b/integration-tests/maven/src/test/java/io/quarkus/maven/it/FlakyDevMojoIT.java @@ -0,0 +1,55 @@ +package io.quarkus.maven.it; + +import static org.awaitility.Awaitility.await; +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.equalTo; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +import org.apache.maven.shared.invoker.MavenInvocationException; +import org.junit.jupiter.api.Test; + +import io.quarkus.test.devmode.util.DevModeClient; + +/** + * This test has been isolated as it is very flaky and causing issues with Develocity PTS. + */ +@DisableForNative +public class FlakyDevMojoIT extends RunAndCheckMojoTestBase { + + protected DevModeClient devModeClient = new DevModeClient(getPort()); + + @Test + public void testThatNewResourcesAreServed() throws MavenInvocationException, IOException { + testDir = initProject("projects/classic-with-log", "projects/project-classic-run-resource-change"); + runAndCheck(); + + // Create a new resource + Path source = testDir.toPath().resolve("src/main/resources/META-INF/resources/lorem.txt"); + Files.writeString(source, + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."); + await() + .pollDelay(100, TimeUnit.MILLISECONDS) + .atMost(TestUtils.getDefaultTimeout(), TimeUnit.MINUTES) + .until(() -> devModeClient.getHttpResponse("/lorem.txt"), containsString("Lorem ipsum")); + + // Update the resource + String uuid = UUID.randomUUID().toString(); + Files.writeString(source, uuid); + await() + .pollDelay(100, TimeUnit.MILLISECONDS) + .atMost(TestUtils.getDefaultTimeout(), TimeUnit.MINUTES) + .until(() -> devModeClient.getHttpResponse("/lorem.txt"), equalTo(uuid)); + + // Delete the resource + Files.delete(source); + await() + .pollDelay(100, TimeUnit.MILLISECONDS) + .atMost(TestUtils.getDefaultTimeout(), TimeUnit.MINUTES) + .until(() -> devModeClient.getHttpResponse("/lorem.txt", 404)); + } +} diff --git a/integration-tests/maven/src/test/resources-filtered/projects/classic-with-log/.env b/integration-tests/maven/src/test/resources-filtered/projects/classic-with-log/.env new file mode 100644 index 0000000000000..98fb9ae1398c6 --- /dev/null +++ b/integration-tests/maven/src/test/resources-filtered/projects/classic-with-log/.env @@ -0,0 +1 @@ +OTHER_GREETING=Hola \ No newline at end of file diff --git a/integration-tests/maven/src/test/resources-filtered/projects/classic-with-log/pom.xml b/integration-tests/maven/src/test/resources-filtered/projects/classic-with-log/pom.xml new file mode 100644 index 0000000000000..6e8f3dfe1026f --- /dev/null +++ b/integration-tests/maven/src/test/resources-filtered/projects/classic-with-log/pom.xml @@ -0,0 +1,108 @@ + + + 4.0.0 + org.acme + acme + 1.0-SNAPSHOT + + io.quarkus + quarkus-bom + @project.version@ + @project.version@ + ${compiler-plugin.version} + UTF-8 + ${maven.compiler.source} + ${maven.compiler.target} + + + 1.13.0 + + + + + + \${quarkus.platform.group-id} + \${quarkus.platform.artifact-id} + \${quarkus.platform.version} + pom + import + + + + + + + io.quarkus + quarkus-resteasy + + + io.quarkus + quarkus-smallrye-context-propagation + + + io.quarkus + quarkus-websockets + + + io.smallrye.common + smallrye-common-vertx-context + 1.13.2 + + + org.webjars + jquery-ui + \${webjar.jquery-ui.version} + + + commons-io + commons-io + + + io.quarkus + quarkus-junit5 + test + + + io.rest-assured + rest-assured + test + + + + + + maven-compiler-plugin + \${compiler-plugin.version} + + + io.quarkus + quarkus-maven-plugin + \${quarkus-plugin.version} + + + + generate-code + generate-code-tests + build + + + + + + + + + native + + true + + + + customOutputDir + + target-other + + + + diff --git a/integration-tests/maven/src/test/resources-filtered/projects/classic-with-log/src/main/java/org/acme/ClasspathResources.java b/integration-tests/maven/src/test/resources-filtered/projects/classic-with-log/src/main/java/org/acme/ClasspathResources.java new file mode 100644 index 0000000000000..a8a4efacded05 --- /dev/null +++ b/integration-tests/maven/src/test/resources-filtered/projects/classic-with-log/src/main/java/org/acme/ClasspathResources.java @@ -0,0 +1,195 @@ +package org.acme; + +import jakarta.ws.rs.QueryParam; +import org.apache.commons.io.IOUtils; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import java.io.InputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Paths; +import java.util.Collections; +import java.util.List; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.function.Supplier; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +@Path("/classpathResources") +public class ClasspathResources { + + private static final String SUCCESS = "success"; + + @GET + public String readClassPathResources() { + return runAssertions( + () -> assertInvalidExactFileLocation(), + () -> assertCorrectExactFileLocation(), + () -> assertInvalidDirectory(), + () -> assertCorrectDirectory(), + () -> assertTopLevelDirectory(), + () -> assertMultiRelease() + ); + } + + private String runAssertions(Supplier... assertions) { + String result; + for (Supplier assertion : assertions) { + result = assertion.get(); + if (!SUCCESS.equals(result)) { + return result; + } + } + return SUCCESS; + } + + private String assertInvalidExactFileLocation() { + final String testType = "invalid-exact-location"; + try { + Enumeration exactFileLocationEnumeration = this.getClass().getClassLoader().getResources("db/location/test2.sql"); + List exactFileLocationList = urlList(exactFileLocationEnumeration); + if (exactFileLocationList.size() != 0) { + return errorResult(testType, "wrong number of urls"); + } + return SUCCESS; + } catch (Exception e) { + e.printStackTrace(); + return errorResult(testType, "exception during resolution of resource"); + } + } + + private String assertMultiRelease() { + final String testType = "assert-multi-release-jar"; + if (System.getProperty("java.version").startsWith("1.")) { + return SUCCESS; + } + try { + //this class is only present in multi release jars + //for fast-jar we need to make sure it is loaded correctly + Class clazz = this.getClass().getClassLoader().loadClass("io.smallrye.common.vertx.VertxContext"); + if (clazz.getClassLoader() == getClass().getClassLoader()) { + return SUCCESS; + } + return errorResult(testType, "Incorrect ClassLoader for " + clazz); + } catch (Exception e) { + e.printStackTrace(); + return errorResult(testType, "exception during resolution of resource"); + } + } + private String assertCorrectExactFileLocation() { + final String testType = "correct-exact-location"; + try { + Enumeration exactFileLocationEnumeration = this.getClass().getClassLoader().getResources("db/location/test.sql"); + List exactFileLocationList = urlList(exactFileLocationEnumeration); + if (exactFileLocationList.size() != 1) { + return errorResult(testType, "wrong number of urls"); + } + String fileContent = IOUtils.toString(exactFileLocationList.get(0).toURI(), StandardCharsets.UTF_8); + if (!fileContent.contains("CREATE TABLE")) { + return errorResult(testType, "wrong file content"); + } + return SUCCESS; + } catch (Exception e) { + e.printStackTrace(); + return errorResult(testType, "exception during resolution of resource"); + } + } + + private String assertInvalidDirectory() { + final String testType = "invalid-directory"; + try { + Enumeration exactFileLocationEnumeration = this.getClass().getClassLoader().getResources("db/location2"); + List exactFileLocationList = urlList(exactFileLocationEnumeration); + if (exactFileLocationList.size() != 0) { + return errorResult(testType, "wrong number of urls"); + } + return SUCCESS; + } catch (Exception e) { + e.printStackTrace(); + return errorResult(testType, "exception during resolution of resource"); + } + } + + private String assertCorrectDirectory() { + final String testType = "correct-directory"; + try { + Enumeration directoryEnumeration = this.getClass().getClassLoader().getResources("db/location"); + List directoryURLList = urlList(directoryEnumeration); + if (directoryURLList.size() != 1) { + return errorResult(testType, "wrong number of directory urls"); + } + + URL singleURL = directoryURLList.get(0); + + int separatorIndex = singleURL.getPath().lastIndexOf('!'); + String jarPath = singleURL.getPath().substring(0, separatorIndex); + String directoryName = singleURL.getPath().substring(separatorIndex + 2) + "/"; + + try (JarFile jarFile = new JarFile(Paths.get(new URI(jarPath)).toFile())) { + Enumeration entries = jarFile.entries(); + List entriesInDirectory = new ArrayList<>(); + while (entries.hasMoreElements()) { + JarEntry currentEntry = entries.nextElement(); + String entryName = currentEntry.getName(); + if (entryName.startsWith(directoryName) && !entryName.equals(directoryName)) { + entriesInDirectory.add(currentEntry); + } + } + + if (entriesInDirectory.size() != 1) { + return errorResult(testType, "wrong number of entries in jar directory"); + } + + try (InputStream is = jarFile.getInputStream(entriesInDirectory.get(0))) { + String fileContent = IOUtils.toString(is, StandardCharsets.UTF_8); + if (!fileContent.contains("CREATE TABLE")) { + return errorResult(testType, "wrong file content"); + } + return SUCCESS; + } + } + + + } catch (Exception e) { + e.printStackTrace(); + return errorResult(testType, "exception during resolution of resource"); + } + } + + private String assertTopLevelDirectory() { + final String testType = "top-level-directory"; + try { + Enumeration directoryEnumeration = this.getClass().getClassLoader().getResources("assets"); + List directoryURLList = urlList(directoryEnumeration); + if (directoryURLList.size() != 1) { + return errorResult(testType, "wrong number of directory urls"); + } + + return SUCCESS; + } catch (Exception e) { + e.printStackTrace(); + return errorResult(testType, "exception during resolution of resource"); + } + } + + private List urlList(Enumeration enumeration) { + if (enumeration == null) { + return Collections.emptyList(); + } + List result = new ArrayList<>(); + while (enumeration.hasMoreElements()) { + result.add(enumeration.nextElement()); + } + return result; + } + + private String errorResult(String testType, String message) { + return testType + " / " + message; + } +} diff --git a/integration-tests/maven/src/test/resources-filtered/projects/classic-with-log/src/main/java/org/acme/HelloResource.java b/integration-tests/maven/src/test/resources-filtered/projects/classic-with-log/src/main/java/org/acme/HelloResource.java new file mode 100644 index 0000000000000..c21e5305ea793 --- /dev/null +++ b/integration-tests/maven/src/test/resources-filtered/projects/classic-with-log/src/main/java/org/acme/HelloResource.java @@ -0,0 +1,74 @@ +package org.acme; + +import org.eclipse.microprofile.config.inject.ConfigProperty; + +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +@Path("/hello") +public class HelloResource { + + @ConfigProperty(name = "greeting") + String greeting; + + @ConfigProperty(name = "quarkus.application.version") + String applicationVersion; + + @ConfigProperty(name = "quarkus.application.name") + String applicationName; + + @ConfigProperty(name = "other.greeting", defaultValue = "other") + String otherGreeting; + + @ConfigProperty(name = "quarkus.profile") + String profile; + + @GET + @Produces(MediaType.TEXT_PLAIN) + public String hello() { + return "hello"; + } + + @GET + @Path("/greeting") + @Produces(MediaType.TEXT_PLAIN) + public String greeting() { + return greeting; + } + + @GET + @Path("/package") + @Produces(MediaType.TEXT_PLAIN) + public String pkg() { + return Blah.class.getPackage().getName(); + } + + @GET + @Path("/nameAndVersion") + @Produces(MediaType.TEXT_PLAIN) + public String nameAndVersion() { + return applicationName + "/" + applicationVersion; + } + + @GET + @Path("/otherGreeting") + @Produces(MediaType.TEXT_PLAIN) + public String otherGreeting() { + return otherGreeting; + } + + @GET + @Path("/profile") + @Produces(MediaType.TEXT_PLAIN) + public String profile() { + return profile; + } + + + public static class Blah { + + } +} diff --git a/integration-tests/maven/src/test/resources-filtered/projects/classic-with-log/src/main/java/org/acme/MyApplication.java b/integration-tests/maven/src/test/resources-filtered/projects/classic-with-log/src/main/java/org/acme/MyApplication.java new file mode 100644 index 0000000000000..a6d66f8b9eda2 --- /dev/null +++ b/integration-tests/maven/src/test/resources-filtered/projects/classic-with-log/src/main/java/org/acme/MyApplication.java @@ -0,0 +1,9 @@ +package org.acme; + +import jakarta.ws.rs.ApplicationPath; +import jakarta.ws.rs.core.Application; + +@ApplicationPath("/app") +public class MyApplication extends Application { + +} diff --git a/integration-tests/maven/src/test/resources-filtered/projects/classic-with-log/src/main/java/org/acme/ProtectionDomain.java b/integration-tests/maven/src/test/resources-filtered/projects/classic-with-log/src/main/java/org/acme/ProtectionDomain.java new file mode 100644 index 0000000000000..36cfbe3d82e5e --- /dev/null +++ b/integration-tests/maven/src/test/resources-filtered/projects/classic-with-log/src/main/java/org/acme/ProtectionDomain.java @@ -0,0 +1,77 @@ +package org.acme; + +import org.apache.commons.io.IOUtils; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.jar.JarInputStream; +import java.util.jar.Manifest; +import java.nio.charset.StandardCharsets; +import java.nio.file.Paths; +import java.util.Collections; +import java.util.List; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.function.Supplier; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.jar.JarInputStream; +import java.util.jar.Manifest; + +@Path("/protectionDomain") +public class ProtectionDomain { + + private static final String SUCCESS = "success"; + + @GET + public String useProtectionDomain() { + return runAssertions( + () -> assertReadManifestFromJar() + ); + } + + private String runAssertions(Supplier... assertions) { + String result; + for (Supplier assertion : assertions) { + result = assertion.get(); + if (!SUCCESS.equals(result)) { + return result; + } + } + return SUCCESS; + } + + private String assertReadManifestFromJar() { + final String testType = "manifest-from-jar"; + try { + URL location = org.apache.commons.io.Charsets.class.getProtectionDomain().getCodeSource().getLocation(); + if (location == null) { + return errorResult(testType, "location should not be null"); + } + + try (InputStream inputStream = location.openStream()) { + try (JarInputStream jarInputStream = new JarInputStream(inputStream)) { + Manifest manifest = jarInputStream.getManifest(); + if (manifest == null) { + return errorResult(testType, "manifest should not be null"); + } + String implementationVersion = manifest.getMainAttributes().getValue("Implementation-Version"); + if (implementationVersion == null) { + return errorResult(testType, "implementation-version should not be null"); + } + } + } + return SUCCESS; + } catch (Exception e) { + e.printStackTrace(); + return errorResult(testType, "exception during resolution of resource"); + } + } + + private String errorResult(String testType, String message) { + return testType + " / " + message; + } +} diff --git a/integration-tests/maven/src/test/resources-filtered/projects/classic-with-log/src/main/resources/META-INF/resources/index.html b/integration-tests/maven/src/test/resources-filtered/projects/classic-with-log/src/main/resources/META-INF/resources/index.html new file mode 100644 index 0000000000000..c09bb5c96b869 --- /dev/null +++ b/integration-tests/maven/src/test/resources-filtered/projects/classic-with-log/src/main/resources/META-INF/resources/index.html @@ -0,0 +1,156 @@ + + + + + acme - 1.0-SNAPSHOT + + + + + + +
+
+

Congratulations, you have created a new Quarkus application.

+ +

Why do you see this?

+ +

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

+ +

What can I do from here?

+ +

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

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

Do you like Quarkus?

+

Go give it a star on GitHub.

+ +

How do I get rid of this page?

+

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

+
+
+
+

Application

+
    +
  • GroupId: org.acme
  • +
  • ArtifactId: acme
  • +
  • Version: 1.0-SNAPSHOT
  • +
  • Quarkus Version: 999-SNAPSHOT
  • +
+
+
+

Next steps

+ +
+
+
+ + + + \ No newline at end of file diff --git a/integration-tests/maven/src/test/resources-filtered/projects/classic-with-log/src/main/resources/application.properties b/integration-tests/maven/src/test/resources-filtered/projects/classic-with-log/src/main/resources/application.properties new file mode 100644 index 0000000000000..4afcfeef83dca --- /dev/null +++ b/integration-tests/maven/src/test/resources-filtered/projects/classic-with-log/src/main/resources/application.properties @@ -0,0 +1,9 @@ +# Configuration file +key = value +greeting=bonjour +quarkus.log.level=INFO +quarkus.log.file.enable=false +quarkus.log.console.format=%d{yyyy-MM-dd HH:mm:ss,SSS} %-5p [%c{3.}] (%t) %s%e%n +quarkus.log.file.format=%d{yyyy-MM-dd HH:mm:ss,SSS} %h %N[%i] %-5p [%c{3.}] (%t) %s%e%n +quarkus.log.category."io.quarkus".level=INFO +quarkus.log.category."io.quarkus.deployment.dev".level=DEBUG diff --git a/integration-tests/maven/src/test/resources-filtered/projects/classic-with-log/src/main/resources/assets/test.txt b/integration-tests/maven/src/test/resources-filtered/projects/classic-with-log/src/main/resources/assets/test.txt new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/integration-tests/maven/src/test/resources-filtered/projects/classic-with-log/src/main/resources/db/location/test.sql b/integration-tests/maven/src/test/resources-filtered/projects/classic-with-log/src/main/resources/db/location/test.sql new file mode 100644 index 0000000000000..cddc725179c67 --- /dev/null +++ b/integration-tests/maven/src/test/resources-filtered/projects/classic-with-log/src/main/resources/db/location/test.sql @@ -0,0 +1,7 @@ +CREATE TABLE TEST_SCHEMA.quarkus_table2 +( + id INT, + name VARCHAR(20) +); +INSERT INTO TEST_SCHEMA.quarkus_table2(id, name) +VALUES (1, '1.0.1 QUARKED'); diff --git a/integration-tests/maven/src/test/resources-filtered/projects/classic-with-log/src/test/java/org/acme/HelloResourceTest.java b/integration-tests/maven/src/test/resources-filtered/projects/classic-with-log/src/test/java/org/acme/HelloResourceTest.java new file mode 100644 index 0000000000000..c2f29e2c9f711 --- /dev/null +++ b/integration-tests/maven/src/test/resources-filtered/projects/classic-with-log/src/test/java/org/acme/HelloResourceTest.java @@ -0,0 +1,21 @@ +package org.acme; + +import io.quarkus.test.junit.QuarkusTest; +import org.junit.jupiter.api.Test; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.CoreMatchers.is; + +@QuarkusTest +public class HelloResourceTest { + + @Test + public void testHelloEndpoint() { + given() + .when().get("/app/hello") + .then() + .statusCode(200) + .body(is("hello")); + } + +} diff --git a/integration-tests/oidc-client-reactive/pom.xml b/integration-tests/oidc-client-reactive/pom.xml index b753e79946d43..35cb2616b04d7 100644 --- a/integration-tests/oidc-client-reactive/pom.xml +++ b/integration-tests/oidc-client-reactive/pom.xml @@ -12,7 +12,7 @@ quarkus-integration-test-oidc-client-reactive Quarkus - Integration Tests - OpenID Connect Client Reactive - Module that contains OpenID Connect Client tests using RESTEasy Reactive + Module that contains OpenID Connect Client tests using Quarkus REST http://localhost:8180/auth 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 59664d4e21510..866a6d9ea0a40 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 @@ -61,6 +61,14 @@ public String bearerCertificateFullChain() { return "granted:" + identity.getRoles(); } + @Path("bearer-chain-custom-validator") + @GET + @RolesAllowed("admin") + @Produces(MediaType.APPLICATION_JSON) + public String bearerCertificateCustomValidator() { + return "granted:" + identity.getRoles(); + } + @Path("bearer-certificate-full-chain-root-only") @GET @RolesAllowed("admin") diff --git a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/BearerGlobalTokenChainValidator.java b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/BearerGlobalTokenChainValidator.java new file mode 100644 index 0000000000000..d7e3589470420 --- /dev/null +++ b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/BearerGlobalTokenChainValidator.java @@ -0,0 +1,29 @@ +package io.quarkus.it.keycloak; + +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.List; + +import jakarta.enterprise.context.ApplicationScoped; + +import io.quarkus.arc.Unremovable; +import io.quarkus.oidc.OidcTenantConfig; +import io.quarkus.oidc.TokenCertificateValidator; +import io.quarkus.oidc.runtime.TrustStoreUtils; +import io.vertx.core.json.JsonObject; + +@ApplicationScoped +@Unremovable +public class BearerGlobalTokenChainValidator implements TokenCertificateValidator { + + @Override + public void validate(OidcTenantConfig oidcConfig, List chain, String tokenClaims) + throws CertificateException { + String rootCertificateThumbprint = TrustStoreUtils.calculateThumprint(chain.get(chain.size() - 1)); + JsonObject claims = new JsonObject(tokenClaims); + if (!rootCertificateThumbprint.equals(claims.getString("root-certificate-thumbprint"))) { + throw new CertificateException("Invalid root certificate"); + } + } + +} diff --git a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/BearerTenantTokenChainValidator.java b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/BearerTenantTokenChainValidator.java new file mode 100644 index 0000000000000..39a1ce4c06837 --- /dev/null +++ b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/BearerTenantTokenChainValidator.java @@ -0,0 +1,34 @@ +package io.quarkus.it.keycloak; + +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.List; + +import jakarta.enterprise.context.ApplicationScoped; + +import io.quarkus.arc.Unremovable; +import io.quarkus.oidc.OidcTenantConfig; +import io.quarkus.oidc.TenantFeature; +import io.quarkus.oidc.TokenCertificateValidator; +import io.quarkus.oidc.runtime.TrustStoreUtils; +import io.vertx.core.json.JsonObject; + +@ApplicationScoped +@Unremovable +@TenantFeature("bearer-chain-custom-validator") +public class BearerTenantTokenChainValidator implements TokenCertificateValidator { + + @Override + public void validate(OidcTenantConfig oidcConfig, List chain, String tokenClaims) + throws CertificateException { + if (!"bearer-chain-custom-validator".equals(oidcConfig.tenantId.get())) { + throw new RuntimeException("Unexpected tenant id"); + } + String leafCertificateThumbprint = TrustStoreUtils.calculateThumprint(chain.get(0)); + JsonObject claims = new JsonObject(tokenClaims); + if (!leafCertificateThumbprint.equals(claims.getString("leaf-certificate-thumbprint"))) { + throw new CertificateException("Invalid leaf certificate"); + } + } + +} diff --git a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CodeFlowUserInfoResource.java b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CodeFlowUserInfoResource.java index bbad887921b4c..eae1b792e0bd1 100644 --- a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CodeFlowUserInfoResource.java +++ b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CodeFlowUserInfoResource.java @@ -54,6 +54,12 @@ public String accessGitHubCachedInIdToken() { return access(); } + @GET + @Path("/code-flow-user-info-github-cache-disabled") + public String accessGitHubCacheDisabled() { + return access(); + } + @GET @Path("/code-flow-user-info-dynamic-github") public String accessDynamicGitHub() { diff --git a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CustomSecurityIdentityAugmentor.java b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CustomSecurityIdentityAugmentor.java index 3fe9d26540d5f..75fdcdfe253c4 100644 --- a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CustomSecurityIdentityAugmentor.java +++ b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CustomSecurityIdentityAugmentor.java @@ -46,7 +46,8 @@ public Uni augment(SecurityIdentity identity, AuthenticationRe || routingContext.normalizedPath().endsWith("code-flow-user-info-github") || routingContext.normalizedPath().endsWith("code-flow-user-info-dynamic-github") || routingContext.normalizedPath().endsWith("code-flow-token-introspection") - || routingContext.normalizedPath().endsWith("code-flow-user-info-github-cached-in-idtoken"))) { + || routingContext.normalizedPath().endsWith("code-flow-user-info-github-cached-in-idtoken") + || routingContext.normalizedPath().endsWith("code-flow-user-info-github-cache-disabled"))) { QuarkusSecurityIdentity.Builder builder = QuarkusSecurityIdentity.builder(identity); UserInfo userInfo = identity.getAttribute("userinfo"); builder.setPrincipal(new Principal() { diff --git a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CustomTenantConfigResolver.java b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CustomTenantConfigResolver.java index cfd09752a900b..78ff79e9f8d16 100644 --- a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CustomTenantConfigResolver.java +++ b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CustomTenantConfigResolver.java @@ -36,6 +36,10 @@ public Uni resolve(RoutingContext context, + "a tenant id on the '" + path + "' request path"); } if (context.get(OidcUtils.TENANT_ID_ATTRIBUTE) != null) { + if (context.get(OidcUtils.TENANT_ID_SET_BY_ANNOTATION) != null) { + throw new RuntimeException( + "Calling TenantConfigResolver after @Tenant has already resolved tenant id is unnecessary"); + } if (context.get(OidcUtils.TENANT_ID_SET_BY_SESSION_COOKIE) == null && context.get(OidcUtils.TENANT_ID_SET_BY_STATE_COOKIE) == null) { throw new RuntimeException("Tenant id must have been set by either the session or state cookie"); @@ -62,6 +66,7 @@ public Uni resolve(RoutingContext context, config.getCodeGrant().setHeaders(Map.of("X-Custom", "XCustomHeaderValue")); config.getCodeGrant().setExtraParams(Map.of("extra-param", "extra-param-value")); config.getAuthentication().setInternalIdTokenLifespan(Duration.ofSeconds(301)); + config.setAllowUserInfoCache(false); return Uni.createFrom().item(config); } else if (path.endsWith("bearer-certificate-full-chain-root-only")) { OidcTenantConfig config = new OidcTenantConfig(); diff --git a/integration-tests/oidc-wiremock/src/main/resources/application.properties b/integration-tests/oidc-wiremock/src/main/resources/application.properties index 9c1814178a724..15e351b94c6bf 100644 --- a/integration-tests/oidc-wiremock/src/main/resources/application.properties +++ b/integration-tests/oidc-wiremock/src/main/resources/application.properties @@ -85,6 +85,20 @@ quarkus.oidc.code-flow-user-info-github.code-grant.extra-params.extra-param=extr quarkus.oidc.code-flow-user-info-github.code-grant.headers.X-Custom=XCustomHeaderValue quarkus.oidc.code-flow-user-info-github.client-id=quarkus-web-app quarkus.oidc.code-flow-user-info-github.credentials.secret=AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow +quarkus.oidc.code-flow-user-info-github.cache-user-info-in-idtoken=false + +quarkus.oidc.code-flow-user-info-github-cache-disabled.provider=github +quarkus.oidc.code-flow-user-info-github-cache-disabled.authentication.internal-id-token-lifespan=7H +quarkus.oidc.code-flow-user-info-github-cache-disabled.authentication.verify-access-token=false +quarkus.oidc.code-flow-user-info-github-cache-disabled.auth-server-url=${keycloak.url}/realms/quarkus/ +quarkus.oidc.code-flow-user-info-github-cache-disabled.authorization-path=/ +quarkus.oidc.code-flow-user-info-github-cache-disabled.user-info-path=protocol/openid-connect/userinfo +quarkus.oidc.code-flow-user-info-github-cache-disabled.code-grant.extra-params.extra-param=extra-param-value +quarkus.oidc.code-flow-user-info-github-cache-disabled.code-grant.headers.X-Custom=XCustomHeaderValue +quarkus.oidc.code-flow-user-info-github-cache-disabled.client-id=quarkus-web-app +quarkus.oidc.code-flow-user-info-github-cache-disabled.credentials.secret=AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow +quarkus.oidc.code-flow-user-info-github-cache-disabled.cache-user-info-in-idtoken=false +quarkus.oidc.code-flow-user-info-github-cache-disabled.allow-user-info-cache=false quarkus.oidc.bearer-user-info-github-service.provider=github quarkus.oidc.bearer-user-info-github-service.token.principal-claim=preferred_username @@ -106,6 +120,7 @@ quarkus.oidc.code-flow-user-info-github-cached-in-idtoken.jwks-path=${keycloak.u quarkus.oidc.code-flow-user-info-github-cached-in-idtoken.code-grant.extra-params.extra-param=extra-param-value quarkus.oidc.code-flow-user-info-github-cached-in-idtoken.code-grant.headers.X-Custom=XCustomHeaderValue quarkus.oidc.code-flow-user-info-github-cached-in-idtoken.cache-user-info-in-idtoken=true +quarkus.oidc.code-flow-user-info-github-cached-in-idtoken.allow-user-info-cache=false quarkus.oidc.code-flow-user-info-github-cached-in-idtoken.token.refresh-token-time-skew=298 quarkus.oidc.code-flow-user-info-github-cached-in-idtoken.authentication.verify-access-token=true quarkus.oidc.code-flow-user-info-github-cached-in-idtoken.client-id=quarkus-web-app @@ -181,6 +196,9 @@ 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-chain-custom-validator.certificate-chain.trust-store-file=truststore.p12 +quarkus.oidc.bearer-chain-custom-validator.certificate-chain.trust-store-password=storepassword + quarkus.oidc.bearer-certificate-full-chain-root-only-wrongcname.certificate-chain.trust-store-file=truststore-rootcert.p12 quarkus.oidc.bearer-certificate-full-chain-root-only-wrongcname.certificate-chain.trust-store-password=storepassword quarkus.oidc.bearer-certificate-full-chain-root-only-wrongcname.certificate-chain.leaf-certificate-name=www.quarkusio.com 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 d95361d301e6c..af9862304184f 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 @@ -29,6 +29,7 @@ import io.quarkus.deployment.util.FileUtil; import io.quarkus.oidc.runtime.OidcUtils; +import io.quarkus.oidc.runtime.TrustStoreUtils; import io.quarkus.test.common.QuarkusTestResource; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.oidc.server.OidcWireMock; @@ -36,6 +37,7 @@ import io.restassured.RestAssured; import io.smallrye.jwt.algorithm.SignatureAlgorithm; import io.smallrye.jwt.build.Jwt; +import io.smallrye.jwt.build.JwtClaimsBuilder; import io.smallrye.jwt.util.KeyUtils; import io.smallrye.jwt.util.ResourceUtils; import io.vertx.core.json.JsonObject; @@ -187,13 +189,44 @@ public void testAccessAdminResourceWithWrongCertS256Thumbprint() { .statusCode(401); } + @Test + public void testCertChainWithCustomValidator() 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 and bind it to the token claim + String accessToken = getAccessTokenForCustomValidator( + List.of(subjectCert, intermediateCert, rootCert), + subjectPrivateKey, true); + + RestAssured.given().auth().oauth2(accessToken) + .when().get("/api/admin/bearer-chain-custom-validator") + .then() + .statusCode(200) + .body(Matchers.containsString("admin")); + + // Send the token with the valid certificate chain but do bind it to the token claim + accessToken = getAccessTokenForCustomValidator( + List.of(subjectCert, intermediateCert, rootCert), + subjectPrivateKey, false); + + RestAssured.given().auth().oauth2(accessToken) + .when().get("/api/admin/bearer-chain-custom-validator") + .then() + .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 + + // Send the token with the valid certificate chain and bind it to the token claim String accessToken = getAccessTokenWithCertChain( List.of(subjectCert, intermediateCert, rootCert), subjectPrivateKey); @@ -708,7 +741,24 @@ private String getAccessTokenWithCertChain(List chain, .groups("admin") .issuer("https://server.example.com") .audience("https://service.example.com") - .jws().chain(chain) + .claim("root-certificate-thumbprint", TrustStoreUtils.calculateThumprint(chain.get(chain.size() - 1))) + .jws() + .chain(chain) + .sign(privateKey); + } + + private String getAccessTokenForCustomValidator(List chain, + PrivateKey privateKey, boolean setLeafCertThumbprint) throws Exception { + JwtClaimsBuilder builder = Jwt.preferredUserName("alice") + .groups("admin") + .issuer("https://server.example.com") + .audience("https://service.example.com") + .claim("root-certificate-thumbprint", TrustStoreUtils.calculateThumprint(chain.get(chain.size() - 1))); + if (setLeafCertThumbprint) { + builder.claim("leaf-certificate-thumbprint", TrustStoreUtils.calculateThumprint(chain.get(0))); + } + return builder.jws() + .chain(chain) .sign(privateKey); } diff --git a/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/CodeFlowAuthorizationTest.java b/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/CodeFlowAuthorizationTest.java index 0b83d6d39e19a..9f3dd12fd764a 100644 --- a/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/CodeFlowAuthorizationTest.java +++ b/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/CodeFlowAuthorizationTest.java @@ -251,16 +251,23 @@ public void testCodeFlowFormPostAndFrontChannelLogout() throws Exception { public void testCodeFlowUserInfo() throws Exception { defineCodeFlowAuthorizationOauth2TokenStub(); wireMockServer.resetRequests(); - doTestCodeFlowUserInfo("code-flow-user-info-only", 300, false); + // No internal ID token + doTestCodeFlowUserInfo("code-flow-user-info-only", 300, false, false, 1, 1); clearCache(); - doTestCodeFlowUserInfo("code-flow-user-info-github", 25200, false); + // Internal ID token, allow in memory cache = true, cacheUserInfoInIdtoken = false without having to be configured + doTestCodeFlowUserInfo("code-flow-user-info-github", 25200, false, false, 1, 1); clearCache(); - doTestCodeFlowUserInfo("code-flow-user-info-dynamic-github", 301, true); + // Internal ID token, allow in memory cache = false, cacheUserInfoInIdtoken = true without having to be configured + doTestCodeFlowUserInfo("code-flow-user-info-dynamic-github", 301, true, true, 0, 1); + clearCache(); + // Internal ID token, allow in memory cache = false, cacheUserInfoInIdtoken = false + doTestCodeFlowUserInfo("code-flow-user-info-github-cache-disabled", 25200, false, false, 0, 4); clearCache(); } @Test public void testCodeFlowUserInfoCachedInIdToken() throws Exception { + // Internal ID token, allow in memory cache = false, cacheUserInfoInIdtoken = true defineCodeFlowUserInfoCachedInIdTokenStub(); try (final WebClient webClient = createWebClient()) { webClient.getOptions().setRedirectEnabled(true); @@ -277,11 +284,35 @@ public void testCodeFlowUserInfoCachedInIdToken() throws Exception { JsonObject idTokenClaims = decryptIdToken(webClient, "code-flow-user-info-github-cached-in-idtoken"); assertNotNull(idTokenClaims.getJsonObject(OidcUtils.USER_INFO_ATTRIBUTE)); + long issuedAt = idTokenClaims.getLong("iat"); + long expiresAt = idTokenClaims.getLong("exp"); + assertEquals(299, expiresAt - issuedAt); + + Cookie sessionCookie = getSessionCookie(webClient, "code-flow-user-info-github-cached-in-idtoken"); + Date date = sessionCookie.getExpires(); + assertTrue(date.toInstant().getEpochSecond() - issuedAt >= 299 + 300); + // This test enables the token refresh, in this case the cookie age is extended by additional 5 mins + // to minimize the risk of the browser losing immediately after it has expired, for this cookie + // be returned to Quarkus, analyzed and refreshed + assertTrue(date.toInstant().getEpochSecond() - issuedAt <= 299 + 300 + 3); + // refresh Thread.sleep(3000); textPage = webClient.getPage("http://localhost:8081/code-flow-user-info-github-cached-in-idtoken"); assertEquals("alice:alice:bob, cache size: 0, TenantConfigResolver: false", textPage.getContent()); + idTokenClaims = decryptIdToken(webClient, "code-flow-user-info-github-cached-in-idtoken"); + assertNotNull(idTokenClaims.getJsonObject(OidcUtils.USER_INFO_ATTRIBUTE)); + + issuedAt = idTokenClaims.getLong("iat"); + expiresAt = idTokenClaims.getLong("exp"); + assertEquals(305, expiresAt - issuedAt); + + sessionCookie = getSessionCookie(webClient, "code-flow-user-info-github-cached-in-idtoken"); + date = sessionCookie.getExpires(); + assertTrue(date.toInstant().getEpochSecond() - issuedAt >= 305 + 300); + assertTrue(date.toInstant().getEpochSecond() - issuedAt <= 305 + 300 + 3); + webClient.getCookieManager().clearCookies(); } @@ -323,8 +354,8 @@ public void testCodeFlowTokenIntrospection() throws Exception { clearCache(); } - private void doTestCodeFlowUserInfo(String tenantId, long internalIdTokenLifetime, - boolean tenantConfigResolver) throws Exception { + private void doTestCodeFlowUserInfo(String tenantId, long internalIdTokenLifetime, boolean cacheUserInfoInIdToken, + boolean tenantConfigResolver, int inMemoryCacheSize, int userInfoRequests) throws Exception { try (final WebClient webClient = createWebClient()) { webClient.getOptions().setRedirectEnabled(true); wireMockServer.verify(0, getRequestedFor(urlPathMatching("/auth/realms/quarkus/protocol/openid-connect/userinfo"))); @@ -336,20 +367,24 @@ private void doTestCodeFlowUserInfo(String tenantId, long internalIdTokenLifetim TextPage textPage = form.getInputByValue("login").click(); - assertEquals("alice:alice:alice, cache size: 1, TenantConfigResolver: " + tenantConfigResolver, + assertEquals( + "alice:alice:alice, cache size: " + inMemoryCacheSize + ", TenantConfigResolver: " + tenantConfigResolver, textPage.getContent()); textPage = webClient.getPage("http://localhost:8081/" + tenantId); - assertEquals("alice:alice:alice, cache size: 1, TenantConfigResolver: " + tenantConfigResolver, + assertEquals( + "alice:alice:alice, cache size: " + inMemoryCacheSize + ", TenantConfigResolver: " + tenantConfigResolver, textPage.getContent()); textPage = webClient.getPage("http://localhost:8081/" + tenantId); - assertEquals("alice:alice:alice, cache size: 1, TenantConfigResolver: " + tenantConfigResolver, + assertEquals( + "alice:alice:alice, cache size: " + inMemoryCacheSize + ", TenantConfigResolver: " + tenantConfigResolver, textPage.getContent()); - wireMockServer.verify(1, getRequestedFor(urlPathMatching("/auth/realms/quarkus/protocol/openid-connect/userinfo"))); + wireMockServer.verify(userInfoRequests, + getRequestedFor(urlPathMatching("/auth/realms/quarkus/protocol/openid-connect/userinfo"))); wireMockServer.resetRequests(); JsonObject idTokenClaims = decryptIdToken(webClient, tenantId); - assertNull(idTokenClaims.getJsonObject(OidcUtils.USER_INFO_ATTRIBUTE)); + assertEquals(cacheUserInfoInIdToken, idTokenClaims.containsKey(OidcUtils.USER_INFO_ATTRIBUTE)); long issuedAt = idTokenClaims.getLong("iat"); long expiresAt = idTokenClaims.getLong("exp"); assertEquals(internalIdTokenLifetime, expiresAt - issuedAt); @@ -434,6 +469,7 @@ private void defineCodeFlowUserInfoCachedInIdTokenStub() { .withBody("{\n" + " \"access_token\": \"" + OidcWiremockTestResource.getAccessToken("alice", Set.of()) + "\"," + + "\"expires_in\": 299," + " \"refresh_token\": \"refresh1234\"" + "}"))); wireMockServer @@ -450,7 +486,8 @@ private void defineCodeFlowUserInfoCachedInIdTokenStub() { .withHeader("Content-Type", "application/json") .withBody("{\n" + " \"access_token\": \"" - + OidcWiremockTestResource.getAccessToken("bob", Set.of()) + "\"" + + OidcWiremockTestResource.getAccessToken("bob", Set.of()) + "\"," + + "\"expires_in\": 305" + "}"))); } diff --git a/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/TestUtils.java b/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/TestUtils.java index a7439ceacd048..591ca8c360f4d 100644 --- a/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/TestUtils.java +++ b/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/TestUtils.java @@ -8,6 +8,7 @@ import java.util.List; import io.quarkus.oidc.runtime.OidcUtils; +import io.quarkus.oidc.runtime.TrustStoreUtils; import io.smallrye.jwt.build.Jwt; import io.smallrye.jwt.util.KeyUtils; import io.smallrye.jwt.util.ResourceUtils; @@ -36,6 +37,7 @@ public static String getAccessTokenWithCertChain(List chain, .groups("admin") .issuer("https://server.example.com") .audience("https://service.example.com") + .claim("root-certificate-thumbprint", TrustStoreUtils.calculateThumprint(chain.get(chain.size() - 1))) .jws().chain(chain) .sign(privateKey); } diff --git a/integration-tests/opentelemetry-redis-instrumentation/src/test/java/io/quarkus/it/opentelemetry/QuarkusOpenTelemetryRedisIT.java b/integration-tests/opentelemetry-redis-instrumentation/src/test/java/io/quarkus/it/opentelemetry/QuarkusOpenTelemetryRedisIT.java index f67e195d26ce9..85463e6b768d0 100644 --- a/integration-tests/opentelemetry-redis-instrumentation/src/test/java/io/quarkus/it/opentelemetry/QuarkusOpenTelemetryRedisIT.java +++ b/integration-tests/opentelemetry-redis-instrumentation/src/test/java/io/quarkus/it/opentelemetry/QuarkusOpenTelemetryRedisIT.java @@ -1,12 +1,14 @@ package io.quarkus.it.opentelemetry; +import java.util.Map; + import io.quarkus.test.junit.QuarkusIntegrationTest; @QuarkusIntegrationTest class QuarkusOpenTelemetryRedisIT extends QuarkusOpenTelemetryRedisTest { - @Override - String getKey(String k) { - return "native-" + k; + void checkForException(Map exception) { + // Ignore it + // The exception is not passed in native mode. (need to be investigated) } } diff --git a/integration-tests/opentelemetry-redis-instrumentation/src/test/java/io/quarkus/it/opentelemetry/QuarkusOpenTelemetryRedisTest.java b/integration-tests/opentelemetry-redis-instrumentation/src/test/java/io/quarkus/it/opentelemetry/QuarkusOpenTelemetryRedisTest.java index f10a0be951849..b0ed21891c967 100644 --- a/integration-tests/opentelemetry-redis-instrumentation/src/test/java/io/quarkus/it/opentelemetry/QuarkusOpenTelemetryRedisTest.java +++ b/integration-tests/opentelemetry-redis-instrumentation/src/test/java/io/quarkus/it/opentelemetry/QuarkusOpenTelemetryRedisTest.java @@ -117,6 +117,12 @@ public void syncInvalidOperation() { assertEquals("bazinga", span.get("name")); assertEquals("ERROR", status.get("statusCode")); assertEquals("exception", event.get("name")); + + checkForException(exception); + + } + + void checkForException(Map exception) { assertThat((String) exception.get("message"), containsString("ERR unknown command 'bazinga'")); } @@ -185,7 +191,8 @@ public void reactiveInvalidOperation() { assertEquals("bazinga", span.get("name")); assertEquals("ERROR", status.get("statusCode")); assertEquals("exception", event.get("name")); - assertThat((String) exception.get("message"), containsString("ERR unknown command 'bazinga'")); + + checkForException(exception); } private List> getSpans() { diff --git a/integration-tests/resteasy-reactive-kotlin/pom.xml b/integration-tests/resteasy-reactive-kotlin/pom.xml index 92515457261da..a2858bbd856e1 100644 --- a/integration-tests/resteasy-reactive-kotlin/pom.xml +++ b/integration-tests/resteasy-reactive-kotlin/pom.xml @@ -11,7 +11,7 @@ quarkus-integration-test-resteasy-reactive-kotlin-parent - Quarkus - Integration Tests - RESTEasy Reactive Kotlin - Parent + Quarkus - Integration Tests - Quarkus REST Kotlin - Parent pom diff --git a/integration-tests/vertx-http/src/main/resources/META-INF/resources/dummy/index.html b/integration-tests/vertx-http/src/main/resources/META-INF/resources/dummy/index.html new file mode 100644 index 0000000000000..6cb29f572ede2 --- /dev/null +++ b/integration-tests/vertx-http/src/main/resources/META-INF/resources/dummy/index.html @@ -0,0 +1,7 @@ + + + Hello world + +Hello World + + diff --git a/integration-tests/vertx-http/src/main/resources/META-INF/resources/dummy2/test.html b/integration-tests/vertx-http/src/main/resources/META-INF/resources/dummy2/test.html new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/integration-tests/vertx-http/src/main/resources/META-INF/resources/l1/l2/index.html b/integration-tests/vertx-http/src/main/resources/META-INF/resources/l1/l2/index.html new file mode 100644 index 0000000000000..6cb29f572ede2 --- /dev/null +++ b/integration-tests/vertx-http/src/main/resources/META-INF/resources/l1/l2/index.html @@ -0,0 +1,7 @@ + + + Hello world + +Hello World + + diff --git a/integration-tests/vertx-http/src/main/resources/META-INF/resources/test.txt b/integration-tests/vertx-http/src/main/resources/META-INF/resources/test.txt new file mode 100644 index 0000000000000..9daeafb9864cf --- /dev/null +++ b/integration-tests/vertx-http/src/main/resources/META-INF/resources/test.txt @@ -0,0 +1 @@ +test diff --git a/integration-tests/vertx-http/src/test/java/io/quarkus/it/vertx/StaticResourcesIT.java b/integration-tests/vertx-http/src/test/java/io/quarkus/it/vertx/StaticResourcesIT.java new file mode 100644 index 0000000000000..d20e98d4a2c53 --- /dev/null +++ b/integration-tests/vertx-http/src/test/java/io/quarkus/it/vertx/StaticResourcesIT.java @@ -0,0 +1,7 @@ +package io.quarkus.it.vertx; + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +public class StaticResourcesIT extends StaticResourcesTest { +} diff --git a/integration-tests/vertx-http/src/test/java/io/quarkus/it/vertx/StaticResourcesTest.java b/integration-tests/vertx-http/src/test/java/io/quarkus/it/vertx/StaticResourcesTest.java new file mode 100644 index 0000000000000..028044c2feda7 --- /dev/null +++ b/integration-tests/vertx-http/src/test/java/io/quarkus/it/vertx/StaticResourcesTest.java @@ -0,0 +1,41 @@ +package io.quarkus.it.vertx; + +import static io.restassured.RestAssured.when; + +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +public class StaticResourcesTest { + + @Test + public void testExisting() { + when().get("/test.txt").then().statusCode(200); + } + + @Test + public void testNonExisting() { + when().get("/test2.txt").then().statusCode(404); + } + + @Test + public void testIndexInDirectory() { + when().get("/dummy/").then().statusCode(200); + } + + @Test + public void testIndexInNestedDirectory() { + when().get("/l1/l2/").then().statusCode(200); + } + + @Test + public void testNonIndexInDirectory() { + when().get("/dummy2/").then().statusCode(404); + } + + @Test + public void testIndexInNonExistingDirectory() { + when().get("/dummy3/").then().statusCode(404); + } +} diff --git a/integration-tests/virtual-threads/security-webauthn-virtual-threads/src/test/java/io/quarkus/virtual/security/webauthn/RunOnVirtualThreadIT.java b/integration-tests/virtual-threads/security-webauthn-virtual-threads/src/test/java/io/quarkus/virtual/security/webauthn/RunOnVirtualThreadIT.java index c834a4ca97654..6ff6f8303ec7c 100644 --- a/integration-tests/virtual-threads/security-webauthn-virtual-threads/src/test/java/io/quarkus/virtual/security/webauthn/RunOnVirtualThreadIT.java +++ b/integration-tests/virtual-threads/security-webauthn-virtual-threads/src/test/java/io/quarkus/virtual/security/webauthn/RunOnVirtualThreadIT.java @@ -1,8 +1,55 @@ package io.quarkus.virtual.security.webauthn; +import static io.quarkus.virtual.security.webauthn.RunOnVirtualThreadTest.checkLoggedIn; + +import org.hamcrest.Matchers; +import org.junit.jupiter.api.Test; + import io.quarkus.test.junit.QuarkusIntegrationTest; +import io.quarkus.test.security.webauthn.WebAuthnEndpointHelper; +import io.quarkus.test.security.webauthn.WebAuthnHardware; +import io.restassured.RestAssured; +import io.restassured.filter.cookie.CookieFilter; +import io.vertx.core.json.JsonObject; @QuarkusIntegrationTest -class RunOnVirtualThreadIT extends RunOnVirtualThreadTest { +class RunOnVirtualThreadIT { + + @Test + public void test() { + + RestAssured.get("/open").then().statusCode(200).body(Matchers.is("Hello")); + RestAssured + .given().redirects().follow(false) + .get("/secure").then().statusCode(302); + RestAssured + .given().redirects().follow(false) + .get("/admin").then().statusCode(302); + RestAssured + .given().redirects().follow(false) + .get("/cheese").then().statusCode(302); + + CookieFilter cookieFilter = new CookieFilter(); + WebAuthnHardware hardwareKey = new WebAuthnHardware(); + String challenge = WebAuthnEndpointHelper.invokeRegistration("stef", cookieFilter); + JsonObject registration = hardwareKey.makeRegistrationJson(challenge); + + // now finalise + WebAuthnEndpointHelper.invokeCallback(registration, cookieFilter); + + // make sure our login cookie works + checkLoggedIn(cookieFilter); + + // reset cookies for the login phase + cookieFilter = new CookieFilter(); + // now try to log in + challenge = WebAuthnEndpointHelper.invokeLogin("stef", cookieFilter); + JsonObject login = hardwareKey.makeLoginJson(challenge); + + // now finalise + WebAuthnEndpointHelper.invokeCallback(login, cookieFilter); + // make sure our login cookie still works + checkLoggedIn(cookieFilter); + } } diff --git a/integration-tests/virtual-threads/security-webauthn-virtual-threads/src/test/java/io/quarkus/virtual/security/webauthn/RunOnVirtualThreadTest.java b/integration-tests/virtual-threads/security-webauthn-virtual-threads/src/test/java/io/quarkus/virtual/security/webauthn/RunOnVirtualThreadTest.java index 2cbd9f85afd5d..4d73fc4210d59 100644 --- a/integration-tests/virtual-threads/security-webauthn-virtual-threads/src/test/java/io/quarkus/virtual/security/webauthn/RunOnVirtualThreadTest.java +++ b/integration-tests/virtual-threads/security-webauthn-virtual-threads/src/test/java/io/quarkus/virtual/security/webauthn/RunOnVirtualThreadTest.java @@ -80,7 +80,7 @@ public void test() throws Exception { checkLoggedIn(cookieFilter); } - private void checkLoggedIn(CookieFilter cookieFilter) { + public static void checkLoggedIn(CookieFilter cookieFilter) { RestAssured .given() .filter(cookieFilter) diff --git a/pom.xml b/pom.xml index 9398fb32cce29..a6156b23d4f24 100644 --- a/pom.xml +++ b/pom.xml @@ -65,6 +65,7 @@ 0.8.12 6.11.0 + 5.4.0 1.62.2 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 81d0fd3b41bf1..93a3d3f9568d1 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 @@ -289,7 +289,9 @@ static ArtifactLauncher.InitContext.DevServicesLaunchResult handleDevServices(Ex // when the application is going to be launched as a docker container, we need to make containers started by DevServices // use a shared network that the application container can then use as well augmentAction = curatedApplication.createAugmentor( - "io.quarkus.deployment.builditem.DevServicesSharedNetworkBuildItem$Factory", Collections.emptyMap()); + "io.quarkus.deployment.builditem.DevServicesSharedNetworkBuildItem$Factory", + Map.of(io.quarkus.deployment.builditem.DevServicesSharedNetworkBuildItem.SOURCE_PROPERTY, + "io.quarkus.test.junit")); } else { augmentAction = curatedApplication.createAugmentor(); }