diff --git a/.github/native-tests.json b/.github/native-tests.json index 1b05bb4e979894..dbe32b8def3278 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 4c62bcbc07290e..06178aae8af146 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] diff --git a/.github/virtual-threads-tests.json b/.github/virtual-threads-tests.json index 9a8a190876a797..a17c515aeeb5b2 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/native-it-selected-graalvm.yml b/.github/workflows/native-it-selected-graalvm.yml new file mode 100644 index 00000000000000..8103432d9f1dfc --- /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 ba6d5146d4d6dc..32208ac9fb5d6e 100644 --- a/.github/workflows/preview.yml +++ b/.github/workflows/preview.yml @@ -90,6 +90,7 @@ jobs: 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 diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml index 33adbbdecaa231..c7ebbe6f2f7432 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/bom/application/pom.xml b/bom/application/pom.xml index 9449c48011b9c0..841bbd2f06c1fe 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -57,12 +57,12 @@ 3.10.0 2.8.2 6.3.0 - 4.5.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 @@ -140,8 +140,8 @@ 1.2.6 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 0878028369f03e..852e752a75c70d 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 8f3e059be79f02..fc4e1e776034c0 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/configuration/BuildTimeConfigurationReader.java b/core/deployment/src/main/java/io/quarkus/deployment/configuration/BuildTimeConfigurationReader.java index 8b0cb7c4b1076b..62ef5f74e30430 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 54007f07a354ff..20b1853ae0c171 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 1b57c9aa9ab574..de09063b148bb9 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/pkg/steps/JarResultBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/JarResultBuildStep.java index ade96118461b75..708dbb4e3c31ef 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 cb87d130aa054d..4e668d0ca9cad6 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 4d4bc3f0881b71..a55543f9c17101 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 a26d42a488b7f9..bcd424cd8cbf32 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 9f73397e60e3d4..aa6a14336d5567 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 00000000000000..7cfb2c4ece4967 --- /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 0b7a314795338d..d9886d3bdd8e00 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 00000000000000..77d6c52bee6c10 --- /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 00000000000000..5614d62716483d --- /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 8427a552eb841f..aee2ce4090171d 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 08a043382dbe54..3d978555bbecc0 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 00000000000000..d9d85a19ab53a0 --- /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 00000000000000..432bd86b334e9d --- /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 00000000000000..8d28a7ae672a63 --- /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 00000000000000..9582882517a776 --- /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 00000000000000..944813a9d720af --- /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 00000000000000..ecb7fb4ee6ee69 --- /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 00000000000000..8dbecc4bff2bc9 --- /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 00000000000000..b40b6d25d2059f --- /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 00000000000000..9f57f399efdc61 --- /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 00000000000000..90a3793ec29639 --- /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/devtools/cli/distribution/jreleaser-maintenance.yml b/devtools/cli/distribution/jreleaser-maintenance.yml index 15772e91a8ba74..5847f0b4229eb4 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 cbfeab135468c8..1de4d9a832d177 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 d8be28fdd50d28..e4e37b0216b866 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 50427c0789cc16..8871081257ab46 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.1" + 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 22891e8f44de45..6e027daff769dd 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 9dc4a552aad8cb..05bd7d3b816220 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; @@ -1360,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 1d8f7559b2fb22..d83cb4fa9f46b6 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/test/java/io/quarkus/maven/BasicDependencyTreeTestBase.java b/devtools/maven/src/test/java/io/quarkus/maven/BasicDependencyTreeTestBase.java new file mode 100644 index 00000000000000..ef929a5567a5e6 --- /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 00000000000000..26509fd19828f3 --- /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 00000000000000..45d10a0417b62a --- /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 bd99f28420ec75..db3a359c1d6d38 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 bef26188da25e3..5dd5c52df7412f 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 81aa3c190b2586..7e93473d3dc1b9 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 fab83b72936e58..4ee403ebf5cf56 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 00000000000000..5780caf8fafcae --- /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 00000000000000..77508e6965d591 --- /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 1fb959fdae99b4..d43162a11db94b 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 5c460135a1273a..bf7d0a9836aae8 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 40bc76f40c28c2..832397770b902c 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/config-yaml.adoc b/docs/src/main/asciidoc/config-yaml.adoc index 074132c8bc895b..8d420476d9517b 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 09eb23d61ae751..b4500631fa7578 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/http-reference.adoc b/docs/src/main/asciidoc/http-reference.adoc index 6f90ed502831c4..5201ebbc4df5ed 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 00000000000000..def6da4167c26b 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 8761d8f9c1fdc3..e72b1c18a794ef 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 a9d105aa244ab4..ff532b0c198ab2 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 0f6ab858131f07..84bef6c0cc3d10 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 @@ -514,6 +517,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/qute-reference.adoc b/docs/src/main/asciidoc/qute-reference.adoc index 942ecaef36afd9..8b31a7c50f1394 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(); } } ---- @@ -2829,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> } } ---- @@ -2856,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 e2947d7d4b57d5..1a20c1d74f0fa4 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 4df2562e291fe0..3f06f8291c7e03 100644 --- a/docs/src/main/asciidoc/rest-client.adoc +++ b/docs/src/main/asciidoc/rest-client.adoc @@ -1386,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 @@ -1402,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. @@ -1423,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. @@ -1535,7 +1533,7 @@ public ClientMultipartForm buildClientMultipartForm(Request request) { // <1> ClientMultipartForm multiPartForm = ClientMultipartForm.create(); multiPartForm.attribute("jsonPayload", request.getJsonPayload(), "jsonPayload"); // <2> request.getFiles().forEach(fu -> { - multiPartForm.binaryFileUpload("file", fu.name(), fu.filePath().toString(), fu.contentType()); // <3> + multiPartForm.fileUpload(fu); // <3> }); return multiPartForm; } @@ -1543,7 +1541,7 @@ public ClientMultipartForm buildClientMultipartForm(Request request) { // <1> <1> `Request` representing the request the server parts accepts <2> A `jsonPayload` attribute is added directly to `ClientMultipartForm` -<3> A `binaryFileUpload` is created from the request's `FileUpload` (which is a Quarkus REST (Server) type used to represent a binary file upload) +<3> A `fileUpload` is created from the request's `FileUpload` [NOTE] ==== diff --git a/docs/src/main/asciidoc/security-jdbc.adoc b/docs/src/main/asciidoc/security-jdbc.adoc index 7a3e8a906f15d1..fba33d27df3257 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 2a02b3292ca038..4d764c915a9e93 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 6eb16fe378aefa..e2bda15eeca668 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. 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 48a482d4b20916..db214a15dbe159 100644 --- a/docs/src/main/asciidoc/security-openid-connect-client-reference.adoc +++ b/docs/src/main/asciidoc/security-openid-connect-client-reference.adoc @@ -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/smallrye-graphql-client.adoc b/docs/src/main/asciidoc/smallrye-graphql-client.adoc index de25e13e8ff090..4aa0a118e0d6b7 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/web.adoc b/docs/src/main/asciidoc/web.adoc new file mode 100644 index 00000000000000..1f58a62ef29fbb --- /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 f2438f7f08a56d..6f23fd809cd4cd 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/arc/deployment/src/main/java/io/quarkus/arc/deployment/SplitPackageProcessor.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/SplitPackageProcessor.java index f68b4256900b6d..998c040af65d45 100644 --- a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/SplitPackageProcessor.java +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/SplitPackageProcessor.java @@ -39,16 +39,6 @@ public class SplitPackageProcessor { private static final Logger LOGGER = Logger.getLogger(SplitPackageProcessor.class); - private static final Predicate IGNORE_PACKAGE = new Predicate<>() { - - @Override - public boolean test(String packageName) { - // Remove the elements from this list when the original issue is fixed - // so that we can detect further issues. - return packageName.startsWith("io.fabric8.kubernetes"); - } - }; - @BuildStep void splitPackageDetection(ApplicationArchivesBuildItem archivesBuildItem, ArcConfig config, @@ -82,9 +72,6 @@ void splitPackageDetection(ApplicationArchivesBuildItem archivesBuildItem, // - "com.me.app.sub" found in [archiveA, archiveB] StringBuilder splitPackagesWarning = new StringBuilder(); for (String packageName : packageToArchiveMap.keySet()) { - if (IGNORE_PACKAGE.test(packageName)) { - continue; - } // skip packages based on pre-built predicates boolean skipEvaluation = false; 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 477eee7c6d506c..53923c2136643a 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 548d2afe985c25..03b232b3111bc2 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 559fcbe6fa3d09..a5ab0f15c4e3eb 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/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 5848f998f5836a..d7c43a70a89f66 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 c90a9da278e2c1..6f43ceb731a5a7 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 36118d8864f6ad..1fe3324d0aa50b 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/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/ArcConstraintValidatorFactoryImpl.java b/extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/ArcConstraintValidatorFactoryImpl.java index 6662d05dfb3f7b..174ce705d62afb 100644 --- a/extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/ArcConstraintValidatorFactoryImpl.java +++ b/extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/ArcConstraintValidatorFactoryImpl.java @@ -1,7 +1,5 @@ package io.quarkus.hibernate.validator.runtime; -import java.security.AccessController; -import java.security.PrivilegedAction; import java.util.IdentityHashMap; import java.util.Map; @@ -34,7 +32,7 @@ public class ArcConstraintValidatorFactoryImpl implements ConstraintValidatorFac } return instance; } - return run(NewInstance.action(key, "ConstraintValidator")); + return NewInstance.action(key, "ConstraintValidator").run(); } @Override @@ -45,13 +43,4 @@ public void releaseInstance(ConstraintValidator 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/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 258c33815d843e..1e4f916ab50d90 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 d26d6d25c480aa..db1c460fa1b64c 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 3af8105e1c621c..de1f2e47a1f36e 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 c3d20635e86120..a6c06d69fb47ab 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 639590f5d5b584..c8cb0d1cf3e562 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 ec95d7fb5a59c1..dba33ea8e0d72e 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/oidc-client/deployment/src/test/java/io/quarkus/oidc/client/OidcClientCredentialsJwtPrivateKeyTestCase.java b/extensions/oidc-client/deployment/src/test/java/io/quarkus/oidc/client/OidcClientCredentialsJwtPrivateKeyTestCase.java index 46e96feb39b376..d8252bd1a9614b 100644 --- a/extensions/oidc-client/deployment/src/test/java/io/quarkus/oidc/client/OidcClientCredentialsJwtPrivateKeyTestCase.java +++ b/extensions/oidc-client/deployment/src/test/java/io/quarkus/oidc/client/OidcClientCredentialsJwtPrivateKeyTestCase.java @@ -34,4 +34,14 @@ public void testClientCredentialsToken() { .statusCode(200) .body(equalTo("service-account-quarkus-app")); } + + @Test + public void testPrivateKeyToken() { + String token = RestAssured.when().get("/client/token-key").body().asString(); + RestAssured.given().auth().oauth2(token) + .when().get("/protected") + .then() + .statusCode(200) + .body(equalTo("service-account-quarkus-app")); + } } diff --git a/extensions/oidc-client/deployment/src/test/java/io/quarkus/oidc/client/OidcClientResource.java b/extensions/oidc-client/deployment/src/test/java/io/quarkus/oidc/client/OidcClientResource.java index 75c38a064d0ec3..bad7734d503870 100644 --- a/extensions/oidc-client/deployment/src/test/java/io/quarkus/oidc/client/OidcClientResource.java +++ b/extensions/oidc-client/deployment/src/test/java/io/quarkus/oidc/client/OidcClientResource.java @@ -14,6 +14,16 @@ public class OidcClientResource { @Inject OidcClient client; + @Inject + @NamedOidcClient("key") + OidcClient keyClient; + + @GET + @Path("token-key") + public Uni 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 e575a1e45fa924..2cb8795032d7a6 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 593d2f8161cdfb..93ce6084ed4f88 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 93834a53fb41e3..959265accfed36 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 2da30b8da5bf5e..c16645774ba651 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 6997a29ec767c4..fa855e47ec8277 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/src/main/java/io/quarkus/oidc/deployment/OidcBuildStep.java b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/OidcBuildStep.java index b4827b8306bf48..9bc2cae8475746 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/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 00000000000000..f6914aedfd2cca --- /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 133be33ae688c1..d8d1999f0d20f8 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/DynamicVerificationKeyResolver.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/DynamicVerificationKeyResolver.java index dbb2adeb2af491..a2a2d85a2ab969 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/OidcProvider.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProvider.java index 54c96cbff24e17..a3a826541673cb 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/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 d09633054b5fa8..11a918f6dd5ba1 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 6598d2a15f0ac8..0967428012cb26 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 a0d5486ba0a3da..393aa22568fb1c 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 debb921c7350b0..11873ab9292251 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 3509d439e58215..56809719f7b0af 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 78be8027ee3d0f..3b45e64f29e946 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; @@ -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) { @@ -393,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); @@ -431,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); @@ -568,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()); @@ -690,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<>(); @@ -701,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, @@ -739,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 @@ -785,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('='); @@ -799,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()); @@ -817,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()); @@ -834,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); @@ -879,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))); @@ -922,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) { @@ -935,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, @@ -943,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); @@ -970,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 @@ -997,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 */ @@ -1030,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) @@ -1101,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++; @@ -1142,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() @@ -1195,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) @@ -1419,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 917c311f6aee75..7353f30506eaa4 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/i18n/MessageBundleDefaultedNameTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleDefaultedNameTest.java index c171a5f3e1a224..1db40628268385 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 00000000000000..8ac3a9e7398104 --- /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 2131cc87cac11d..31d2bdcf22a256 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 cd6d3f735c2809..fcc4f14a9c4149 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/MessageBundleTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleTest.java index 3b998b0a02af69..c9349a722dd846 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 00000000000000..7e26e81d95345c --- /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 7e26be68d78340..9095a01599387a 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/enu.properties b/extensions/qute/deployment/src/test/resources/messages/enu.properties new file mode 100644 index 00000000000000..072f933eb08818 --- /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 00000000000000..e3f5c0a2ae6def --- /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 4b54f8bf586b82..e322d21914f7d7 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 8f4c68664af850..93c5fbe6b1327c 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 b460ac5b144d85..6191405d631fc3 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/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 ead9b449449227..7ecc462c52d120 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 3816b652ac5534..9529ffff88426d 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); @@ -1176,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); } @@ -1793,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); @@ -1888,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()) { 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 ab0a0675b8de39..b3fd11d635c78f 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 2a70e77f31ff75..69bec01287d099 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/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 00000000000000..534494892a3a44 --- /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 f923fc6dcaf70a..341986949891a2 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 00000000000000..9f334e8c3fc7cd --- /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 b1a40ce4a00c7a..4955a2bbf54b75 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/SmallRyeFaultToleranceProcessor.java b/extensions/smallrye-fault-tolerance/deployment/src/main/java/io/quarkus/smallrye/faulttolerance/deployment/SmallRyeFaultToleranceProcessor.java index 58c7a022dda81d..6bd18bf866affa 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(); 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 8fc89fdeb99c46..359f9c4417aaa3 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 f9f174a2839004..7237c7008c4fb3 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/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 00000000000000..ec46f533c45643 --- /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 00000000000000..2ffe0778d69f79 --- /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 9ef02fe878268c..9a79b8d12fda0f 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 ff38df72391d65..e1c76dc33dde36 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 6edb66693f9062..9e4b7a4e815256 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 5018b1aee2b35e..5233fd4a1cc34f 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 a0b98d13b12094..c53d15645b01dd 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 c9df9d7965a50b..0e94f7df30f001 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 48c92af011388d..97a4bbd430776d 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 79b2f7af860b2e..ebccb00134a45c 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 1574f72bc58be4..04b8bc44075efa 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 94d6c17e59d128..25e8e75258b5d5 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 72d4853101f833..9527ac27ae1c58 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 5de156b6170d51..43b6c612db3c85 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 6b3f020fc9481a..76a368267c4725 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 d7ef2fede1a11d..f548a6087b6a45 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 a67ea603cc11dd..0b1a67c849e3af 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 0163206d6ca7c6..7cb2c87660d918 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 5dd28847b94b04..081d16751e608e 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 0fc33ce7e6c964..7580d82d48e39f 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 4c84a064e46d40..2ce4864251c0a3 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 66c7f7b1b8261d..f65b917fd3f5de 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 0dcc5c24da3257..95784e1755be8f 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 d245c1cad796cf..b03ebc134dbcb3 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 5a15d4b205b381..8751c22210ce48 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 641c677f562dd8..cfc4b0539fbe91 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/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 ade5086b191476..dc86cd8b16d823 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 880ef8079b0bb2..5a5d2666f48e5c 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 21004a9983db86..c13b3c844e6deb 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/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 e7109757aa759a..037ad003fea3ff 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 00000000000000..9a3b9e1855b4e2 --- /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 7a3fd7574fb5df..006cd4923d840b 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 14d4f5ef80f2b8..c489b9c958b6ab 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 00000000000000..d9cb55946daac0 --- /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/util/DependencyUtils.java b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/util/DependencyUtils.java index 66998179e9e7c6..cc5f42ddec40a2 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/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 3e528969e5899c..62c02e8fe08d3b 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) { 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 25d496ec4a206d..5b5a2f1f9a42a7 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,5 +1,6 @@ package io.quarkus.qute; +import java.util.Locale; import java.util.concurrent.CompletionStage; import java.util.function.Consumer; import java.util.function.Function; @@ -31,6 +32,11 @@ 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)} and {@link #computedData(String, Function)}. @@ -70,7 +76,6 @@ default TemplateInstance computedData(String key, Function funct } /** - * * @param key * @param value * @return self @@ -80,7 +85,6 @@ default TemplateInstance setAttribute(String key, Object value) { } /** - * * @param key * @return the attribute or null */ @@ -142,7 +146,6 @@ default CompletionStage consume(Consumer consumer) { } /** - * * @return the timeout * @see TemplateInstance#TIMEOUT */ @@ -151,7 +154,6 @@ default long getTimeout() { } /** - * * @return the original template */ default Template getTemplate() { @@ -159,7 +161,6 @@ default Template getTemplate() { } /** - * * @param id * @return the fragment or {@code null} * @see Template#getFragment(String) @@ -178,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/test/java/io/quarkus/qute/TemplateInstanceTest.java b/independent-projects/qute/core/src/test/java/io/quarkus/qute/TemplateInstanceTest.java index ba675fb3c6fe12..1ee22f343b255f 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 @@ -4,6 +4,7 @@ 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; @@ -64,4 +65,33 @@ public void testComputeData() { 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/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 7b7bc74d9c2205..b2fcbb5852922e 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/FileUpload.java b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/multipart/FileUpload.java index 57a2bc9996ffaf..b844cf4eab2504 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 @@ -5,7 +5,7 @@ /** * Represent a file that has been uploaded. *

- * WARNING: This type is currently only supported on the server + * 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/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 57125c8938306d..2e4c52b4228f4a 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/cache/src/main/java/io/quarkus/it/cache/ExpensiveResource.java b/integration-tests/cache/src/main/java/io/quarkus/it/cache/ExpensiveResource.java index 8efdcf9abfdc04..84cdc5a3a75bd4 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 00000000000000..14e9feb8082c47 --- /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 b94edbb9836783..9f87c194343406 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 48fc07d224685a..64197281e8775d 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 9e8da069b8d21d..02b4a89c883238 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 51853a2854b11b..00000000000000 --- 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 5fe588155b6220..5fe0786811fbae 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 bef37a3a9e0537..701b6b0e85216c 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 632306895da849..00000000000000 --- 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 85511daf4fef96..b3f51254a7fea3 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 ef8aa9634fae04..1127443b989b9f 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/liquibase/src/main/java/io/quarkus/it/liquibase/LiquibaseFunctionalityResource.java b/integration-tests/liquibase/src/main/java/io/quarkus/it/liquibase/LiquibaseFunctionalityResource.java index b71310c69e38cd..501fbdb58a82f9 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 f2bc258ff694d1..036f9cef122dc8 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 00000000000000..8d79230fa4d328 --- /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 00000000000000..7878e39dd51fd3 --- /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 00000000000000..f1f2c732613c68 --- /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 00000000000000..60c8d153f099a6 --- /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 52246e1254d69c..447537cdef628d 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 39fd504562a81b..262bc40ceab095 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 00000000000000..a2b4beaacf7148 --- /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 00000000000000..98fb9ae1398c6c --- /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 00000000000000..6e8f3dfe1026fa --- /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 00000000000000..a8a4efacded055 --- /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 00000000000000..c21e5305ea7935 --- /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 00000000000000..a6d66f8b9eda28 --- /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 00000000000000..36cfbe3d82e5e3 --- /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 00000000000000..c09bb5c96b8694 --- /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 00000000000000..4afcfeef83dcac --- /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 00000000000000..e69de29bb2d1d6 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 00000000000000..cddc725179c67e --- /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 00000000000000..c2f29e2c9f7114 --- /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-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 59664d4e215107..866a6d9ea0a404 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 00000000000000..d7e35894704209 --- /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 00000000000000..39a1ce4c06837b --- /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/resources/application.properties b/integration-tests/oidc-wiremock/src/main/resources/application.properties index 807d6199060367..15e351b94c6bf2 100644 --- a/integration-tests/oidc-wiremock/src/main/resources/application.properties +++ b/integration-tests/oidc-wiremock/src/main/resources/application.properties @@ -196,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 d95361d301e6c4..af9862304184f7 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/TestUtils.java b/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/TestUtils.java index a7439ceacd0480..591ca8c360f4dc 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 f67e195d26ce94..85463e6b768d0a 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 f10a0be9518492..b0ed21891c967f 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/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 c834a4ca976540..6ff6f8303ec7c2 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 2cbd9f85afd5d0..4d73fc4210d596 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)