diff --git a/.github/actions/await-http-resource/action.yml b/.github/actions/await-http-resource/action.yml index 7d2b3462b537..ba177fb757b5 100644 --- a/.github/actions/await-http-resource/action.yml +++ b/.github/actions/await-http-resource/action.yml @@ -1,8 +1,8 @@ name: Await HTTP Resource -description: Waits for an HTTP resource to be available (a HEAD request succeeds) +description: 'Waits for an HTTP resource to be available (a HEAD request succeeds)' inputs: url: - description: 'The URL of the resource to await' + description: 'URL of the resource to await' required: true runs: using: composite diff --git a/.github/actions/build/action.yml b/.github/actions/build/action.yml index 39adc65be1c7..b45040d0348f 100644 --- a/.github/actions/build/action.yml +++ b/.github/actions/build/action.yml @@ -1,35 +1,51 @@ -name: 'Build' +name: Build description: 'Builds the project, optionally publishing it to a local deployment repository' inputs: - java-version: + commercial-release-repository-url: + description: 'URL of the release repository' required: false - default: '17' - description: 'The Java version to compile and test with' + commercial-repository-password: + description: 'Password for authentication with the commercial repository' + required: false + commercial-repository-username: + description: 'Username for authentication with the commercial repository' + required: false + commercial-snapshot-repository-url: + description: 'URL of the snapshot repository' + required: false + develocity-access-key: + description: 'Access key for authentication with ge.spring.io' + required: false + gradle-cache-read-only: + description: 'Whether Gradle''s cache should be read only' + required: false + default: 'true' + java-distribution: + description: 'Java distribution to use' + required: false + default: 'liberica' java-early-access: + description: 'Whether the Java version is in early access' required: false default: 'false' - description: 'Whether the Java version is in early access' java-toolchain: + description: 'Whether a Java toolchain should be used' required: false default: 'false' - description: 'Whether a Java toolchain should be used' - java-distribution: + java-version: + description: 'Java version to compile and test with' required: false - default: 'liberica' - description: 'The distribution of Java to use' + default: '17' publish: - required: false - default: 'false' description: 'Whether to publish artifacts ready for deployment to Artifactory' - develocity-access-key: required: false - description: 'The access key for authentication with ge.spring.io' + default: 'false' outputs: build-scan-url: - description: 'The URL, if any, of the build scan produced by the build' + description: 'URL, if any, of the build scan produced by the build' value: ${{ (inputs.publish == 'true' && steps.publish.outputs.build-scan-url) || steps.build.outputs.build-scan-url }} version: - description: 'The version that was built' + description: 'Version that was built' value: ${{ steps.read-version.outputs.version }} runs: using: composite @@ -37,20 +53,31 @@ runs: - name: Prepare Gradle Build uses: ./.github/actions/prepare-gradle-build with: + cache-read-only: ${{ inputs.gradle-cache-read-only }} develocity-access-key: ${{ inputs.develocity-access-key }} - java-version: ${{ inputs.java-version }} + java-distribution: ${{ inputs.java-distribution }} java-early-access: ${{ inputs.java-early-access }} java-toolchain: ${{ inputs.java-toolchain }} - java-distribution: ${{ inputs.java-distribution }} + java-version: ${{ inputs.java-version }} - name: Build id: build if: ${{ inputs.publish == 'false' }} shell: bash + env: + COMMERCIAL_RELEASE_REPO_URL: ${{ inputs.commercial-release-repository-url }} + COMMERCIAL_REPO_PASSWORD: ${{ inputs.commercial-repository-password }} + COMMERCIAL_REPO_USERNAME: ${{ inputs.commercial-repository-username }} + COMMERCIAL_SNAPSHOT_REPO_URL: ${{ inputs.commercial-snapshot-repository-url }} run: ./gradlew build - name: Publish id: publish if: ${{ inputs.publish == 'true' }} shell: bash + env: + COMMERCIAL_RELEASE_REPO_URL: ${{ inputs.commercial-release-repository-url }} + COMMERCIAL_REPO_PASSWORD: ${{ inputs.commercial-repository-password }} + COMMERCIAL_REPO_USERNAME: ${{ inputs.commercial-repository-username }} + COMMERCIAL_SNAPSHOT_REPO_URL: ${{ inputs.commercial-snapshot-repository-url }} run: ./gradlew -PdeploymentRepository=$(pwd)/deployment-repository ${{ !startsWith(github.event.head_commit.message, 'Next development version') && 'build' || '' }} publishAllPublicationsToDeploymentRepository - name: Read Version From gradle.properties id: read-version diff --git a/.github/actions/create-github-release/action.yml b/.github/actions/create-github-release/action.yml index e0120764f1e4..8137274282fc 100644 --- a/.github/actions/create-github-release/action.yml +++ b/.github/actions/create-github-release/action.yml @@ -1,11 +1,14 @@ name: Create GitHub Release -description: Create the release on GitHub with a changelog +description: 'Create the release on GitHub with a changelog' inputs: milestone: - description: Name of the GitHub milestone for which a release will be created + description: 'Name of the GitHub milestone for which a release will be created' required: true token: - description: Token to use for authentication with GitHub + description: 'Token to use for authentication with GitHub' + required: true + commercial: + description: 'Whether to generate the changelog for the commercial release' required: true runs: using: composite @@ -13,11 +16,11 @@ runs: - name: Generate Changelog uses: spring-io/github-changelog-generator@185319ad7eaa75b0e8e72e4b6db19c8b2cb8c4c1 #v0.0.11 with: + config-file: ${{ inputs.commercial && '.github/actions/create-github-release/changelog-generator-commercial.yml' || '.github/actions/create-github-release/changelog-generator-oss.yml' }} milestone: ${{ inputs.milestone }} token: ${{ inputs.token }} - config-file: .github/actions/create-github-release/changelog-generator.yml - name: Create GitHub Release + shell: bash env: GITHUB_TOKEN: ${{ inputs.token }} - shell: bash run: gh release create ${{ format('v{0}', inputs.milestone) }} --notes-file changelog.md diff --git a/.github/actions/create-github-release/changelog-generator.yml b/.github/actions/create-github-release/changelog-generator-commercial.yml similarity index 93% rename from .github/actions/create-github-release/changelog-generator.yml rename to .github/actions/create-github-release/changelog-generator-commercial.yml index 67db9ca4f9d7..2bc74db2a75a 100644 --- a/.github/actions/create-github-release/changelog-generator.yml +++ b/.github/actions/create-github-release/changelog-generator-commercial.yml @@ -1,5 +1,4 @@ changelog: - repository: spring-projects/spring-boot sections: - title: ":star: New Features" labels: @@ -16,6 +15,7 @@ changelog: labels: - "type: dependency-upgrade" issues: + generate_links: false ports: - label: "status: forward-port" bodyExpression: 'Forward port of issue #(\d+).*' diff --git a/.github/actions/create-github-release/changelog-generator-oss.yml b/.github/actions/create-github-release/changelog-generator-oss.yml new file mode 100644 index 000000000000..dea85e8267a2 --- /dev/null +++ b/.github/actions/create-github-release/changelog-generator-oss.yml @@ -0,0 +1,23 @@ +changelog: + sections: + - title: ":star: New Features" + labels: + - "type: enhancement" + - title: ":lady_beetle: Bug Fixes" + labels: + - "type: bug" + - "type: regression" + - title: ":notebook_with_decorative_cover: Documentation" + labels: + - "type: documentation" + - title: ":hammer: Dependency Upgrades" + sort: "title" + labels: + - "type: dependency-upgrade" + issues: + generate_links: true + ports: + - label: "status: forward-port" + bodyExpression: 'Forward port of issue #(\d+).*' + - label: "status: back-port" + bodyExpression: 'Back port of issue #(\d+).*' diff --git a/.github/actions/prepare-gradle-build/action.yml b/.github/actions/prepare-gradle-build/action.yml index 5404276dd71e..bbb5f13585b8 100644 --- a/.github/actions/prepare-gradle-build/action.yml +++ b/.github/actions/prepare-gradle-build/action.yml @@ -1,25 +1,29 @@ -name: 'Prepare Gradle Build' +name: Prepare Gradle Build description: 'Prepares a Gradle build. Sets up Java and Gradle and configures Gradle properties' inputs: - java-version: + cache-read-only: + description: 'Whether Gradle''s cache should be read only' required: false - default: '17' - description: 'The Java version to use for the build' - java-early-access: + default: 'true' + develocity-access-key: + description: 'Access key for authentication with ge.spring.io' required: false - default: 'false' + java-distribution: + description: 'Java distribution to use' + required: false + default: 'liberica' + java-early-access: description: 'Whether the Java version is in early access. When true, forces java-distribution to temurin' - java-toolchain: required: false default: 'false' + java-toolchain: description: 'Whether a Java toolchain should be used' - java-distribution: required: false - default: 'liberica' - description: 'The distribution of Java to use' - develocity-access-key: + default: 'false' + java-version: + description: 'Java version to use for the build' required: false - description: 'The access key for authentication with ge.spring.io' + default: '17' runs: using: composite steps: @@ -27,6 +31,7 @@ runs: if: ${{ runner.os == 'Linux' }} uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1 with: + tool-cache: true docker-images: false - name: Set Up Java uses: actions/setup-java@v4 @@ -35,11 +40,17 @@ runs: java-version: | ${{ inputs.java-early-access == 'true' && format('{0}-ea', inputs.java-version) || inputs.java-version }} ${{ inputs.java-toolchain == 'true' && '17' || '' }} - - name: Set Up Gradle + - name: Set Up Gradle With Read/Write Cache + if: ${{ inputs.cache-read-only == 'false' }} uses: gradle/actions/setup-gradle@d156388eb19639ec20ade50009f3d199ce1e2808 # v4.1.0 with: cache-read-only: false develocity-access-key: ${{ inputs.develocity-access-key }} + - name: Set Up Gradle + uses: gradle/actions/setup-gradle@d156388eb19639ec20ade50009f3d199ce1e2808 # v4.1.0 + with: + develocity-access-key: ${{ inputs.develocity-access-key }} + develocity-token-expiry: 4 - name: Configure Gradle Properties shell: bash run: | diff --git a/.github/actions/print-jvm-thread-dumps/action.yml b/.github/actions/print-jvm-thread-dumps/action.yml index 9b0905b77725..bcaebf3676aa 100644 --- a/.github/actions/print-jvm-thread-dumps/action.yml +++ b/.github/actions/print-jvm-thread-dumps/action.yml @@ -1,5 +1,5 @@ name: Print JVM thread dumps -description: Prints a thread dump for all running JVMs +description: 'Prints a thread dump for all running JVMs' runs: using: composite steps: diff --git a/.github/actions/publish-gradle-plugin/action.yml b/.github/actions/publish-gradle-plugin/action.yml index eed0b5a89053..50d45f972a2a 100644 --- a/.github/actions/publish-gradle-plugin/action.yml +++ b/.github/actions/publish-gradle-plugin/action.yml @@ -1,22 +1,22 @@ name: Publish Gradle Plugin -description: Publishes Spring Boot's Gradle plugin to the Plugin Portal +description: 'Publishes Spring Boot''s Gradle plugin to the Plugin Portal' inputs: - jfrog-cli-config-token: - description: 'Config token for the JFrog CLI' - required: true - plugin-version: - description: 'Version of the plugin' - required: true + build-number: + description: 'Build number to use when downloading plugin artifacts' + required: false + default: ${{ github.run_number }} gradle-plugin-publish-key: description: 'Gradle publishing key' required: true gradle-plugin-publish-secret: description: 'Gradle publishing secret' required: true - build-number: - description: 'The build number to use when downloading plugin artifacts' - required: false - default: ${{ github.run_number }} + jfrog-cli-config-token: + description: 'Config token for the JFrog CLI' + required: true + plugin-version: + description: 'Version of the plugin' + required: true runs: using: composite steps: diff --git a/.github/actions/publish-to-sdkman/action.yml b/.github/actions/publish-to-sdkman/action.yml index 7458c863ac57..3abdd67e27bd 100644 --- a/.github/actions/publish-to-sdkman/action.yml +++ b/.github/actions/publish-to-sdkman/action.yml @@ -1,6 +1,10 @@ name: Publish to SDKMAN! -description: Publishes the release as a new candidate version on SDKMAN! +description: 'Publishes the release as a new candidate version on SDKMAN!' inputs: + make-default: + description: 'Whether the release should be made the default version' + required: false + default: 'false' sdkman-consumer-key: description: 'Key for publishing to SDKMAN!' required: true @@ -8,16 +12,13 @@ inputs: description: 'Token for publishing to SDKMAN!' required: true spring-boot-version: - description: 'The version to publish' + description: 'Version to publish' required: true - make-default: - description: 'Whether the release should be made the default version' - required: false - default: false runs: using: composite steps: - - shell: bash + - name: Publish Release + shell: bash run: > curl -X POST \ -H "Consumer-Key: ${{ inputs.sdkman-consumer-key }}" \ @@ -26,8 +27,9 @@ runs: -H "Accept: application/json" \ -d '{"candidate": "springboot", "version": "${{ inputs.spring-boot-version }}", "url": "${{ format('https://repo.maven.apache.org/maven2/org/springframework/boot/spring-boot-cli/{0}/spring-boot-cli-{0}-bin.zip', inputs.spring-boot-version) }}"}' \ https://vendors.sdkman.io/release - - shell: bash + - name: Flag Release as Default if: ${{ inputs.make-default == 'true' }} + shell: bash run: > curl -X PUT \ -H "Consumer-Key: ${{ inputs.sdkman-consumer-key }}" \ diff --git a/.github/actions/send-notification/action.yml b/.github/actions/send-notification/action.yml index d1389776397a..b379e67897d1 100644 --- a/.github/actions/send-notification/action.yml +++ b/.github/actions/send-notification/action.yml @@ -1,33 +1,39 @@ name: Send Notification -description: Sends a Google Chat message as a notification of the job's outcome +description: 'Sends a Google Chat message as a notification of the job''s outcome' inputs: - webhook-url: - description: 'Google Chat Webhook URL' - required: true - status: - description: 'Status of the job' - required: true build-scan-url: description: 'URL of the build scan to include in the notification' + required: false run-name: description: 'Name of the run to include in the notification' + required: false default: ${{ format('{0} {1}', github.ref_name, github.job) }} + status: + description: 'Status of the job' + required: true + webhook-url: + description: 'Google Chat Webhook URL' + required: true runs: using: composite steps: - - shell: bash + - name: Prepare Variables + shell: bash run: | echo "BUILD_SCAN=${{ inputs.build-scan-url == '' && ' [build scan unavailable]' || format(' [<{0}|Build Scan>]', inputs.build-scan-url) }}" >> "$GITHUB_ENV" echo "RUN_URL=${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" >> "$GITHUB_ENV" - - shell: bash + - name: Success Notification if: ${{ inputs.status == 'success' }} + shell: bash run: | curl -X POST '${{ inputs.webhook-url }}' -H 'Content-Type: application/json' -d '{ text: "<${{ env.RUN_URL }}|${{ inputs.run-name }}> was successful ${{ env.BUILD_SCAN }}"}' || true - - shell: bash + - name: Failure Notification if: ${{ inputs.status == 'failure' }} + shell: bash run: | curl -X POST '${{ inputs.webhook-url }}' -H 'Content-Type: application/json' -d '{ text: " *<${{ env.RUN_URL }}|${{ inputs.run-name }}> failed* ${{ env.BUILD_SCAN }}"}' || true - - shell: bash + - name: Cancel Notification if: ${{ inputs.status == 'cancelled' }} + shell: bash run: | curl -X POST '${{ inputs.webhook-url }}' -H 'Content-Type: application/json' -d '{ text: "<${{ env.RUN_URL }}|${{ inputs.run-name }}> was cancelled"}' || true diff --git a/.github/actions/sync-to-maven-central/action.yml b/.github/actions/sync-to-maven-central/action.yml index df08b447f68f..74dd611fc728 100644 --- a/.github/actions/sync-to-maven-central/action.yml +++ b/.github/actions/sync-to-maven-central/action.yml @@ -1,20 +1,20 @@ name: Sync to Maven Central -description: Syncs a release to Maven Central and waits for it to be available for use +description: 'Syncs a release to Maven Central and waits for it to be available for use' inputs: jfrog-cli-config-token: description: 'Config token for the JFrog CLI' required: true - spring-boot-version: - description: 'The version of Spring Boot that is being synced to Central' - required: true - ossrh-s01-token-username: - description: 'Username for authentication with s01.oss.sonatype.org' + ossrh-s01-staging-profile: + description: 'Staging profile to use when syncing to Central' required: true ossrh-s01-token-password: description: 'Password for authentication with s01.oss.sonatype.org' required: true - ossrh-s01-staging-profile: - description: 'Staging profile to use when syncing to Central' + ossrh-s01-token-username: + description: 'Username for authentication with s01.oss.sonatype.org' + required: true + spring-boot-version: + description: 'Version of Spring Boot that is being synced to Central' required: true runs: using: composite @@ -29,14 +29,14 @@ runs: - name: Sync uses: spring-io/nexus-sync-action@42477a2230a2f694f9eaa4643fa9e76b99b7ab84 # v0.0.1 with: - username: ${{ inputs.ossrh-s01-token-username }} + close: true + create: true + generate-checksums: true password: ${{ inputs.ossrh-s01-token-password }} + release: true staging-profile-name: ${{ inputs.ossrh-s01-staging-profile }} - create: true upload: true - close: true - release: true - generate-checksums: true + username: ${{ inputs.ossrh-s01-token-username }} - name: Await uses: ./.github/actions/await-http-resource with: diff --git a/.github/scripts/reclaim-docker-diskspace.sh b/.github/scripts/reclaim-docker-diskspace.sh new file mode 100755 index 000000000000..e32f3b1d8c88 --- /dev/null +++ b/.github/scripts/reclaim-docker-diskspace.sh @@ -0,0 +1,29 @@ +#!/bin/bash + +echo "Reclaiming Docker Disk Space" +echo + +docker image ls --format "{{.Size}} {{.ID}} {{.Repository}} {{.Tag}}" | LANG=en_US sort -rh | while read line; do + size=$( echo "$line" | cut -d' ' -f1 | sed -e 's/\.[0-9]*//' | sed -e 's/MB/000000/' | sed -e 's/GB/000000000/' ) + image=$( echo "$line" | cut -d' ' -f2 ) + repository=$( echo "$line" | cut -d' ' -f3 ) + tag=$( echo "$line" | cut -d' ' -f4 ) + echo "Considering $image $repository:$tag $size" + if [[ "$tag" =~ ^[a-f0-9]{32}$ ]]; then + echo "Ignoring GitHub action image $image $repository:$tag" + elif [[ "$tag" == "" ]]; then + echo "Ignoring untagged image $image $repository:$tag" + elif [[ "$size" -lt 200000000 ]]; then + echo "Ignoring small image $image $repository:$tag" + else + echo "Cleaning $image $repository:$tag" + docker image rm $image + fi +done + +echo "Finished cleanup, leaving the following containers:" +echo +docker image ls --format "{{.Size}} {{.ID}} {{.Repository}}:{{.Tag}}" | LANG=en_US sort -rh +echo +df -h +echo diff --git a/.github/workflows/build-and-deploy-snapshot.yml b/.github/workflows/build-and-deploy-snapshot.yml index de602f8fd635..7b5b3c9c87cc 100644 --- a/.github/workflows/build-and-deploy-snapshot.yml +++ b/.github/workflows/build-and-deploy-snapshot.yml @@ -8,8 +8,8 @@ concurrency: jobs: build-and-deploy-snapshot: name: Build and Deploy Snapshot + if: ${{ github.repository == 'spring-projects/spring-boot' || github.repository == 'spring-projects/spring-boot-commercial' }} runs-on: ${{ vars.UBUNTU_MEDIUM || 'ubuntu-latest' }} - if: ${{ github.repository == 'spring-projects/spring-boot' }} steps: - name: Check Out Code uses: actions/checkout@v4 @@ -17,35 +17,41 @@ jobs: id: build-and-publish uses: ./.github/actions/build with: - develocity-access-key: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }} + commercial-release-repository-url: ${{ vars.COMMERCIAL_RELEASE_REPO_URL }} + commercial-repository-password: ${{ secrets.COMMERCIAL_ARTIFACTORY_RO_PASSWORD }} + commercial-repository-username: ${{ secrets.COMMERCIAL_ARTIFACTORY_RO_USERNAME }} + commercial-snapshot-repository-url: ${{ vars.COMMERCIAL_SNAPSHOT_REPO_URL }} + develocity-access-key: ${{ secrets.DEVELOCITY_ACCESS_KEY }} + gradle-cache-read-only: false publish: true - name: Deploy uses: spring-io/artifactory-deploy-action@26bbe925a75f4f863e1e529e85be2d0093cac116 # v0.0.1 with: - uri: 'https://repo.spring.io' - username: ${{ secrets.ARTIFACTORY_USERNAME }} - password: ${{ secrets.ARTIFACTORY_PASSWORD }} - build-name: 'spring-boot-3.3.x' - repository: 'libs-snapshot-local' + build-name: ${{ vars.COMMERCIAL && format('spring-boot-commercial-{0}', github.ref_name) || format('spring-boot-{0}', github.ref_name) }} folder: 'deployment-repository' + password: ${{ vars.COMMERCIAL && secrets.COMMERCIAL_ARTIFACTORY_PASSWORD || secrets.ARTIFACTORY_PASSWORD }} + project: ${{ vars.COMMERCIAL && 'spring' }} + repository: ${{ vars.COMMERCIAL && 'spring-commercial-snapshot-local' || 'libs-snapshot-local' }} signing-key: ${{ secrets.GPG_PRIVATE_KEY }} signing-passphrase: ${{ secrets.GPG_PASSPHRASE }} + uri: ${{ vars.COMMERCIAL_DEPLOY_REPO_URL || 'https://repo.spring.io' }} + username: ${{ vars.COMMERCIAL && secrets.COMMERCIAL_ARTIFACTORY_USERNAME || secrets.ARTIFACTORY_USERNAME }} - name: Send Notification - uses: ./.github/actions/send-notification if: always() + uses: ./.github/actions/send-notification with: - webhook-url: ${{ secrets.GOOGLE_CHAT_WEBHOOK_URL }} - status: ${{ job.status }} build-scan-url: ${{ steps.build-and-publish.outputs.build-scan-url }} run-name: ${{ format('{0} | Linux | Java 17', github.ref_name) }} + status: ${{ job.status }} + webhook-url: ${{ secrets.GOOGLE_CHAT_WEBHOOK_URL }} outputs: version: ${{ steps.build-and-publish.outputs.version }} trigger-docs-build: name: Trigger Docs Build - runs-on: ubuntu-latest needs: build-and-deploy-snapshot permissions: actions: write + runs-on: ubuntu-latest steps: - name: Run Deploy Docs Workflow env: @@ -56,9 +62,11 @@ jobs: needs: build-and-deploy-snapshot uses: ./.github/workflows/verify.yml secrets: + commercial-repository-password: ${{ secrets.COMMERCIAL_ARTIFACTORY_RO_PASSWORD }} + commercial-repository-username: ${{ secrets.COMMERCIAL_ARTIFACTORY_RO_USERNAME }} google-chat-webhook-url: ${{ secrets.GOOGLE_CHAT_WEBHOOK_URL }} - repository-password: ${{ secrets.ARTIFACTORY_PASSWORD }} - repository-username: ${{ secrets.ARTIFACTORY_USERNAME }} + opensource-repository-password: ${{ secrets.ARTIFACTORY_PASSWORD }} + opensource-repository-username: ${{ secrets.ARTIFACTORY_USERNAME }} token: ${{ secrets.GH_ACTIONS_REPO_TOKEN }} with: version: ${{ needs.build-and-deploy-snapshot.outputs.version }} diff --git a/.github/workflows/build-pull-request.yml b/.github/workflows/build-pull-request.yml index 96667f8684b7..7a28e943cf5b 100644 --- a/.github/workflows/build-pull-request.yml +++ b/.github/workflows/build-pull-request.yml @@ -1,42 +1,24 @@ name: Build Pull Request on: pull_request - permissions: contents: read - jobs: build: name: Build Pull Request - runs-on: ${{ vars.UBUNTU_MEDIUM || 'ubuntu-latest' }} if: ${{ github.repository == 'spring-projects/spring-boot' }} + runs-on: ${{ vars.UBUNTU_MEDIUM || 'ubuntu-latest' }} steps: - - name: Free Disk Space - uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1 - with: - large-packages: false - docker-images: false - - name: Set Up JDK 17 - uses: actions/setup-java@v4 - with: - java-version: '17' - distribution: 'liberica' - - name: Check Out + - name: Check Out Code uses: actions/checkout@v4 - - name: Validate Gradle Wrapper - uses: gradle/actions/wrapper-validation@d156388eb19639ec20ade50009f3d199ce1e2808 # v4.1.0 - - name: Set Up Gradle - uses: gradle/actions/setup-gradle@d156388eb19639ec20ade50009f3d199ce1e2808 # v4.1.0 - name: Build - env: - CI: 'true' - GRADLE_ENTERPRISE_URL: 'https://ge.spring.io' - run: ./gradlew -Dorg.gradle.internal.launcher.welcomeMessageEnabled=false --no-daemon --no-parallel --continue build + id: build + uses: ./.github/actions/build - name: Print JVM Thread Dumps When Cancelled - uses: ./.github/actions/print-jvm-thread-dumps if: cancelled() + uses: ./.github/actions/print-jvm-thread-dumps - name: Upload Build Reports - uses: actions/upload-artifact@v4 if: failure() + uses: actions/upload-artifact@v4 with: name: build-reports path: '**/build/reports/' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c479e54d5819..00d5f647c9ca 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,8 +8,8 @@ concurrency: jobs: ci: name: '${{ matrix.os.name}} | Java ${{ matrix.java.version}}' + if: ${{ github.repository == 'spring-projects/spring-boot' || github.repository == 'spring-projects/spring-boot-commercial' }} runs-on: ${{ matrix.os.id }} - if: ${{ github.repository == 'spring-projects/spring-boot' }} strategy: fail-fast: false matrix: @@ -45,16 +45,21 @@ jobs: id: build uses: ./.github/actions/build with: - java-version: ${{ matrix.java.version }} + commercial-release-repository-url: ${{ vars.COMMERCIAL_RELEASE_REPO_URL }} + commercial-repository-password: ${{ secrets.COMMERCIAL_ARTIFACTORY_RO_PASSWORD }} + commercial-repository-username: ${{ secrets.COMMERCIAL_ARTIFACTORY_RO_USERNAME }} + commercial-snapshot-repository-url: ${{ vars.COMMERCIAL_SNAPSHOT_REPO_URL }} + develocity-access-key: ${{ secrets.DEVELOCITY_ACCESS_KEY }} + gradle-cache-read-only: false java-early-access: ${{ matrix.java.early-access || 'false' }} - java-toolchain: ${{ matrix.java.toolchain }} java-distribution: ${{ matrix.java.distribution }} - develocity-access-key: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }} + java-toolchain: ${{ matrix.java.toolchain }} + java-version: ${{ matrix.java.version }} - name: Send Notification - uses: ./.github/actions/send-notification if: always() + uses: ./.github/actions/send-notification with: - webhook-url: ${{ secrets.GOOGLE_CHAT_WEBHOOK_URL }} - status: ${{ job.status }} build-scan-url: ${{ steps.build.outputs.build-scan-url }} run-name: ${{ format('{0} | {1} | Java {2}', github.ref_name, matrix.os.name, matrix.java.version) }} + status: ${{ job.status }} + webhook-url: ${{ secrets.GOOGLE_CHAT_WEBHOOK_URL }} diff --git a/.github/workflows/distribute.yml b/.github/workflows/distribute.yml new file mode 100644 index 000000000000..55f9a9c36855 --- /dev/null +++ b/.github/workflows/distribute.yml @@ -0,0 +1,41 @@ +name: Distribute +on: + workflow_dispatch: + inputs: + build-number: + description: 'Number of the build to use to create the bundle' + required: true + type: string + create-bundle: + description: 'Whether to create the bundle. If unchecked, only the bundle distribution is executed' + required: true + type: boolean + default: true + version: + description: 'Version to bundle and distribute' + required: true + type: string +jobs: + distribute-spring-enterprise-release-bundle: + runs-on: ${{ vars.UBUNTU_SMALL || 'ubuntu-latest' }} + steps: + - name: Create Bundle + if: ${{ vars.COMMERCIAL && inputs.create-bundle }} + shell: bash + run: | + curl -s -u "${{ secrets.COMMERCIAL_ARTIFACTORY_USERNAME }}:${{ secrets.COMMERCIAL_ARTIFACTORY_PASSWORD }}" \ + -X POST -H "X-JFrog-Signing-Key-Name: packagesKey" -H "Content-Type: application/json" \ + "https://usw1.packages.broadcom.com/lifecycle/api/v2/release_bundle?project=spring" \ + -d '{"release_bundle_name": "TNZ-spring-boot-commercial", "release_bundle_version": "${{ inputs.version }}", "skip_docker_manifest_resolution": true, "source_type": "builds", "source": {"builds": [ {"build_repository": "spring-build-info", "build_name": "spring-boot-commercial-${{ inputs.version }}", "build_number": "${{ inputs.build-number }}", "include_dependencies": false}]}}' + - name: Sleep + if: ${{ vars.COMMERCIAL && inputs.create-bundle }} + shell: bash + run: sleep 30 + - name: Distribute Bundle + if: ${{ vars.COMMERCIAL }} + shell: bash + run: | + curl -s -u "${{ secrets.COMMERCIAL_ARTIFACTORY_USERNAME }}:${{ secrets.COMMERCIAL_ARTIFACTORY_PASSWORD }}" \ + -X POST -H "Content-Type: application/json" \ + "https://usw1.packages.broadcom.com/lifecycle/api/v2/distribution/distribute/TNZ-spring-boot-commercial/${{ inputs.version }}?project=spring" \ + -d '{"auto_create_missing_repositories": "false", "distribution_rules": [{"site_name": "JP-SaaS"}], "modifications": {"mappings": [{"input": "spring-enterprise-maven-prod-local/(.*)", "output": "spring-enterprise/$1"}]}}' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 058c1492ff9a..fe898921cdf9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,8 +8,8 @@ concurrency: jobs: build-and-stage-release: name: Build and Stage Release + if: ${{ github.repository == 'spring-projects/spring-boot' || github.repository == 'spring-projects/spring-boot-commercial' }} runs-on: ${{ vars.UBUNTU_MEDIUIM || 'ubuntu-latest' }} - if: ${{ github.repository == 'spring-projects/spring-boot' }} steps: - name: Check Out Code uses: actions/checkout@v4 @@ -17,35 +17,44 @@ jobs: id: build-and-publish uses: ./.github/actions/build with: - develocity-access-key: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }} + commercial-release-repository-url: ${{ vars.COMMERCIAL_RELEASE_REPO_URL }} + commercial-repository-password: ${{ secrets.COMMERCIAL_ARTIFACTORY_RO_PASSWORD }} + commercial-repository-username: ${{ secrets.COMMERCIAL_ARTIFACTORY_RO_USERNAME }} + commercial-snapshot-repository-url: ${{ vars.COMMERCIAL_SNAPSHOT_REPO_URL }} + develocity-access-key: ${{ secrets.DEVELOCITY_ACCESS_KEY }} + gradle-cache-read-only: false publish: true - name: Stage Release uses: spring-io/artifactory-deploy-action@26bbe925a75f4f863e1e529e85be2d0093cac116 # v0.0.1 with: - uri: 'https://repo.spring.io' - username: ${{ secrets.ARTIFACTORY_USERNAME }} - password: ${{ secrets.ARTIFACTORY_PASSWORD }} - build-name: ${{ format('spring-boot-{0}', steps.build-and-publish.outputs.version)}} - repository: 'libs-staging-local' + build-name: ${{ vars.COMMERCIAL && format('spring-boot-commercial-{0}', steps.build-and-publish.outputs.version) || format('spring-boot-{0}', steps.build-and-publish.outputs.version) }} folder: 'deployment-repository' + password: ${{ vars.COMMERCIAL && secrets.COMMERCIAL_ARTIFACTORY_PASSWORD || secrets.ARTIFACTORY_PASSWORD }} + project: ${{ vars.COMMERCIAL && 'spring' }} + repository: ${{ vars.COMMERCIAL && 'spring-enterprise-maven-stage-local' || 'libs-staging-local' }} signing-key: ${{ secrets.GPG_PRIVATE_KEY }} signing-passphrase: ${{ secrets.GPG_PASSPHRASE }} + uri: ${{ vars.COMMERCIAL_DEPLOY_REPO_URL || 'https://repo.spring.io' }} + username: ${{ vars.COMMERCIAL && secrets.COMMERCIAL_ARTIFACTORY_USERNAME || secrets.ARTIFACTORY_USERNAME }} outputs: version: ${{ steps.build-and-publish.outputs.version }} verify: name: Verify needs: build-and-stage-release uses: ./.github/workflows/verify.yml - with: - staging: true - version: ${{ needs.build-and-stage-release.outputs.version }} secrets: + commercial-repository-password: ${{ secrets.COMMERCIAL_ARTIFACTORY_RO_PASSWORD }} + commercial-repository-username: ${{ secrets.COMMERCIAL_ARTIFACTORY_RO_USERNAME }} google-chat-webhook-url: ${{ secrets.GOOGLE_CHAT_WEBHOOK_URL }} - repository-password: ${{ secrets.ARTIFACTORY_PASSWORD }} - repository-username: ${{ secrets.ARTIFACTORY_USERNAME }} + opensource-repository-password: ${{ secrets.ARTIFACTORY_PASSWORD }} + opensource-repository-username: ${{ secrets.ARTIFACTORY_USERNAME }} token: ${{ secrets.GH_ACTIONS_REPO_TOKEN }} + with: + staging: true + version: ${{ needs.build-and-stage-release.outputs.version }} sync-to-maven-central: name: Sync to Maven Central + if: ${{ !vars.COMMERCIAL }} needs: - build-and-stage-release - verify @@ -71,11 +80,16 @@ jobs: - name: Set up JFrog CLI uses: jfrog/setup-jfrog-cli@9fe0f98bd45b19e6e931d457f4e98f8f84461fb5 # v4.4.1 env: - JF_ENV_SPRING: ${{ secrets.JF_ARTIFACTORY_SPRING }} - - name: Promote build + JF_ENV_SPRING: ${{ vars.COMMERCIAL && secrets.COMMERCIAL_JF_ARTIFACTORY_SPRING || secrets.JF_ARTIFACTORY_SPRING }} + - name: Promote open source build + if: ${{ !vars.COMMERCIAL }} run: jfrog rt build-promote ${{ format('spring-boot-{0}', needs.build-and-stage-release.outputs.version)}} ${{ github.run_number }} libs-release-local + - name: Promote commercial build + if: ${{ vars.COMMERCIAL }} + run: jfrog rt build-promote ${{ format('spring-boot-commercial-{0}', needs.build-and-stage-release.outputs.version)}} ${{ github.run_number }} spring-enterprise-maven-prod-local --project spring publish-gradle-plugin: name: Publish Gradle Plugin + if: ${{ !vars.COMMERCIAL }} needs: - build-and-stage-release - sync-to-maven-central @@ -92,6 +106,7 @@ jobs: plugin-version: ${{ needs.build-and-stage-release.outputs.version }} publish-to-sdkman: name: Publish to SDKMAN! + if: ${{ !vars.COMMERCIAL }} needs: - build-and-stage-release - sync-to-maven-central @@ -120,6 +135,19 @@ jobs: with: spring-boot-version: ${{ needs.build-and-stage-release.outputs.version }} token: ${{ secrets.GH_ACTIONS_REPO_TOKEN }} + trigger-docs-build: + name: Trigger Docs Build + needs: + - build-and-stage-release + - sync-to-maven-central + permissions: + actions: write + runs-on: ubuntu-latest + steps: + - name: Run Deploy Docs Workflow + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: gh workflow run deploy-docs.yml --repo spring-projects/spring-boot -r docs-build -f build-refname=${{ github.ref_name }} -f build-version=${{ needs.build-and-stage-release.outputs.version }} create-github-release: name: Create GitHub Release needs: @@ -127,6 +155,7 @@ jobs: - promote-release - publish-gradle-plugin - publish-to-sdkman + - trigger-docs-build - update-homebrew-tap runs-on: ${{ vars.UBUNTU_SMALL || 'ubuntu-latest' }} steps: @@ -137,3 +166,4 @@ jobs: with: milestone: ${{ needs.build-and-stage-release.outputs.version }} token: ${{ secrets.GH_ACTIONS_REPO_TOKEN }} + commercial: ${{ vars.COMMERCIAL }} diff --git a/.github/workflows/run-system-tests.yml b/.github/workflows/run-system-tests.yml index dcd426b07d22..1be51140409a 100644 --- a/.github/workflows/run-system-tests.yml +++ b/.github/workflows/run-system-tests.yml @@ -8,8 +8,8 @@ concurrency: jobs: run-system-tests: name: 'Java ${{ matrix.java.version}}' - runs-on: ${{ vars.UBUNTU_MEDIUM || 'ubuntu-latest' }} if: ${{ github.repository == 'spring-projects/spring-boot' }} + runs-on: ${{ vars.UBUNTU_MEDIUM || 'ubuntu-latest' }} strategy: matrix: java: @@ -23,18 +23,18 @@ jobs: - name: Prepare Gradle Build uses: ./.github/actions/prepare-gradle-build with: - develocity-access-key: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }} - java-version: ${{ matrix.java.version }} + develocity-access-key: ${{ secrets.DEVELOCITY_ACCESS_KEY }} java-toolchain: ${{ matrix.java.toolchain }} + java-version: ${{ matrix.java.version }} - name: Run System Tests id: run-system-tests shell: bash run: ./gradlew systemTest - name: Send Notification - uses: ./.github/actions/send-notification if: always() + uses: ./.github/actions/send-notification with: - webhook-url: ${{ secrets.GOOGLE_CHAT_WEBHOOK_URL }} - status: ${{ job.status }} build-scan-url: ${{ steps.run-system-tests.outputs.build-scan-url }} - run-name: ${{ format('{0} | System Tests | Java {1}', github.ref_name, matrix.java.version) }} \ No newline at end of file + run-name: ${{ format('{0} | System Tests | Java {1}', github.ref_name, matrix.java.version) }} + status: ${{ job.status }} + webhook-url: ${{ secrets.GOOGLE_CHAT_WEBHOOK_URL }} diff --git a/.github/workflows/trigger-docs-build.yml b/.github/workflows/trigger-docs-build.yml index 686f168c25ae..db3fc19b0477 100644 --- a/.github/workflows/trigger-docs-build.yml +++ b/.github/workflows/trigger-docs-build.yml @@ -8,8 +8,8 @@ permissions: jobs: trigger-docs-build: name: Trigger Docs Build - runs-on: ${{ vars.UBUNTU_SMALL || 'ubuntu-latest' }} if: github.repository_owner == 'spring-projects' + runs-on: ${{ vars.UBUNTU_SMALL || 'ubuntu-latest' }} steps: - name: Check Out uses: actions/checkout@v4 diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index ca8b9fb09ef9..2a16a8f68c21 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -2,21 +2,33 @@ name: Verify on: workflow_call: inputs: - version: - required: true - type: string staging: + description: 'Whether the release to verify is in the staging repository' required: false default: false type: boolean + version: + description: 'Version to verify' + required: true + type: string secrets: - repository-username: + commercial-repository-password: + description: 'Password for authentication with the commercial repository' required: false - repository-password: + commercial-repository-username: + description: 'Username for authentication with the commercial repository' required: false google-chat-webhook-url: + description: 'Google Chat Webhook URL' required: true + opensource-repository-password: + description: 'Password for authentication with the open-source repository' + required: false + opensource-repository-username: + description: 'Username for authentication with the open-source repository' + required: false token: + description: 'Token to use for authentication with GitHub' required: true jobs: verify: @@ -26,13 +38,13 @@ jobs: - name: Check Out Release Verification Tests uses: actions/checkout@v4 with: + ref: 'v0.0.6' repository: spring-projects/spring-boot-release-verification - ref: 'v0.0.3' token: ${{ secrets.token }} - name: Check Out Send Notification Action uses: actions/checkout@v4 with: - path: spring-boot + path: send-notification sparse-checkout: .github/actions/send-notification - name: Set Up Java uses: actions/setup-java@v4 @@ -40,6 +52,7 @@ jobs: distribution: 'liberica' java-version: 17 - name: Set Up Homebrew + if: ${{ !vars.COMMERCIAL }} uses: Homebrew/actions/setup-homebrew@7657c9512f50e1c35b640971116425935bab3eea - name: Set Up Gradle uses: gradle/actions/setup-gradle@d156388eb19639ec20ade50009f3d199ce1e2808 # v4.1.0 @@ -52,22 +65,24 @@ jobs: echo 'org.gradle.daemon=false' >> $HOME/.gradle/gradle.properties - name: Run Release Verification Tests env: - RVT_VERSION: ${{ inputs.version }} - RVT_RELEASE_TYPE: oss + RVT_COMMERCIAL_REPOSITORY_PASSWORD: ${{ secrets.commercial-repository-password }} + RVT_COMMERCIAL_REPOSITORY_USERNAME: ${{ secrets.commercial-repository-username }} + RVT_OSS_REPOSITORY_PASSWORD: ${{ secrets.opensource-repository-password }} + RVT_OSS_REPOSITORY_USERNAME: ${{ secrets.opensource-repository-username }} + RVT_RELEASE_TYPE: ${{ vars.COMMERCIAL && 'commercial' || 'oss' }} RVT_STAGING: ${{ inputs.staging }} - RVT_OSS_REPOSITORY_USERNAME: ${{ secrets.repository-username }} - RVT_OSS_REPOSITORY_PASSWORD: ${{ secrets.repository-password }} + RVT_VERSION: ${{ inputs.version }} run: ./gradlew spring-boot-release-verification-tests:test - name: Upload Build Reports on Failure - uses: actions/upload-artifact@v4 if: failure() + uses: actions/upload-artifact@v4 with: name: build-reports path: '**/build/reports/' - name: Send Notification - uses: ./spring-boot/.github/actions/send-notification if: always() + uses: ./send-notification/.github/actions/send-notification with: - webhook-url: ${{ secrets.google-chat-webhook-url }} - status: ${{ job.status }} run-name: ${{ format('{0} | Verification | {1}', github.ref_name, inputs.version) }} + status: ${{ job.status }} + webhook-url: ${{ secrets.google-chat-webhook-url }} diff --git a/antora/package.json b/antora/package.json index ea8a2b4af854..2677e5e4bb29 100644 --- a/antora/package.json +++ b/antora/package.json @@ -13,6 +13,6 @@ "@springio/asciidoctor-extensions": "1.0.0-alpha.11" }, "config": { - "ui-bundle-url": "https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.16/ui-bundle.zip" + "ui-bundle-url": "https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.17/ui-bundle.zip" } } diff --git a/build.gradle b/build.gradle index 4f490c8e7c19..5838bd2f1bd6 100644 --- a/build.gradle +++ b/build.gradle @@ -9,9 +9,14 @@ defaultTasks 'build' allprojects { group "org.springframework.boot" +} + +subprojects { + apply plugin: "org.springframework.boot.conventions" repositories { mavenCentral() + spring.mavenRepositories() if (version.contains('-')) { maven { url "https://repo.spring.io/milestone" } } @@ -24,3 +29,4 @@ allprojects { resolutionStrategy.cacheChangingModulesFor 0, "minutes" } } + diff --git a/buildSrc/SpringRepositorySupport.groovy b/buildSrc/SpringRepositorySupport.groovy new file mode 100644 index 000000000000..778f173ca37a --- /dev/null +++ b/buildSrc/SpringRepositorySupport.groovy @@ -0,0 +1,156 @@ +/* + * Copyright 2012-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// +// This script can be used in the `pluginManagement` block of a `settings.gradle` file to provide +// support for spring maven repositories. +// +// To use the script add the following as the first line in the `pluginManagement` block: +// +// evaluate(new File("${rootDir}/buildSrc/SpringRepositorySupport.groovy")).apply(this) +// +// You can then use `spring.mavenRepositories()` to add the Spring repositories required for the +// version being built. +// + +import java.util.function.* + +def apply(settings) { + def version = property(settings, 'version') + def buildType = property(settings, 'spring.build-type') + SpringRepositoriesExtension.addTo(settings.pluginManagement.repositories, version, buildType) + settings.gradle.allprojects { + SpringRepositoriesExtension.addTo(repositories, version, buildType) + } +} + +private def property(settings, name) { + def value = null + try { + value = settings.gradle.parent?.rootProject?.findProperty(name) + } + catch (Exception ex) { + } + try { + value = (value != null) ? value : settings.ext.find(name) + } + catch (Exception ex) { + } + value = (value != null) ? value : loadProperty(settings, name) + return value +} + +private def loadProperty(settings, name) { + def scriptDir = new File(getClass().protectionDomain.codeSource.location.path).parent + new File(scriptDir, "../gradle.properties").withInputStream { + def properties = new Properties() + properties.load(it) + return properties.get(name) + } +} + +return this + +class SpringRepositoriesExtension { + + private final def repositories + private final def version + private final def buildType + private final UnaryOperator environment + + @javax.inject.Inject + SpringRepositoriesExtension(repositories, version, buildType) { + this(repositories, version, buildType, System::getenv) + } + + SpringRepositoriesExtension(repositories, version, buildType, environment) { + this.repositories = repositories + this.version = version + this.buildType = buildType + this.environment = environment + } + + def mavenRepositories() { + addRepositories { } + } + + def mavenRepositories(condition) { + if (condition) addRepositories { } + } + + def mavenRepositoriesExcludingBootGroup() { + addRepositories { maven -> + maven.content { content -> + content.excludeGroup("org.springframework.boot") + } + } + } + + private void addRepositories(action) { + addCommercialRepository("release", "/spring-enterprise-maven-prod-local", action) + if (this.version.contains("-")) { + addOssRepository("milestone", "/milestone", action) + } + if (this.version.endsWith("-SNAPSHOT")) { + addCommercialRepository("snapshot", "/spring-enterprise-maven-dev-local", action) + addOssRepository("snapshot", "/snapshot", action) + } + } + + private void addOssRepository(id, path, action) { + def name = "spring-oss-" + id + def url = "https://repo.spring.io" + path + addRepository(name, url, action) + } + + private void addCommercialRepository(id, path, action) { + if (!"commercial".equalsIgnoreCase(this.buildType)) return + def name = "spring-commercial-" + id + def url = fromEnv("COMMERCIAL_%SREPO_URL", id, "https://usw1.packages.broadcom.com" + path) + def username = fromEnv("COMMERCIAL_%SREPO_USERNAME", id) + def password = fromEnv("COMMERCIAL_%SREPO_PASSWORD", id) + addRepository(name, url, { maven -> + maven.credentials { credentials -> + credentials.setUsername(username) + credentials.setPassword(password) + } + action(maven) + }) + } + + private void addRepository(name, url, action) { + this.repositories.maven { maven -> + maven.setName(name) + maven.setUrl(url) + action(maven) + } + } + + private String fromEnv(template, id) { + return fromEnv(template, id, null) + } + + private String fromEnv(template, id, defaultValue) { + String value = this.environment.apply(template.formatted(id.toUpperCase() + "_")) + value = (value != null) ? value : this.environment.apply(template.formatted("")) + return (value != null) ? value : defaultValue + } + + static def addTo(repositories, version, buildType) { + repositories.extensions.create("spring", SpringRepositoriesExtension.class, repositories, version, buildType) + } + +} \ No newline at end of file diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index 4c5ea71768dd..b6152071cf49 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -10,59 +10,49 @@ repositories { gradlePluginPortal() } -sourceCompatibility = 17 -targetCompatibility = 17 - -def versions = [:] -new File(projectDir.parentFile, "gradle.properties").withInputStream { - def properties = new Properties() - properties.load(it) - ["assertj", "commonsCodec", "hamcrest", "junitJupiter", "kotlin", "maven", "snakeYaml"].each { - versions[it] = properties[it + "Version"] - } +java { + sourceCompatibility = 17 + targetCompatibility = 17 } -versions["jackson"] = "2.15.3" -versions["springFramework"] = "6.0.12" -ext.set("versions", versions) -if (versions.springFramework.contains("-")) { - repositories { - maven { url "https://repo.spring.io/milestone" } - maven { url "https://repo.spring.io/snapshot" } - } + +repositories { + spring.mavenRepositories("${springFrameworkVersion}".contains("-")) } checkstyle { - toolVersion = "10.12.4" + toolVersion = "${checkstyleToolVersion}" } dependencies { checkstyle("com.puppycrawl.tools:checkstyle:${checkstyle.toolVersion}") checkstyle("io.spring.javaformat:spring-javaformat-checkstyle:${javaFormatVersion}") - implementation(platform("org.springframework:spring-framework-bom:${versions.springFramework}")) + implementation(platform("org.springframework:spring-framework-bom:${springFrameworkVersion}")) implementation("dev.adamko.dokkatoo:dokkatoo-plugin:2.3.1") - implementation("com.fasterxml.jackson.core:jackson-databind:${versions.jackson}") + implementation("com.fasterxml.jackson.core:jackson-databind:${jacksonVersion}") implementation("com.github.node-gradle:gradle-node-plugin:3.5.1") implementation("com.gradle:develocity-gradle-plugin:3.17.2") implementation("com.tngtech.archunit:archunit:1.3.0") - implementation("commons-codec:commons-codec:${versions.commonsCodec}") + implementation("commons-codec:commons-codec:${commonsCodecVersion}") implementation("de.undercouch.download:de.undercouch.download.gradle.plugin:5.5.0") + implementation("dev.adamko.dokkatoo:dokkatoo-plugin:2.3.1") implementation("io.spring.gradle.antora:spring-antora-plugin:0.0.1") implementation("io.spring.javaformat:spring-javaformat-gradle-plugin:${javaFormatVersion}") implementation("io.spring.nohttp:nohttp-gradle:0.0.11") implementation("org.apache.httpcomponents.client5:httpclient5:5.3.1") - implementation("org.apache.maven:maven-embedder:${versions.maven}") + implementation("org.apache.maven:maven-embedder:${mavenVersion}") implementation("org.antora:gradle-antora-plugin:1.0.0") - implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:${versions.kotlin}") - implementation("org.jetbrains.kotlin:kotlin-compiler-embeddable:${versions.kotlin}") + implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}") + implementation("org.jetbrains.kotlin:kotlin-compiler-embeddable:${kotlinVersion}") implementation("org.springframework:spring-context") implementation("org.springframework:spring-core") implementation("org.springframework:spring-web") - implementation("org.yaml:snakeyaml:${versions.snakeYaml}") + implementation("org.yaml:snakeyaml:${snakeYamlVersion}") - testImplementation("org.assertj:assertj-core:${versions.assertj}") - testImplementation("org.hamcrest:hamcrest:${versions.hamcrest}") - testImplementation("org.junit.jupiter:junit-jupiter:${versions.junitJupiter}") + testImplementation("org.assertj:assertj-core:${assertjVersion}") + testImplementation("org.hamcrest:hamcrest:${hamcrestVersion}") + testImplementation("org.junit.jupiter:junit-jupiter:${junitJupiterVersion}") + testImplementation("org.mockito:mockito-core:${mockitoVersion}") testImplementation("org.springframework:spring-test") testRuntimeOnly("org.junit.platform:junit-platform-launcher") diff --git a/buildSrc/gradle.properties b/buildSrc/gradle.properties deleted file mode 100644 index 2a3dd72a1830..000000000000 --- a/buildSrc/gradle.properties +++ /dev/null @@ -1 +0,0 @@ -javaFormatVersion=0.0.43 diff --git a/buildSrc/settings.gradle b/buildSrc/settings.gradle index 4cdfcbf3bd5a..c0862a7b4629 100644 --- a/buildSrc/settings.gradle +++ b/buildSrc/settings.gradle @@ -1,4 +1,13 @@ pluginManagement { + new File(rootDir.parentFile, "gradle.properties").withInputStream { + def properties = new Properties() + properties.load(it) + properties.forEach(settings.ext::set) + gradle.rootProject { + properties.forEach(project.ext::set) + } + } + evaluate(new File("${rootDir}/SpringRepositorySupport.groovy")).apply(this) repositories { mavenCentral() gradlePluginPortal() diff --git a/buildSrc/src/main/java/org/springframework/boot/build/AntoraConventions.java b/buildSrc/src/main/java/org/springframework/boot/build/AntoraConventions.java index 841afc509985..a38d946d47e9 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/AntoraConventions.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/AntoraConventions.java @@ -35,6 +35,7 @@ import org.antora.gradle.AntoraTask; import org.gradle.StartParameter; import org.gradle.api.Project; +import org.gradle.api.file.Directory; import org.gradle.api.logging.LogLevel; import org.gradle.api.plugins.JavaBasePlugin; import org.gradle.api.provider.Provider; @@ -86,14 +87,15 @@ private void apply(Project project, AntoraPlugin antoraPlugin) { private void configureGenerateAntoraPlaybookTask(Project project, GenerateAntoraPlaybook generateAntoraPlaybookTask) { - File nodeProjectDir = getNodeProjectDir(project.getBuildDir()); - generateAntoraPlaybookTask.getOutputFile().set(new File(nodeProjectDir, "antora-playbook.yml")); + Provider nodeProjectDir = getNodeProjectDir(project); + generateAntoraPlaybookTask.getOutputFile() + .set(nodeProjectDir.map((directory) -> directory.file("antora-playbook.yml"))); } private void configureCopyAntoraPackageJsonTask(Project project, Copy copyAntoraPackageJsonTask) { copyAntoraPackageJsonTask .from(project.getRootProject().file("antora"), (spec) -> spec.include("package.json", "package-lock.json")) - .into(getNodeProjectDir(project.getBuildDir())); + .into(getNodeProjectDir(project)); } private void configureNpmInstallTask(Project project, NpmInstallTask npmInstallTask, Copy copyAntoraPackageJson) { @@ -117,7 +119,7 @@ private void configureGenerateAntoraYmlTask(Project project, GenerateAntoraYmlTa generateAntoraYmlTask.dependsOn(dependencyVersionsTask); generateAntoraYmlTask.setProperty("componentName", "boot"); generateAntoraYmlTask.setProperty("outputFile", - new File(project.getBuildDir(), "generated/docs/antora-yml/antora.yml")); + project.getLayout().getBuildDirectory().file("generated/docs/antora-yml/antora.yml")); generateAntoraYmlTask.setProperty("yml", getDefaultYml(project)); generateAntoraYmlTask.getAsciidocAttributes().putAll(getAsciidocAttributes(project, dependencyVersionsTask)); } @@ -202,14 +204,13 @@ private String getUiBundleUrl(Project project) { } private void configureNodeExtension(Project project, NodeExtension nodeExtension) { - File buildDir = project.getBuildDir(); - nodeExtension.getWorkDir().set(buildDir.toPath().resolve(".gradle/nodejs").toFile()); - nodeExtension.getNpmWorkDir().set(buildDir.toPath().resolve(".gradle/npm").toFile()); - nodeExtension.getNodeProjectDir().set(getNodeProjectDir(buildDir)); + nodeExtension.getWorkDir().set(project.getLayout().getBuildDirectory().dir(".gradle/nodejs")); + nodeExtension.getNpmWorkDir().set(project.getLayout().getBuildDirectory().dir(".gradle/npm")); + nodeExtension.getNodeProjectDir().set(getNodeProjectDir(project)); } - private File getNodeProjectDir(File buildDir) { - return buildDir.toPath().resolve(".gradle/nodeproject").toFile(); + private Provider getNodeProjectDir(Project project) { + return project.getLayout().getBuildDirectory().dir(".gradle/nodeproject"); } } diff --git a/buildSrc/src/main/java/org/springframework/boot/build/ConventionsPlugin.java b/buildSrc/src/main/java/org/springframework/boot/build/ConventionsPlugin.java index 8b4769be00e0..8bd7f2fb21d1 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/ConventionsPlugin.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/ConventionsPlugin.java @@ -50,6 +50,7 @@ public void apply(Project project) { new KotlinConventions().apply(project); new WarConventions().apply(project); new EclipseConventions().apply(project); + RepositoryTransformersExtension.apply(project); } } diff --git a/buildSrc/src/main/java/org/springframework/boot/build/KotlinConventions.java b/buildSrc/src/main/java/org/springframework/boot/build/KotlinConventions.java index 9975dcfca550..32b38af396de 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/KotlinConventions.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/KotlinConventions.java @@ -51,7 +51,7 @@ class KotlinConventions { void apply(Project project) { project.getPlugins().withId("org.jetbrains.kotlin.jvm", (plugin) -> { project.getTasks().withType(KotlinCompile.class, this::configure); - configureDokkatoo(project); + project.getPlugins().withType(DokkatooHtmlPlugin.class, (dokkatooPlugin) -> configureDokkatoo(project)); }); } @@ -61,31 +61,33 @@ private void configure(KotlinCompile compile) { kotlinOptions.setLanguageVersion("1.7"); kotlinOptions.setJvmTarget("17"); kotlinOptions.setAllWarningsAsErrors(true); - List freeCompilerArgs = new ArrayList<>(compile.getKotlinOptions().getFreeCompilerArgs()); + List freeCompilerArgs = new ArrayList<>(kotlinOptions.getFreeCompilerArgs()); freeCompilerArgs.add("-Xsuppress-version-warnings"); - compile.getKotlinOptions().setFreeCompilerArgs(freeCompilerArgs); + kotlinOptions.setFreeCompilerArgs(freeCompilerArgs); } private void configureDokkatoo(Project project) { - project.getPlugins().apply(DokkatooHtmlPlugin.class); DokkatooExtension dokkatoo = project.getExtensions().getByType(DokkatooExtension.class); - dokkatoo.getDokkatooSourceSets().named(SourceSet.MAIN_SOURCE_SET_NAME).configure((sourceSet) -> { - sourceSet.getSourceRoots().setFrom(project.file("src/main/kotlin")); - sourceSet.getClasspath() - .from(project.getExtensions() - .getByType(SourceSetContainer.class) - .getByName(SourceSet.MAIN_SOURCE_SET_NAME) - .getOutput()); - sourceSet.getExternalDocumentationLinks().create("spring-boot-javadoc", (link) -> { - link.getUrl().set(URI.create("https://docs.spring.io/spring-boot/api/java/")); - link.getPackageListUrl().set(URI.create("https://docs.spring.io/spring-boot/api/java/element-list")); - }); - sourceSet.getExternalDocumentationLinks().create("spring-framework-javadoc", (link) -> { - String url = "https://docs.spring.io/spring-framework/docs/%s/javadoc-api/" - .formatted(project.property("springFrameworkVersion")); - link.getUrl().set(URI.create(url)); - link.getPackageListUrl().set(URI.create(url + "/element-list")); - }); + dokkatoo.getDokkatooSourceSets().configureEach((sourceSet) -> { + if (SourceSet.MAIN_SOURCE_SET_NAME.equals(sourceSet.getName())) { + sourceSet.getSourceRoots().setFrom(project.file("src/main/kotlin")); + sourceSet.getClasspath() + .from(project.getExtensions() + .getByType(SourceSetContainer.class) + .getByName(SourceSet.MAIN_SOURCE_SET_NAME) + .getOutput()); + sourceSet.getExternalDocumentationLinks().create("spring-boot-javadoc", (link) -> { + link.getUrl().set(URI.create("https://docs.spring.io/spring-boot/api/java/")); + link.getPackageListUrl() + .set(URI.create("https://docs.spring.io/spring-boot/api/java/element-list")); + }); + sourceSet.getExternalDocumentationLinks().create("spring-framework-javadoc", (link) -> { + String url = "https://docs.spring.io/spring-framework/docs/%s/javadoc-api/" + .formatted(project.property("springFrameworkVersion")); + link.getUrl().set(URI.create(url)); + link.getPackageListUrl().set(URI.create(url + "/element-list")); + }); + } }); } diff --git a/buildSrc/src/main/java/org/springframework/boot/build/MavenPublishingConventions.java b/buildSrc/src/main/java/org/springframework/boot/build/MavenPublishingConventions.java index 259607815604..9bcd9fe892fa 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/MavenPublishingConventions.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/MavenPublishingConventions.java @@ -31,6 +31,11 @@ import org.gradle.api.publish.maven.MavenPomScm; import org.gradle.api.publish.maven.MavenPublication; import org.gradle.api.publish.maven.plugins.MavenPublishPlugin; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.springframework.boot.build.properties.BuildProperties; +import org.springframework.boot.build.properties.BuildType; /** * Conventions that are applied in the presence of the {@link MavenPublishPlugin}. When @@ -56,6 +61,8 @@ */ class MavenPublishingConventions { + private static final Logger logger = LoggerFactory.getLogger(MavenPublishingConventions.class); + void apply(Project project) { project.getPlugins().withType(MavenPublishPlugin.class).all((mavenPublish) -> { PublishingExtension publishing = project.getExtensions().getByType(PublishingExtension.class); @@ -93,9 +100,7 @@ private void customizePom(MavenPom pom, Project project) { pom.licenses(this::customizeLicences); pom.developers(this::customizeDevelopers); pom.scm((scm) -> customizeScm(scm, project)); - if (!isUserInherited(project)) { - pom.issueManagement(this::customizeIssueManagement); - } + pom.issueManagement((issueManagement) -> customizeIssueManagement(issueManagement, project)); } private void customizeJavaMavenPublication(MavenPublication publication, Project project) { @@ -130,16 +135,26 @@ private void customizeDevelopers(MavenPomDeveloperSpec developers) { } private void customizeScm(MavenPomScm scm, Project project) { + if (BuildProperties.get(project).buildType() != BuildType.OPEN_SOURCE) { + logger.debug("Skipping Maven POM SCM for non open source build type"); + return; + } + scm.getUrl().set("https://github.com/spring-projects/spring-boot"); if (!isUserInherited(project)) { scm.getConnection().set("scm:git:git://github.com/spring-projects/spring-boot.git"); scm.getDeveloperConnection().set("scm:git:ssh://git@github.com/spring-projects/spring-boot.git"); } - scm.getUrl().set("https://github.com/spring-projects/spring-boot"); } - private void customizeIssueManagement(MavenPomIssueManagement issueManagement) { - issueManagement.getSystem().set("GitHub"); - issueManagement.getUrl().set("https://github.com/spring-projects/spring-boot/issues"); + private void customizeIssueManagement(MavenPomIssueManagement issueManagement, Project project) { + if (BuildProperties.get(project).buildType() != BuildType.OPEN_SOURCE) { + logger.debug("Skipping Maven POM SCM for non open source build type"); + return; + } + if (!isUserInherited(project)) { + issueManagement.getSystem().set("GitHub"); + issueManagement.getUrl().set("https://github.com/spring-projects/spring-boot/issues"); + } } private boolean isUserInherited(Project project) { diff --git a/buildSrc/src/main/java/org/springframework/boot/build/MavenRepositoryPlugin.java b/buildSrc/src/main/java/org/springframework/boot/build/MavenRepositoryPlugin.java index e08af1285cf7..258a4ef8dd6c 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/MavenRepositoryPlugin.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/MavenRepositoryPlugin.java @@ -58,7 +58,7 @@ public class MavenRepositoryPlugin implements Plugin { public void apply(Project project) { project.getPlugins().apply(MavenPublishPlugin.class); PublishingExtension publishing = project.getExtensions().getByType(PublishingExtension.class); - File repositoryLocation = new File(project.getBuildDir(), "maven-repository"); + File repositoryLocation = project.getLayout().getBuildDirectory().dir("maven-repository").get().getAsFile(); publishing.getRepositories().maven((mavenRepository) -> { mavenRepository.setName("project"); mavenRepository.setUrl(repositoryLocation.toURI()); diff --git a/buildSrc/src/main/java/org/springframework/boot/build/RepositoryTransformersExtension.java b/buildSrc/src/main/java/org/springframework/boot/build/RepositoryTransformersExtension.java new file mode 100644 index 000000000000..982594f42661 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/boot/build/RepositoryTransformersExtension.java @@ -0,0 +1,116 @@ +/* + * Copyright 2024-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.build; + +import javax.inject.Inject; + +import org.gradle.api.Project; +import org.gradle.api.Transformer; +import org.gradle.api.artifacts.repositories.MavenArtifactRepository; + +/** + * Extension to add {@code springRepositoryTransformers} utility methods. + * + * @author Phillip Webb + */ +public class RepositoryTransformersExtension { + + private static final String MARKER = "{spring.mavenRepositories}"; + + private static final String MARKER_PLUGIN = "{spring.mavenPluginRepositories}"; + + private final Project project; + + @Inject + public RepositoryTransformersExtension(Project project) { + this.project = project; + } + + public Transformer ant() { + return this::transformAnt; + } + + private String transformAnt(String line) { + if (line.contains(MARKER)) { + StringBuilder result = new StringBuilder(); + String indent = getIndent(line); + this.project.getRepositories().withType(MavenArtifactRepository.class, (repository) -> { + String name = repository.getName(); + if (name.startsWith("spring-")) { + result.append(!result.isEmpty() ? "\n" : ""); + result.append("%s".formatted(indent, name, + repository.getUrl())); + } + }); + return result.toString(); + } + return line; + } + + public Transformer mavenSettings() { + return this::transformMavenSettings; + } + + private String transformMavenSettings(String line) { + if (line.contains(MARKER)) { + return transformMarker(line, false); + } + if (line.contains(MARKER_PLUGIN)) { + return transformMarker(line, true); + } + return line; + } + + private String transformMarker(String line, boolean pluginRepository) { + StringBuilder result = new StringBuilder(); + String indent = getIndent(line); + this.project.getRepositories().withType(MavenArtifactRepository.class, (repository) -> { + String name = repository.getName(); + if (name.startsWith("spring-")) { + result.append(!result.isEmpty() ? "\n" : ""); + result.append(mavenRepositoryXml(indent, repository, pluginRepository)); + } + }); + return result.toString(); + } + + private String mavenRepositoryXml(String indent, MavenArtifactRepository repository, boolean pluginRepository) { + String rootTag = pluginRepository ? "pluginRepository" : "repository"; + boolean snapshots = repository.getName().endsWith("-snapshot"); + StringBuilder xml = new StringBuilder(); + xml.append("%s<%s>%n".formatted(indent, rootTag)); + xml.append("%s\t%s%n".formatted(indent, repository.getName())); + xml.append("%s\t%s%n".formatted(indent, repository.getUrl())); + xml.append("%s\t%n".formatted(indent)); + xml.append("%s\t\t%s%n".formatted(indent, !snapshots)); + xml.append("%s\t%n".formatted(indent)); + xml.append("%s\t%n".formatted(indent)); + xml.append("%s\t\t%s%n".formatted(indent, snapshots)); + xml.append("%s\t%n".formatted(indent)); + xml.append("%s".formatted(indent, rootTag)); + return xml.toString(); + } + + private String getIndent(String line) { + return line.substring(0, line.length() - line.stripLeading().length()); + } + + static void apply(Project project) { + project.getExtensions().create("springRepositoryTransformers", RepositoryTransformersExtension.class, project); + } + +} diff --git a/buildSrc/src/main/java/org/springframework/boot/build/antora/AntoraAsciidocAttributes.java b/buildSrc/src/main/java/org/springframework/boot/build/antora/AntoraAsciidocAttributes.java index 475a57206719..3153e7b201be 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/antora/AntoraAsciidocAttributes.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/antora/AntoraAsciidocAttributes.java @@ -32,6 +32,8 @@ import org.springframework.boot.build.artifacts.ArtifactRelease; import org.springframework.boot.build.bom.BomExtension; import org.springframework.boot.build.bom.Library; +import org.springframework.boot.build.properties.BuildProperties; +import org.springframework.boot.build.properties.BuildType; import org.springframework.util.Assert; /** @@ -47,6 +49,8 @@ public class AntoraAsciidocAttributes { private final boolean latestVersion; + private final BuildType buildType; + private final ArtifactRelease artifactRelease; private final List libraries; @@ -59,16 +63,18 @@ public AntoraAsciidocAttributes(Project project, BomExtension dependencyBom, Map dependencyVersions) { this.version = String.valueOf(project.getVersion()); this.latestVersion = Boolean.parseBoolean(String.valueOf(project.findProperty("latestVersion"))); + this.buildType = BuildProperties.get(project).buildType(); this.artifactRelease = ArtifactRelease.forProject(project); this.libraries = dependencyBom.getLibraries(); this.dependencyVersions = dependencyVersions; this.projectProperties = project.getProperties(); } - AntoraAsciidocAttributes(String version, boolean latestVersion, List libraries, + AntoraAsciidocAttributes(String version, boolean latestVersion, BuildType buildType, List libraries, Map dependencyVersions, Map projectProperties) { this.version = version; this.latestVersion = latestVersion; + this.buildType = buildType; this.artifactRelease = ArtifactRelease.forVersion(version); this.libraries = (libraries != null) ? libraries : Collections.emptyList(); this.dependencyVersions = (dependencyVersions != null) ? dependencyVersions : Collections.emptyMap(); @@ -77,6 +83,7 @@ public AntoraAsciidocAttributes(Project project, BomExtension dependencyBom, public Map get() { Map attributes = new LinkedHashMap<>(); + addBuildTypeAttribute(attributes); addGitHubAttributes(attributes); addVersionAttributes(attributes); addArtifactAttributes(attributes); @@ -86,6 +93,10 @@ public Map get() { return attributes; } + private void addBuildTypeAttribute(Map attributes) { + attributes.put("build-type", this.buildType.toIdentifier()); + } + private void addGitHubAttributes(Map attributes) { attributes.put("github-repo", "spring-projects/spring-boot"); attributes.put("github-ref", determineGitHubRef()); @@ -125,6 +136,7 @@ private void addVersionAttributes(Map attributes) { addSpringDataDependencyVersion(attributes, "spring-data-neo4j"); addSpringDataDependencyVersion(attributes, "spring-data-r2dbc"); addSpringDataDependencyVersion(attributes, "spring-data-rest", "spring-data-rest-core"); + addSpringDataDependencyVersion(attributes, "spring-data-ldap"); } private void addSpringDataDependencyVersion(Map attributes, String artifactId) { @@ -152,6 +164,8 @@ private String getVersion(String groupAndArtifactId) { private void addArtifactAttributes(Map attributes) { attributes.put("url-artifact-repository", this.artifactRelease.getDownloadRepo()); attributes.put("artifact-release-type", this.artifactRelease.getType()); + attributes.put("build-and-artifact-release-type", + this.buildType.toIdentifier() + "-" + this.artifactRelease.getType()); } private void addUrlJava(Map attributes) { diff --git a/buildSrc/src/main/java/org/springframework/boot/build/antora/GenerateAntoraPlaybook.java b/buildSrc/src/main/java/org/springframework/boot/build/antora/GenerateAntoraPlaybook.java index 42514c24fd6a..3df45f7892e1 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/antora/GenerateAntoraPlaybook.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/antora/GenerateAntoraPlaybook.java @@ -174,7 +174,8 @@ private void addAntoraContentStartPaths(Set startPaths) { private void addDir(Map data) { Path playbookDir = toRealPath(getOutputFile().get().getAsFile().toPath()).getParent(); - Path outputDir = toRealPath(getProject().getBuildDir().toPath().resolve("site")); + Path outputDir = toRealPath( + getProject().getLayout().getBuildDirectory().dir("site").get().getAsFile().toPath()); data.put("output", Map.of("dir", "." + File.separator + playbookDir.relativize(outputDir).toString())); } diff --git a/buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitectureCheck.java b/buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitectureCheck.java index 0131943efb94..abb0be2218c6 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitectureCheck.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitectureCheck.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2024 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -89,7 +89,8 @@ public ArchitectureCheck() { noClassesShouldCallStepVerifierStepVerifyComplete(), noClassesShouldConfigureDefaultStepVerifierTimeout(), noClassesShouldCallCollectorsToList(), noClassesShouldCallURLEncoderWithStringEncoding(), noClassesShouldCallURLDecoderWithStringEncoding(), - noClassesShouldLoadResourcesUsingResourceUtils()); + noClassesShouldLoadResourcesUsingResourceUtils(), noClassesShouldCallStringToUpperCaseWithoutLocale(), + noClassesShouldCallStringToLowerCaseWithoutLocale()); getRules().addAll(getProhibitObjectsRequireNonNull() .map((prohibit) -> prohibit ? noClassesShouldCallObjectsRequireNonNull() : Collections.emptyList())); getRuleDescriptions().set(getRules().map((rules) -> rules.stream().map(ArchRule::getDescription).toList())); @@ -189,6 +190,20 @@ public void check(JavaMethod item, ConditionEvents events) { }; } + private ArchRule noClassesShouldCallStringToLowerCaseWithoutLocale() { + return ArchRuleDefinition.noClasses() + .should() + .callMethod(String.class, "toLowerCase") + .because("String.toLowerCase(Locale.ROOT) should be used instead"); + } + + private ArchRule noClassesShouldCallStringToUpperCaseWithoutLocale() { + return ArchRuleDefinition.noClasses() + .should() + .callMethod(String.class, "toUpperCase") + .because("String.toUpperCase(Locale.ROOT) should be used instead"); + } + private ArchRule noClassesShouldCallStepVerifierStepVerifyComplete() { return ArchRuleDefinition.noClasses() .should() diff --git a/buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitecturePlugin.java b/buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitecturePlugin.java index 4c73e021df27..44bf79a32d46 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitecturePlugin.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitecturePlugin.java @@ -22,7 +22,7 @@ import org.gradle.api.Plugin; import org.gradle.api.Project; import org.gradle.api.Task; -import org.gradle.api.plugins.JavaBasePlugin; +import org.gradle.api.plugins.JavaPlugin; import org.gradle.api.plugins.JavaPluginExtension; import org.gradle.api.tasks.SourceSet; import org.gradle.api.tasks.TaskProvider; @@ -39,7 +39,7 @@ public class ArchitecturePlugin implements Plugin { @Override public void apply(Project project) { - project.getPlugins().withType(JavaBasePlugin.class, (javaPlugin) -> registerTasks(project)); + project.getPlugins().withType(JavaPlugin.class, (javaPlugin) -> registerTasks(project)); } private void registerTasks(Project project) { diff --git a/buildSrc/src/main/java/org/springframework/boot/build/autoconfigure/AutoConfigurationPlugin.java b/buildSrc/src/main/java/org/springframework/boot/build/autoconfigure/AutoConfigurationPlugin.java index 97f36eb2c46c..496115cffda5 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/autoconfigure/AutoConfigurationPlugin.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/autoconfigure/AutoConfigurationPlugin.java @@ -93,28 +93,41 @@ public void apply(Project project) { .getByName(SourceSet.MAIN_SOURCE_SET_NAME); task.setSourceSet(main); task.dependsOn(main.getClassesTaskName()); - task.getOutputFile().set(new File(project.getBuildDir(), "auto-configuration-metadata.properties")); + task.getOutputFile() + .set(project.getLayout().getBuildDirectory().file("auto-configuration-metadata.properties")); project.getArtifacts() .add(AutoConfigurationPlugin.AUTO_CONFIGURATION_METADATA_CONFIGURATION_NAME, task.getOutputFile(), (artifact) -> artifact.builtBy(task)); }); - project.getPlugins().withType(ArchitecturePlugin.class, (architecturePlugin) -> { - project.getTasks().named("checkArchitectureMain", ArchitectureCheck.class).configure((task) -> { - SourceSet main = project.getExtensions() - .getByType(JavaPluginExtension.class) - .getSourceSets() - .getByName(SourceSet.MAIN_SOURCE_SET_NAME); - File resourcesDirectory = main.getOutput().getResourcesDir(); - task.dependsOn(main.getProcessResourcesTaskName()); - task.getInputs().files(resourcesDirectory).optional().withPathSensitivity(PathSensitivity.RELATIVE); - task.getRules() - .add(allClassesAnnotatedWithAutoConfigurationShouldBeListedInAutoConfigurationImports( - autoConfigurationImports(project, resourcesDirectory))); - }); - }); + project.getPlugins() + .withType(ArchitecturePlugin.class, (plugin) -> configureArchitecturePluginTasks(project)); }); } + private void configureArchitecturePluginTasks(Project project) { + project.getTasks().configureEach((task) -> { + if ("checkArchitectureMain".equals(task.getName()) && task instanceof ArchitectureCheck architectureCheck) { + configureCheckArchitectureMain(project, architectureCheck); + } + }); + } + + private void configureCheckArchitectureMain(Project project, ArchitectureCheck architectureCheck) { + SourceSet main = project.getExtensions() + .getByType(JavaPluginExtension.class) + .getSourceSets() + .getByName(SourceSet.MAIN_SOURCE_SET_NAME); + File resourcesDirectory = main.getOutput().getResourcesDir(); + architectureCheck.dependsOn(main.getProcessResourcesTaskName()); + architectureCheck.getInputs() + .files(resourcesDirectory) + .optional() + .withPathSensitivity(PathSensitivity.RELATIVE); + architectureCheck.getRules() + .add(allClassesAnnotatedWithAutoConfigurationShouldBeListedInAutoConfigurationImports( + autoConfigurationImports(project, resourcesDirectory))); + } + private ArchRule allClassesAnnotatedWithAutoConfigurationShouldBeListedInAutoConfigurationImports( Provider imports) { return ArchRuleDefinition.classes() diff --git a/buildSrc/src/main/java/org/springframework/boot/build/autoconfigure/DocumentAutoConfigurationClasses.java b/buildSrc/src/main/java/org/springframework/boot/build/autoconfigure/DocumentAutoConfigurationClasses.java index 3eb1a4b36c1b..4d34cba44d91 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/autoconfigure/DocumentAutoConfigurationClasses.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/autoconfigure/DocumentAutoConfigurationClasses.java @@ -30,8 +30,8 @@ import org.gradle.api.DefaultTask; import org.gradle.api.Task; +import org.gradle.api.file.DirectoryProperty; import org.gradle.api.file.FileCollection; -import org.gradle.api.file.RegularFileProperty; import org.gradle.api.tasks.InputFiles; import org.gradle.api.tasks.OutputDirectory; import org.gradle.api.tasks.TaskAction; @@ -57,7 +57,7 @@ public void setAutoConfiguration(FileCollection autoConfiguration) { } @OutputDirectory - public abstract RegularFileProperty getOutputDir(); + public abstract DirectoryProperty getOutputDir(); @TaskAction void documentAutoConfigurationClasses() throws IOException { diff --git a/buildSrc/src/main/java/org/springframework/boot/build/bom/BomExtension.java b/buildSrc/src/main/java/org/springframework/boot/build/bom/BomExtension.java index 59bc52845431..f00ddb6228b4 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/bom/BomExtension.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/bom/BomExtension.java @@ -66,6 +66,7 @@ import org.springframework.boot.build.bom.Library.VersionAlignment; import org.springframework.boot.build.bom.bomr.version.DependencyVersion; import org.springframework.boot.build.mavenplugin.MavenExec; +import org.springframework.boot.build.properties.BuildProperties; import org.springframework.util.FileCopyUtils; import org.springframework.util.PropertyPlaceholderHelper; import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver; @@ -78,22 +79,19 @@ */ public class BomExtension { + private final Project project; + + private final UpgradeHandler upgradeHandler; + private final Map properties = new LinkedHashMap<>(); private final Map artifactVersionProperties = new HashMap<>(); private final List libraries = new ArrayList<>(); - private final UpgradeHandler upgradeHandler; - - private final DependencyHandler dependencyHandler; - - private final Project project; - - public BomExtension(DependencyHandler dependencyHandler, Project project) { - this.dependencyHandler = dependencyHandler; - this.upgradeHandler = project.getObjects().newInstance(UpgradeHandler.class); + public BomExtension(Project project) { this.project = project; + this.upgradeHandler = project.getObjects().newInstance(UpgradeHandler.class, project); } public List getLibraries() { @@ -105,8 +103,9 @@ public void upgrade(Action action) { } public Upgrade getUpgrade() { - return new Upgrade(this.upgradeHandler.upgradePolicy, new GitHub(this.upgradeHandler.gitHub.organization, - this.upgradeHandler.gitHub.repository, this.upgradeHandler.gitHub.issueLabels)); + GitHubHandler gitHub = this.upgradeHandler.gitHub; + return new Upgrade(this.upgradeHandler.upgradePolicy, + new GitHub(gitHub.organization, gitHub.repository, gitHub.issueLabels)); } public void library(String name, Action action) { @@ -136,7 +135,11 @@ public void effectiveBomArtifact() { .all((task) -> { Sync syncBom = this.project.getTasks().create("syncBom", Sync.class); syncBom.dependsOn(task); - File generatedBomDir = new File(this.project.getBuildDir(), "generated/bom"); + File generatedBomDir = this.project.getLayout() + .getBuildDirectory() + .dir("generated/bom") + .get() + .getAsFile(); syncBom.setDestinationDir(generatedBomDir); syncBom.from(((GenerateMavenPom) task).getDestination(), (pom) -> pom.rename((name) -> "pom.xml")); try { @@ -145,7 +148,12 @@ public void effectiveBomArtifact() { getClass().getClassLoader().getResourceAsStream("effective-bom-settings.xml"), StandardCharsets.UTF_8)) .replace("localRepositoryPath", - new File(this.project.getBuildDir(), "local-m2-repository").getAbsolutePath()); + this.project.getLayout() + .getBuildDirectory() + .dir("local-m2-repository") + .get() + .getAsFile() + .getAbsolutePath()); syncBom.from(this.project.getResources().getText().fromString(settingsXmlContent), (settingsXml) -> settingsXml.rename((name) -> "settings.xml")); } @@ -155,8 +163,11 @@ public void effectiveBomArtifact() { MavenExec generateEffectiveBom = this.project.getTasks() .create("generateEffectiveBom", MavenExec.class); generateEffectiveBom.getProjectDir().set(generatedBomDir); - File effectiveBom = new File(this.project.getBuildDir(), - "generated/effective-bom/" + this.project.getName() + "-effective-bom.xml"); + File effectiveBom = this.project.getLayout() + .getBuildDirectory() + .file("generated/effective-bom/" + this.project.getName() + "-effective-bom.xml") + .get() + .getAsFile(); generateEffectiveBom.args("--settings", "settings.xml", "help:effective-pom", "-Doutput=" + effectiveBom); generateEffectiveBom.dependsOn(syncBom); @@ -196,6 +207,7 @@ private void putArtifactVersionProperty(String groupId, String artifactId, Strin } private void addLibrary(Library library) { + DependencyHandler dependencies = this.project.getDependencies(); this.libraries.add(library); String versionProperty = library.getVersionProperty(); if (versionProperty != null) { @@ -203,23 +215,30 @@ private void addLibrary(Library library) { } for (Group group : library.getGroups()) { for (Module module : group.getModules()) { - putArtifactVersionProperty(group.getId(), module.getName(), module.getClassifier(), versionProperty); - this.dependencyHandler.getConstraints() - .add(JavaPlatformPlugin.API_CONFIGURATION_NAME, createDependencyNotation(group.getId(), - module.getName(), library.getVersion().getVersion())); + addModule(library, dependencies, versionProperty, group, module); } for (String bomImport : group.getBoms()) { - putArtifactVersionProperty(group.getId(), bomImport, versionProperty); - String bomDependency = createDependencyNotation(group.getId(), bomImport, - library.getVersion().getVersion()); - this.dependencyHandler.add(JavaPlatformPlugin.API_CONFIGURATION_NAME, - this.dependencyHandler.platform(bomDependency)); - this.dependencyHandler.add(BomPlugin.API_ENFORCED_CONFIGURATION_NAME, - this.dependencyHandler.enforcedPlatform(bomDependency)); + addBomImport(library, dependencies, versionProperty, group, bomImport); } } } + private void addModule(Library library, DependencyHandler dependencies, String versionProperty, Group group, + Module module) { + putArtifactVersionProperty(group.getId(), module.getName(), module.getClassifier(), versionProperty); + String constraint = createDependencyNotation(group.getId(), module.getName(), + library.getVersion().getVersion()); + dependencies.getConstraints().add(JavaPlatformPlugin.API_CONFIGURATION_NAME, constraint); + } + + private void addBomImport(Library library, DependencyHandler dependencies, String versionProperty, Group group, + String bomImport) { + putArtifactVersionProperty(group.getId(), bomImport, versionProperty); + String bomDependency = createDependencyNotation(group.getId(), bomImport, library.getVersion().getVersion()); + dependencies.add(JavaPlatformPlugin.API_CONFIGURATION_NAME, dependencies.platform(bomDependency)); + dependencies.add(BomPlugin.API_ENFORCED_CONFIGURATION_NAME, dependencies.enforcedPlatform(bomDependency)); + } + public static class LibraryHandler { private final List groups = new ArrayList<>(); @@ -502,7 +521,12 @@ public static class UpgradeHandler { private UpgradePolicy upgradePolicy; - private final GitHubHandler gitHub = new GitHubHandler(); + private final GitHubHandler gitHub; + + @Inject + public UpgradeHandler(Project project) { + this.gitHub = new GitHubHandler(project); + } public void setPolicy(UpgradePolicy upgradePolicy) { this.upgradePolicy = upgradePolicy; @@ -537,12 +561,18 @@ public GitHub getGitHub() { public static class GitHubHandler { - private String organization = "spring-projects"; + private String organization; - private String repository = "spring-boot"; + private String repository; private List issueLabels; + public GitHubHandler(Project project) { + BuildProperties buildProperties = BuildProperties.get(project); + this.organization = buildProperties.gitHub().organization(); + this.repository = buildProperties.gitHub().repository(); + } + public void setOrganization(String organization) { this.organization = organization; } @@ -559,9 +589,9 @@ public void setIssueLabels(List issueLabels) { public static final class GitHub { - private String organization = "spring-projects"; + private String organization; - private String repository = "spring-boot"; + private String repository; private final List issueLabels; diff --git a/buildSrc/src/main/java/org/springframework/boot/build/bom/BomPlugin.java b/buildSrc/src/main/java/org/springframework/boot/build/bom/BomPlugin.java index 07474bd79b79..86ec205bf238 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/bom/BomPlugin.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/bom/BomPlugin.java @@ -60,8 +60,7 @@ public void apply(Project project) { JavaPlatformExtension javaPlatform = project.getExtensions().getByType(JavaPlatformExtension.class); javaPlatform.allowDependencies(); createApiEnforcedConfiguration(project); - BomExtension bom = project.getExtensions() - .create("bom", BomExtension.class, project.getDependencies(), project); + BomExtension bom = project.getExtensions().create("bom", BomExtension.class, project); CheckBom checkBom = project.getTasks().create("bomrCheck", CheckBom.class, bom); project.getTasks().named("check").configure((check) -> check.dependsOn(checkBom)); project.getTasks().create("bomrUpgrade", UpgradeBom.class, bom); diff --git a/buildSrc/src/main/java/org/springframework/boot/build/bom/CheckLinks.java b/buildSrc/src/main/java/org/springframework/boot/build/bom/CheckLinks.java index ca5a71b187d9..77bcfebf32c5 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/bom/CheckLinks.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/bom/CheckLinks.java @@ -64,8 +64,8 @@ void releaseNotes() { try { uri = new URI(link); ResponseEntity response = restTemplate.exchange(uri, HttpMethod.HEAD, null, String.class); - System.out.println("[%3d] %s - %s (%s)".formatted(response.getStatusCode().value(), - library.getName(), name, uri)); + System.out.printf("[%3d] %s - %s (%s)%n", response.getStatusCode().value(), library.getName(), name, + uri); } catch (URISyntaxException ex) { throw new RuntimeException(ex); diff --git a/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/InteractiveUpgradeResolver.java b/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/InteractiveUpgradeResolver.java index 840665d78d45..532dfc972c2a 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/InteractiveUpgradeResolver.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/InteractiveUpgradeResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,9 @@ package org.springframework.boot.build.bom.bomr; +import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -51,20 +53,37 @@ public List resolveUpgrades(Collection librariesToUpgrade, Col } List libraryUpdates = this.libraryUpdateResolver .findLibraryUpdates(librariesToUpgrade, librariesByName); - return libraryUpdates.stream().map(this::resolveUpgrade).filter(Objects::nonNull).toList(); + try { + return libraryUpdates.stream().map(this::resolveUpgrade).filter(Objects::nonNull).toList(); + } + catch (UpgradesInterruptedException ex) { + return Collections.emptyList(); + } } private Upgrade resolveUpgrade(LibraryWithVersionOptions libraryWithVersionOptions) { if (libraryWithVersionOptions.getVersionOptions().isEmpty()) { return null; } - VersionOption current = new VersionOption(libraryWithVersionOptions.getLibrary().getVersion().getVersion()); - VersionOption selected = this.userInputHandler.selectOption( - libraryWithVersionOptions.getLibrary().getName() + " " - + libraryWithVersionOptions.getLibrary().getVersion().getVersion(), - libraryWithVersionOptions.getVersionOptions(), current); - return (selected.equals(current)) ? null + VersionOption defaultOption = new VersionOption( + libraryWithVersionOptions.getLibrary().getVersion().getVersion()); + VersionOption selected = this.userInputHandler.askUser((questions) -> { + String question = libraryWithVersionOptions.getLibrary().getName() + " " + + libraryWithVersionOptions.getLibrary().getVersion().getVersion(); + List options = new ArrayList<>(); + options.add(defaultOption); + options.addAll(libraryWithVersionOptions.getVersionOptions()); + return questions.selectOption(question, options, defaultOption); + }).get(); + if (this.userInputHandler.interrupted()) { + throw new UpgradesInterruptedException(); + } + return (selected.equals(defaultOption)) ? null : new Upgrade(libraryWithVersionOptions.getLibrary(), selected.getVersion()); } + static class UpgradesInterruptedException extends RuntimeException { + + } + } diff --git a/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/MavenMetadataVersionResolver.java b/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/MavenMetadataVersionResolver.java index fbd12f571f28..f7b5c5819145 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/MavenMetadataVersionResolver.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/MavenMetadataVersionResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,15 +30,20 @@ import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathFactory; +import org.gradle.api.artifacts.repositories.MavenArtifactRepository; import org.w3c.dom.Document; import org.w3c.dom.NodeList; import org.xml.sax.InputSource; import org.springframework.boot.build.bom.bomr.version.DependencyVersion; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.converter.StringHttpMessageConverter; import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; /** * A {@link VersionResolver} that examines {@code maven-metadata.xml} to determine the @@ -50,42 +55,40 @@ final class MavenMetadataVersionResolver implements VersionResolver { private final RestTemplate rest; - private final Collection repositoryUrls; + private final Collection repositories; - MavenMetadataVersionResolver(Collection repositoryUrls) { - this(new RestTemplate(Collections.singletonList(new StringHttpMessageConverter())), repositoryUrls); + MavenMetadataVersionResolver(Collection repositories) { + this(new RestTemplate(Collections.singletonList(new StringHttpMessageConverter())), repositories); } - MavenMetadataVersionResolver(RestTemplate restTemplate, Collection repositoryUrls) { + MavenMetadataVersionResolver(RestTemplate restTemplate, Collection repositories) { this.rest = restTemplate; - this.repositoryUrls = normalize(repositoryUrls); - } - - private Collection normalize(Collection uris) { - return uris.stream().map(this::normalize).toList(); - } - - private URI normalize(URI uri) { - if ("/".equals(uri.getPath())) { - return uri; - } - return URI.create(uri + "/"); + this.repositories = repositories; } @Override public SortedSet resolveVersions(String groupId, String artifactId) { Set versions = new HashSet<>(); - for (URI repositoryUrl : this.repositoryUrls) { - versions.addAll(resolveVersions(groupId, artifactId, repositoryUrl)); + for (MavenArtifactRepository repository : this.repositories) { + versions.addAll(resolveVersions(groupId, artifactId, repository)); } return versions.stream().map(DependencyVersion::parse).collect(Collectors.toCollection(TreeSet::new)); } - private Set resolveVersions(String groupId, String artifactId, URI repositoryUrl) { + private Set resolveVersions(String groupId, String artifactId, MavenArtifactRepository repository) { Set versions = new HashSet<>(); - URI url = repositoryUrl.resolve(groupId.replace('.', '/') + "/" + artifactId + "/maven-metadata.xml"); + URI url = UriComponentsBuilder.fromUri(repository.getUrl()) + .pathSegment(groupId.replace('.', '/'), artifactId, "maven-metadata.xml") + .build() + .toUri(); try { - String metadata = this.rest.getForObject(url, String.class); + HttpHeaders headers = new HttpHeaders(); + String username = repository.getCredentials().getUsername(); + if (username != null) { + headers.setBasicAuth(username, repository.getCredentials().getPassword()); + } + HttpEntity request = new HttpEntity<>(headers); + String metadata = this.rest.exchange(url, HttpMethod.GET, request, String.class).getBody(); Document metadataDocument = DocumentBuilderFactory.newInstance() .newDocumentBuilder() .parse(new InputSource(new StringReader(metadata))); @@ -104,7 +107,7 @@ private Set resolveVersions(String groupId, String artifactId, URI repos } catch (Exception ex) { System.err.println("Failed to resolve versions for module " + groupId + ":" + artifactId + " in repository " - + repositoryUrl + ": " + ex.getMessage()); + + repository + ": " + ex.getMessage()); } return versions; } diff --git a/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/MoveToSnapshots.java b/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/MoveToSnapshots.java index b47545839378..6fee71461895 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/MoveToSnapshots.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/MoveToSnapshots.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 the original author or authors. + * Copyright 2021-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,6 @@ package org.springframework.boot.build.bom.bomr; -import java.net.URI; import java.time.OffsetDateTime; import java.util.List; import java.util.Map; @@ -25,6 +24,7 @@ import javax.inject.Inject; import org.gradle.api.Task; +import org.gradle.api.artifacts.repositories.MavenArtifactRepository; import org.gradle.api.tasks.TaskAction; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -34,6 +34,7 @@ import org.springframework.boot.build.bom.bomr.ReleaseSchedule.Release; import org.springframework.boot.build.bom.bomr.github.Milestone; import org.springframework.boot.build.bom.bomr.version.DependencyVersion; +import org.springframework.boot.build.properties.BuildProperties; /** * A {@link Task} to move to snapshot dependencies. @@ -42,14 +43,17 @@ */ public abstract class MoveToSnapshots extends UpgradeDependencies { - private static final Logger log = LoggerFactory.getLogger(MoveToSnapshots.class); - - private final URI REPOSITORY_URI = URI.create("https://repo.spring.io/snapshot/"); + private static final Logger logger = LoggerFactory.getLogger(MoveToSnapshots.class); @Inject public MoveToSnapshots(BomExtension bom) { super(bom, true); - getRepositoryUris().add(this.REPOSITORY_URI); + getProject().getRepositories().withType(MavenArtifactRepository.class, (repository) -> { + String name = repository.getName(); + if (name.startsWith("spring-") && name.endsWith("-snapshot")) { + getRepositoryNames().add(name); + } + }); } @Override @@ -83,26 +87,31 @@ protected boolean eligible(Library library) { @Override protected List> determineUpdatePredicates(Milestone milestone) { - ReleaseSchedule releaseSchedule = new ReleaseSchedule(); - Map> releases = releaseSchedule.releasesBetween(OffsetDateTime.now(), - milestone.getDueOn()); + return switch (BuildProperties.get(getProject()).buildType()) { + case OPEN_SOURCE -> determineOpenSourceUpdatePredicates(milestone); + case COMMERCIAL -> super.determineUpdatePredicates(milestone); + }; + } + + private List> determineOpenSourceUpdatePredicates(Milestone milestone) { + Map> scheduledReleases = getScheduledOpenSourceReleases(milestone); List> predicates = super.determineUpdatePredicates(milestone); predicates.add((library, candidate) -> { - List releasesForLibrary = releases.get(library.getCalendarName()); - if (releasesForLibrary != null) { - for (Release release : releasesForLibrary) { - if (candidate.isSnapshotFor(release.getVersion())) { - return true; - } - } - } - if (log.isInfoEnabled()) { - log.info("Ignoring " + candidate + ". No release of " + library.getName() + " scheduled before " - + milestone.getDueOn()); + List releases = scheduledReleases.get(library.getCalendarName()); + boolean match = (releases != null) + && releases.stream().anyMatch((release) -> candidate.isSnapshotFor(release.getVersion())); + if (logger.isInfoEnabled() && !match) { + logger.info("Ignoring {}. No release of {} scheduled before {}", candidate, library.getName(), + milestone.getDueOn()); } - return false; + return match; }); return predicates; } + private Map> getScheduledOpenSourceReleases(Milestone milestone) { + ReleaseSchedule releaseSchedule = new ReleaseSchedule(); + return releaseSchedule.releasesBetween(OffsetDateTime.now(), milestone.getDueOn()); + } + } diff --git a/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/MultithreadedLibraryUpdateResolver.java b/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/MultithreadedLibraryUpdateResolver.java index 032ad59b1fb7..10edc7e644f2 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/MultithreadedLibraryUpdateResolver.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/MultithreadedLibraryUpdateResolver.java @@ -41,7 +41,7 @@ */ class MultithreadedLibraryUpdateResolver implements LibraryUpdateResolver { - private static final Logger LOGGER = LoggerFactory.getLogger(MultithreadedLibraryUpdateResolver.class); + private static final Logger logger = LoggerFactory.getLogger(MultithreadedLibraryUpdateResolver.class); private final int threads; @@ -55,7 +55,7 @@ class MultithreadedLibraryUpdateResolver implements LibraryUpdateResolver { @Override public List findLibraryUpdates(Collection librariesToUpgrade, Map librariesByName) { - LOGGER.info("Looking for updates using {} threads", this.threads); + logger.info("Looking for updates using {} threads", this.threads); ExecutorService executorService = Executors.newFixedThreadPool(this.threads); try { return librariesToUpgrade.stream().map((library) -> { diff --git a/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/StandardLibraryUpdateResolver.java b/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/StandardLibraryUpdateResolver.java index f5ffd02cac42..78fde9f147b3 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/StandardLibraryUpdateResolver.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/StandardLibraryUpdateResolver.java @@ -42,7 +42,7 @@ */ class StandardLibraryUpdateResolver implements LibraryUpdateResolver { - private static final Logger LOGGER = LoggerFactory.getLogger(StandardLibraryUpdateResolver.class); + private static final Logger logger = LoggerFactory.getLogger(StandardLibraryUpdateResolver.class); private final VersionResolver versionResolver; @@ -63,11 +63,11 @@ public List findLibraryUpdates(Collection li if (isLibraryExcluded(library)) { continue; } - LOGGER.info("Looking for updates for {}", library.getName()); + logger.info("Looking for updates for {}", library.getName()); long start = System.nanoTime(); List versionOptions = getVersionOptions(library); result.add(new LibraryWithVersionOptions(library, versionOptions)); - LOGGER.info("Found {} updates for {}, took {}", versionOptions.size(), library.getName(), + logger.info("Found {} updates for {}, took {}", versionOptions.size(), library.getName(), Duration.ofNanos(System.nanoTime() - start)); } return result; diff --git a/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/UpgradeBom.java b/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/UpgradeBom.java index 16e4dbdc0687..fced373524f6 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/UpgradeBom.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/UpgradeBom.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,14 +16,14 @@ package org.springframework.boot.build.bom.bomr; -import java.net.URI; - import javax.inject.Inject; import org.gradle.api.Task; +import org.gradle.api.artifacts.ArtifactRepositoryContainer; import org.gradle.api.artifacts.repositories.MavenArtifactRepository; import org.springframework.boot.build.bom.BomExtension; +import org.springframework.boot.build.properties.BuildProperties; /** * {@link Task} to upgrade the libraries managed by a bom. @@ -36,14 +36,27 @@ public abstract class UpgradeBom extends UpgradeDependencies { @Inject public UpgradeBom(BomExtension bom) { super(bom); + switch (BuildProperties.get(getProject()).buildType()) { + case OPEN_SOURCE -> addOpenSourceRepositories(); + case COMMERCIAL -> addCommercialRepositories(); + } + } + + private void addOpenSourceRepositories() { + getRepositoryNames().add(ArtifactRepositoryContainer.DEFAULT_MAVEN_CENTRAL_REPO_NAME); getProject().getRepositories().withType(MavenArtifactRepository.class, (repository) -> { - URI repositoryUrl = repository.getUrl(); - if (!repositoryUrl.toString().endsWith("snapshot")) { - getRepositoryUris().add(repositoryUrl); + String name = repository.getName(); + if (name.startsWith("spring-") && !name.endsWith("-snapshot")) { + getRepositoryNames().add(name); } }); } + private void addCommercialRepositories() { + getRepositoryNames().addAll(ArtifactRepositoryContainer.DEFAULT_MAVEN_CENTRAL_REPO_NAME, + "spring-commercial-release"); + } + @Override protected String issueTitle(Upgrade upgrade) { return "Upgrade to " + upgrade.getLibrary().getName() + " " + upgrade.getVersion(); diff --git a/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/UpgradeDependencies.java b/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/UpgradeDependencies.java index 19e897cb823d..4a4635e2ddfa 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/UpgradeDependencies.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/UpgradeDependencies.java @@ -20,10 +20,10 @@ import java.io.FileReader; import java.io.IOException; import java.io.Reader; -import java.net.URI; import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.List; import java.util.Properties; import java.util.Set; @@ -35,6 +35,7 @@ import org.gradle.api.DefaultTask; import org.gradle.api.InvalidUserDataException; +import org.gradle.api.artifacts.repositories.MavenArtifactRepository; import org.gradle.api.internal.tasks.userinput.UserInputHandler; import org.gradle.api.provider.ListProperty; import org.gradle.api.provider.Property; @@ -54,7 +55,7 @@ import org.springframework.util.StringUtils; /** - * Base class for tasks that upgrade dependencies in a bom. + * Base class for tasks that upgrade dependencies in a BOM. * * @author Andy Wilkinson * @author Moritz Halbritter @@ -91,7 +92,7 @@ protected UpgradeDependencies(BomExtension bom, boolean movingToSnapshots) { public abstract Property getLibraries(); @Input - abstract ListProperty getRepositoryUris(); + abstract ListProperty getRepositoryNames(); @TaskAction void upgradeDependencies() { @@ -217,12 +218,27 @@ private Issue findExistingUpgradeIssue(List existingUpgradeIssues, Upgrad @SuppressWarnings("deprecation") private List resolveUpgrades(Milestone milestone) { - List upgrades = new InteractiveUpgradeResolver(getServices().get(UserInputHandler.class), - new MultithreadedLibraryUpdateResolver(getThreads().get(), - new StandardLibraryUpdateResolver(new MavenMetadataVersionResolver(getRepositoryUris().get()), - determineUpdatePredicates(milestone)))) - .resolveUpgrades(matchingLibraries(), this.bom.getLibraries()); - return upgrades; + InteractiveUpgradeResolver upgradeResolver = new InteractiveUpgradeResolver( + getServices().get(UserInputHandler.class), getLibraryUpdateResolver(milestone)); + return upgradeResolver.resolveUpgrades(matchingLibraries(), this.bom.getLibraries()); + } + + private LibraryUpdateResolver getLibraryUpdateResolver(Milestone milestone) { + VersionResolver versionResolver = new MavenMetadataVersionResolver(getRepositories()); + LibraryUpdateResolver libraryResolver = new StandardLibraryUpdateResolver(versionResolver, + determineUpdatePredicates(milestone)); + return new MultithreadedLibraryUpdateResolver(getThreads().get(), libraryResolver); + } + + private Collection getRepositories() { + return getRepositoryNames().map(this::asRepositories).get(); + } + + private List asRepositories(List repositoryNames) { + return repositoryNames.stream() + .map(getProject().getRepositories()::getByName) + .map(MavenArtifactRepository.class::cast) + .toList(); } protected List> determineUpdatePredicates(Milestone milestone) { diff --git a/buildSrc/src/main/java/org/springframework/boot/build/cli/HomebrewFormula.java b/buildSrc/src/main/java/org/springframework/boot/build/cli/HomebrewFormula.java index e96487213da0..3edf6097d3c6 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/cli/HomebrewFormula.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/cli/HomebrewFormula.java @@ -36,8 +36,12 @@ import org.gradle.api.tasks.PathSensitivity; import org.gradle.api.tasks.TaskAction; import org.gradle.api.tasks.TaskExecutionException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.boot.build.artifacts.ArtifactRelease; +import org.springframework.boot.build.properties.BuildProperties; +import org.springframework.boot.build.properties.BuildType; /** * A {@link Task} for creating a Homebrew formula manifest. @@ -46,16 +50,18 @@ */ public abstract class HomebrewFormula extends DefaultTask { + private static final Logger logger = LoggerFactory.getLogger(HomebrewFormula.class); + private final FileSystemOperations fileSystemOperations; @Inject public HomebrewFormula(FileSystemOperations fileSystemOperations) { + this.fileSystemOperations = fileSystemOperations; Project project = getProject(); MapProperty properties = getProperties(); properties.put("hash", getArchive().map((archive) -> sha256(archive.getAsFile()))); getProperties().put("repo", ArtifactRelease.forProject(project).getDownloadRepo()); getProperties().put("version", project.getVersion().toString()); - this.fileSystemOperations = fileSystemOperations; } private String sha256(File file) { @@ -84,6 +90,11 @@ private String sha256(File file) { @TaskAction void createFormula() { + BuildType buildType = BuildProperties.get(getProject()).buildType(); + if (buildType != BuildType.OPEN_SOURCE) { + logger.debug("Skipping Homebrew formula for non open source build type"); + return; + } this.fileSystemOperations.copy((copy) -> { copy.from(getTemplate()); copy.into(getOutputDir()); diff --git a/buildSrc/src/main/java/org/springframework/boot/build/mavenplugin/MavenExec.java b/buildSrc/src/main/java/org/springframework/boot/build/mavenplugin/MavenExec.java index 73ef436429ac..d8ffb743beb3 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/mavenplugin/MavenExec.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/mavenplugin/MavenExec.java @@ -43,7 +43,7 @@ */ public abstract class MavenExec extends JavaExec { - private final Logger log = LoggerFactory.getLogger(MavenExec.class); + private final Logger logger = LoggerFactory.getLogger(MavenExec.class); public MavenExec() { setClasspath(mavenConfiguration(getProject())); @@ -69,8 +69,8 @@ public void exec() { try { args("--log-file", logFile.toFile().getAbsolutePath()); super.exec(); - if (this.log.isInfoEnabled()) { - Files.readAllLines(logFile).forEach(this.log::info); + if (this.logger.isInfoEnabled()) { + Files.readAllLines(logFile).forEach(this.logger::info); } } catch (ExecException ex) { diff --git a/buildSrc/src/main/java/org/springframework/boot/build/mavenplugin/MavenPluginPlugin.java b/buildSrc/src/main/java/org/springframework/boot/build/mavenplugin/MavenPluginPlugin.java index 689d13e6f219..485f731cc4a4 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/mavenplugin/MavenPluginPlugin.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/mavenplugin/MavenPluginPlugin.java @@ -58,6 +58,7 @@ import org.gradle.api.component.ConfigurationVariantDetails; import org.gradle.api.component.SoftwareComponent; import org.gradle.api.file.CopySpec; +import org.gradle.api.file.Directory; import org.gradle.api.file.DirectoryProperty; import org.gradle.api.file.FileCollection; import org.gradle.api.file.RegularFileProperty; @@ -65,6 +66,7 @@ import org.gradle.api.plugins.JavaLibraryPlugin; import org.gradle.api.plugins.JavaPlugin; import org.gradle.api.plugins.JavaPluginExtension; +import org.gradle.api.provider.Provider; import org.gradle.api.publish.PublishingExtension; import org.gradle.api.publish.maven.MavenPublication; import org.gradle.api.publish.maven.plugins.MavenPublishPlugin; @@ -170,11 +172,11 @@ private void addPopulateIntTestMavenRepositoryTask(Project project) { RuntimeClasspathMavenRepository runtimeClasspathMavenRepository = project.getTasks() .create("runtimeClasspathMavenRepository", RuntimeClasspathMavenRepository.class); runtimeClasspathMavenRepository.getOutputDir() - .set(new File(project.getBuildDir(), "runtime-classpath-repository")); + .set(project.getLayout().getBuildDirectory().dir("runtime-classpath-repository")); project.getDependencies() .components((components) -> components.all(MavenRepositoryComponentMetadataRule.class)); Sync task = project.getTasks().create("populateTestMavenRepository", Sync.class); - task.setDestinationDir(new File(project.getBuildDir(), "test-maven-repository")); + task.setDestinationDir(project.getLayout().getBuildDirectory().dir("test-maven-repository").get().getAsFile()); task.with(copyIntTestMavenRepositoryFiles(project, runtimeClasspathMavenRepository)); task.dependsOn(project.getTasks().getByName(MavenRepositoryPlugin.PUBLISH_TO_PROJECT_REPOSITORY_TASK_NAME)); project.getTasks().getByName(IntegrationTestPlugin.INT_TEST_TASK_NAME).dependsOn(task); @@ -188,7 +190,7 @@ private CopySpec copyIntTestMavenRepositoryFiles(Project project, RuntimeClasspathMavenRepository runtimeClasspathMavenRepository) { CopySpec copySpec = project.copySpec(); copySpec.from(project.getConfigurations().getByName(MavenRepositoryPlugin.MAVEN_REPOSITORY_CONFIGURATION_NAME)); - copySpec.from(new File(project.getBuildDir(), "maven-repository")); + copySpec.from(project.getLayout().getBuildDirectory().dir("maven-repository")); copySpec.from(runtimeClasspathMavenRepository); return copySpec; } @@ -197,29 +199,29 @@ private void addDocumentPluginGoalsTask(Project project, MavenExec generatePlugi DocumentPluginGoals task = project.getTasks().create("documentPluginGoals", DocumentPluginGoals.class); File pluginXml = new File(generatePluginDescriptorTask.getOutputs().getFiles().getSingleFile(), "plugin.xml"); task.getPluginXml().set(pluginXml); - task.getOutputDir().set(new File(project.getBuildDir(), "generated/docs/maven-plugin-goals/")); + task.getOutputDir().set(project.getLayout().getBuildDirectory().dir("docs/generated/goals/")); task.dependsOn(generatePluginDescriptorTask); } private MavenExec addGenerateHelpMojoTask(Project project, Jar jarTask) { - File helpMojoDir = new File(project.getBuildDir(), "help-mojo"); + Provider helpMojoDir = project.getLayout().getBuildDirectory().dir("help-mojo"); MavenExec task = createGenerateHelpMojoTask(project, helpMojoDir); task.dependsOn(createSyncHelpMojoInputsTask(project, helpMojoDir)); includeHelpMojoInJar(jarTask, task); return task; } - private MavenExec createGenerateHelpMojoTask(Project project, File helpMojoDir) { + private MavenExec createGenerateHelpMojoTask(Project project, Provider helpMojoDir) { MavenExec task = project.getTasks().create("generateHelpMojo", MavenExec.class); task.getProjectDir().set(helpMojoDir); task.args("org.apache.maven.plugins:maven-plugin-plugin:3.6.1:helpmojo"); - task.getOutputs().dir(new File(helpMojoDir, "target/generated-sources/plugin")); + task.getOutputs().dir(helpMojoDir.map((directory) -> directory.dir("target/generated-sources/plugin"))); return task; } - private Sync createSyncHelpMojoInputsTask(Project project, File helpMojoDir) { + private Sync createSyncHelpMojoInputsTask(Project project, Provider helpMojoDir) { Sync task = project.getTasks().create("syncHelpMojoInputs", Sync.class); - task.setDestinationDir(helpMojoDir); + task.setDestinationDir(helpMojoDir.get().getAsFile()); File pomFile = new File(project.getProjectDir(), "src/maven/resources/pom.xml"); task.from(pomFile, (copy) -> replaceVersionPlaceholder(copy, project)); return task; @@ -231,8 +233,10 @@ private void includeHelpMojoInJar(Jar jarTask, JavaExec generateHelpMojoTask) { } private MavenExec addGeneratePluginDescriptorTask(Project project, Jar jarTask, MavenExec generateHelpMojoTask) { - File pluginDescriptorDir = new File(project.getBuildDir(), "plugin-descriptor"); - File generatedHelpMojoDir = new File(project.getBuildDir(), "generated/sources/helpMojo"); + Provider pluginDescriptorDir = project.getLayout().getBuildDirectory().dir("plugin-descriptor"); + Provider generatedHelpMojoDir = project.getLayout() + .getBuildDirectory() + .dir("generated/sources/helpMojo"); SourceSet mainSourceSet = getMainSourceSet(project); project.getTasks().withType(Javadoc.class, this::setJavadocOptions); FormatHelpMojoSource formattedHelpMojoSource = createFormatHelpMojoSource(project, generateHelpMojoTask, @@ -258,7 +262,7 @@ private void setJavadocOptions(Javadoc javadoc) { } private FormatHelpMojoSource createFormatHelpMojoSource(Project project, MavenExec generateHelpMojoTask, - File generatedHelpMojoDir) { + Provider generatedHelpMojoDir) { FormatHelpMojoSource formatHelpMojoSource = project.getTasks() .create("formatHelpMojoSource", FormatHelpMojoSource.class); formatHelpMojoSource.setGenerator(generateHelpMojoTask); @@ -266,9 +270,10 @@ private FormatHelpMojoSource createFormatHelpMojoSource(Project project, MavenEx return formatHelpMojoSource; } - private Sync createSyncPluginDescriptorInputs(Project project, File destination, SourceSet sourceSet) { + private Sync createSyncPluginDescriptorInputs(Project project, Provider destination, + SourceSet sourceSet) { Sync pluginDescriptorInputs = project.getTasks().create("syncPluginDescriptorInputs", Sync.class); - pluginDescriptorInputs.setDestinationDir(destination); + pluginDescriptorInputs.setDestinationDir(destination.get().getAsFile()); File pomFile = new File(project.getProjectDir(), "src/maven/resources/pom.xml"); pluginDescriptorInputs.from(pomFile, (copy) -> replaceVersionPlaceholder(copy, project)); pluginDescriptorInputs.from(sourceSet.getOutput().getClassesDirs(), (sync) -> sync.into("target/classes")); @@ -277,12 +282,13 @@ private Sync createSyncPluginDescriptorInputs(Project project, File destination, return pluginDescriptorInputs; } - private MavenExec createGeneratePluginDescriptorTask(Project project, File mavenDir) { + private MavenExec createGeneratePluginDescriptorTask(Project project, Provider mavenDir) { MavenExec generatePluginDescriptor = project.getTasks().create("generatePluginDescriptor", MavenExec.class); generatePluginDescriptor.args("org.apache.maven.plugins:maven-plugin-plugin:3.6.1:descriptor"); - generatePluginDescriptor.getOutputs().dir(new File(mavenDir, "target/classes/META-INF/maven")); + generatePluginDescriptor.getOutputs() + .dir(mavenDir.map((directory) -> directory.dir("target/classes/META-INF/maven"))); generatePluginDescriptor.getInputs() - .dir(new File(mavenDir, "target/classes/org")) + .dir(mavenDir.map((directory) -> directory.dir("target/classes/org"))) .withPathSensitivity(PathSensitivity.RELATIVE) .withPropertyName("plugin classes"); generatePluginDescriptor.getProjectDir().set(mavenDir); @@ -298,7 +304,7 @@ private void addPrepareMavenBinariesTask(Project project) { TaskProvider task = project.getTasks() .register("prepareMavenBinaries", PrepareMavenBinaries.class, (prepareMavenBinaries) -> prepareMavenBinaries.getOutputDir() - .set(new File(project.getBuildDir(), "maven-binaries"))); + .set(project.getLayout().getBuildDirectory().dir("maven-binaries"))); project.getTasks() .getByName(IntegrationTestPlugin.INT_TEST_TASK_NAME) .getInputs() diff --git a/buildSrc/src/main/java/org/springframework/boot/build/properties/BuildProperties.java b/buildSrc/src/main/java/org/springframework/boot/build/properties/BuildProperties.java new file mode 100644 index 000000000000..81cac24c1890 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/boot/build/properties/BuildProperties.java @@ -0,0 +1,78 @@ +/* + * Copyright 2024-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.build.properties; + +import org.gradle.api.Project; + +/** + * Properties that can influence the build. + * + * @param buildType the build type + * @param gitHub GitHub details + * @author Phillip Webb + */ +public record BuildProperties(BuildType buildType, GitHub gitHub) { + + private static final String PROPERTY_NAME = BuildProperties.class.getName(); + + /** + * Get the {@link BuildProperties} for the given {@link Project}. + * @param project the source project + * @return the build properties + */ + public static BuildProperties get(Project project) { + BuildProperties buildProperties = (BuildProperties) project.findProperty(PROPERTY_NAME); + if (buildProperties == null) { + buildProperties = load(project); + project.getExtensions().getExtraProperties().set(PROPERTY_NAME, buildProperties); + } + return buildProperties; + } + + private static BuildProperties load(Project project) { + BuildType buildType = buildType(project.findProperty("spring.build-type")); + return switch (buildType) { + case OPEN_SOURCE -> new BuildProperties(buildType, GitHub.OPEN_SOURCE); + case COMMERCIAL -> new BuildProperties(buildType, GitHub.COMMERCIAL); + }; + } + + private static BuildType buildType(Object value) { + if (value == null || "oss".equals(value.toString())) { + return BuildType.OPEN_SOURCE; + } + if ("commercial".equals(value.toString())) { + return BuildType.COMMERCIAL; + } + throw new IllegalStateException("Unknown build type property '" + value + "'"); + } + + /** + * GitHub properties. + * + * @param organization the GitHub organization + * @param repository the GitHub repository + */ + public record GitHub(String organization, String repository) { + + static final GitHub OPEN_SOURCE = new GitHub("spring-projects", "spring-boot"); + + static final GitHub COMMERCIAL = new GitHub("spring-projects", "spring-boot-commercial"); + + } + +} diff --git a/buildSrc/src/main/java/org/springframework/boot/build/properties/BuildType.java b/buildSrc/src/main/java/org/springframework/boot/build/properties/BuildType.java new file mode 100644 index 000000000000..428b7b423ca0 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/boot/build/properties/BuildType.java @@ -0,0 +1,42 @@ +/* + * Copyright 2024-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.build.properties; + +import java.util.Locale; + +/** + * The type of build being performed. + * + * @author Phillip Webb + */ +public enum BuildType { + + /** + * An open source build. + */ + OPEN_SOURCE, + + /** + * A commercial build. + */ + COMMERCIAL; + + public String toIdentifier() { + return toString().replace("_", "").toLowerCase(Locale.ROOT); + } + +} diff --git a/buildSrc/src/main/java/org/springframework/boot/build/starters/StarterPlugin.java b/buildSrc/src/main/java/org/springframework/boot/build/starters/StarterPlugin.java index 84afee6efeee..115c72214013 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/starters/StarterPlugin.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/starters/StarterPlugin.java @@ -16,7 +16,6 @@ package org.springframework.boot.build.starters; -import java.io.File; import java.util.Map; import java.util.TreeMap; @@ -24,10 +23,12 @@ import org.gradle.api.Project; import org.gradle.api.artifacts.Configuration; import org.gradle.api.artifacts.ConfigurationContainer; +import org.gradle.api.file.RegularFile; import org.gradle.api.plugins.JavaBasePlugin; import org.gradle.api.plugins.JavaLibraryPlugin; import org.gradle.api.plugins.JavaPlugin; import org.gradle.api.plugins.PluginContainer; +import org.gradle.api.provider.Provider; import org.gradle.api.tasks.bundling.Jar; import org.springframework.boot.build.ConventionsPlugin; @@ -56,7 +57,7 @@ public void apply(Project project) { ConfigurationContainer configurations = project.getConfigurations(); Configuration runtimeClasspath = configurations.getByName(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME); starterMetadata.setDependencies(runtimeClasspath); - File destination = new File(project.getBuildDir(), "starter-metadata.properties"); + Provider destination = project.getLayout().getBuildDirectory().file("starter-metadata.properties"); starterMetadata.getDestination().set(destination); configurations.create("starterMetadata"); project.getArtifacts() diff --git a/buildSrc/src/main/java/org/springframework/boot/build/test/DockerTestPlugin.java b/buildSrc/src/main/java/org/springframework/boot/build/test/DockerTestPlugin.java index 017ca001a43b..d614c388c960 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/test/DockerTestPlugin.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/test/DockerTestPlugin.java @@ -18,10 +18,12 @@ import org.gradle.api.Plugin; import org.gradle.api.Project; +import org.gradle.api.Task; import org.gradle.api.plugins.JavaPlugin; import org.gradle.api.plugins.JavaPluginExtension; import org.gradle.api.provider.Provider; import org.gradle.api.services.BuildService; +import org.gradle.api.tasks.Exec; import org.gradle.api.tasks.SourceSet; import org.gradle.api.tasks.SourceSetContainer; import org.gradle.api.tasks.testing.Test; @@ -43,17 +45,19 @@ public class DockerTestPlugin implements Plugin { /** * Name of the {@code dockerTest} task. */ - public static String DOCKER_TEST_TASK_NAME = "dockerTest"; + public static final String DOCKER_TEST_TASK_NAME = "dockerTest"; /** * Name of the {@code dockerTest} source set. */ - public static String DOCKER_TEST_SOURCE_SET_NAME = "dockerTest"; + public static final String DOCKER_TEST_SOURCE_SET_NAME = "dockerTest"; /** * Name of the {@code dockerTest} shared service. */ - public static String DOCKER_TEST_SERVICE_NAME = "dockerTest"; + public static final String DOCKER_TEST_SERVICE_NAME = "dockerTest"; + + private static final String RECLAIM_DOCKER_SPACE_TASK_NAME = "reclaimDockerSpace"; @Override public void apply(Project project) { @@ -71,6 +75,10 @@ private void configureDockerTesting(Project project) { .add(project.getConfigurations() .getByName(dockerTestSourceSet.getRuntimeClasspathConfigurationName()))); }); + project.getDependencies() + .add(dockerTestSourceSet.getRuntimeOnlyConfigurationName(), "org.junit.platform:junit-platform-launcher"); + Provider reclaimDockerSpace = createReclaimDockerSpaceTask(project, buildService); + project.getTasks().getByName(LifecycleBasePlugin.CHECK_TASK_NAME).dependsOn(reclaimDockerSpace); } private SourceSet createSourceSet(Project project) { @@ -98,7 +106,7 @@ private SourceSet createSourceSet(Project project) { private Provider createTestTask(Project project, SourceSet dockerTestSourceSet, Provider buildService) { - Provider dockerTest = project.getTasks().register(DOCKER_TEST_TASK_NAME, Test.class, (task) -> { + return project.getTasks().register(DOCKER_TEST_TASK_NAME, Test.class, (task) -> { task.usesService(buildService); task.setGroup(LifecycleBasePlugin.VERIFICATION_GROUP); task.setDescription("Runs Docker-based tests."); @@ -106,7 +114,30 @@ private Provider createTestTask(Project project, SourceSet dockerTestSourc task.setClasspath(dockerTestSourceSet.getRuntimeClasspath()); task.shouldRunAfter(JavaPlugin.TEST_TASK_NAME); }); - return dockerTest; + } + + private Provider createReclaimDockerSpaceTask(Project project, + Provider buildService) { + return project.getTasks().register(RECLAIM_DOCKER_SPACE_TASK_NAME, Exec.class, (task) -> { + task.usesService(buildService); + task.setGroup(LifecycleBasePlugin.VERIFICATION_GROUP); + task.setDescription("Reclaims Docker space on CI."); + task.shouldRunAfter(DOCKER_TEST_TASK_NAME); + task.onlyIf(this::shouldReclaimDockerSpace); + task.executable("bash"); + task.args("-c", + project.getRootDir() + .toPath() + .resolve(".github/scripts/reclaim-docker-diskspace.sh") + .toAbsolutePath()); + }); + } + + private boolean shouldReclaimDockerSpace(Task task) { + if (System.getProperty("os.name").startsWith("Windows")) { + return false; + } + return System.getenv("GITHUB_ACTIONS") != null || System.getenv("RECLAIM_DOCKER_SPACE") != null; } } diff --git a/buildSrc/src/main/java/org/springframework/boot/build/test/IntegrationTestPlugin.java b/buildSrc/src/main/java/org/springframework/boot/build/test/IntegrationTestPlugin.java index 6286df7601cf..34ff445b8899 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/test/IntegrationTestPlugin.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/test/IntegrationTestPlugin.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -58,6 +58,8 @@ private void configureIntegrationTesting(Project project) { eclipse.classpath((classpath) -> classpath.getPlusConfigurations() .add(project.getConfigurations().getByName(intTestSourceSet.getRuntimeClasspathConfigurationName()))); }); + project.getDependencies() + .add(intTestSourceSet.getRuntimeOnlyConfigurationName(), "org.junit.platform:junit-platform-launcher"); } private SourceSet createSourceSet(Project project) { diff --git a/buildSrc/src/main/resources/org/springframework/boot/build/antora/antora-asciidoc-attributes.properties b/buildSrc/src/main/resources/org/springframework/boot/build/antora/antora-asciidoc-attributes.properties index 81f258cd81b5..992d303bef70 100644 --- a/buildSrc/src/main/resources/org/springframework/boot/build/antora/antora-asciidoc-attributes.properties +++ b/buildSrc/src/main/resources/org/springframework/boot/build/antora/antora-asciidoc-attributes.properties @@ -7,6 +7,9 @@ include-kotlin= ROOT:example$kotlin/org/springframework/boot/docs url-ant-docs=https://ant.apache.org/manual url-buildpacks-docs=https://buildpacks.io/docs +url-cyclonedx-docs-gradle-plugin=https://github.com/CycloneDX/cyclonedx-gradle-plugin +url-cyclonedx-docs-maven-plugin=https://github.com/CycloneDX/cyclonedx-maven-plugin +url-download-liberica-nik=https://bell-sw.com/pages/downloads/native-image-kit/#/nik-22-17 url-dynatrace-docs=https://docs.dynatrace.com/docs url-dynatrace-docs-shortlink={url-dynatrace-docs}/shortlink url-github-raw=https://raw.githubusercontent.com/{github-repo}/{github-ref} @@ -25,7 +28,6 @@ url-gradle-javadoc=https://docs.gradle.org/current/javadoc url-kotlin-docs-kotlin-plugin={url-kotlin-docs}/using-gradle.html url-micrometer-docs-concepts={url-micrometer-docs}/concepts url-micrometer-docs-implementations={url-micrometer-docs}/implementations -url-download-liberica-nik=https://bell-sw.com/pages/downloads/native-image-kit/#/nik-22-17 url-native-build-tools-docs=https://graalvm.github.io/native-build-tools/{version-native-build-tools} url-native-build-tools-docs-gradle-plugin={url-native-build-tools-docs}/gradle-plugin.html url-native-build-tools-docs-maven-plugin={url-native-build-tools-docs}/maven-plugin.html @@ -35,6 +37,7 @@ url-spring-boot-for-apache-geode-docs=https://docs.spring.io/spring-boot-data-ge url-spring-boot-for-apache-geode-site=https://github.com/spring-projects/spring-boot-data-geode url-spring-data-cassandra-javadoc=https://docs.spring.io/spring-data/cassandra/docs/{version-spring-data-cassandra-javadoc}/api url-spring-data-cassandra-site=https://spring.io/projects/spring-data-cassandra +url-spring-data-cassandra-docs=https://docs.spring.io/spring-data/cassandra/reference/{version-spring-data-cassandra-docs} url-spring-data-commons-javadoc=https://docs.spring.io/spring-data/commons/docs/{version-spring-data-commons-javadoc}/api url-spring-data-couchbase-docs=https://docs.spring.io/spring-data/couchbase/reference/{version-spring-data-couchbase-docs} url-spring-data-couchbase-site=https://spring.io/projects/spring-data-couchbase @@ -43,13 +46,13 @@ url-spring-data-elasticsearch-javadoc=https://docs.spring.io/spring-data/elastic url-spring-data-elasticsearch-docs=https://docs.spring.io/spring-data/elasticsearch/reference/{version-spring-data-elasticsearch-docs} url-spring-data-elasticsearch-site=https://spring.io/projects/spring-data-elasticsearch url-spring-data-envers-site=https://spring.io/projects/spring-data-envers -url-spring-data-gemfire-site=https://spring.io/projects/spring-data-gemfire url-spring-data-geode-site=https://spring.io/projects/spring-data-geode url-spring-data-jdbc-docs=https://docs.spring.io/spring-data/relational/reference/{version-spring-data-jdbc-docs} url-spring-data-jpa-javadoc=https://docs.spring.io/spring-data/jpa/docs/{version-spring-data-jpa-javadoc}/api -url-spring-data-jpa-site=https://spring.io/projects/spring-jpa +url-spring-data-jpa-site=https://spring.io/projects/spring-data-jpa url-spring-data-jpa-docs=https://docs.spring.io/spring-data/jpa/reference/{version-spring-data-jpa-docs} url-spring-data-ldap-site=https://spring.io/projects/spring-data-ldap +url-spring-data-ldap-docs=https://docs.spring.io/spring-data/ldap/reference/{version-spring-data-ldap-docs} url-spring-data-mongodb-javadoc=https://docs.spring.io/spring-data/mongodb/docs/{version-spring-data-mongodb-javadoc}/api url-spring-data-mongodb-site=https://spring.io/projects/spring-data-mongodb url-spring-data-mongodb-docs=https://docs.spring.io/spring-data/mongodb/reference/{version-spring-data-mongodb-docs} diff --git a/buildSrc/src/test/java/org/springframework/boot/build/antora/AntoraAsciidocAttributesTests.java b/buildSrc/src/test/java/org/springframework/boot/build/antora/AntoraAsciidocAttributesTests.java index dbba3f414ab6..6df1bd64897b 100644 --- a/buildSrc/src/test/java/org/springframework/boot/build/antora/AntoraAsciidocAttributesTests.java +++ b/buildSrc/src/test/java/org/springframework/boot/build/antora/AntoraAsciidocAttributesTests.java @@ -31,6 +31,7 @@ import org.springframework.boot.build.bom.Library.ProhibitedVersion; import org.springframework.boot.build.bom.Library.VersionAlignment; import org.springframework.boot.build.bom.bomr.version.DependencyVersion; +import org.springframework.boot.build.properties.BuildType; import static org.assertj.core.api.Assertions.assertThat; @@ -42,45 +43,59 @@ */ class AntoraAsciidocAttributesTests { + @Test + void buildTypeWhenOpenSource() { + AntoraAsciidocAttributes attributes = new AntoraAsciidocAttributes("1.2.3", true, BuildType.OPEN_SOURCE, null, + mockDependencyVersions(), null); + assertThat(attributes.get()).containsEntry("build-type", "opensource"); + } + + @Test + void buildTypeWhenCommercial() { + AntoraAsciidocAttributes attributes = new AntoraAsciidocAttributes("1.2.3", true, BuildType.COMMERCIAL, null, + mockDependencyVersions(), null); + assertThat(attributes.get()).containsEntry("build-type", "commercial"); + } + @Test void githubRefWhenReleasedVersionIsTag() { - AntoraAsciidocAttributes attributes = new AntoraAsciidocAttributes("1.2.3", true, null, + AntoraAsciidocAttributes attributes = new AntoraAsciidocAttributes("1.2.3", true, BuildType.OPEN_SOURCE, null, mockDependencyVersions(), null); assertThat(attributes.get()).containsEntry("github-ref", "v1.2.3"); } @Test void githubRefWhenLatestSnapshotVersionIsMainBranch() { - AntoraAsciidocAttributes attributes = new AntoraAsciidocAttributes("1.2.3-SNAPSHOT", true, null, - mockDependencyVersions(), null); + AntoraAsciidocAttributes attributes = new AntoraAsciidocAttributes("1.2.3-SNAPSHOT", true, + BuildType.OPEN_SOURCE, null, mockDependencyVersions(), null); assertThat(attributes.get()).containsEntry("github-ref", "main"); } @Test void githubRefWhenOlderSnapshotVersionIsBranch() { - AntoraAsciidocAttributes attributes = new AntoraAsciidocAttributes("1.2.3-SNAPSHOT", false, null, - mockDependencyVersions(), null); + AntoraAsciidocAttributes attributes = new AntoraAsciidocAttributes("1.2.3-SNAPSHOT", false, + BuildType.OPEN_SOURCE, null, mockDependencyVersions(), null); assertThat(attributes.get()).containsEntry("github-ref", "1.2.x"); } @Test void githubRefWhenOlderSnapshotHotFixVersionIsBranch() { - AntoraAsciidocAttributes attributes = new AntoraAsciidocAttributes("1.2.3.1-SNAPSHOT", false, null, - mockDependencyVersions(), null); + AntoraAsciidocAttributes attributes = new AntoraAsciidocAttributes("1.2.3.1-SNAPSHOT", false, + BuildType.OPEN_SOURCE, null, mockDependencyVersions(), null); assertThat(attributes.get()).containsEntry("github-ref", "1.2.3.x"); } @Test void versionReferenceFromLibrary() { Library library = mockLibrary(Collections.emptyMap()); - AntoraAsciidocAttributes attributes = new AntoraAsciidocAttributes("1.2.3.1-SNAPSHOT", false, List.of(library), - mockDependencyVersions(), null); + AntoraAsciidocAttributes attributes = new AntoraAsciidocAttributes("1.2.3.1-SNAPSHOT", false, + BuildType.OPEN_SOURCE, List.of(library), mockDependencyVersions(), null); assertThat(attributes.get()).containsEntry("version-spring-framework", "1.2.3"); } @Test void versionReferenceFromSpringDataDependencyReleaseVersion() { - AntoraAsciidocAttributes attributes = new AntoraAsciidocAttributes("1.2.3", true, null, + AntoraAsciidocAttributes attributes = new AntoraAsciidocAttributes("1.2.3", true, BuildType.OPEN_SOURCE, null, mockDependencyVersions("3.2.5"), null); assertThat(attributes.get()).containsEntry("version-spring-data-mongodb-docs", "3.2"); assertThat(attributes.get()).containsEntry("version-spring-data-mongodb-javadoc", "3.2.x"); @@ -88,7 +103,7 @@ void versionReferenceFromSpringDataDependencyReleaseVersion() { @Test void versionReferenceFromSpringDataDependencySnapshotVersion() { - AntoraAsciidocAttributes attributes = new AntoraAsciidocAttributes("1.2.3", true, null, + AntoraAsciidocAttributes attributes = new AntoraAsciidocAttributes("1.2.3", true, BuildType.OPEN_SOURCE, null, mockDependencyVersions("3.2.0-SNAPSHOT"), null); assertThat(attributes.get()).containsEntry("version-spring-data-mongodb-docs", "3.2-SNAPSHOT"); assertThat(attributes.get()).containsEntry("version-spring-data-mongodb-javadoc", "3.2.x"); @@ -96,51 +111,78 @@ void versionReferenceFromSpringDataDependencySnapshotVersion() { @Test void versionNativeBuildTools() { - AntoraAsciidocAttributes attributes = new AntoraAsciidocAttributes("1.2.3", true, null, + AntoraAsciidocAttributes attributes = new AntoraAsciidocAttributes("1.2.3", true, BuildType.OPEN_SOURCE, null, mockDependencyVersions(), Map.of("nativeBuildToolsVersion", "3.4.5")); assertThat(attributes.get()).containsEntry("version-native-build-tools", "3.4.5"); } @Test void urlArtifactRepositoryWhenRelease() { - AntoraAsciidocAttributes attributes = new AntoraAsciidocAttributes("1.2.3", true, null, + AntoraAsciidocAttributes attributes = new AntoraAsciidocAttributes("1.2.3", true, BuildType.OPEN_SOURCE, null, mockDependencyVersions(), null); assertThat(attributes.get()).containsEntry("url-artifact-repository", "https://repo.maven.apache.org/maven2"); } @Test void urlArtifactRepositoryWhenMilestone() { - AntoraAsciidocAttributes attributes = new AntoraAsciidocAttributes("1.2.3-M1", true, null, - mockDependencyVersions(), null); + AntoraAsciidocAttributes attributes = new AntoraAsciidocAttributes("1.2.3-M1", true, BuildType.OPEN_SOURCE, + null, mockDependencyVersions(), null); assertThat(attributes.get()).containsEntry("url-artifact-repository", "https://repo.spring.io/milestone"); } @Test void urlArtifactRepositoryWhenSnapshot() { - AntoraAsciidocAttributes attributes = new AntoraAsciidocAttributes("1.2.3-SNAPSHOT", true, null, - mockDependencyVersions(), null); + AntoraAsciidocAttributes attributes = new AntoraAsciidocAttributes("1.2.3-SNAPSHOT", true, + BuildType.OPEN_SOURCE, null, mockDependencyVersions(), null); assertThat(attributes.get()).containsEntry("url-artifact-repository", "https://repo.spring.io/snapshot"); } @Test - void artifactReleaseTypeWhenRelease() { - AntoraAsciidocAttributes attributes = new AntoraAsciidocAttributes("1.2.3", true, null, + void artifactReleaseTypeWhenOpenSourceRelease() { + AntoraAsciidocAttributes attributes = new AntoraAsciidocAttributes("1.2.3", true, BuildType.OPEN_SOURCE, null, mockDependencyVersions(), null); assertThat(attributes.get()).containsEntry("artifact-release-type", "release"); + assertThat(attributes.get()).containsEntry("build-and-artifact-release-type", "opensource-release"); } @Test - void artifactReleaseTypeWhenMilestone() { - AntoraAsciidocAttributes attributes = new AntoraAsciidocAttributes("1.2.3-M1", true, null, - mockDependencyVersions(), null); + void artifactReleaseTypeWhenOpenSourceMilestone() { + AntoraAsciidocAttributes attributes = new AntoraAsciidocAttributes("1.2.3-M1", true, BuildType.OPEN_SOURCE, + null, mockDependencyVersions(), null); assertThat(attributes.get()).containsEntry("artifact-release-type", "milestone"); + assertThat(attributes.get()).containsEntry("build-and-artifact-release-type", "opensource-milestone"); } @Test - void artifactReleaseTypeWhenSnapshot() { - AntoraAsciidocAttributes attributes = new AntoraAsciidocAttributes("1.2.3-SNAPSHOT", true, null, + void artifactReleaseTypeWhenOpenSourceSnapshot() { + AntoraAsciidocAttributes attributes = new AntoraAsciidocAttributes("1.2.3-SNAPSHOT", true, + BuildType.OPEN_SOURCE, null, mockDependencyVersions(), null); + assertThat(attributes.get()).containsEntry("artifact-release-type", "snapshot"); + assertThat(attributes.get()).containsEntry("build-and-artifact-release-type", "opensource-snapshot"); + } + + @Test + void artifactReleaseTypeWhenCommercialRelease() { + AntoraAsciidocAttributes attributes = new AntoraAsciidocAttributes("1.2.3", true, BuildType.COMMERCIAL, null, + mockDependencyVersions(), null); + assertThat(attributes.get()).containsEntry("artifact-release-type", "release"); + assertThat(attributes.get()).containsEntry("build-and-artifact-release-type", "commercial-release"); + } + + @Test + void artifactReleaseTypeWhenCommercialMilestone() { + AntoraAsciidocAttributes attributes = new AntoraAsciidocAttributes("1.2.3-M1", true, BuildType.COMMERCIAL, null, mockDependencyVersions(), null); + assertThat(attributes.get()).containsEntry("artifact-release-type", "milestone"); + assertThat(attributes.get()).containsEntry("build-and-artifact-release-type", "commercial-milestone"); + } + + @Test + void artifactReleaseTypeWhenCommercialSnapshot() { + AntoraAsciidocAttributes attributes = new AntoraAsciidocAttributes("1.2.3-SNAPSHOT", true, BuildType.COMMERCIAL, + null, mockDependencyVersions(), null); assertThat(attributes.get()).containsEntry("artifact-release-type", "snapshot"); + assertThat(attributes.get()).containsEntry("build-and-artifact-release-type", "commercial-snapshot"); } @Test @@ -149,16 +191,16 @@ void urlLinksFromLibrary() { links.put("site", (version) -> "https://example.com/site/" + version); links.put("docs", (version) -> "https://example.com/docs/" + version); Library library = mockLibrary(links); - AntoraAsciidocAttributes attributes = new AntoraAsciidocAttributes("1.2.3.1-SNAPSHOT", false, List.of(library), - mockDependencyVersions(), null); + AntoraAsciidocAttributes attributes = new AntoraAsciidocAttributes("1.2.3.1-SNAPSHOT", false, + BuildType.OPEN_SOURCE, List.of(library), mockDependencyVersions(), null); assertThat(attributes.get()).containsEntry("url-spring-framework-site", "https://example.com/site/1.2.3") .containsEntry("url-spring-framework-docs", "https://example.com/docs/1.2.3"); } @Test void linksFromProperties() { - Map attributes = new AntoraAsciidocAttributes("1.2.3-SNAPSHOT", true, null, - mockDependencyVersions(), null) + Map attributes = new AntoraAsciidocAttributes("1.2.3-SNAPSHOT", true, BuildType.OPEN_SOURCE, + null, mockDependencyVersions(), null) .get(); assertThat(attributes).containsEntry("include-java", "ROOT:example$java/org/springframework/boot/docs"); assertThat(attributes).containsEntry("url-spring-data-cassandra-site", @@ -198,6 +240,7 @@ private Map mockDependencyVersions(String version) { addMockSpringDataVersion(versions, "spring-data-neo4j", version); addMockSpringDataVersion(versions, "spring-data-r2dbc", version); addMockSpringDataVersion(versions, "spring-data-rest-core", version); + addMockSpringDataVersion(versions, "spring-data-ldap", version); addMockJacksonVersion(versions, "jackson-annotations", version); addMockJacksonVersion(versions, "jackson-core", version); addMockJacksonVersion(versions, "jackson-databind", version); diff --git a/buildSrc/src/test/java/org/springframework/boot/build/architecture/ArchitectureCheckTests.java b/buildSrc/src/test/java/org/springframework/boot/build/architecture/ArchitectureCheckTests.java index 2610811889a6..ff419b347abc 100644 --- a/buildSrc/src/test/java/org/springframework/boot/build/architecture/ArchitectureCheckTests.java +++ b/buildSrc/src/test/java/org/springframework/boot/build/architecture/ArchitectureCheckTests.java @@ -61,7 +61,12 @@ void whenPackagesAreNotTangledTaskSucceedsAndWritesAnEmptyReport() throws Except } File failureReport(ArchitectureCheck architectureCheck) { - return new File(architectureCheck.getProject().getBuildDir(), "checkArchitecture/failure-report.txt"); + return architectureCheck.getProject() + .getLayout() + .getBuildDirectory() + .file("checkArchitecture/failure-report.txt") + .get() + .getAsFile(); } @Test @@ -163,6 +168,42 @@ void whenClassCallsObjectsRequireNonNullWithSupplierTaskFailsAndWritesReport() t }); } + @Test + void whenClassCallsStringToUpperCaseWithoutLocaleFailsAndWritesReport() throws Exception { + prepareTask("string/toUpperCase", (architectureCheck) -> { + assertThatExceptionOfType(GradleException.class).isThrownBy(architectureCheck::checkArchitecture); + assertThat(failureReport(architectureCheck)).isNotEmpty() + .content() + .contains("because String.toUpperCase(Locale.ROOT) should be used instead"); + }); + } + + @Test + void whenClassCallsStringToLowerCaseWithoutLocaleFailsAndWritesReport() throws Exception { + prepareTask("string/toLowerCase", (architectureCheck) -> { + assertThatExceptionOfType(GradleException.class).isThrownBy(architectureCheck::checkArchitecture); + assertThat(failureReport(architectureCheck)).isNotEmpty() + .content() + .contains("because String.toLowerCase(Locale.ROOT) should be used instead"); + }); + } + + @Test + void whenClassCallsStringToLowerCaseWithLocaleShouldNotFail() throws Exception { + prepareTask("string/toLowerCaseWithLocale", (architectureCheck) -> { + architectureCheck.checkArchitecture(); + assertThat(failureReport(architectureCheck)).isEmpty(); + }); + } + + @Test + void whenClassCallsStringToUpperCaseWithLocaleShouldNotFail() throws Exception { + prepareTask("string/toUpperCaseWithLocale", (architectureCheck) -> { + architectureCheck.checkArchitecture(); + assertThat(failureReport(architectureCheck)).isEmpty(); + }); + } + private void prepareTask(String classes, Callback callback) throws Exception { File projectDir = new File(this.temp, "project"); projectDir.mkdirs(); diff --git a/buildSrc/src/test/java/org/springframework/boot/build/architecture/string/toLowerCase/ToLowerCase.java b/buildSrc/src/test/java/org/springframework/boot/build/architecture/string/toLowerCase/ToLowerCase.java new file mode 100644 index 000000000000..bbdfd9abb3d4 --- /dev/null +++ b/buildSrc/src/test/java/org/springframework/boot/build/architecture/string/toLowerCase/ToLowerCase.java @@ -0,0 +1,26 @@ +/* + * Copyright 2012-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.build.architecture.string.toLowerCase; + +class ToLowerCase { + + void exampleMethod() { + String test = "Object must not be null"; + System.out.println(test.toLowerCase()); + } + +} diff --git a/buildSrc/src/test/java/org/springframework/boot/build/architecture/string/toLowerCaseWithLocale/ToLowerCaseWithLocale.java b/buildSrc/src/test/java/org/springframework/boot/build/architecture/string/toLowerCaseWithLocale/ToLowerCaseWithLocale.java new file mode 100644 index 000000000000..1f3c3225cd0f --- /dev/null +++ b/buildSrc/src/test/java/org/springframework/boot/build/architecture/string/toLowerCaseWithLocale/ToLowerCaseWithLocale.java @@ -0,0 +1,28 @@ +/* + * Copyright 2012-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.build.architecture.string.toLowerCaseWithLocale; + +import java.util.Locale; + +class ToLowerCaseWithLocale { + + void exampleMethod() { + String test = "Object must not be null"; + System.out.println(test.toLowerCase(Locale.ENGLISH)); + } + +} diff --git a/buildSrc/src/test/java/org/springframework/boot/build/architecture/string/toUpperCase/ToUpperCase.java b/buildSrc/src/test/java/org/springframework/boot/build/architecture/string/toUpperCase/ToUpperCase.java new file mode 100644 index 000000000000..97d3ab615179 --- /dev/null +++ b/buildSrc/src/test/java/org/springframework/boot/build/architecture/string/toUpperCase/ToUpperCase.java @@ -0,0 +1,26 @@ +/* + * Copyright 2012-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.build.architecture.string.toUpperCase; + +class ToUpperCase { + + void exampleMethod() { + String test = "Object must not be null"; + System.out.println(test.toUpperCase()); + } + +} diff --git a/buildSrc/src/test/java/org/springframework/boot/build/architecture/string/toUpperCaseWithLocale/ToUpperCaseWithLocale.java b/buildSrc/src/test/java/org/springframework/boot/build/architecture/string/toUpperCaseWithLocale/ToUpperCaseWithLocale.java new file mode 100644 index 000000000000..0ac9d136051e --- /dev/null +++ b/buildSrc/src/test/java/org/springframework/boot/build/architecture/string/toUpperCaseWithLocale/ToUpperCaseWithLocale.java @@ -0,0 +1,28 @@ +/* + * Copyright 2012-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.build.architecture.string.toUpperCaseWithLocale; + +import java.util.Locale; + +class ToUpperCaseWithLocale { + + void exampleMethod() { + String test = "Object must not be null"; + System.out.println(test.toUpperCase(Locale.ROOT)); + } + +} diff --git a/buildSrc/src/test/java/org/springframework/boot/build/groovyscripts/SpringRepositoriesExtensionTests.java b/buildSrc/src/test/java/org/springframework/boot/build/groovyscripts/SpringRepositoriesExtensionTests.java new file mode 100644 index 000000000000..5458b600dfff --- /dev/null +++ b/buildSrc/src/test/java/org/springframework/boot/build/groovyscripts/SpringRepositoriesExtensionTests.java @@ -0,0 +1,273 @@ +/* + * Copyright 2024-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.build.groovyscripts; + +import java.io.File; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.UnaryOperator; + +import groovy.lang.Closure; +import groovy.lang.GroovyClassLoader; +import org.gradle.api.Action; +import org.gradle.api.artifacts.dsl.RepositoryHandler; +import org.gradle.api.artifacts.repositories.MavenArtifactRepository; +import org.gradle.api.artifacts.repositories.PasswordCredentials; +import org.gradle.api.artifacts.repositories.RepositoryContentDescriptor; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.mockito.invocation.InvocationOnMock; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * Tests for {@code SpringRepositorySupport.groovy}. + * + * @author Phillip Webb + */ +class SpringRepositoriesExtensionTests { + + private static GroovyClassLoader groovyClassLoader; + + private static Class supportClass; + + @BeforeAll + static void loadGroovyClass() throws Exception { + groovyClassLoader = new GroovyClassLoader(SpringRepositoriesExtensionTests.class.getClassLoader()); + supportClass = groovyClassLoader.parseClass(new File("SpringRepositorySupport.groovy")); + } + + @AfterAll + static void cleanup() throws Exception { + groovyClassLoader.close(); + } + + private final List repositories = new ArrayList<>(); + + private final List contents = new ArrayList<>(); + + private final List credentials = new ArrayList<>(); + + @Test + void mavenRepositoriesWhenNotCommercialSnapshot() { + SpringRepositoriesExtension extension = createExtension("0.0.0-SNAPSHOT", "oss"); + extension.mavenRepositories(); + assertThat(this.repositories).hasSize(2); + verify(this.repositories.get(0)).setName("spring-oss-milestone"); + verify(this.repositories.get(0)).setUrl("https://repo.spring.io/milestone"); + verify(this.repositories.get(1)).setName("spring-oss-snapshot"); + verify(this.repositories.get(1)).setUrl("https://repo.spring.io/snapshot"); + } + + @Test + void mavenRepositoriesWhenCommercialSnapshot() { + SpringRepositoriesExtension extension = createExtension("0.0.0-SNAPSHOT", "commercial"); + extension.mavenRepositories(); + assertThat(this.repositories).hasSize(4); + verify(this.repositories.get(0)).setName("spring-commercial-release"); + verify(this.repositories.get(0)) + .setUrl("https://usw1.packages.broadcom.com/spring-enterprise-maven-prod-local"); + verify(this.repositories.get(1)).setName("spring-oss-milestone"); + verify(this.repositories.get(1)).setUrl("https://repo.spring.io/milestone"); + verify(this.repositories.get(2)).setName("spring-commercial-snapshot"); + verify(this.repositories.get(2)).setUrl("https://usw1.packages.broadcom.com/spring-enterprise-maven-dev-local"); + verify(this.repositories.get(3)).setName("spring-oss-snapshot"); + verify(this.repositories.get(3)).setUrl("https://repo.spring.io/snapshot"); + } + + @Test + void mavenRepositoriesWhenNotCommercialMilestone() { + SpringRepositoriesExtension extension = createExtension("0.0.0-M1", "oss"); + extension.mavenRepositories(); + assertThat(this.repositories).hasSize(1); + verify(this.repositories.get(0)).setName("spring-oss-milestone"); + verify(this.repositories.get(0)).setUrl("https://repo.spring.io/milestone"); + } + + @Test + void mavenRepositoriesWhenCommercialMilestone() { + SpringRepositoriesExtension extension = createExtension("0.0.0-M1", "commercial"); + extension.mavenRepositories(); + assertThat(this.repositories).hasSize(2); + verify(this.repositories.get(0)).setName("spring-commercial-release"); + verify(this.repositories.get(0)) + .setUrl("https://usw1.packages.broadcom.com/spring-enterprise-maven-prod-local"); + verify(this.repositories.get(1)).setName("spring-oss-milestone"); + verify(this.repositories.get(1)).setUrl("https://repo.spring.io/milestone"); + } + + @Test + void mavenRepositoriesWhenNotCommercialRelease() { + SpringRepositoriesExtension extension = createExtension("0.0.1", "oss"); + extension.mavenRepositories(); + assertThat(this.repositories).isEmpty(); + } + + @Test + void mavenRepositoriesWhenCommercialRelease() { + SpringRepositoriesExtension extension = createExtension("0.0.1", "commercial"); + extension.mavenRepositories(); + assertThat(this.repositories).hasSize(1); + verify(this.repositories.get(0)).setName("spring-commercial-release"); + verify(this.repositories.get(0)) + .setUrl("https://usw1.packages.broadcom.com/spring-enterprise-maven-prod-local"); + } + + @Test + void mavenRepositoriesWhenConditionMatches() { + SpringRepositoriesExtension extension = createExtension("0.0.0-SNAPSHOT", "oss"); + extension.mavenRepositories(true); + assertThat(this.repositories).hasSize(2); + } + + @Test + void mavenRepositoriesWhenConditionDoesNotMatch() { + SpringRepositoriesExtension extension = createExtension("0.0.0-SNAPSHOT", "oss"); + extension.mavenRepositories(false); + assertThat(this.repositories).isEmpty(); + } + + @Test + void mavenRepositoriesExcludingBootGroup() { + SpringRepositoriesExtension extension = createExtension("0.0.0-SNAPSHOT", "oss"); + extension.mavenRepositoriesExcludingBootGroup(); + assertThat(this.contents).hasSize(2); + verify(this.contents.get(0)).excludeGroup("org.springframework.boot"); + verify(this.contents.get(1)).excludeGroup("org.springframework.boot"); + } + + @Test + void mavenRepositoriesWithRepositorySpecificEnvironmentVariables() { + Map environment = new HashMap<>(); + environment.put("COMMERCIAL_RELEASE_REPO_URL", "curl"); + environment.put("COMMERCIAL_RELEASE_REPO_USERNAME", "cuser"); + environment.put("COMMERCIAL_RELEASE_REPO_PASSWORD", "cpass"); + environment.put("COMMERCIAL_SNAPSHOT_REPO_URL", "surl"); + environment.put("COMMERCIAL_SNAPSHOT_REPO_USERNAME", "suser"); + environment.put("COMMERCIAL_SNAPSHOT_REPO_PASSWORD", "spass"); + SpringRepositoriesExtension extension = createExtension("0.0.0-SNAPSHOT", "commercial", environment::get); + extension.mavenRepositories(); + assertThat(this.repositories).hasSize(4); + verify(this.repositories.get(0)).setUrl("curl"); + verify(this.repositories.get(2)).setUrl("surl"); + assertThat(this.credentials).hasSize(2); + verify(this.credentials.get(0)).setUsername("cuser"); + verify(this.credentials.get(0)).setPassword("cpass"); + verify(this.credentials.get(1)).setUsername("suser"); + verify(this.credentials.get(1)).setPassword("spass"); + } + + @Test + void mavenRepositoriesWhenRepositoryEnvironmentVariables() { + Map environment = new HashMap<>(); + environment.put("COMMERCIAL_REPO_URL", "url"); + environment.put("COMMERCIAL_REPO_USERNAME", "user"); + environment.put("COMMERCIAL_REPO_PASSWORD", "pass"); + SpringRepositoriesExtension extension = createExtension("0.0.0-SNAPSHOT", "commercial", environment::get); + extension.mavenRepositories(); + assertThat(this.repositories).hasSize(4); + verify(this.repositories.get(0)).setUrl("url"); + verify(this.repositories.get(2)).setUrl("url"); + assertThat(this.credentials).hasSize(2); + verify(this.credentials.get(0)).setUsername("user"); + verify(this.credentials.get(0)).setPassword("pass"); + verify(this.credentials.get(1)).setUsername("user"); + verify(this.credentials.get(1)).setPassword("pass"); + } + + private SpringRepositoriesExtension createExtension(String version, String buildType) { + return createExtension(version, buildType, (name) -> null); + } + + @SuppressWarnings({ "unchecked", "unchecked" }) + private SpringRepositoriesExtension createExtension(String version, String buildType, + UnaryOperator environment) { + RepositoryHandler repositoryHandler = mock(RepositoryHandler.class); + given(repositoryHandler.maven(any(Closure.class))).willAnswer(this::mavenClosure); + return SpringRepositoriesExtension.get(repositoryHandler, version, buildType, environment); + } + + @SuppressWarnings({ "unchecked", "unchecked" }) + private Object mavenClosure(InvocationOnMock invocation) { + MavenArtifactRepository repository = mock(MavenArtifactRepository.class); + willAnswer(this::contentAction).given(repository).content(any(Action.class)); + willAnswer(this::credentialsAction).given(repository).credentials(any(Action.class)); + Closure closure = invocation.getArgument(0); + closure.call(repository); + this.repositories.add(repository); + return null; + } + + private Object contentAction(InvocationOnMock invocation) { + RepositoryContentDescriptor content = mock(RepositoryContentDescriptor.class); + Action action = invocation.getArgument(0); + action.execute(content); + this.contents.add(content); + return null; + } + + private Object credentialsAction(InvocationOnMock invocation) { + PasswordCredentials credentials = mock(PasswordCredentials.class); + Action action = invocation.getArgument(0); + action.execute(credentials); + this.credentials.add(credentials); + return null; + } + + interface SpringRepositoriesExtension { + + void mavenRepositories(); + + void mavenRepositories(boolean condition); + + void mavenRepositoriesExcludingBootGroup(); + + static SpringRepositoriesExtension get(RepositoryHandler repositoryHandler, String version, String buildType, + UnaryOperator environment) { + try { + Class extensionClass = supportClass.getClassLoader().loadClass("SpringRepositoriesExtension"); + Object extension = extensionClass + .getDeclaredConstructor(Object.class, Object.class, Object.class, Object.class) + .newInstance(repositoryHandler, version, buildType, environment); + return (SpringRepositoriesExtension) Proxy.newProxyInstance( + SpringRepositoriesExtensionTests.class.getClassLoader(), + new Class[] { SpringRepositoriesExtension.class }, (instance, method, args) -> { + Class[] params = new Class[(args != null) ? args.length : 0]; + Arrays.fill(params, Object.class); + Method groovyMethod = extension.getClass().getDeclaredMethod(method.getName(), params); + return groovyMethod.invoke(extension, args); + }); + } + catch (Exception ex) { + throw new RuntimeException(ex); + } + } + + } + +} diff --git a/git/hooks/forward-merge b/git/hooks/forward-merge index 14872f472746..03d83cbf8aa8 100755 --- a/git/hooks/forward-merge +++ b/git/hooks/forward-merge @@ -65,7 +65,7 @@ def find_milestone(username, password, repository, title) prefix = title.delete_suffix('.x') $log.debug "Finding nearest milestone from candidates starting with #{prefix}" titles = milestones.map { |milestone| milestone['title'] } - titles = titles.select{ |title| title.start_with?(prefix) unless title.end_with?('.x')} + titles = titles.select{ |title| title.start_with?(prefix) unless title.end_with?('.x') || (title.count('.') > 2)} titles = titles.sort_by { |v| Gem::Version.new(v) } $log.debug "Considering candidates #{titles}" if(titles.empty?) diff --git a/gradle.properties b/gradle.properties index 5dd8b0065247..9a7613f2ca93 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=3.3.4-SNAPSHOT +version=3.3.5 latestVersion=false spring.build-type=oss @@ -7,17 +7,20 @@ org.gradle.parallel=true org.gradle.jvmargs=-Xmx2g -Dfile.encoding=UTF-8 assertjVersion=3.25.3 +checkstyleToolVersion=10.12.4 commonsCodecVersion=1.16.1 graalVersion=22.3 hamcrestVersion=2.2 jacksonVersion=2.17.2 -junitJupiterVersion=5.10.3 +javaFormatVersion=0.0.43 +junitJupiterVersion=5.10.5 kotlinVersion=1.9.25 mavenVersion=3.9.4 +mockitoVersion=5.11.0 nativeBuildToolsVersion=0.10.3 -springFrameworkVersion=6.1.13 -springFramework60xVersion=6.0.23 -tomcatVersion=10.1.30 snakeYamlVersion=2.2 +springFrameworkVersion=6.1.14 +springFramework60xVersion=6.0.23 +tomcatVersion=10.1.31 kotlin.stdlib.default.dependency=false diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index d64cd4917707..a4b76b9530d6 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 3e593191a337..df97d72b8b91 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.4-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index 1aa94a426907..f5feea6d6b11 100755 --- a/gradlew +++ b/gradlew @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -55,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -84,7 +86,8 @@ done # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum diff --git a/gradlew.bat b/gradlew.bat index 25da30dbdeee..9b42019c7915 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,92 +1,94 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%"=="" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%"=="" set DIRNAME=. -@rem This is normally unused -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if %ERRORLEVEL% equ 0 goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle index 0eb1c0a52bce..c56db1cf5d85 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,10 +1,9 @@ pluginManagement { + evaluate(new File("${rootDir}/buildSrc/SpringRepositorySupport.groovy")).apply(this) repositories { mavenCentral() gradlePluginPortal() - if (version.endsWith('-SNAPSHOT')) { - maven { url "https://repo.spring.io/snapshot" } - } + spring.mavenRepositories(); } resolutionStrategy { eachPlugin { @@ -19,7 +18,7 @@ pluginManagement { } plugins { - id "io.spring.develocity.conventions" version "0.0.21" + id "io.spring.develocity.conventions" version "0.0.22" } rootProject.name="spring-boot-build" diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle b/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle index 3b4e685872b0..2a0f9500f79d 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle @@ -3,7 +3,6 @@ plugins { id "org.antora" id "org.springframework.boot.auto-configuration" id "org.springframework.boot.configuration-properties" - id "org.springframework.boot.conventions" id "org.springframework.boot.deployed" id "org.springframework.boot.optional-dependencies" } @@ -202,11 +201,13 @@ tasks.named("test") { } def documentationTest = tasks.register("documentationTest", Test) { + testClassesDirs = testing.suites.test.sources.output.classesDirs + classpath = testing.suites.test.sources.runtimeClasspath jvmArgs += "--add-opens=java.base/java.net=ALL-UNNAMED" filter { includeTestsMatching("org.springframework.boot.actuate.autoconfigure.endpoint.web.documentation.*") } - outputs.dir("${buildDir}/generated-snippets") + outputs.dir(layout.buildDirectory.dir("generated-snippets")) develocity { predictiveTestSelection { enabled = false @@ -224,12 +225,12 @@ def antoraActuatorRestApiLocalAggregateContent = tasks.register("antoraActuator def antoraActuatorRestApiAggregateContent = tasks.register("antoraActuatorRestApiAggregateContent", Zip) { dependsOn documentationTest - inputs.dir("${buildDir}/generated-snippets") + inputs.dir(layout.buildDirectory.dir("generated-snippets")) .withPathSensitivity(PathSensitivity.RELATIVE) .withPropertyName("generatedSnippets") destinationDirectory = layout.buildDirectory.dir('generated/docs/antora-content') archiveClassifier = "actuator-rest-api-aggregate-content" - from("${buildDir}/generated-snippets") { + from(layout.buildDirectory.dir("generated-snippets")) { into "modules/api/partials/rest/actuator" } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/condition/OnAvailableEndpointCondition.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/condition/OnAvailableEndpointCondition.java index a485aa2a4a10..24e75a9005db 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/condition/OnAvailableEndpointCondition.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/condition/OnAvailableEndpointCondition.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,7 @@ import java.util.EnumSet; import java.util.HashSet; import java.util.LinkedHashSet; +import java.util.Locale; import java.util.Map; import java.util.Optional; import java.util.Set; @@ -118,7 +119,7 @@ private ConditionOutcome getMatchOutcome(Environment environment, for (ExposureFilter exposureFilter : exposureFilters) { if (exposuresToCheck.contains(exposureFilter.getExposure()) && exposureFilter.isExposed(endpointId)) { return ConditionOutcome.match(message.because("marked as exposed by a 'management.endpoints." - + exposureFilter.getExposure().name().toLowerCase() + ".exposure' property")); + + exposureFilter.getExposure().name().toLowerCase(Locale.ROOT) + ".exposure' property")); } } return ConditionOutcome.noMatch(message.because("no 'management.endpoints' property marked it as exposed")); @@ -187,7 +188,7 @@ private static String getCanonicalName(EndpointExposure exposure) { if (EndpointExposure.CLOUD_FOUNDRY.equals(exposure)) { return "cloud-foundry"; } - return exposure.name().toLowerCase(); + return exposure.name().toLowerCase(Locale.ROOT); } EndpointExposure getExposure() { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/reactive/WebFluxEndpointManagementContextConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/reactive/WebFluxEndpointManagementContextConfiguration.java index 1ba83a7a9b07..d5c3da994bcd 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/reactive/WebFluxEndpointManagementContextConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/reactive/WebFluxEndpointManagementContextConfiguration.java @@ -47,7 +47,6 @@ import org.springframework.boot.actuate.endpoint.web.WebEndpointsSupplier; import org.springframework.boot.actuate.endpoint.web.WebServerNamespace; import org.springframework.boot.actuate.endpoint.web.reactive.AdditionalHealthEndpointPathsWebFluxHandlerMapping; -import org.springframework.boot.actuate.endpoint.web.reactive.ControllerEndpointHandlerMapping; import org.springframework.boot.actuate.endpoint.web.reactive.WebFluxEndpointHandlerMapping; import org.springframework.boot.actuate.health.HealthEndpoint; import org.springframework.boot.actuate.health.HealthEndpointGroups; @@ -129,12 +128,13 @@ public AdditionalHealthEndpointPathsWebFluxHandlerMapping managementHealthEndpoi @Bean @ConditionalOnMissingBean @SuppressWarnings("removal") - public ControllerEndpointHandlerMapping controllerEndpointHandlerMapping( + @Deprecated(since = "3.3.5", forRemoval = true) + public org.springframework.boot.actuate.endpoint.web.reactive.ControllerEndpointHandlerMapping controllerEndpointHandlerMapping( org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpointsSupplier controllerEndpointsSupplier, CorsEndpointProperties corsProperties, WebEndpointProperties webEndpointProperties) { EndpointMapping endpointMapping = new EndpointMapping(webEndpointProperties.getBasePath()); - return new ControllerEndpointHandlerMapping(endpointMapping, controllerEndpointsSupplier.getEndpoints(), - corsProperties.toCorsConfiguration()); + return new org.springframework.boot.actuate.endpoint.web.reactive.ControllerEndpointHandlerMapping( + endpointMapping, controllerEndpointsSupplier.getEndpoints(), corsProperties.toCorsConfiguration()); } @Bean diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/servlet/WebMvcEndpointManagementContextConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/servlet/WebMvcEndpointManagementContextConfiguration.java index ada988dd31c2..2650048e7c90 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/servlet/WebMvcEndpointManagementContextConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/servlet/WebMvcEndpointManagementContextConfiguration.java @@ -43,7 +43,6 @@ import org.springframework.boot.actuate.endpoint.web.WebEndpointsSupplier; import org.springframework.boot.actuate.endpoint.web.WebServerNamespace; import org.springframework.boot.actuate.endpoint.web.servlet.AdditionalHealthEndpointPathsWebMvcHandlerMapping; -import org.springframework.boot.actuate.endpoint.web.servlet.ControllerEndpointHandlerMapping; import org.springframework.boot.actuate.endpoint.web.servlet.WebMvcEndpointHandlerMapping; import org.springframework.boot.actuate.health.HealthEndpoint; import org.springframework.boot.actuate.health.HealthEndpointGroups; @@ -124,12 +123,13 @@ public AdditionalHealthEndpointPathsWebMvcHandlerMapping managementHealthEndpoin @Bean @ConditionalOnMissingBean @SuppressWarnings("removal") - public ControllerEndpointHandlerMapping controllerEndpointHandlerMapping( + @Deprecated(since = "3.3.5", forRemoval = true) + public org.springframework.boot.actuate.endpoint.web.servlet.ControllerEndpointHandlerMapping controllerEndpointHandlerMapping( org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpointsSupplier controllerEndpointsSupplier, CorsEndpointProperties corsProperties, WebEndpointProperties webEndpointProperties) { EndpointMapping endpointMapping = new EndpointMapping(webEndpointProperties.getBasePath()); - return new ControllerEndpointHandlerMapping(endpointMapping, controllerEndpointsSupplier.getEndpoints(), - corsProperties.toCorsConfiguration()); + return new org.springframework.boot.actuate.endpoint.web.servlet.ControllerEndpointHandlerMapping( + endpointMapping, controllerEndpointsSupplier.getEndpoints(), corsProperties.toCorsConfiguration()); } @Bean diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MeterRegistryPostProcessor.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MeterRegistryPostProcessor.java index 06a0789ec6d5..801a65f7e601 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MeterRegistryPostProcessor.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MeterRegistryPostProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -42,7 +42,7 @@ */ class MeterRegistryPostProcessor implements BeanPostProcessor, SmartInitializingSingleton { - private final boolean hasNoCompositeMeterRegistryBeans; + private final CompositeMeterRegistries compositeMeterRegistries; private final ObjectProvider properties; @@ -59,17 +59,13 @@ class MeterRegistryPostProcessor implements BeanPostProcessor, SmartInitializing MeterRegistryPostProcessor(ApplicationContext applicationContext, ObjectProvider metricsProperties, ObjectProvider> customizers, ObjectProvider filters, ObjectProvider binders) { - this(hasNoCompositeMeterRegistryBeans(applicationContext), metricsProperties, customizers, filters, binders); + this(CompositeMeterRegistries.of(applicationContext), metricsProperties, customizers, filters, binders); } - private static boolean hasNoCompositeMeterRegistryBeans(ApplicationContext applicationContext) { - return applicationContext.getBeanNamesForType(CompositeMeterRegistry.class, false, false).length == 0; - } - - MeterRegistryPostProcessor(boolean hasNoCompositeMeterRegistryBeans, ObjectProvider properties, - ObjectProvider> customizers, ObjectProvider filters, - ObjectProvider binders) { - this.hasNoCompositeMeterRegistryBeans = hasNoCompositeMeterRegistryBeans; + MeterRegistryPostProcessor(CompositeMeterRegistries compositeMeterRegistries, + ObjectProvider properties, ObjectProvider> customizers, + ObjectProvider filters, ObjectProvider binders) { + this.compositeMeterRegistries = compositeMeterRegistries; this.properties = properties; this.customizers = customizers; this.filters = filters; @@ -130,11 +126,21 @@ private boolean isGlobalRegistry(MeterRegistry meterRegistry) { } private boolean isBindable(MeterRegistry meterRegistry) { - return this.hasNoCompositeMeterRegistryBeans || isCompositeMeterRegistry(meterRegistry); + return isAutoConfiguredComposite(meterRegistry) || isCompositeWithOnlyUserDefinedComposites(meterRegistry) + || noCompositeMeterRegistries(); + } + + private boolean isAutoConfiguredComposite(MeterRegistry meterRegistry) { + return meterRegistry instanceof AutoConfiguredCompositeMeterRegistry; + } + + private boolean isCompositeWithOnlyUserDefinedComposites(MeterRegistry meterRegistry) { + return this.compositeMeterRegistries == CompositeMeterRegistries.ONLY_USER_DEFINED + && meterRegistry instanceof CompositeMeterRegistry; } - private boolean isCompositeMeterRegistry(MeterRegistry meterRegistry) { - return meterRegistry instanceof CompositeMeterRegistry; + private boolean noCompositeMeterRegistries() { + return this.compositeMeterRegistries == CompositeMeterRegistries.NONE; } void applyBinders(MeterRegistry meterRegistry) { @@ -149,4 +155,21 @@ void applyBinders(MeterRegistry meterRegistry) { this.binders.orderedStream().forEach((binder) -> binder.bindTo(meterRegistry)); } + enum CompositeMeterRegistries { + + NONE, AUTO_CONFIGURED, ONLY_USER_DEFINED; + + private static CompositeMeterRegistries of(ApplicationContext context) { + if (hasBeansOfType(AutoConfiguredCompositeMeterRegistry.class, context)) { + return AUTO_CONFIGURED; + } + return hasBeansOfType(CompositeMeterRegistry.class, context) ? ONLY_USER_DEFINED : NONE; + } + + private static boolean hasBeansOfType(Class type, ApplicationContext context) { + return context.getBeanNamesForType(type, false, false).length > 0; + } + + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpTracingConfigurations.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpTracingConfigurations.java index 492e43792d02..9adac14d696f 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpTracingConfigurations.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpTracingConfigurations.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package org.springframework.boot.actuate.autoconfigure.tracing.otlp; +import java.util.Locale; import java.util.Map.Entry; import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter; @@ -78,7 +79,7 @@ OtlpHttpSpanExporter otlpHttpSpanExporter(OtlpProperties properties, OtlpHttpSpanExporterBuilder builder = OtlpHttpSpanExporter.builder() .setEndpoint(connectionDetails.getUrl()) .setTimeout(properties.getTimeout()) - .setCompression(properties.getCompression().name().toLowerCase()); + .setCompression(properties.getCompression().name().toLowerCase(Locale.ROOT)); for (Entry header : properties.getHeaders().entrySet()) { builder.addHeader(header.getKey(), header.getValue()); } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/MeterRegistryPostProcessorTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/MeterRegistryPostProcessorTests.java index 54d3bbcc4167..4621577027a2 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/MeterRegistryPostProcessorTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/MeterRegistryPostProcessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,8 +17,10 @@ package org.springframework.boot.actuate.autoconfigure.metrics; import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import io.micrometer.core.instrument.Clock; import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.MeterRegistry.Config; import io.micrometer.core.instrument.Metrics; @@ -32,6 +34,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.actuate.autoconfigure.metrics.MeterRegistryPostProcessor.CompositeMeterRegistries; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; @@ -76,21 +79,34 @@ class MeterRegistryPostProcessorTests { } @Test - void postProcessAndInitializeWhenCompositeAppliesCustomizer() { + void postProcessAndInitializeWhenUserDefinedCompositeAppliesCustomizer() { this.customizers.add(this.mockCustomizer); - MeterRegistryPostProcessor processor = new MeterRegistryPostProcessor(false, - createObjectProvider(this.properties), createObjectProvider(this.customizers), - createObjectProvider(this.filters), createObjectProvider(this.binders)); + MeterRegistryPostProcessor processor = new MeterRegistryPostProcessor( + CompositeMeterRegistries.ONLY_USER_DEFINED, createObjectProvider(this.properties), + createObjectProvider(this.customizers), createObjectProvider(this.filters), + createObjectProvider(this.binders)); CompositeMeterRegistry composite = new CompositeMeterRegistry(); postProcessAndInitialize(processor, composite); then(this.mockCustomizer).should().customize(composite); } + @Test + void postProcessAndInitializeWhenAutoConfiguredCompositeAppliesCustomizer() { + this.customizers.add(this.mockCustomizer); + MeterRegistryPostProcessor processor = new MeterRegistryPostProcessor(CompositeMeterRegistries.AUTO_CONFIGURED, + createObjectProvider(this.properties), createObjectProvider(this.customizers), null, + createObjectProvider(this.binders)); + AutoConfiguredCompositeMeterRegistry composite = new AutoConfiguredCompositeMeterRegistry(Clock.SYSTEM, + Collections.emptyList()); + postProcessAndInitialize(processor, composite); + then(this.mockCustomizer).should().customize(composite); + } + @Test void postProcessAndInitializeAppliesCustomizer() { given(this.mockRegistry.config()).willReturn(this.mockConfig); this.customizers.add(this.mockCustomizer); - MeterRegistryPostProcessor processor = new MeterRegistryPostProcessor(true, + MeterRegistryPostProcessor processor = new MeterRegistryPostProcessor(CompositeMeterRegistries.NONE, createObjectProvider(this.properties), createObjectProvider(this.customizers), createObjectProvider(this.filters), createObjectProvider(this.binders)); postProcessAndInitialize(processor, this.mockRegistry); @@ -101,7 +117,7 @@ void postProcessAndInitializeAppliesCustomizer() { void postProcessAndInitializeAppliesFilter() { given(this.mockRegistry.config()).willReturn(this.mockConfig); this.filters.add(this.mockFilter); - MeterRegistryPostProcessor processor = new MeterRegistryPostProcessor(true, + MeterRegistryPostProcessor processor = new MeterRegistryPostProcessor(CompositeMeterRegistries.NONE, createObjectProvider(this.properties), createObjectProvider(this.customizers), createObjectProvider(this.filters), createObjectProvider(this.binders)); postProcessAndInitialize(processor, this.mockRegistry); @@ -112,7 +128,7 @@ void postProcessAndInitializeAppliesFilter() { void postProcessAndInitializeBindsTo() { given(this.mockRegistry.config()).willReturn(this.mockConfig); this.binders.add(this.mockBinder); - MeterRegistryPostProcessor processor = new MeterRegistryPostProcessor(true, + MeterRegistryPostProcessor processor = new MeterRegistryPostProcessor(CompositeMeterRegistries.NONE, createObjectProvider(this.properties), createObjectProvider(this.customizers), createObjectProvider(this.filters), createObjectProvider(this.binders)); postProcessAndInitialize(processor, this.mockRegistry); @@ -120,20 +136,54 @@ void postProcessAndInitializeBindsTo() { } @Test - void postProcessAndInitializeWhenCompositeBindsTo() { + void whenUserDefinedCompositeThenPostProcessAndInitializeCompositeBindsTo() { this.binders.add(this.mockBinder); - MeterRegistryPostProcessor processor = new MeterRegistryPostProcessor(false, - createObjectProvider(this.properties), createObjectProvider(this.customizers), - createObjectProvider(this.filters), createObjectProvider(this.binders)); + MeterRegistryPostProcessor processor = new MeterRegistryPostProcessor( + CompositeMeterRegistries.ONLY_USER_DEFINED, createObjectProvider(this.properties), + createObjectProvider(this.customizers), createObjectProvider(this.filters), + createObjectProvider(this.binders)); CompositeMeterRegistry composite = new CompositeMeterRegistry(); postProcessAndInitialize(processor, composite); then(this.mockBinder).should().bindTo(composite); } @Test - void postProcessAndInitializeWhenCompositeExistsDoesNotBindTo() { + void whenUserDefinedCompositeThenPostProcessAndInitializeStandardRegistryDoesNotBindTo() { + given(this.mockRegistry.config()).willReturn(this.mockConfig); + MeterRegistryPostProcessor processor = new MeterRegistryPostProcessor( + CompositeMeterRegistries.ONLY_USER_DEFINED, createObjectProvider(this.properties), + createObjectProvider(this.customizers), createObjectProvider(this.filters), null); + postProcessAndInitialize(processor, this.mockRegistry); + then(this.mockBinder).shouldHaveNoInteractions(); + } + + @Test + void whenAutoConfiguredCompositeThenPostProcessAndInitializeAutoConfiguredCompositeBindsTo() { + this.binders.add(this.mockBinder); + MeterRegistryPostProcessor processor = new MeterRegistryPostProcessor(CompositeMeterRegistries.AUTO_CONFIGURED, + createObjectProvider(this.properties), createObjectProvider(this.customizers), null, + createObjectProvider(this.binders)); + AutoConfiguredCompositeMeterRegistry composite = new AutoConfiguredCompositeMeterRegistry(Clock.SYSTEM, + Collections.emptyList()); + postProcessAndInitialize(processor, composite); + then(this.mockBinder).should().bindTo(composite); + } + + @Test + void whenAutoConfiguredCompositeThenPostProcessAndInitializeCompositeDoesNotBindTo() { + this.binders.add(this.mockBinder); + MeterRegistryPostProcessor processor = new MeterRegistryPostProcessor(CompositeMeterRegistries.AUTO_CONFIGURED, + createObjectProvider(this.properties), createObjectProvider(this.customizers), + createObjectProvider(this.filters), null); + CompositeMeterRegistry composite = new CompositeMeterRegistry(); + postProcessAndInitialize(processor, composite); + then(this.mockBinder).shouldHaveNoInteractions(); + } + + @Test + void whenAutoConfiguredCompositeThenPostProcessAndInitializeStandardRegistryDoesNotBindTo() { given(this.mockRegistry.config()).willReturn(this.mockConfig); - MeterRegistryPostProcessor processor = new MeterRegistryPostProcessor(false, + MeterRegistryPostProcessor processor = new MeterRegistryPostProcessor(CompositeMeterRegistries.AUTO_CONFIGURED, createObjectProvider(this.properties), createObjectProvider(this.customizers), createObjectProvider(this.filters), null); postProcessAndInitialize(processor, this.mockRegistry); @@ -141,12 +191,12 @@ void postProcessAndInitializeWhenCompositeExistsDoesNotBindTo() { } @Test - void postProcessAndInitializeBeOrderedCustomizerThenFilterThenBindTo() { + void postProcessAndInitializeIsOrderedCustomizerThenFilterThenBindTo() { given(this.mockRegistry.config()).willReturn(this.mockConfig); this.customizers.add(this.mockCustomizer); this.filters.add(this.mockFilter); this.binders.add(this.mockBinder); - MeterRegistryPostProcessor processor = new MeterRegistryPostProcessor(true, + MeterRegistryPostProcessor processor = new MeterRegistryPostProcessor(CompositeMeterRegistries.NONE, createObjectProvider(this.properties), createObjectProvider(this.customizers), createObjectProvider(this.filters), createObjectProvider(this.binders)); postProcessAndInitialize(processor, this.mockRegistry); @@ -160,7 +210,7 @@ void postProcessAndInitializeBeOrderedCustomizerThenFilterThenBindTo() { void postProcessAndInitializeWhenUseGlobalRegistryTrueAddsToGlobalRegistry() { given(this.mockRegistry.config()).willReturn(this.mockConfig); this.properties.setUseGlobalRegistry(true); - MeterRegistryPostProcessor processor = new MeterRegistryPostProcessor(true, + MeterRegistryPostProcessor processor = new MeterRegistryPostProcessor(CompositeMeterRegistries.NONE, createObjectProvider(this.properties), createObjectProvider(this.customizers), createObjectProvider(this.filters), createObjectProvider(this.binders)); try { @@ -175,7 +225,7 @@ void postProcessAndInitializeWhenUseGlobalRegistryTrueAddsToGlobalRegistry() { @Test void postProcessAndInitializeWhenUseGlobalRegistryFalseDoesNotAddToGlobalRegistry() { given(this.mockRegistry.config()).willReturn(this.mockConfig); - MeterRegistryPostProcessor processor = new MeterRegistryPostProcessor(true, + MeterRegistryPostProcessor processor = new MeterRegistryPostProcessor(CompositeMeterRegistries.NONE, createObjectProvider(this.properties), createObjectProvider(this.customizers), createObjectProvider(this.filters), createObjectProvider(this.binders)); postProcessAndInitialize(processor, this.mockRegistry); @@ -186,7 +236,7 @@ void postProcessAndInitializeWhenUseGlobalRegistryFalseDoesNotAddToGlobalRegistr void postProcessDoesNotBindToUntilSingletonsInitialized() { given(this.mockRegistry.config()).willReturn(this.mockConfig); this.binders.add(this.mockBinder); - MeterRegistryPostProcessor processor = new MeterRegistryPostProcessor(true, + MeterRegistryPostProcessor processor = new MeterRegistryPostProcessor(CompositeMeterRegistries.NONE, createObjectProvider(this.properties), createObjectProvider(this.customizers), createObjectProvider(this.filters), createObjectProvider(this.binders)); processor.postProcessAfterInitialization(this.mockRegistry, "meterRegistry"); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinWebClientSenderTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinWebClientSenderTests.java index 9bce4b83d147..71ad42aa5457 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinWebClientSenderTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinWebClientSenderTests.java @@ -51,6 +51,8 @@ @SuppressWarnings({ "deprecation", "removal" }) class ZipkinWebClientSenderTests extends ZipkinHttpSenderTests { + private static final Duration TIMEOUT = Duration.ofSeconds(30); + private static ClearableDispatcher dispatcher; private static MockWebServer mockBackEnd; @@ -81,7 +83,7 @@ void beforeEach() { @Override BytesMessageSender createSender() { - return createSender(Encoding.JSON, Duration.ofSeconds(10)); + return createSender(Encoding.JSON, TIMEOUT); } ZipkinWebClientSender createSender(Encoding encoding, Duration timeout) { @@ -110,7 +112,7 @@ void sendShouldSendSpansToZipkin() throws IOException, InterruptedException { void sendShouldSendSpansToZipkinInProto3() throws IOException, InterruptedException { mockBackEnd.enqueue(new MockResponse()); List encodedSpans = List.of(toByteArray("span1"), toByteArray("span2")); - try (BytesMessageSender sender = createSender(Encoding.PROTO3, Duration.ofSeconds(10))) { + try (BytesMessageSender sender = createSender(Encoding.PROTO3, TIMEOUT)) { sender.send(encodedSpans); } requestAssertions((request) -> { @@ -125,8 +127,7 @@ void sendUsesDynamicEndpoint() throws Exception { mockBackEnd.enqueue(new MockResponse()); mockBackEnd.enqueue(new MockResponse()); try (HttpEndpointSupplier httpEndpointSupplier = new TestHttpEndpointSupplier(ZIPKIN_URL)) { - try (BytesMessageSender sender = createSender((endpoint) -> httpEndpointSupplier, Encoding.JSON, - Duration.ofSeconds(10))) { + try (BytesMessageSender sender = createSender((endpoint) -> httpEndpointSupplier, Encoding.JSON, TIMEOUT)) { sender.send(Collections.emptyList()); sender.send(Collections.emptyList()); } diff --git a/spring-boot-project/spring-boot-actuator/build.gradle b/spring-boot-project/spring-boot-actuator/build.gradle index a5460910c480..1e4a51dcfb09 100644 --- a/spring-boot-project/spring-boot-actuator/build.gradle +++ b/spring-boot-project/spring-boot-actuator/build.gradle @@ -1,6 +1,5 @@ plugins { id "java-library" - id "org.springframework.boot.conventions" id "org.springframework.boot.configuration-properties" id "org.springframework.boot.optional-dependencies" id "org.springframework.boot.docker-test" @@ -11,7 +10,7 @@ description = "Spring Boot Actuator" dependencies { api(project(":spring-boot-project:spring-boot")) - + dockerTestImplementation(project(":spring-boot-project:spring-boot-autoconfigure")) dockerTestImplementation(project(":spring-boot-project:spring-boot-test")) dockerTestImplementation(project(":spring-boot-project:spring-boot-tools:spring-boot-test-support-docker")) diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/reactive/ControllerEndpointHandlerMapping.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/reactive/ControllerEndpointHandlerMapping.java index 834bc98301b7..e36021c59fd7 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/reactive/ControllerEndpointHandlerMapping.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/reactive/ControllerEndpointHandlerMapping.java @@ -41,7 +41,9 @@ * * @author Phillip Webb * @since 2.0.0 + * @deprecated since 3.3.5 in favor of {@code @Endpoint} and {@code @WebEndpoint} support */ +@Deprecated(since = "3.3.5", forRemoval = true) @SuppressWarnings("removal") public class ControllerEndpointHandlerMapping extends RequestMappingHandlerMapping { diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/servlet/ControllerEndpointHandlerMapping.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/servlet/ControllerEndpointHandlerMapping.java index 4921fccce1a8..c6bc83611b8f 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/servlet/ControllerEndpointHandlerMapping.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/servlet/ControllerEndpointHandlerMapping.java @@ -42,7 +42,9 @@ * * @author Phillip Webb * @since 2.0.0 + * @deprecated since 3.3.5 in favor of {@code @Endpoint} and {@code @WebEndpoint} support */ +@Deprecated(since = "3.3.5", forRemoval = true) @SuppressWarnings("removal") public class ControllerEndpointHandlerMapping extends RequestMappingHandlerMapping { diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusScrapeEndpoint.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusScrapeEndpoint.java index 151a00fb50cb..2e795433b5b6 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusScrapeEndpoint.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusScrapeEndpoint.java @@ -69,6 +69,7 @@ public PrometheusScrapeEndpoint(PrometheusRegistry prometheusRegistry) { * @param prometheusRegistry the Prometheus registry to use * @param exporterProperties the properties used to configure Prometheus' * {@link ExpositionFormats} + * @since 3.3.1 */ public PrometheusScrapeEndpoint(PrometheusRegistry prometheusRegistry, Properties exporterProperties) { this.prometheusRegistry = prometheusRegistry; diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/exchanges/HttpExchange.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/exchanges/HttpExchange.java index 375b5fc853ca..061586ba9eaa 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/exchanges/HttpExchange.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/exchanges/HttpExchange.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,6 +25,7 @@ import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.function.Supplier; @@ -434,7 +435,7 @@ private static class HeadersFilter { void excludeUnless(String header, Include exception) { if (!this.includes.contains(exception)) { - this.filteredHeaderNames.add(header.toLowerCase()); + this.filteredHeaderNames.add(header.toLowerCase(Locale.ROOT)); } } @@ -444,7 +445,7 @@ Map> apply(Map> headers) { } Map> filtered = new LinkedHashMap<>(); headers.forEach((name, value) -> { - if (!this.filteredHeaderNames.contains(name.toLowerCase())) { + if (!this.filteredHeaderNames.contains(name.toLowerCase(Locale.ROOT))) { filtered.put(name, value); } }); diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/annotation/DiscoveredOperationMethodTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/annotation/DiscoveredOperationMethodTests.java index 3227e787f401..9c85115b4161 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/annotation/DiscoveredOperationMethodTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/annotation/DiscoveredOperationMethodTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ package org.springframework.boot.actuate.endpoint.annotation; import java.lang.reflect.Method; +import java.util.Locale; import org.junit.jupiter.api.Test; @@ -76,7 +77,7 @@ enum ExampleProducible implements Producible { @Override public MimeType getProducedMimeType() { - return new MimeType(toString().toLowerCase()); + return new MimeType(toString().toLowerCase(Locale.ROOT)); } } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/annotation/DiscoveredOperationsFactoryTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/annotation/DiscoveredOperationsFactoryTests.java index 3fcefeb69f24..e2fb451c8ad6 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/annotation/DiscoveredOperationsFactoryTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/annotation/DiscoveredOperationsFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,7 @@ import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.Locale; import java.util.Map; import org.junit.jupiter.api.BeforeEach; @@ -254,7 +255,7 @@ enum ExampleProducible implements Producible { @Override public MimeType getProducedMimeType() { - return new MimeType(toString().toLowerCase()); + return new MimeType(toString().toLowerCase(Locale.ROOT)); } } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/reactive/ControllerEndpointHandlerMappingIntegrationTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/reactive/ControllerEndpointHandlerMappingIntegrationTests.java index f48587d71846..b140a0997154 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/reactive/ControllerEndpointHandlerMappingIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/reactive/ControllerEndpointHandlerMappingIntegrationTests.java @@ -57,8 +57,10 @@ * * @author Phillip Webb * @author Stephane Nicoll + * @deprecated since 3.3.5 in favor of {@code @Endpoint} and {@code @WebEndpoint} support */ -@SuppressWarnings({ "deprecation", "removal" }) +@SuppressWarnings("removal") +@Deprecated(since = "3.3.5", forRemoval = true) class ControllerEndpointHandlerMappingIntegrationTests { private final ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner( diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/reactive/ControllerEndpointHandlerMappingTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/reactive/ControllerEndpointHandlerMappingTests.java index 6ee8f382544b..ad1e91d5a04d 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/reactive/ControllerEndpointHandlerMappingTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/reactive/ControllerEndpointHandlerMappingTests.java @@ -45,8 +45,10 @@ * * @author Phillip Webb * @author Stephane Nicoll + * @deprecated since 3.3.5 in favor of {@code @Endpoint} and {@code @WebEndpoint} support */ -@SuppressWarnings({ "deprecation", "removal" }) +@Deprecated(since = "3.3.5", forRemoval = true) +@SuppressWarnings("removal") class ControllerEndpointHandlerMappingTests { private final StaticApplicationContext context = new StaticApplicationContext(); diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/servlet/ControllerEndpointHandlerMappingIntegrationTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/servlet/ControllerEndpointHandlerMappingIntegrationTests.java index 6741c77d047d..48d674573ad4 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/servlet/ControllerEndpointHandlerMappingIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/servlet/ControllerEndpointHandlerMappingIntegrationTests.java @@ -56,8 +56,10 @@ * * @author Phillip Webb * @author Stephane Nicoll + * @deprecated since 3.3.5 in favor of {@code @Endpoint} and {@code @WebEndpoint} support */ -@SuppressWarnings({ "deprecation", "removal" }) +@Deprecated(since = "3.3.5", forRemoval = true) +@SuppressWarnings("removal") class ControllerEndpointHandlerMappingIntegrationTests { private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner( diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/servlet/ControllerEndpointHandlerMappingTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/servlet/ControllerEndpointHandlerMappingTests.java index 6296c257678e..8460fa0e2dbf 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/servlet/ControllerEndpointHandlerMappingTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/servlet/ControllerEndpointHandlerMappingTests.java @@ -42,8 +42,10 @@ * * @author Phillip Webb * @author Stephane Nicoll + * @deprecated since 3.3.5 in favor of {@code @Endpoint} and {@code @WebEndpoint} support */ -@SuppressWarnings({ "deprecation", "removal" }) +@Deprecated(since = "3.3.5", forRemoval = true) +@SuppressWarnings("removal") class ControllerEndpointHandlerMappingTests { private final StaticApplicationContext context = new StaticApplicationContext(); diff --git a/spring-boot-project/spring-boot-autoconfigure/build.gradle b/spring-boot-project/spring-boot-autoconfigure/build.gradle index 02f4ab537804..7626f8799d00 100644 --- a/spring-boot-project/spring-boot-autoconfigure/build.gradle +++ b/spring-boot-project/spring-boot-autoconfigure/build.gradle @@ -2,7 +2,6 @@ plugins { id "java-library" id "org.springframework.boot.auto-configuration" id "org.springframework.boot.configuration-properties" - id "org.springframework.boot.conventions" id "org.springframework.boot.deployed" id "org.springframework.boot.docker-test" id "org.springframework.boot.optional-dependencies" diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitStreamConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitStreamConfiguration.java index fa2ec57e7ff2..6e3047d1dd53 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitStreamConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitStreamConfiguration.java @@ -68,9 +68,9 @@ StreamRabbitListenerContainerFactory streamRabbitListenerContainerFactory(Enviro @Bean(name = "rabbitStreamEnvironment") @ConditionalOnMissingBean(name = "rabbitStreamEnvironment") - Environment rabbitStreamEnvironment(RabbitProperties properties, + Environment rabbitStreamEnvironment(RabbitProperties properties, RabbitConnectionDetails connectionDetails, ObjectProvider customizers) { - EnvironmentBuilder builder = configure(Environment.builder(), properties); + EnvironmentBuilder builder = configure(Environment.builder(), properties, connectionDetails); customizers.orderedStream().forEach((customizer) -> customizer.customize(builder)); return builder.build(); } @@ -99,18 +99,29 @@ RabbitStreamTemplate rabbitStreamTemplate(Environment rabbitStreamEnvironment, R return template; } - static EnvironmentBuilder configure(EnvironmentBuilder builder, RabbitProperties properties) { + static EnvironmentBuilder configure(EnvironmentBuilder builder, RabbitProperties properties, + RabbitConnectionDetails connectionDetails) { + return configure(builder, properties.getStream(), connectionDetails); + } + + private static EnvironmentBuilder configure(EnvironmentBuilder builder, RabbitProperties.Stream stream, + RabbitConnectionDetails connectionDetails) { builder.lazyInitialization(true); - RabbitProperties.Stream stream = properties.getStream(); PropertyMapper map = PropertyMapper.get(); map.from(stream.getHost()).to(builder::host); map.from(stream.getPort()).to(builder::port); map.from(stream.getVirtualHost()) - .as(withFallback(properties::getVirtualHost)) + .as(withFallback(connectionDetails::getVirtualHost)) .whenNonNull() .to(builder::virtualHost); - map.from(stream.getUsername()).as(withFallback(properties::getUsername)).whenNonNull().to(builder::username); - map.from(stream.getPassword()).as(withFallback(properties::getPassword)).whenNonNull().to(builder::password); + map.from(stream.getUsername()) + .as(withFallback(connectionDetails::getUsername)) + .whenNonNull() + .to(builder::username); + map.from(stream.getPassword()) + .as(withFallback(connectionDetails::getPassword)) + .whenNonNull() + .to(builder::password); return builder; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionalOnBean.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionalOnBean.java index f136d0b1a0bf..d710455fc652 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionalOnBean.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionalOnBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,6 +24,7 @@ import java.lang.annotation.Target; import org.springframework.beans.factory.BeanFactory; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Conditional; /** @@ -32,8 +33,9 @@ * must be met for the condition to match, but they do not have to be met by the same * bean. *

- * When placed on a {@code @Bean} method, the bean class defaults to the return type of - * the factory method: + * When placed on a {@link Bean @Bean} method and none of {@link #value}, {@link #type}, + * or {@link #name} has been specified, the bean type to match defaults to the return type + * of the {@code @Bean} method: * *

  * @Configuration
diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionalOnMissingBean.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionalOnMissingBean.java
index c08e6200c7bb..0819e92bc39e 100644
--- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionalOnMissingBean.java
+++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionalOnMissingBean.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2012-2021 the original author or authors.
+ * Copyright 2012-2024 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -24,6 +24,7 @@
 import java.lang.annotation.Target;
 
 import org.springframework.beans.factory.BeanFactory;
+import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Conditional;
 
 /**
@@ -32,8 +33,9 @@
  * must be met for the condition to match and the requirements do not have to be met by
  * the same bean.
  * 

- * When placed on a {@code @Bean} method, the bean class defaults to the return type of - * the factory method: + * When placed on a {@link Bean @Bean} method and none of {@link #value}, {@link #type}, + * or {@link #name} has been specified, the bean type to match defaults to the return type + * of the {@code @Bean} method: * *

  * @Configuration
diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceProperties.java
index 29985d01c3f8..03603c56d425 100644
--- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceProperties.java
+++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceProperties.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2012-2023 the original author or authors.
+ * Copyright 2012-2024 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -199,7 +199,6 @@ private boolean driverClassIsLoadable() {
 			throw ex;
 		}
 		catch (Throwable ex) {
-			ex.printStackTrace();
 			return false;
 		}
 	}
diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisConnectionFactoryConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisConnectionFactoryConfiguration.java
index 5f4f879431d6..ab4e1f6939c4 100644
--- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisConnectionFactoryConfiguration.java
+++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisConnectionFactoryConfiguration.java
@@ -57,7 +57,7 @@ ActiveMQConnectionFactory jmsConnectionFactory(ArtemisProperties properties, Lis
 		private static ActiveMQConnectionFactory createJmsConnectionFactory(ArtemisProperties properties,
 				ArtemisConnectionDetails connectionDetails, ListableBeanFactory beanFactory) {
 			return new ArtemisConnectionFactoryFactory(beanFactory, properties, connectionDetails)
-				.createConnectionFactory(ActiveMQConnectionFactory.class);
+				.createConnectionFactory(ActiveMQConnectionFactory::new, ActiveMQConnectionFactory::new);
 		}
 
 		@Configuration(proxyBeanMethods = false)
@@ -93,7 +93,7 @@ JmsPoolConnectionFactory jmsConnectionFactory(ListableBeanFactory beanFactory, A
 				ArtemisConnectionDetails connectionDetails) {
 			ActiveMQConnectionFactory connectionFactory = new ArtemisConnectionFactoryFactory(beanFactory, properties,
 					connectionDetails)
-				.createConnectionFactory(ActiveMQConnectionFactory.class);
+				.createConnectionFactory(ActiveMQConnectionFactory::new, ActiveMQConnectionFactory::new);
 			return new JmsPoolConnectionFactoryFactory(properties.getPool())
 				.createPooledConnectionFactory(connectionFactory);
 		}
diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisConnectionFactoryFactory.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisConnectionFactoryFactory.java
index 70e74e4b3e44..957c17cc4b84 100644
--- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisConnectionFactoryFactory.java
+++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisConnectionFactoryFactory.java
@@ -16,7 +16,7 @@
 
 package org.springframework.boot.autoconfigure.jms.artemis;
 
-import java.lang.reflect.Constructor;
+import java.util.function.Function;
 
 import org.apache.activemq.artemis.api.core.TransportConfiguration;
 import org.apache.activemq.artemis.api.core.client.ActiveMQClient;
@@ -61,10 +61,11 @@ class ArtemisConnectionFactoryFactory {
 		this.connectionDetails = connectionDetails;
 	}
 
-	 T createConnectionFactory(Class factoryClass) {
+	 T createConnectionFactory(Function nativeFactoryCreator,
+			Function embeddedFactoryCreator) {
 		try {
 			startEmbeddedJms();
-			return doCreateConnectionFactory(factoryClass);
+			return doCreateConnectionFactory(nativeFactoryCreator, embeddedFactoryCreator);
 		}
 		catch (Exception ex) {
 			throw new IllegalStateException("Unable to create ActiveMQConnectionFactory", ex);
@@ -84,15 +85,16 @@ private void startEmbeddedJms() {
 		}
 	}
 
-	private  T doCreateConnectionFactory(Class factoryClass) throws Exception {
+	private  T doCreateConnectionFactory(Function nativeFactoryCreator,
+			Function embeddedFactoryCreator) throws Exception {
 		ArtemisMode mode = this.connectionDetails.getMode();
 		if (mode == null) {
 			mode = deduceMode();
 		}
 		if (mode == ArtemisMode.EMBEDDED) {
-			return createEmbeddedConnectionFactory(factoryClass);
+			return createEmbeddedConnectionFactory(embeddedFactoryCreator);
 		}
-		return createNativeConnectionFactory(factoryClass);
+		return createNativeConnectionFactory(nativeFactoryCreator);
 	}
 
 	/**
@@ -115,13 +117,13 @@ private boolean isEmbeddedJmsClassPresent() {
 		return false;
 	}
 
-	private  T createEmbeddedConnectionFactory(Class factoryClass)
-			throws Exception {
+	private  T createEmbeddedConnectionFactory(
+			Function factoryCreator) throws Exception {
 		try {
 			TransportConfiguration transportConfiguration = new TransportConfiguration(
 					InVMConnectorFactory.class.getName(), this.properties.getEmbedded().generateTransportParameters());
-			ServerLocator serviceLocator = ActiveMQClient.createServerLocatorWithoutHA(transportConfiguration);
-			return factoryClass.getConstructor(ServerLocator.class).newInstance(serviceLocator);
+			ServerLocator serverLocator = ActiveMQClient.createServerLocatorWithoutHA(transportConfiguration);
+			return factoryCreator.apply(serverLocator);
 		}
 		catch (NoClassDefFoundError ex) {
 			throw new IllegalStateException("Unable to create InVM "
@@ -129,9 +131,8 @@ private  T createEmbeddedConnectionFactory(
 		}
 	}
 
-	private  T createNativeConnectionFactory(Class factoryClass)
-			throws Exception {
-		T connectionFactory = newNativeConnectionFactory(factoryClass);
+	private  T createNativeConnectionFactory(Function factoryCreator) {
+		T connectionFactory = newNativeConnectionFactory(factoryCreator);
 		String user = this.connectionDetails.getUser();
 		if (StringUtils.hasText(user)) {
 			connectionFactory.setUser(user);
@@ -140,12 +141,10 @@ private  T createNativeConnectionFactory(Cl
 		return connectionFactory;
 	}
 
-	private  T newNativeConnectionFactory(Class factoryClass) throws Exception {
+	private  T newNativeConnectionFactory(Function factoryCreator) {
 		String brokerUrl = StringUtils.hasText(this.connectionDetails.getBrokerUrl())
 				? this.connectionDetails.getBrokerUrl() : DEFAULT_BROKER_URL;
-		Constructor constructor = factoryClass.getConstructor(String.class);
-		return constructor.newInstance(brokerUrl);
-
+		return factoryCreator.apply(brokerUrl);
 	}
 
 }
diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisXAConnectionFactoryConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisXAConnectionFactoryConfiguration.java
index 89e7823ca226..0d71430d595a 100644
--- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisXAConnectionFactoryConfiguration.java
+++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisXAConnectionFactoryConfiguration.java
@@ -47,14 +47,14 @@ ConnectionFactory jmsConnectionFactory(ListableBeanFactory beanFactory, ArtemisP
 			ArtemisConnectionDetails connectionDetails, XAConnectionFactoryWrapper wrapper) throws Exception {
 		return wrapper
 			.wrapConnectionFactory(new ArtemisConnectionFactoryFactory(beanFactory, properties, connectionDetails)
-				.createConnectionFactory(ActiveMQXAConnectionFactory.class));
+				.createConnectionFactory(ActiveMQXAConnectionFactory::new, ActiveMQXAConnectionFactory::new));
 	}
 
 	@Bean
 	ActiveMQXAConnectionFactory nonXaJmsConnectionFactory(ListableBeanFactory beanFactory, ArtemisProperties properties,
 			ArtemisConnectionDetails connectionDetails) {
 		return new ArtemisConnectionFactoryFactory(beanFactory, properties, connectionDetails)
-			.createConnectionFactory(ActiveMQXAConnectionFactory.class);
+			.createConnectionFactory(ActiveMQXAConnectionFactory::new, ActiveMQXAConnectionFactory::new);
 	}
 
 }
diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jmx/JmxProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jmx/JmxProperties.java
index 371071efb865..57b10a74ff5b 100644
--- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jmx/JmxProperties.java
+++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jmx/JmxProperties.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2012-2023 the original author or authors.
+ * Copyright 2012-2024 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientProperties.java
index 36fa40eeda44..48a30efc8cdc 100644
--- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientProperties.java
+++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientProperties.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2012-2020 the original author or authors.
+ * Copyright 2012-2024 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/server/servlet/OAuth2AuthorizationServerPropertiesMapper.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/server/servlet/OAuth2AuthorizationServerPropertiesMapper.java
index 58083756c537..72bec9c23ecb 100644
--- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/server/servlet/OAuth2AuthorizationServerPropertiesMapper.java
+++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/server/servlet/OAuth2AuthorizationServerPropertiesMapper.java
@@ -18,6 +18,7 @@
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Locale;
 
 import org.springframework.boot.autoconfigure.security.oauth2.server.servlet.OAuth2AuthorizationServerProperties.Client;
 import org.springframework.boot.autoconfigure.security.oauth2.server.servlet.OAuth2AuthorizationServerProperties.Registration;
@@ -124,7 +125,7 @@ private TokenSettings getTokenSettings(Client client, PropertyMapper map) {
 	}
 
 	private JwsAlgorithm jwsAlgorithm(String signingAlgorithm) {
-		String name = signingAlgorithm.toUpperCase();
+		String name = signingAlgorithm.toUpperCase(Locale.ROOT);
 		JwsAlgorithm jwsAlgorithm = SignatureAlgorithm.from(name);
 		if (jwsAlgorithm == null) {
 			jwsAlgorithm = MacAlgorithm.from(name);
@@ -133,7 +134,7 @@ private JwsAlgorithm jwsAlgorithm(String signingAlgorithm) {
 	}
 
 	private SignatureAlgorithm signatureAlgorithm(String signatureAlgorithm) {
-		return SignatureAlgorithm.from(signatureAlgorithm.toUpperCase());
+		return SignatureAlgorithm.from(signatureAlgorithm.toUpperCase(Locale.ROOT));
 	}
 
 }
diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/BundleContentProperty.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/BundleContentProperty.java
index 1a15a67ad3fc..fdb251c4576f 100644
--- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/BundleContentProperty.java
+++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/BundleContentProperty.java
@@ -18,9 +18,9 @@
 
 import java.nio.file.Path;
 
-import org.springframework.boot.io.ApplicationResourceLoader;
 import org.springframework.boot.ssl.pem.PemContent;
 import org.springframework.core.io.Resource;
+import org.springframework.core.io.ResourceLoader;
 import org.springframework.util.Assert;
 import org.springframework.util.StringUtils;
 
@@ -51,9 +51,10 @@ boolean hasValue() {
 		return StringUtils.hasText(this.value);
 	}
 
-	Path toWatchPath() {
+	Path toWatchPath(ResourceLoader resourceLoader) {
 		try {
-			Resource resource = getResource();
+			Assert.state(!isPemContent(), "Value contains PEM content");
+			Resource resource = resourceLoader.getResource(this.value);
 			if (!resource.isFile()) {
 				throw new BundleContentNotWatchableException(this);
 			}
@@ -68,9 +69,4 @@ Path toWatchPath() {
 		}
 	}
 
-	private Resource getResource() {
-		Assert.state(!isPemContent(), "Value contains PEM content");
-		return new ApplicationResourceLoader().getResource(this.value);
-	}
-
 }
diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/PropertiesSslBundle.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/PropertiesSslBundle.java
index 29baa16e69ee..e56ab8901628 100644
--- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/PropertiesSslBundle.java
+++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/PropertiesSslBundle.java
@@ -17,6 +17,7 @@
 package org.springframework.boot.autoconfigure.ssl;
 
 import org.springframework.boot.autoconfigure.ssl.SslBundleProperties.Key;
+import org.springframework.boot.io.ApplicationResourceLoader;
 import org.springframework.boot.ssl.SslBundle;
 import org.springframework.boot.ssl.SslBundleKey;
 import org.springframework.boot.ssl.SslManagerBundle;
@@ -27,6 +28,7 @@
 import org.springframework.boot.ssl.pem.PemSslStore;
 import org.springframework.boot.ssl.pem.PemSslStoreBundle;
 import org.springframework.boot.ssl.pem.PemSslStoreDetails;
+import org.springframework.core.io.ResourceLoader;
 import org.springframework.core.style.ToStringCreator;
 import org.springframework.util.Assert;
 
@@ -97,18 +99,31 @@ public SslManagerBundle getManagers() {
 	 * @return an {@link SslBundle} instance
 	 */
 	public static SslBundle get(PemSslBundleProperties properties) {
-		PemSslStore keyStore = getPemSslStore("keystore", properties.getKeystore());
+		return get(properties, new ApplicationResourceLoader());
+	}
+
+	/**
+	 * Get an {@link SslBundle} for the given {@link PemSslBundleProperties}.
+	 * @param properties the source properties
+	 * @param resourceLoader the resource loader used to load content
+	 * @return an {@link SslBundle} instance
+	 * @since 3.3.5
+	 */
+	public static SslBundle get(PemSslBundleProperties properties, ResourceLoader resourceLoader) {
+		PemSslStore keyStore = getPemSslStore("keystore", properties.getKeystore(), resourceLoader);
 		if (keyStore != null) {
 			keyStore = keyStore.withAlias(properties.getKey().getAlias())
 				.withPassword(properties.getKey().getPassword());
 		}
-		PemSslStore trustStore = getPemSslStore("truststore", properties.getTruststore());
+		PemSslStore trustStore = getPemSslStore("truststore", properties.getTruststore(), resourceLoader);
 		SslStoreBundle storeBundle = new PemSslStoreBundle(keyStore, trustStore);
 		return new PropertiesSslBundle(storeBundle, properties);
 	}
 
-	private static PemSslStore getPemSslStore(String propertyName, PemSslBundleProperties.Store properties) {
-		PemSslStore pemSslStore = PemSslStore.load(asPemSslStoreDetails(properties));
+	private static PemSslStore getPemSslStore(String propertyName, PemSslBundleProperties.Store properties,
+			ResourceLoader resourceLoader) {
+		PemSslStoreDetails details = asPemSslStoreDetails(properties);
+		PemSslStore pemSslStore = PemSslStore.load(details, resourceLoader);
 		if (properties.isVerifyKeys()) {
 			CertificateMatcher certificateMatcher = new CertificateMatcher(pemSslStore.privateKey());
 			Assert.state(certificateMatcher.matchesAny(pemSslStore.certificates()),
@@ -128,14 +143,25 @@ private static PemSslStoreDetails asPemSslStoreDetails(PemSslBundleProperties.St
 	 * @return an {@link SslBundle} instance
 	 */
 	public static SslBundle get(JksSslBundleProperties properties) {
-		SslStoreBundle storeBundle = asSslStoreBundle(properties);
+		return get(properties, new ApplicationResourceLoader());
+	}
+
+	/**
+	 * Get an {@link SslBundle} for the given {@link JksSslBundleProperties}.
+	 * @param properties the source properties
+	 * @param resourceLoader the resource loader used to load content
+	 * @return an {@link SslBundle} instance
+	 * @since 3.3.5
+	 */
+	public static SslBundle get(JksSslBundleProperties properties, ResourceLoader resourceLoader) {
+		SslStoreBundle storeBundle = asSslStoreBundle(properties, resourceLoader);
 		return new PropertiesSslBundle(storeBundle, properties);
 	}
 
-	private static SslStoreBundle asSslStoreBundle(JksSslBundleProperties properties) {
+	private static SslStoreBundle asSslStoreBundle(JksSslBundleProperties properties, ResourceLoader resourceLoader) {
 		JksSslStoreDetails keyStoreDetails = asStoreDetails(properties.getKeystore());
 		JksSslStoreDetails trustStoreDetails = asStoreDetails(properties.getTruststore());
-		return new JksSslStoreBundle(keyStoreDetails, trustStoreDetails);
+		return new JksSslStoreBundle(keyStoreDetails, trustStoreDetails, resourceLoader);
 	}
 
 	private static JksSslStoreDetails asStoreDetails(JksSslBundleProperties.Store properties) {
diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/SslAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/SslAutoConfiguration.java
index 1348f16b37b8..c53356a71e15 100644
--- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/SslAutoConfiguration.java
+++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/SslAutoConfiguration.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2012-2023 the original author or authors.
+ * Copyright 2012-2024 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -21,10 +21,12 @@
 import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
 import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.boot.io.ApplicationResourceLoader;
 import org.springframework.boot.ssl.DefaultSslBundleRegistry;
 import org.springframework.boot.ssl.SslBundleRegistry;
 import org.springframework.boot.ssl.SslBundles;
 import org.springframework.context.annotation.Bean;
+import org.springframework.core.io.ResourceLoader;
 
 /**
  * {@link EnableAutoConfiguration Auto-configuration} for SSL.
@@ -36,9 +38,12 @@
 @EnableConfigurationProperties(SslProperties.class)
 public class SslAutoConfiguration {
 
+	private final ApplicationResourceLoader resourceLoader;
+
 	private final SslProperties sslProperties;
 
-	SslAutoConfiguration(SslProperties sslProperties) {
+	SslAutoConfiguration(ResourceLoader resourceLoader, SslProperties sslProperties) {
+		this.resourceLoader = new ApplicationResourceLoader(resourceLoader.getClassLoader());
 		this.sslProperties = sslProperties;
 	}
 
@@ -49,7 +54,7 @@ FileWatcher fileWatcher() {
 
 	@Bean
 	SslPropertiesBundleRegistrar sslPropertiesSslBundleRegistrar(FileWatcher fileWatcher) {
-		return new SslPropertiesBundleRegistrar(this.sslProperties, fileWatcher);
+		return new SslPropertiesBundleRegistrar(this.sslProperties, fileWatcher, this.resourceLoader);
 	}
 
 	@Bean
diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/SslPropertiesBundleRegistrar.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/SslPropertiesBundleRegistrar.java
index c8f595b0d254..e1cbe6fbc4d6 100644
--- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/SslPropertiesBundleRegistrar.java
+++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/SslPropertiesBundleRegistrar.java
@@ -21,12 +21,14 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.function.BiFunction;
 import java.util.function.Function;
 import java.util.function.Supplier;
 import java.util.stream.Collectors;
 
 import org.springframework.boot.ssl.SslBundle;
 import org.springframework.boot.ssl.SslBundleRegistry;
+import org.springframework.core.io.ResourceLoader;
 
 /**
  * A {@link SslBundleRegistrar} that registers SSL bundles based
@@ -42,9 +44,12 @@ class SslPropertiesBundleRegistrar implements SslBundleRegistrar {
 
 	private final FileWatcher fileWatcher;
 
-	SslPropertiesBundleRegistrar(SslProperties properties, FileWatcher fileWatcher) {
+	private final ResourceLoader resourceLoader;
+
+	SslPropertiesBundleRegistrar(SslProperties properties, FileWatcher fileWatcher, ResourceLoader resourceLoader) {
 		this.properties = properties.getBundle();
 		this.fileWatcher = fileWatcher;
+		this.resourceLoader = resourceLoader;
 	}
 
 	@Override
@@ -54,9 +59,9 @@ public void registerBundles(SslBundleRegistry registry) {
 	}
 
 	private 

void registerBundles(SslBundleRegistry registry, Map properties, - Function bundleFactory, Function, Set> watchedPaths) { + BiFunction bundleFactory, Function, Set> watchedPaths) { properties.forEach((bundleName, bundleProperties) -> { - Supplier bundleSupplier = () -> bundleFactory.apply(bundleProperties); + Supplier bundleSupplier = () -> bundleFactory.apply(bundleProperties, this.resourceLoader); try { registry.registerBundle(bundleName, bundleSupplier.get()); if (bundleProperties.isReloadOnUpdate()) { @@ -106,7 +111,7 @@ private Set watchedPaths(String bundleName, List pr try { return properties.stream() .filter(BundleContentProperty::hasValue) - .map(BundleContentProperty::toWatchPath) + .map((content) -> content.toWatchPath(this.resourceLoader)) .collect(Collectors.toSet()); } catch (BundleContentNotWatchableException ex) { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java index 8647ddae3afb..f1486c636a41 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java @@ -102,7 +102,11 @@ public class ServerProperties { private String serverHeader; /** - * Maximum size of the HTTP request header. + * Maximum size of the HTTP request header. Refer to the documentation for your chosen + * embedded server for details of exactly how this limit is applied. For example, + * Netty applies the limit separately to each individual header in the request whereas + * Tomcat applies the limit to the combined size of the request line and all of the + * header names and values in the request. */ private DataSize maxHttpRequestHeaderSize = DataSize.ofKilobytes(8); @@ -937,7 +941,8 @@ public static class Threads { private int minSpare = 10; /** - * Maximum capacity of the thread pool's backing queue. + * Maximum capacity of the thread pool's backing queue. This setting only has + * an effect if the value is greater than 0. */ private int maxQueueCapacity = 2147483647; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/amqp/RabbitStreamConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/amqp/RabbitStreamConfigurationTests.java index e7d05326cfaa..5ab6f5d4fad6 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/amqp/RabbitStreamConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/amqp/RabbitStreamConfigurationTests.java @@ -17,6 +17,7 @@ package org.springframework.boot.autoconfigure.amqp; import java.time.Duration; +import java.util.List; import com.rabbitmq.stream.BackOffDelayPolicy; import com.rabbitmq.stream.Codec; @@ -124,12 +125,14 @@ void whenCustomMessageListenerContainerFactoryIsDefinedThenAutoConfiguredContain } @Test - void environmentUsesPropertyDefaultsByDefault() { + void environmentUsesConnectionDetailsByDefault() { EnvironmentBuilder builder = mock(EnvironmentBuilder.class); RabbitProperties properties = new RabbitProperties(); - RabbitStreamConfiguration.configure(builder, properties); + RabbitStreamConfiguration.configure(builder, properties, + new TestRabbitConnectionDetails("guest", "guest", "vhost")); then(builder).should().port(5552); then(builder).should().host("localhost"); + then(builder).should().virtualHost("vhost"); then(builder).should().lazyInitialization(true); then(builder).should().username("guest"); then(builder).should().password("guest"); @@ -141,7 +144,8 @@ void whenStreamPortIsSetThenEnvironmentUsesCustomPort() { EnvironmentBuilder builder = mock(EnvironmentBuilder.class); RabbitProperties properties = new RabbitProperties(); properties.getStream().setPort(5553); - RabbitStreamConfiguration.configure(builder, properties); + RabbitStreamConfiguration.configure(builder, properties, + new TestRabbitConnectionDetails("guest", "guest", "vhost")); then(builder).should().port(5553); } @@ -150,7 +154,8 @@ void whenStreamHostIsSetThenEnvironmentUsesCustomHost() { EnvironmentBuilder builder = mock(EnvironmentBuilder.class); RabbitProperties properties = new RabbitProperties(); properties.getStream().setHost("stream.rabbit.example.com"); - RabbitStreamConfiguration.configure(builder, properties); + RabbitStreamConfiguration.configure(builder, properties, + new TestRabbitConnectionDetails("guest", "guest", "vhost")); then(builder).should().host("stream.rabbit.example.com"); } @@ -159,7 +164,8 @@ void whenStreamVirtualHostIsSetThenEnvironmentUsesCustomVirtualHost() { EnvironmentBuilder builder = mock(EnvironmentBuilder.class); RabbitProperties properties = new RabbitProperties(); properties.getStream().setVirtualHost("stream-virtual-host"); - RabbitStreamConfiguration.configure(builder, properties); + RabbitStreamConfiguration.configure(builder, properties, + new TestRabbitConnectionDetails("guest", "guest", "vhost")); then(builder).should().virtualHost("stream-virtual-host"); } @@ -167,20 +173,22 @@ void whenStreamVirtualHostIsSetThenEnvironmentUsesCustomVirtualHost() { void whenStreamVirtualHostIsNotSetButDefaultVirtualHostIsSetThenEnvironmentUsesDefaultVirtualHost() { EnvironmentBuilder builder = mock(EnvironmentBuilder.class); RabbitProperties properties = new RabbitProperties(); - properties.setVirtualHost("default-virtual-host"); - RabbitStreamConfiguration.configure(builder, properties); + properties.setVirtualHost("properties-virtual-host"); + RabbitStreamConfiguration.configure(builder, properties, + new TestRabbitConnectionDetails("guest", "guest", "default-virtual-host")); then(builder).should().virtualHost("default-virtual-host"); } @Test - void whenStreamCredentialsAreNotSetThenEnvironmentUsesRabbitCredentials() { + void whenStreamCredentialsAreNotSetThenEnvironmentUsesConnectionDetailsCredentials() { EnvironmentBuilder builder = mock(EnvironmentBuilder.class); RabbitProperties properties = new RabbitProperties(); properties.setUsername("alice"); properties.setPassword("secret"); - RabbitStreamConfiguration.configure(builder, properties); - then(builder).should().username("alice"); - then(builder).should().password("secret"); + RabbitStreamConfiguration.configure(builder, properties, + new TestRabbitConnectionDetails("bob", "password", "vhost")); + then(builder).should().username("bob"); + then(builder).should().password("password"); } @Test @@ -191,7 +199,8 @@ void whenStreamCredentialsAreSetThenEnvironmentUsesStreamCredentials() { properties.setPassword("secret"); properties.getStream().setUsername("bob"); properties.getStream().setPassword("confidential"); - RabbitStreamConfiguration.configure(builder, properties); + RabbitStreamConfiguration.configure(builder, properties, + new TestRabbitConnectionDetails("charlotte", "hidden", "vhost")); then(builder).should().username("bob"); then(builder).should().password("confidential"); } @@ -345,4 +354,40 @@ EnvironmentBuilderCustomizer customizerB() { } + private static final class TestRabbitConnectionDetails implements RabbitConnectionDetails { + + private final String username; + + private final String password; + + private final String virtualHost; + + private TestRabbitConnectionDetails(String username, String password, String virtualHost) { + this.username = username; + this.password = password; + this.virtualHost = virtualHost; + } + + @Override + public String getUsername() { + return this.username; + } + + @Override + public String getPassword() { + return this.password; + } + + @Override + public String getVirtualHost() { + return this.virtualHost; + } + + @Override + public List

getAddresses() { + throw new UnsupportedOperationException(); + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientPropertiesTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientPropertiesTests.java index 7e12b24b18e8..523f1d1a5245 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientPropertiesTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientPropertiesTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/BundleContentPropertyTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/BundleContentPropertyTests.java index caf822da60e4..b4e6477f002c 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/BundleContentPropertyTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/BundleContentPropertyTests.java @@ -22,9 +22,15 @@ import org.junit.jupiter.api.Test; +import org.springframework.boot.io.ApplicationResourceLoader; +import org.springframework.core.io.ResourceLoader; + import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.spy; /** * Tests for {@link BundleContentProperty}. @@ -72,7 +78,7 @@ void hasValueWhenHasEmptyValueReturnsFalse() { @Test void toWatchPathWhenNotPathThrowsException() { BundleContentProperty property = new BundleContentProperty("name", PEM_TEXT); - assertThatIllegalStateException().isThrownBy(property::toWatchPath) + assertThatIllegalStateException().isThrownBy(() -> property.toWatchPath(new ApplicationResourceLoader())) .withMessage("Unable to convert value of property 'name' to a path"); } @@ -81,13 +87,24 @@ void toWatchPathWhenPathReturnsPath() throws URISyntaxException { URL resource = getClass().getResource("keystore.jks"); Path file = Path.of(resource.toURI()).toAbsolutePath(); BundleContentProperty property = new BundleContentProperty("name", file.toString()); - assertThat(property.toWatchPath()).isEqualTo(file); + assertThat(property.toWatchPath(new ApplicationResourceLoader())).isEqualTo(file); + } + + @Test + void toWatchPathUsesResourceLoader() throws URISyntaxException { + URL resource = getClass().getResource("keystore.jks"); + Path file = Path.of(resource.toURI()).toAbsolutePath(); + BundleContentProperty property = new BundleContentProperty("name", file.toString()); + ResourceLoader resourceLoader = spy(new ApplicationResourceLoader()); + assertThat(property.toWatchPath(resourceLoader)).isEqualTo(file); + then(resourceLoader).should(atLeastOnce()).getResource(file.toString()); } @Test void shouldThrowBundleContentNotWatchableExceptionIfContentIsNotWatchable() { BundleContentProperty property = new BundleContentProperty("name", "https://example.com/"); - assertThatExceptionOfType(BundleContentNotWatchableException.class).isThrownBy(property::toWatchPath) + assertThatExceptionOfType(BundleContentNotWatchableException.class) + .isThrownBy(() -> property.toWatchPath(new ApplicationResourceLoader())) .withMessageContaining("Only 'file:' resources are watchable"); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/PropertiesSslBundleTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/PropertiesSslBundleTests.java index d6b770a3d927..66d1c6d1d667 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/PropertiesSslBundleTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/PropertiesSslBundleTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,10 +25,15 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.ssl.SslBundle; +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.ResourceLoader; import org.springframework.util.function.ThrowingConsumer; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.spy; /** * Tests for {@link PropertiesSslBundle}. @@ -137,6 +142,22 @@ void getWithPemSslBundlePropertiesWhenVerifyKeyStoreWithNoMatchThrowsException() .withMessageContaining("Private key in keystore matches none of the certificates"); } + @Test + void getWithResourceLoader() { + PemSslBundleProperties properties = new PemSslBundleProperties(); + properties.getKeystore().setCertificate("classpath:org/springframework/boot/autoconfigure/ssl/key2-chain.crt"); + properties.getKeystore().setPrivateKey("classpath:org/springframework/boot/autoconfigure/ssl/key2.pem"); + properties.getKeystore().setVerifyKeys(true); + properties.getKey().setAlias("test-alias"); + ResourceLoader resourceLoader = spy(new DefaultResourceLoader()); + SslBundle bundle = PropertiesSslBundle.get(properties, resourceLoader); + assertThat(bundle.getStores().getKeyStore()).satisfies(storeContainingCertAndKey("test-alias")); + then(resourceLoader).should(atLeastOnce()) + .getResource("classpath:org/springframework/boot/autoconfigure/ssl/key2-chain.crt"); + then(resourceLoader).should(atLeastOnce()) + .getResource("classpath:org/springframework/boot/autoconfigure/ssl/key2.pem"); + } + private Consumer storeContainingCertAndKey(String keyAlias) { return ThrowingConsumer.of((keyStore) -> { assertThat(keyStore).isNotNull(); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/SslAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/SslAutoConfigurationTests.java index 06cbc412829b..87e2e0c3d1e6 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/SslAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/SslAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -53,29 +53,22 @@ void sslBundlesCreatedWithNoConfiguration() { @Test void sslBundlesCreatedWithCertificates() { List propertyValues = new ArrayList<>(); + String location = "classpath:org/springframework/boot/autoconfigure/ssl/"; propertyValues.add("spring.ssl.bundle.pem.first.key.alias=alias1"); propertyValues.add("spring.ssl.bundle.pem.first.key.password=secret1"); - propertyValues.add( - "spring.ssl.bundle.pem.first.keystore.certificate=classpath:org/springframework/boot/autoconfigure/ssl/rsa-cert.pem"); - propertyValues.add( - "spring.ssl.bundle.pem.first.keystore.private-key=classpath:org/springframework/boot/autoconfigure/ssl/rsa-key.pem"); + propertyValues.add("spring.ssl.bundle.pem.first.keystore.certificate=" + location + "rsa-cert.pem"); + propertyValues.add("spring.ssl.bundle.pem.first.keystore.private-key=" + location + "rsa-key.pem"); propertyValues.add("spring.ssl.bundle.pem.first.keystore.type=PKCS12"); propertyValues.add("spring.ssl.bundle.pem.first.truststore.type=PKCS12"); - propertyValues.add( - "spring.ssl.bundle.pem.first.truststore.certificate=classpath:org/springframework/boot/autoconfigure/ssl/rsa-cert.pem"); - propertyValues.add( - "spring.ssl.bundle.pem.first.truststore.private-key=classpath:org/springframework/boot/autoconfigure/ssl/rsa-key.pem"); + propertyValues.add("spring.ssl.bundle.pem.first.truststore.certificate=" + location + "rsa-cert.pem"); + propertyValues.add("spring.ssl.bundle.pem.first.truststore.private-key=" + location + "rsa-key.pem"); propertyValues.add("spring.ssl.bundle.pem.second.key.alias=alias2"); propertyValues.add("spring.ssl.bundle.pem.second.key.password=secret2"); - propertyValues.add( - "spring.ssl.bundle.pem.second.keystore.certificate=classpath:org/springframework/boot/autoconfigure/ssl/ed25519-cert.pem"); - propertyValues.add( - "spring.ssl.bundle.pem.second.keystore.private-key=classpath:org/springframework/boot/autoconfigure/ssl/ed25519-key.pem"); + propertyValues.add("spring.ssl.bundle.pem.second.keystore.certificate=" + location + "ed25519-cert.pem"); + propertyValues.add("spring.ssl.bundle.pem.second.keystore.private-key=" + location + "ed25519-key.pem"); propertyValues.add("spring.ssl.bundle.pem.second.keystore.type=PKCS12"); - propertyValues.add( - "spring.ssl.bundle.pem.second.truststore.certificate=classpath:org/springframework/boot/autoconfigure/ssl/ed25519-cert.pem"); - propertyValues.add( - "spring.ssl.bundle.pem.second.truststore.private-key=classpath:org/springframework/boot/autoconfigure/ssl/ed25519-key.pem"); + propertyValues.add("spring.ssl.bundle.pem.second.truststore.certificate=" + location + "ed25519-cert.pem"); + propertyValues.add("spring.ssl.bundle.pem.second.truststore.private-key=" + location + "ed25519-key.pem"); propertyValues.add("spring.ssl.bundle.pem.second.truststore.type=PKCS12"); this.contextRunner.withPropertyValues(propertyValues.toArray(String[]::new)).run((context) -> { assertThat(context).hasSingleBean(SslBundles.class); @@ -102,14 +95,12 @@ void sslBundlesCreatedWithCertificates() { @Test void sslBundlesCreatedWithCustomSslBundle() { List propertyValues = new ArrayList<>(); + String location = "classpath:org/springframework/boot/autoconfigure/ssl/"; propertyValues.add("custom.ssl.key.alias=alias1"); propertyValues.add("custom.ssl.key.password=secret1"); - propertyValues - .add("custom.ssl.keystore.certificate=classpath:org/springframework/boot/autoconfigure/ssl/rsa-cert.pem"); - propertyValues.add( - "custom.ssl.keystore.keystore.private-key=classpath:org/springframework/boot/autoconfigure/ssl/rsa-key.pem"); - propertyValues - .add("custom.ssl.truststore.certificate=classpath:org/springframework/boot/autoconfigure/ssl/rsa-cert.pem"); + propertyValues.add("custom.ssl.keystore.certificate=" + location + "rsa-cert.pem"); + propertyValues.add("custom.ssl.keystore.keystore.private-key=" + location + "rsa-key.pem"); + propertyValues.add("custom.ssl.truststore.certificate=" + location + "rsa-cert.pem"); propertyValues.add("custom.ssl.keystore.type=PKCS12"); propertyValues.add("custom.ssl.truststore.type=PKCS12"); this.contextRunner.withUserConfiguration(CustomSslBundleConfiguration.class) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/SslPropertiesBundleRegistrarTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/SslPropertiesBundleRegistrarTests.java index 759bb474609b..dafabd8801b1 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/SslPropertiesBundleRegistrarTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/SslPropertiesBundleRegistrarTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,7 +23,9 @@ import org.junit.jupiter.api.Test; import org.mockito.Mockito; +import org.springframework.boot.ssl.DefaultSslBundleRegistry; import org.springframework.boot.ssl.SslBundleRegistry; +import org.springframework.core.io.DefaultResourceLoader; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; @@ -31,6 +33,8 @@ import static org.mockito.ArgumentMatchers.assertArg; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; /** @@ -44,6 +48,8 @@ class SslPropertiesBundleRegistrarTests { private FileWatcher fileWatcher; + private DefaultResourceLoader resourceLoader; + private SslProperties properties; private SslBundleRegistry registry; @@ -52,7 +58,8 @@ class SslPropertiesBundleRegistrarTests { void setUp() { this.properties = new SslProperties(); this.fileWatcher = Mockito.mock(FileWatcher.class); - this.registrar = new SslPropertiesBundleRegistrar(this.properties, this.fileWatcher); + this.resourceLoader = spy(new DefaultResourceLoader()); + this.registrar = new SslPropertiesBundleRegistrar(this.properties, this.fileWatcher, this.resourceLoader); this.registry = Mockito.mock(SslBundleRegistry.class); } @@ -85,6 +92,21 @@ void shouldWatchPemBundles() { .watch(assertArg((set) -> pathEndingWith(set, "rsa-cert.pem", "rsa-key.pem")), any()); } + @Test + void shouldUseResourceLoader() { + PemSslBundleProperties pem = new PemSslBundleProperties(); + pem.getTruststore().setCertificate("classpath:org/springframework/boot/autoconfigure/ssl/ed25519-cert.pem"); + pem.getTruststore().setPrivateKey("classpath:org/springframework/boot/autoconfigure/ssl/ed25519-key.pem"); + this.properties.getBundle().getPem().put("bundle1", pem); + DefaultSslBundleRegistry registry = new DefaultSslBundleRegistry(); + this.registrar.registerBundles(registry); + registry.getBundle("bundle1").createSslContext(); + then(this.resourceLoader).should(atLeastOnce()) + .getResource("classpath:org/springframework/boot/autoconfigure/ssl/ed25519-cert.pem"); + then(this.resourceLoader).should(atLeastOnce()) + .getResource("classpath:org/springframework/boot/autoconfigure/ssl/ed25519-key.pem"); + } + @Test void shouldFailIfPemKeystoreCertificateIsEmbedded() { PemSslBundleProperties pem = new PemSslBundleProperties(); diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index d347e64f5d02..40535625bab4 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1,6 +1,5 @@ plugins { id "org.springframework.boot.bom" - id "org.springframework.boot.conventions" id "org.springframework.boot.deployed" } @@ -273,7 +272,7 @@ bom { ] } } - library("CycloneDX Maven Plugin", "2.8.1") { + library("CycloneDX Maven Plugin", "2.8.2") { group("org.cyclonedx") { plugins = [ "cyclonedx-maven-plugin" @@ -423,7 +422,7 @@ bom { ] } } - library("GraphQL Java", "22.1") { + library("GraphQL Java", "22.3") { prohibit { startsWith(["2018-", "2019-", "2020-", "2021-", "230521-"]) because "These are snapshots that we don't want to see" @@ -601,7 +600,7 @@ bom { ] } } - library("Infinispan", "15.0.8.Final") { + library("Infinispan", "15.0.10.Final") { group("org.infinispan") { imports = [ "infinispan-bom" @@ -842,7 +841,7 @@ bom { ] } } - library("Jaybird", "5.0.5.java11") { + library("Jaybird", "5.0.6.java11") { prohibit { endsWith ".java8" because "we use the .java11 version" @@ -878,7 +877,7 @@ bom { releaseNotes("https://github.com/redis/jedis/releases/tag/v{version}") } } - library("Jersey", "3.1.8") { + library("Jersey", "3.1.9") { group("org.glassfish.jersey") { imports = [ "jersey-bom" @@ -889,14 +888,14 @@ bom { releaseNotes("https://github.com/eclipse-ee4j/jersey/releases/tag/{version}") } } - library("Jetty Reactive HTTPClient", "4.0.7") { + library("Jetty Reactive HTTPClient", "4.0.8") { group("org.eclipse.jetty") { modules = [ "jetty-reactive-httpclient" ] } } - library("Jetty", "12.0.13") { + library("Jetty", "12.0.14") { group("org.eclipse.jetty.ee10") { imports = [ "jetty-ee10-bom" @@ -919,7 +918,7 @@ bom { ] } } - library("jOOQ", "3.19.11") { + library("jOOQ", "3.19.14") { group("org.jooq") { modules = [ "jooq", @@ -1122,7 +1121,7 @@ bom { releaseNotes("https://github.com/apache/logging-log4j2/releases/tag/rel%2F{version}") } } - library("Logback", "1.5.8") { + library("Logback", "1.5.11") { group("ch.qos.logback") { modules = [ "logback-classic", @@ -1281,7 +1280,7 @@ bom { ] } } - library("Micrometer", "1.13.4") { + library("Micrometer", "1.13.6") { considerSnapshots() group("io.micrometer") { modules = [ @@ -1301,7 +1300,7 @@ bom { releaseNotes("https://github.com/micrometer-metrics/micrometer/releases/tag/v{version}") } } - library("Micrometer Tracing", "1.3.4") { + library("Micrometer Tracing", "1.3.5") { considerSnapshots() group("io.micrometer") { imports = [ @@ -1316,7 +1315,7 @@ bom { releaseNotes("https://github.com/micrometer-metrics/tracing/releases/tag/v{version}") } } - library("Mockito", "5.11.0") { + library("Mockito", "${mockitoVersion}") { group("org.mockito") { imports = [ "mockito-bom" @@ -1394,7 +1393,7 @@ bom { ] } } - library("Neo4j Java Driver", "5.23.0") { + library("Neo4j Java Driver", "5.25.0") { alignWith { version { from "org.springframework.data:spring-data-neo4j" @@ -1411,7 +1410,7 @@ bom { releaseNotes("https://github.com/neo4j/neo4j-java-driver/releases/tag/{version}") } } - library("Netty", "4.1.113.Final") { + library("Netty", "4.1.114.Final") { group("io.netty") { imports = [ "netty-bom" @@ -1512,7 +1511,7 @@ bom { ] } } - library("Pooled JMS", "3.1.6") { + library("Pooled JMS", "3.1.7") { group("org.messaginghub") { modules = [ "pooled-jms" @@ -1565,7 +1564,7 @@ bom { releaseNotes("https://pulsar.apache.org/release-notes/versioned/pulsar-{version}") } } - library("Pulsar Reactive", "0.5.7") { + library("Pulsar Reactive", "0.5.8") { group("org.apache.pulsar") { modules = [ "pulsar-client-reactive-adapter", @@ -1636,7 +1635,7 @@ bom { ] } } - library("R2DBC Pool", "1.0.1.RELEASE") { + library("R2DBC Pool", "1.0.2.RELEASE") { considerSnapshots() group("io.r2dbc") { modules = [ @@ -1648,7 +1647,7 @@ bom { releaseNotes("https://github.com/r2dbc/r2dbc-pool/releases/tag/v{version}") } } - library("R2DBC Postgresql", "1.0.5.RELEASE") { + library("R2DBC Postgresql", "1.0.7.RELEASE") { considerSnapshots() group("org.postgresql") { modules = [ @@ -1706,7 +1705,7 @@ bom { ] } } - library("Reactor Bom", "2023.0.10") { + library("Reactor Bom", "2023.0.11") { considerSnapshots() calendarName = "Reactor" group("io.projectreactor") { @@ -1920,7 +1919,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-amqp/releases/tag/v{version}") } } - library("Spring Authorization Server", "1.3.2") { + library("Spring Authorization Server", "1.3.3") { considerSnapshots() group("org.springframework.security") { modules = [ @@ -1954,7 +1953,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-batch/releases/tag/v{version}") } } - library("Spring Data Bom", "2024.0.4") { + library("Spring Data Bom", "2024.0.5") { considerSnapshots() calendarName = "Spring Data Release" group("org.springframework.data") { @@ -1985,7 +1984,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-framework/releases/tag/v{version}") } } - library("Spring GraphQL", "1.3.2") { + library("Spring GraphQL", "1.3.3") { considerSnapshots() group("org.springframework.graphql") { modules = [ @@ -2020,7 +2019,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-hateoas/releases/tag/{version}") } } - library("Spring Integration", "6.3.4") { + library("Spring Integration", "6.3.5") { considerSnapshots() group("org.springframework.integration") { imports = [ @@ -2055,7 +2054,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-kafka/releases/tag/v{version}") } } - library("Spring LDAP", "3.2.6") { + library("Spring LDAP", "3.2.7") { considerSnapshots() group("org.springframework.ldap") { modules = [ @@ -2075,7 +2074,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-ldap/releases/tag/{version}") } } - library("Spring Pulsar", "1.1.4") { + library("Spring Pulsar", "1.1.5") { considerSnapshots() group("org.springframework.pulsar") { imports = [ @@ -2092,7 +2091,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-pulsar/releases/tag/v{version}") } } - library("Spring RESTDocs", "3.0.1") { + library("Spring RESTDocs", "3.0.2") { considerSnapshots() group("org.springframework.restdocs") { imports = [ @@ -2109,7 +2108,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-restdocs/releases/tag/v{version}") } } - library("Spring Retry", "2.0.9") { + library("Spring Retry", "2.0.10") { considerSnapshots() group("org.springframework.retry") { modules = [ @@ -2121,7 +2120,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-retry/releases/tag/v{version}") } } - library("Spring Security", "6.3.3") { + library("Spring Security", "6.3.4") { considerSnapshots() group("org.springframework.security") { imports = [ @@ -2138,7 +2137,7 @@ bom { releaseNotes("https://github.com/spring-projects/spring-security/releases/tag/{version}") } } - library("Spring Session", "3.3.2") { + library("Spring Session", "3.3.3") { considerSnapshots() prohibit { startsWith(["Apple-", "Bean-", "Corn-", "Dragonfruit-"]) @@ -2261,6 +2260,10 @@ bom { } } library("Undertow", "2.3.17.Final") { + prohibit { + versionRange "[2.3.18.Final]" + because "it contains a regression (https://issues.redhat.com/browse/UNDERTOW-2512)" + } group("io.undertow") { modules = [ "undertow-core", diff --git a/spring-boot-project/spring-boot-devtools/build.gradle b/spring-boot-project/spring-boot-devtools/build.gradle index b3047fd5d52d..eb4fe1b63737 100644 --- a/spring-boot-project/spring-boot-devtools/build.gradle +++ b/spring-boot-project/spring-boot-devtools/build.gradle @@ -2,7 +2,6 @@ plugins { id "java-library" id "org.springframework.boot.auto-configuration" id "org.springframework.boot.configuration-properties" - id "org.springframework.boot.conventions" id "org.springframework.boot.deployed" id "org.springframework.boot.integration-test" id "org.springframework.boot.optional-dependencies" @@ -85,7 +84,7 @@ dependencies { } task syncIntTestDependencies(type: Sync) { - destinationDir = file("${buildDir}/dependencies") + destinationDir = file(layout.buildDirectory.dir("dependencies")) from { configurations.intTestDependencies } diff --git a/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/livereload/Connection.java b/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/livereload/Connection.java index fc68becc1c1d..8fd60738eb38 100644 --- a/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/livereload/Connection.java +++ b/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/livereload/Connection.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,6 +24,7 @@ import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Base64; +import java.util.Locale; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -81,7 +82,7 @@ class Connection { * @throws Exception in case of errors */ void run() throws Exception { - String lowerCaseHeader = this.header.toLowerCase(); + String lowerCaseHeader = this.header.toLowerCase(Locale.ROOT); if (lowerCaseHeader.contains("upgrade: websocket") && lowerCaseHeader.contains("sec-websocket-version: 13")) { runWebSocket(); } diff --git a/spring-boot-project/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/livereload/LiveReloadServerTests.java b/spring-boot-project/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/livereload/LiveReloadServerTests.java index 2211602fcbc8..ff47af1b2714 100644 --- a/spring-boot-project/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/livereload/LiveReloadServerTests.java +++ b/spring-boot-project/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/livereload/LiveReloadServerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,6 +27,7 @@ import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.concurrent.Callable; @@ -338,7 +339,7 @@ private static class UppercaseWebSocketClientConfigurator extends Configurator { @Override public void beforeRequest(Map> requestHeaders) { Map> uppercaseRequestHeaders = new LinkedHashMap<>(); - requestHeaders.forEach((key, value) -> uppercaseRequestHeaders.put(key.toUpperCase(), value)); + requestHeaders.forEach((key, value) -> uppercaseRequestHeaders.put(key.toUpperCase(Locale.ROOT), value)); requestHeaders.clear(); requestHeaders.putAll(uppercaseRequestHeaders); requestHeaders.putAll(this.headers); diff --git a/spring-boot-project/spring-boot-docker-compose/build.gradle b/spring-boot-project/spring-boot-docker-compose/build.gradle index 95479da392bb..be484f8b8dc8 100644 --- a/spring-boot-project/spring-boot-docker-compose/build.gradle +++ b/spring-boot-project/spring-boot-docker-compose/build.gradle @@ -1,7 +1,6 @@ plugins { id "java-library" id "org.springframework.boot.configuration-properties" - id "org.springframework.boot.conventions" id "org.springframework.boot.deployed" id "org.springframework.boot.docker-test" id "org.springframework.boot.optional-dependencies" @@ -17,7 +16,7 @@ dependencies { dockerTestImplementation("org.awaitility:awaitility") dockerTestImplementation("org.junit.jupiter:junit-jupiter") dockerTestImplementation("org.testcontainers:testcontainers") - + dockerTestRuntimeOnly("com.microsoft.sqlserver:mssql-jdbc") dockerTestRuntimeOnly("com.oracle.database.r2dbc:oracle-r2dbc") dockerTestRuntimeOnly("io.r2dbc:r2dbc-mssql") diff --git a/spring-boot-project/spring-boot-docker-compose/src/dockerTest/java/org/springframework/boot/docker/compose/service/connection/postgres/PostgresJdbcDockerComposeConnectionDetailsFactoryIntegrationTests.java b/spring-boot-project/spring-boot-docker-compose/src/dockerTest/java/org/springframework/boot/docker/compose/service/connection/postgres/PostgresJdbcDockerComposeConnectionDetailsFactoryIntegrationTests.java index 31ae113c413a..398bffbc22a0 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/dockerTest/java/org/springframework/boot/docker/compose/service/connection/postgres/PostgresJdbcDockerComposeConnectionDetailsFactoryIntegrationTests.java +++ b/spring-boot-project/spring-boot-docker-compose/src/dockerTest/java/org/springframework/boot/docker/compose/service/connection/postgres/PostgresJdbcDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -16,8 +16,6 @@ package org.springframework.boot.docker.compose.service.connection.postgres; -import org.junit.jupiter.api.Test; - import org.springframework.boot.autoconfigure.jdbc.JdbcConnectionDetails; import org.springframework.boot.docker.compose.service.connection.test.DockerComposeTest; import org.springframework.boot.testsupport.container.TestImage; @@ -39,7 +37,6 @@ void runCreatesConnectionDetails(JdbcConnectionDetails connectionDetails) { assertConnectionDetails(connectionDetails); } - @Test @DockerComposeTest(composeFile = "postgres-bitnami-compose.yaml", image = TestImage.BITNAMI_POSTGRESQL) void runWithBitnamiImageCreatesConnectionDetails(JdbcConnectionDetails connectionDetails) { assertConnectionDetails(connectionDetails); diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/ProcessRunner.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/ProcessRunner.java index a027bbb1757f..b661ba18d48f 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/ProcessRunner.java +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/ProcessRunner.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,6 +23,7 @@ import java.io.InputStreamReader; import java.io.UncheckedIOException; import java.nio.charset.StandardCharsets; +import java.util.Locale; import java.util.concurrent.CountDownLatch; import java.util.function.Consumer; @@ -42,7 +43,7 @@ class ProcessRunner { private static final String USR_LOCAL_BIN = "/usr/local/bin"; - private static final boolean MAC_OS = System.getProperty("os.name").toLowerCase().contains("mac"); + private static final boolean MAC_OS = System.getProperty("os.name").toLowerCase(Locale.ROOT).contains("mac"); private static final Log logger = LogFactory.getLog(ProcessRunner.class); diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/Regex.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/Regex.java index b6ae27942d44..36bb93dde4cc 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/Regex.java +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/Regex.java @@ -19,17 +19,15 @@ import java.util.regex.Pattern; /** - * Regular Expressions for image names and references based on those found in the Docker - * codebase. + * Regular Expressions for image names and references based on those found in the CNCF + * Distribution Project codebase. * * @author Scott Frederick * @author Phillip Webb - * @see Docker - * grammar reference - * @see Docker grammar - * implementation + * @see OCI + * Image grammar reference + * @see OCI Image + * grammar implementation * @see How * are Docker image names parsed? diff --git a/spring-boot-project/spring-boot-docs/build.gradle b/spring-boot-project/spring-boot-docs/build.gradle index 6883de0b76da..13aa4e263b65 100644 --- a/spring-boot-project/spring-boot-docs/build.gradle +++ b/spring-boot-project/spring-boot-docs/build.gradle @@ -1,7 +1,7 @@ plugins { + id "dev.adamko.dokkatoo-html" id "java" id "org.antora" - id "org.springframework.boot.conventions" id "org.springframework.boot.deployed" id 'org.jetbrains.kotlin.jvm' } @@ -43,7 +43,7 @@ sourcesJar { } plugins.withType(EclipsePlugin) { - extensions.getByType(org.gradle.plugins.ide.eclipse.model.EclipseModel).classpath { classpath -> + eclipse.classpath { classpath -> classpath.plusConfigurations.add(configurations.getByName(sourceSets.main.runtimeClasspathConfigurationName)) } } @@ -211,7 +211,7 @@ task aggregatedJavadoc(type: Javadoc) { dependsOn publishedProjects.javadoc source publishedProjects.javadoc.source classpath = project.files(publishedProjects.javadoc.classpath) - destinationDir = project.file "${buildDir}/docs/javadoc" + destinationDir = project.file(project.layout.buildDirectory.dir("docs/javadoc")) options { author = true docTitle = "Spring Boot ${project.version} API" @@ -244,33 +244,33 @@ task aggregatedJavadoc(type: Javadoc) { task documentTestSlices(type: org.springframework.boot.build.test.autoconfigure.DocumentTestSlices) { testSlices = configurations.testSlices - outputFile = file("${buildDir}/generated/docs/test-auto-configuration/documented-slices.adoc") + outputFile = layout.buildDirectory.file("generated/docs/test-auto-configuration/documented-slices.adoc") } task documentStarters(type: org.springframework.boot.build.starters.DocumentStarters) { - outputDir = file("${buildDir}/generated/docs/using/starters/") + outputDir = layout.buildDirectory.dir("generated/docs/using/starters/") } task documentAutoConfigurationClasses(type: org.springframework.boot.build.autoconfigure.DocumentAutoConfigurationClasses) { autoConfiguration = configurations.autoConfiguration - outputDir = file("${buildDir}/generated/docs/auto-configuration-classes/documented-auto-configuration-classes/") + outputDir = layout.buildDirectory.dir("generated/docs/auto-configuration-classes/documented-auto-configuration-classes/") } task documentDependencyVersionCoordinates(type: org.springframework.boot.build.constraints.DocumentConstrainedVersions) { dependsOn dependencyVersions constrainedVersions.set(providers.provider { dependencyVersions.constrainedVersions }) - outputFile = file("${buildDir}/generated/docs/dependency-versions/documented-coordinates.adoc") + outputFile = layout.buildDirectory.file("generated/docs/dependency-versions/documented-coordinates.adoc") } task documentDependencyVersionProperties(type: org.springframework.boot.build.constraints.DocumentVersionProperties) { dependsOn dependencyVersions versionProperties.set(providers.provider { dependencyVersions.versionProperties}) - outputFile = file("${buildDir}/generated/docs/dependency-versions/documented-properties.adoc") + outputFile = layout.buildDirectory.file("generated/docs/dependency-versions/documented-properties.adoc") } task documentConfigurationProperties(type: org.springframework.boot.build.context.properties.DocumentConfigurationProperties) { configurationPropertyMetadata = configurations.configurationProperties - outputDir = file("${buildDir}/generated/docs/application-properties") + outputDir = layout.buildDirectory.dir("generated/docs/application-properties") } task documentDevtoolsPropertyDefaults(type: org.springframework.boot.build.devtools.DocumentDevtoolsPropertyDefaults) {} @@ -279,7 +279,7 @@ task runRemoteSpringApplicationExample(type: org.springframework.boot.build.docs classpath = configurations.remoteSpringApplicationExample mainClass = "org.springframework.boot.devtools.RemoteSpringApplication" args = ["https://myapp.example.com", "--spring.devtools.remote.secret=secret", "--spring.devtools.livereload.port=0"] - output = file("$buildDir/example-output/remote-spring-application.txt") + output = layout.buildDirectory.file("example-output/remote-spring-application.txt") expectedLogging = "Started RemoteSpringApplication in " applicationJar = "/Users/myuser/.m2/repository/org/springframework/boot/spring-boot-devtools/${project.version}/spring-boot-devtools-${project.version}.jar" normalizeLiveReloadPort() @@ -289,7 +289,7 @@ task runSpringApplicationExample(type: org.springframework.boot.build.docs.Appli classpath = configurations.springApplicationExample + sourceSets.main.output mainClass = "org.springframework.boot.docs.features.logexample.MyApplication" args = ["--server.port=0"] - output = file("$buildDir/example-output/spring-application.txt") + output = layout.buildDirectory.file("example-output/spring-application.txt") expectedLogging = "Started MyApplication in " normalizeTomcatPort() } @@ -298,7 +298,7 @@ task runLoggingFormatExample(type: org.springframework.boot.build.docs.Applicati classpath = configurations.springApplicationExample + sourceSets.main.output mainClass = "org.springframework.boot.docs.features.logexample.MyApplication" args = ["--spring.main.banner-mode=off", "--server.port=0", "--spring.application.name=myapp"] - output = file("$buildDir/example-output/logging-format.txt") + output = layout.buildDirectory.file("example-output/logging-format.txt") expectedLogging = "Started MyApplication in " normalizeTomcatPort() } diff --git a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/ROOT/pages/redirect.adoc b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/ROOT/pages/redirect.adoc index b834abc14b04..884612ffdf8b 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/ROOT/pages/redirect.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/ROOT/pages/redirect.adoc @@ -329,7 +329,6 @@ * xref:gradle-plugin:running.adoc#running-your-application.using-a-test-main-class[gradle-plugin#running-your-application.using-a-test-main-class] * xref:how-to:actuator.adoc#howto.actuator[#howto.actuator] * xref:how-to:actuator.adoc#howto.actuator.change-http-port-or-address[#howto.actuator.change-http-port-or-address] -* xref:how-to:actuator.adoc#howto.actuator.customize-whitelabel-error-page[#howto.actuator.customize-whitelabel-error-page] * xref:how-to:actuator.adoc#howto.actuator.customizing-sanitization[#howto.actuator.customizing-sanitization] * xref:how-to:actuator.adoc#howto.actuator.map-health-indicators-to-metrics[#howto.actuator.map-health-indicators-to-metrics] * xref:how-to:aot.adoc#howto.aot[#howto.aot] @@ -435,6 +434,7 @@ * xref:how-to:spring-mvc.adoc#howto.spring-mvc.customize-jackson-objectmapper[#howto.spring-mvc.customize-jackson-objectmapper] * xref:how-to:spring-mvc.adoc#howto.spring-mvc.customize-responsebody-rendering[#howto.spring-mvc.customize-responsebody-rendering] * xref:how-to:spring-mvc.adoc#howto.spring-mvc.customize-view-resolvers[#howto.spring-mvc.customize-view-resolvers] +* xref:how-to:spring-mvc.adoc#howto.spring-mvc.customize-whitelabel-error-page[#howto.actuator.customize-whitelabel-error-page] * xref:how-to:spring-mvc.adoc#howto.spring-mvc.multipart-file-uploads[#howto.spring-mvc.multipart-file-uploads] * xref:how-to:spring-mvc.adoc#howto.spring-mvc.switch-off-default-configuration[#howto.spring-mvc.switch-off-default-configuration] * xref:how-to:spring-mvc.adoc#howto.spring-mvc.switch-off-dispatcherservlet[#howto.spring-mvc.switch-off-dispatcherservlet] diff --git a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/ROOT/pages/system-requirements.adoc b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/ROOT/pages/system-requirements.adoc index 935610d1dc92..018699285531 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/ROOT/pages/system-requirements.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/ROOT/pages/system-requirements.adoc @@ -1,7 +1,7 @@ [[getting-started.system-requirements]] = System Requirements -Spring Boot {version-spring-boot} requires https://www.java.com[Java 17] and is compatible up to and including Java 22. +Spring Boot {version-spring-boot} requires at least https://www.java.com[Java 17] and is compatible with versions up to and including Java 23. {url-spring-framework-docs}/[Spring Framework {version-spring-framework}] or above is also required. Explicit build support is provided for the following build tools: @@ -26,7 +26,7 @@ Spring Boot supports the following embedded servlet containers: |=== | Name | Servlet Version -| Tomcat 10.1 +| Tomcat 10.1 (10.1.25 or later) | 6.0 | Jetty 12.0 diff --git a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/appendix/pages/application-properties/index.adoc b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/appendix/pages/application-properties/index.adoc index 7bbdb5c3576d..21cc3c5acb76 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/appendix/pages/application-properties/index.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/appendix/pages/application-properties/index.adoc @@ -11,39 +11,38 @@ Make sure to review xref:reference:features/external-config.adoc#features.extern NOTE: Property contributions can come from additional jar files on your classpath, so you should not consider this an exhaustive list. Also, you can define your own properties. - -include::partial$configuration-properties/core.adoc[] +include::partial$configuration-properties/actuator.adoc[] include::partial$configuration-properties/cache.adoc[] -include::partial$configuration-properties/mail.adoc[] +include::partial$configuration-properties/core.adoc[] -include::partial$configuration-properties/json.adoc[] +include::partial$configuration-properties/data-migration.adoc[] include::partial$configuration-properties/data.adoc[] -include::partial$configuration-properties/transaction.adoc[] +include::partial$configuration-properties/devtools.adoc[] -include::partial$configuration-properties/data-migration.adoc[] +include::partial$configuration-properties/docker-compose.adoc[] include::partial$configuration-properties/integration.adoc[] -include::partial$configuration-properties/web.adoc[] - -include::partial$configuration-properties/templating.adoc[] - -include::partial$configuration-properties/server.adoc[] +include::partial$configuration-properties/json.adoc[] -include::partial$configuration-properties/security.adoc[] +include::partial$configuration-properties/mail.adoc[] include::partial$configuration-properties/rsocket.adoc[] -include::partial$configuration-properties/actuator.adoc[] +include::partial$configuration-properties/security.adoc[] -include::partial$configuration-properties/devtools.adoc[] +include::partial$configuration-properties/server.adoc[] -include::partial$configuration-properties/docker-compose.adoc[] +include::partial$configuration-properties/templating.adoc[] include::partial$configuration-properties/testcontainers.adoc[] include::partial$configuration-properties/testing.adoc[] + +include::partial$configuration-properties/transaction.adoc[] + +include::partial$configuration-properties/web.adoc[] diff --git a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/how-to/pages/actuator.adoc b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/how-to/pages/actuator.adoc index fb0ca5db2c4e..a1c91e76b4d7 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/how-to/pages/actuator.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/how-to/pages/actuator.adoc @@ -17,26 +17,6 @@ For more detail, see the javadoc:org.springframework.boot.actuate.autoconfigure. -[[howto.actuator.customize-whitelabel-error-page]] -== Customize the '`whitelabel`' Error Page - -Spring Boot installs a '`whitelabel`' error page that you see in a browser client if you encounter a server error (machine clients consuming JSON and other media types should see a sensible response with the right error code). - -NOTE: Set `server.error.whitelabel.enabled=false` to switch the default error page off. -Doing so restores the default of the servlet container that you are using. -Note that Spring Boot still tries to resolve the error view, so you should probably add your own error page rather than disabling it completely. - -Overriding the error page with your own depends on the templating technology that you use. -For example, if you use Thymeleaf, you can add an `error.html` template. -If you use FreeMarker, you can add an `error.ftlh` template. -In general, you need a `View` that resolves with a name of `error` or a `@Controller` that handles the `/error` path. -Unless you replaced some of the default configuration, you should find a `BeanNameViewResolver` in your `ApplicationContext`, so a `@Bean` named `error` would be one way of doing that. -See {code-spring-boot-autoconfigure-src}/web/servlet/error/ErrorMvcAutoConfiguration.java[`ErrorMvcAutoConfiguration`] for more options. - -See also the section on xref:reference:web/servlet.adoc#web.servlet.spring-mvc.error-handling[] for details of how to register handlers in the servlet container. - - - [[howto.actuator.customizing-sanitization]] == Customizing Sanitization diff --git a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/how-to/pages/build.adoc b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/how-to/pages/build.adoc index 49c67dd550fe..a1f69de53832 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/how-to/pages/build.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/how-to/pages/build.adoc @@ -87,6 +87,37 @@ Using this format lets the time be parsed into a `Date` and its format, when ser +[[howto.build.generate-cyclonedx-sbom]] +== Generate a CycloneDX SBOM + +Both Maven and Gradle allow generating a CycloneDX SBOM at project build time. + +For Maven users, the `spring-boot-starter-parent` POM includes a pre-configured plugin to generate the SBOM. +To use it, add the following declaration for the {url-cyclonedx-docs-maven-plugin}[`cyclonedx-maven-plugin`] to your POM: + +[source,xml] +---- + + + + org.cyclonedx + cyclonedx-maven-plugin + + + +---- + +Gradle users can achieve the same result by using the {url-cyclonedx-docs-gradle-plugin}[`cyclonedx-gradle-plugin`] plugin, as shown in the following example: + +[source,gradle] +---- +plugins { + id 'org.cyclonedx.bom' version '1.8.2' +} +---- + + + [[howto.build.customize-dependency-versions]] == Customize Dependency Versions diff --git a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/how-to/pages/deployment/installing.adoc b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/how-to/pages/deployment/installing.adoc index 92bcf1b3a79d..303db28c9a70 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/how-to/pages/deployment/installing.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/how-to/pages/deployment/installing.adoc @@ -24,17 +24,16 @@ After=syslog.target network.target User=myapp Group=myapp -Environment="JAVA_HOME=/path/to/java/home" - -ExecStart=${JAVA_HOME}/bin/java -jar /var/myapp/myapp.jar -ExecStop=/bin/kill -15 $MAINPID +Type=exec +ExecStart=/path/to/java/home/bin/java -jar /var/myapp/myapp.jar +WorkingDirectory=/var/myapp SuccessExitStatus=143 [Install] WantedBy=multi-user.target ---- -IMPORTANT: Remember to change the `Description`, `User`, `Group`, `Environment` and `ExecStart` fields for your application. +IMPORTANT: Remember to change the `Description`, `User`, `Group`, `ExecStart` and `WorkingDirectory` fields for your application. NOTE: The `ExecStart` field does not declare the script action command, which means that the `run` command is used by default. diff --git a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/how-to/pages/deployment/traditional-deployment.adoc b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/how-to/pages/deployment/traditional-deployment.adoc index f7b4c2e651e2..c25ae6348820 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/how-to/pages/deployment/traditional-deployment.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/how-to/pages/deployment/traditional-deployment.adoc @@ -74,7 +74,6 @@ This means that, in addition to being deployable to a servlet container, you can To convert an existing non-web Spring application to a Spring Boot application, replace the code that creates your `ApplicationContext` and replace it with calls to `SpringApplication` or `SpringApplicationBuilder`. Spring MVC web applications are generally amenable to first creating a deployable war application and then migrating it later to an executable war or jar. -See the https://spring.io/guides/gs/convert-jar-to-war/[Getting Started Guide on Converting a jar to a war]. To create a deployable war by extending `SpringBootServletInitializer` (for example, in a class called `Application`) and adding the Spring Boot `@SpringBootApplication` annotation, use code similar to that shown in the following example: diff --git a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/how-to/pages/spring-mvc.adoc b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/how-to/pages/spring-mvc.adoc index 077fbf97035c..979dcfd13ef2 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/how-to/pages/spring-mvc.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/how-to/pages/spring-mvc.adoc @@ -118,7 +118,8 @@ Such customizer beans can be ordered (Boot's own customizer has an order of 0), Any beans of type `com.fasterxml.jackson.databind.Module` are automatically registered with the auto-configured `Jackson2ObjectMapperBuilder` and are applied to any `ObjectMapper` instances that it creates. This provides a global mechanism for contributing custom modules when you add new features to your application. -If you want to replace the default `ObjectMapper` completely, either define a `@Bean` of that type and mark it as `@Primary` or, if you prefer the builder-based approach, define a `Jackson2ObjectMapperBuilder` `@Bean`. +If you want to replace the default `ObjectMapper` completely, either define a `@Bean` of that type or, if you prefer the builder-based approach, define a `Jackson2ObjectMapperBuilder` `@Bean`. +When defining an `ObjectMapper` bean, marking it as `@Primary` is recommended as the auto-configuration's `ObjectMapper` that it will replace is `@Primary`. Note that, in either case, doing so disables all auto-configuration of the `ObjectMapper`. If you provide any `@Beans` of type `MappingJackson2HttpMessageConverter`, they replace the default value in the MVC configuration. @@ -240,3 +241,23 @@ For more detail, see the following sections: * {code-spring-boot-autoconfigure-src}/thymeleaf/ThymeleafAutoConfiguration.java[`ThymeleafAutoConfiguration`] * {code-spring-boot-autoconfigure-src}/freemarker/FreeMarkerAutoConfiguration.java[`FreeMarkerAutoConfiguration`] * {code-spring-boot-autoconfigure-src}/groovy/template/GroovyTemplateAutoConfiguration.java[`GroovyTemplateAutoConfiguration`] + + + +[[howto.spring-mvc.customize-whitelabel-error-page]] +== Customize the '`whitelabel`' Error Page + +Spring Boot installs a '`whitelabel`' error page that you see in a browser client if you encounter a server error (machine clients consuming JSON and other media types should see a sensible response with the right error code). + +NOTE: Set `server.error.whitelabel.enabled=false` to switch the default error page off. +Doing so restores the default of the servlet container that you are using. +Note that Spring Boot still tries to resolve the error view, so you should probably add your own error page rather than disabling it completely. + +Overriding the error page with your own depends on the templating technology that you use. +For example, if you use Thymeleaf, you can add an `error.html` template. +If you use FreeMarker, you can add an `error.ftlh` template. +In general, you need a `View` that resolves with a name of `error` or a `@Controller` that handles the `/error` path. +Unless you replaced some of the default configuration, you should find a `BeanNameViewResolver` in your `ApplicationContext`, so a `@Bean` named `error` would be one way of doing that. +See {code-spring-boot-autoconfigure-src}/web/servlet/error/ErrorMvcAutoConfiguration.java[`ErrorMvcAutoConfiguration`] for more options. + +See also the section on xref:reference:web/servlet.adoc#web.servlet.spring-mvc.error-handling[] for details of how to register handlers in the servlet container. diff --git a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/endpoints.adoc b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/endpoints.adoc index f045bc106cc3..29431b99bcb4 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/endpoints.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/endpoints.adoc @@ -298,9 +298,9 @@ Values can only be viewed in an unsanitized form when: The `show-values` property can be configured for sanitizable endpoints to one of the following values: -- `NEVER` - values are always fully sanitized (replaced by `+******+`) -- `ALWAYS` - values are shown to all users (as long as no `SanitizingFunction` bean applies) -- `WHEN_AUTHORIZED` - values are shown only to authorized users (as long as no `SanitizingFunction` bean applies) +- `never` - values are always fully sanitized (replaced by `+******+`) +- `always` - values are shown to all users (as long as no `SanitizingFunction` bean applies) +- `when-authorized` - values are shown only to authorized users (as long as no `SanitizingFunction` bean applies) For HTTP endpoints, a user is considered to be authorized if they have authenticated and have the roles configured by the endpoint's roles property. By default, any authenticated user is authorized. @@ -315,7 +315,7 @@ Unauthorized users, or users without the `admin` role, will see only sanitized v management: endpoint: env: - show-values: WHEN_AUTHORIZED + show-values: when-authorized roles: "admin" ---- @@ -1256,33 +1256,10 @@ If you reach the `info` endpoint, you should see a response that contains the fo The `sbom` endpoint exposes the https://en.wikipedia.org/wiki/Software_supply_chain[Software Bill of Materials]. CycloneDX SBOMs can be auto-detected, but other formats can be manually configured, too. -The `spring-boot-starter-parent` Maven parent and the Spring Boot Gradle plugin configure the https://github.com/CycloneDX/cyclonedx-maven-plugin[CycloneDX Maven plugin] and the https://github.com/CycloneDX/cyclonedx-gradle-plugin[CycloneDX Gradle plugin] respectively. - -To get a CycloneDX SBOM, you'll need to add this to your Maven build: - -[source,xml] ----- - - - - org.cyclonedx - cyclonedx-maven-plugin - - - ----- - -For Gradle, you'll need to apply the CycloneDX Gradle plugin: - -[source,groovy] ----- -plugins { - id 'org.cyclonedx.bom' version '1.8.2' -} ----- - The `sbom` actuator endpoint will then expose an SBOM called "application", which describes the contents of your application. +TIP: To automatically generate a CycloneDX SBOM at project build time, please see the xref:how-to:build.adoc#howto.build.generate-cyclonedx-sbom[] section. + [[actuator.endpoints.sbom.other-formats]] diff --git a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/metrics.adoc b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/metrics.adoc index ccdf3c9a9faa..624eabe6a3e7 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/metrics.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/metrics.adoc @@ -1126,7 +1126,7 @@ Metrics for Jetty's `Connector` instances are bound by using Micrometer's `Jetty === @Timed Annotation Support To enable scanning of `@Timed` annotations, you will need to set the configprop:management.observations.annotations.enabled[] property to `true`. -Please refer to the {url-micrometer-docs-concepts}#_the_timed_annotation[Micrometer documentation]. +Please refer to the {url-micrometer-docs-concepts}/timers.html#_the_timed_annotation[Micrometer documentation]. @@ -1236,7 +1236,7 @@ configurable buffer length. | Publish a cumulative histogram with buckets defined by your service-level objectives. |=== -For more details on the concepts behind `percentiles-histogram`, `percentiles`, and `slo`, see the {url-micrometer-docs-concepts}#_histograms_and_percentiles[Histograms and percentiles] section of the Micrometer documentation. +For more details on the concepts behind `percentiles-histogram`, `percentiles`, and `slo`, see the {url-micrometer-docs-concepts}/histogram-quantiles.html[Histograms and percentiles] section of the Micrometer documentation. diff --git a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/observability.adoc b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/observability.adoc index 9c236aee528d..0d45c53d1e5a 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/observability.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/observability.adoc @@ -105,4 +105,4 @@ The next sections will provide more details about logging, metrics and traces. == Micrometer Observation Annotations support To enable scanning of metrics and tracing annotations like `@Timed`, `@Counted`, `@MeterTag` and `@NewSpan` annotations, you will need to set the configprop:management.observations.annotations.enabled[] property to `true`. -This feature is supported Micrometer directly. Please refer to the {url-micrometer-docs-concepts}#_the_timed_annotation[Micrometer] and {url-micrometer-tracing-docs}/api.html#_aspect_oriented_programming[Micrometer Tracing] reference docs. +This feature is supported Micrometer directly. Please refer to the {url-micrometer-docs-concepts}/timers.html#_the_timed_annotation[Micrometer] and {url-micrometer-tracing-docs}/api.html#_aspect_oriented_programming[Micrometer Tracing] reference docs. diff --git a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/data/nosql.adoc b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/data/nosql.adoc index 108664b49150..0c0c42c5d1f0 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/data/nosql.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/data/nosql.adoc @@ -6,7 +6,7 @@ Spring Data provides additional projects that help you access a variety of NoSQL * {url-spring-data-cassandra-site}[Cassandra] * {url-spring-data-couchbase-site}[Couchbase] * {url-spring-data-elasticsearch-site}[Elasticsearch] -* {url-spring-data-gemfire-site}[GemFire] or {url-spring-data-geode-site}[Geode] +* {url-spring-data-geode-site}[Geode] * {url-spring-data-ldap-site}[LDAP] * {url-spring-data-mongodb-site}[MongoDB] * {url-spring-data-neo4j-site}[Neo4J] @@ -525,7 +525,7 @@ Repositories and entities are found through scanning. By default, the xref:using/auto-configuration.adoc#using.auto-configuration.packages[auto-configuration packages] are scanned. You can customize the locations to look for repositories and entities by using `@EnableCassandraRepositories` and `@EntityScan` respectively. -TIP: For complete details of Spring Data Cassandra, see the https://docs.spring.io/spring-data/cassandra/docs/[reference documentation]. +TIP: For complete details of Spring Data Cassandra, see the {url-spring-data-cassandra-docs}[reference documentation]. @@ -655,7 +655,7 @@ Repositories and documents are found through scanning. By default, the xref:using/auto-configuration.adoc#using.auto-configuration.packages[auto-configuration packages] are scanned. You can customize the locations to look for repositories and documents by using `@EnableLdapRepositories` and `@EntityScan` respectively. -For complete details of Spring Data LDAP, see the https://docs.spring.io/spring-data/ldap/docs/1.0.x/reference/html/[reference documentation]. +TIP: For complete details of Spring Data LDAP, see the {url-spring-data-ldap-docs}[reference documentation]. You can also inject an auto-configured `LdapTemplate` instance as you would with any other Spring Bean, as shown in the following example: diff --git a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/data/sql.adoc b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/data/sql.adoc index 18f31fcf029e..01fc6f98db0b 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/data/sql.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/data/sql.adoc @@ -165,7 +165,7 @@ spring: [[data.sql.jdbc-template]] == Using JdbcTemplate -Spring's `JdbcTemplate` and `NamedParameterJdbcTemplate` classes are auto-configured, and you can `@Autowire` them directly into your own beans, as shown in the following example: +Spring's `JdbcTemplate` and `NamedParameterJdbcTemplate` classes are auto-configured, and you can autowire them directly into your own beans, as shown in the following example: include-code::MyBean[] @@ -528,7 +528,7 @@ If you want to make sure that each context has a separate embedded database, you [[data.sql.r2dbc.using-database-client]] === Using DatabaseClient -A `DatabaseClient` bean is auto-configured, and you can `@Autowire` it directly into your own beans, as shown in the following example: +A `DatabaseClient` bean is auto-configured, and you can autowire it directly into your own beans, as shown in the following example: include-code::MyBean[] diff --git a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/features/external-config.adoc b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/features/external-config.adoc index 12ccfda9daa2..291153e48828 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/features/external-config.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/features/external-config.adoc @@ -960,6 +960,24 @@ For example, the configuration property `my.service[0].other` would use an envir Support for binding from environment variables is applied to the `systemEnvironment` property source and to any additional property source whose name ends with `-systemEnvironment`. + +[[features.external-config.typesafe-configuration-properties.relaxed-binding.maps-from-environment-variables]] +==== Binding Maps From Environment Variables + +When Spring Boot binds an environment variable to a property class, it lowercases the environment variable name before binding. +Most of the time this detail isn't important, except when binding to `Map` properties. + +The keys in the `Map` are always in lowercase, as seen in the following example: + +include-code::MyMapsProperties[] + +When setting `MY_PROPS_VALUES_KEY=value`, the `values` `Map` contains a `{"key"="value"}` entry. + +Only the environment variable *name* is lower-cased, not the value. +When setting `MY_PROPS_VALUES_KEY=VALUE`, the `values` `Map` contains a `{"key"="VALUE"}` entry. + + + [[features.external-config.typesafe-configuration-properties.relaxed-binding.caching]] ==== Caching diff --git a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/features/logging.adoc b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/features/logging.adoc index fc362970f1c0..21982106a94e 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/features/logging.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/features/logging.adoc @@ -217,7 +217,7 @@ It is also possible to set logging levels using environment variables. For example, `LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_WEB=DEBUG` will set `org.springframework.web` to `DEBUG`. NOTE: The above approach will only work for package level logging. -Since relaxed binding always converts environment variables to lowercase, it is not possible to configure logging for an individual class in this way. +Since relaxed binding xref:features/external-config.adoc#features.external-config.typesafe-configuration-properties.relaxed-binding.maps-from-environment-variables[always converts environment variables to lowercase], it is not possible to configure logging for an individual class in this way. If you need to configure logging for a class, you can use xref:features/external-config.adoc#features.external-config.application-json[the `SPRING_APPLICATION_JSON`] variable. diff --git a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/features/ssl.adoc b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/features/ssl.adoc index 98def66e59c3..23fe8531b2f5 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/features/ssl.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/features/ssl.adoc @@ -45,6 +45,8 @@ When used to secure a client-side connection, a `truststore` is typically config See javadoc:org.springframework.boot.autoconfigure.ssl.JksSslBundleProperties[] for the full set of supported properties. +NOTE: If you're using environment variables to configure the bundle, the name of the bundle is xref:features/external-config.adoc#features.external-config.typesafe-configuration-properties.relaxed-binding.maps-from-environment-variables[always converted to lowercase]. + [[features.ssl.pem]] @@ -109,6 +111,8 @@ The following example shows how a truststore certificate can be defined: See javadoc:org.springframework.boot.autoconfigure.ssl.PemSslBundleProperties[] for the full set of supported properties. +NOTE: If you're using environment variables to configure the bundle, the name of the bundle is xref:features/external-config.adoc#features.external-config.typesafe-configuration-properties.relaxed-binding.maps-from-environment-variables[always converted to lowercase]. + [[features.ssl.applying]] diff --git a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/io/caching.adoc b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/io/caching.adoc index d5c86db19f11..888b5718574c 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/io/caching.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/io/caching.adoc @@ -13,7 +13,7 @@ In a nutshell, to add caching to an operation of your service add the relevant a include-code::MyMathService[] This example demonstrates the use of caching on a potentially costly operation. -Before invoking `computePiDecimal`, the abstraction looks for an entry in the `piDecimals` cache that matches the `i` argument. +Before invoking `computePiDecimal`, the abstraction looks for an entry in the `piDecimals` cache that matches the `precision` argument. If an entry is found, the content in the cache is immediately returned to the caller, and the method is not invoked. Otherwise, the method is invoked, and the cache is updated before returning the value. diff --git a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/io/jta.adoc b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/io/jta.adoc index b49f95f94ba4..2edcd9ac0a8f 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/io/jta.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/io/jta.adoc @@ -26,18 +26,18 @@ Spring Boot tries to auto-configure JMS by looking for a `ConnectionFactory` at When using JTA, the primary JMS `ConnectionFactory` bean is XA-aware and participates in distributed transactions. You can inject into your bean without needing to use any `@Qualifier`: -include-code::primary/MyBean[tag=*] +include-code::primary/MyBean[] In some situations, you might want to process certain JMS messages by using a non-XA `ConnectionFactory`. For example, your JMS processing logic might take longer than the XA timeout. If you want to use a non-XA `ConnectionFactory`, you can the `nonXaJmsConnectionFactory` bean: -include-code::nonxa/MyBean[tag=*] +include-code::nonxa/MyBean[] For consistency, the `jmsConnectionFactory` bean is also provided by using the bean alias `xaJmsConnectionFactory`: -include-code::xa/MyBean[tag=*] +include-code::xa/MyBean[] diff --git a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/messaging/amqp.adoc b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/messaging/amqp.adoc index 6dd49a01ee36..41de38e042ba 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/messaging/amqp.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/messaging/amqp.adoc @@ -110,7 +110,7 @@ The following sample component creates a listener endpoint on the `someQueue` qu include-code::MyBean[] -TIP: See javadoc:{url-spring-amqp-javadoc}/org.springframework.amqp.rabbit.annotation.EnableRabbit.html[format=annotation] for more details. +TIP: See javadoc:{url-spring-amqp-javadoc}/org.springframework.amqp.rabbit.annotation.EnableRabbit[format=annotation] for more details. If you need to create more `RabbitListenerContainerFactory` instances or if you want to override the default, Spring Boot provides a `SimpleRabbitListenerContainerFactoryConfigurer` and a `DirectRabbitListenerContainerFactoryConfigurer` that you can use to initialize a `SimpleRabbitListenerContainerFactory` and a `DirectRabbitListenerContainerFactory` with the same settings as the factories used by the auto-configuration. diff --git a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/spring-boot-applications.adoc b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/spring-boot-applications.adoc index eca1e4bf89d2..c8f658ff8c1a 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/spring-boot-applications.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/spring-boot-applications.adoc @@ -169,7 +169,7 @@ If you need to start a full running server, we recommend that you use random por If you use `@SpringBootTest(webEnvironment=WebEnvironment.RANDOM_PORT)`, an available port is picked at random each time your test runs. The `@LocalServerPort` annotation can be used to xref:how-to:webserver.adoc#howto.webserver.discover-port[inject the actual port used] into your test. -For convenience, tests that need to make REST calls to the started server can additionally `@Autowire` a {url-spring-framework-docs}/testing/webtestclient.html[`WebTestClient`], which resolves relative links to the running server and comes with a dedicated API for verifying responses, as shown in the following example: +For convenience, tests that need to make REST calls to the started server can additionally autowire a {url-spring-framework-docs}/testing/webtestclient.html[`WebTestClient`], which resolves relative links to the running server and comes with a dedicated API for verifying responses, as shown in the following example: include-code::MyRandomPortWebTestClientTests[] diff --git a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/web/graceful-shutdown.adoc b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/web/graceful-shutdown.adoc index 8490b8babae5..af5cb5b8366c 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/web/graceful-shutdown.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/web/graceful-shutdown.adoc @@ -14,8 +14,6 @@ TIP: To learn about more the specific method used with your web server, see the Jetty, Reactor Netty, and Tomcat will stop accepting new requests at the network layer. Undertow will accept new connections but respond immediately with a service unavailable (503) response. -NOTE: Graceful shutdown with Tomcat requires Tomcat 9.0.33 or later. - To enable graceful shutdown, configure the configprop:server.shutdown[] property, as shown in the following example: [configprops,yaml] diff --git a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/specification/pages/executable-jar/nested-jars.adoc b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/specification/pages/executable-jar/nested-jars.adoc index e08d8e9f96f5..52410e079d3f 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/specification/pages/executable-jar/nested-jars.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/specification/pages/executable-jar/nested-jars.adoc @@ -123,6 +123,11 @@ The index file would look like this: - "BOOT-INF/lib/dependency1.jar" ---- +NOTE: Spring Boot only uses the classpath index file when the jar or war file is executed with `java -jar`. +It is not used when running the application from the IDE or when using Maven's `spring-boot:run` or Gradle's `bootRun`. + +NOTE: When enabling reproducible builds, the entries in the classpath index file are sorted alphabetically. + [[appendix.executable-jar.nested-jars.layer-index]] diff --git a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/tutorial/pages/first-application/index.adoc b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/tutorial/pages/first-application/index.adoc index a6a26ab0d56e..a9586e407bfb 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/tutorial/pages/first-application/index.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/tutorial/pages/first-application/index.adoc @@ -100,34 +100,61 @@ Open your favorite text editor and add the following: -ifeval::["{artifact-release-type}" != "release"] - - - - spring-snapshots - https://repo.spring.io/snapshot - true - - - spring-milestones - https://repo.spring.io/milestone - - - - - spring-snapshots - https://repo.spring.io/snapshot - - - spring-milestones - https://repo.spring.io/milestone - - +ifeval::["{build-and-artifact-release-type}" == "opensource-milestone"] + + + + spring-milestones + https://repo.spring.io/milestone + + + + + spring-milestones + https://repo.spring.io/milestone + + +endif::[] +ifeval::["{build-and-artifact-release-type}" == "opensource-snapshot"] + + + + spring-snapshots + https://repo.spring.io/snapshot + true + + + spring-milestones + https://repo.spring.io/milestone + + + + + spring-snapshots + https://repo.spring.io/snapshot + + + spring-milestones + https://repo.spring.io/milestone + + endif::[] ---- +ifeval::["{build-type}" == "opensource"] The preceding listing should give you a working build. +endif::[] + +ifeval::["{build-type}" == "commercial"] +You will also have to configure your build to access the Spring Commercial repository. +This is usual done through a local artifact repository that mirrors the content of the Spring Commercial repository. +Alternatively, while it is not recommended, the Spring Commercial repository can also be accessed directly. +In either case, see https://docs.vmware.com/en/Tanzu-Spring-Runtime/Commercial/Tanzu-Spring-Runtime/spring-enterprise-subscription.html[the Tanzu Spring Runtime documentation] for further details. + +With the addition of the necessary repository configuration, the preceding listing should give you a working build. +endif::[] + You can test it by running `mvn package` (for now, you can ignore the "`jar will be empty - no content was marked for inclusion!`" warning). NOTE: At this point, you could import the project into an IDE (most modern Java IDEs include built-in support for Maven). diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/relaxedbinding/mapsfromenvironmentvariables/MyMapsProperties.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/relaxedbinding/mapsfromenvironmentvariables/MyMapsProperties.java new file mode 100644 index 000000000000..7abc11edb290 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/relaxedbinding/mapsfromenvironmentvariables/MyMapsProperties.java @@ -0,0 +1,33 @@ +/* + * Copyright 2012-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.externalconfig.typesafeconfigurationproperties.relaxedbinding.mapsfromenvironmentvariables; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "my.props") +public class MyMapsProperties { + + private final Map values = new HashMap<>(); + + public Map getValues() { + return this.values; + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/jta/mixingxaandnonxaconnections/nonxa/MyBean.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/jta/mixingxaandnonxaconnections/nonxa/MyBean.java index a7d843c9b180..82621580349e 100644 --- a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/jta/mixingxaandnonxaconnections/nonxa/MyBean.java +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/jta/mixingxaandnonxaconnections/nonxa/MyBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,10 +22,8 @@ public class MyBean { - // tag::code[] public MyBean(@Qualifier("nonXaJmsConnectionFactory") ConnectionFactory connectionFactory) { // ... } - // end::code[] } diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/jta/mixingxaandnonxaconnections/primary/MyBean.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/jta/mixingxaandnonxaconnections/primary/MyBean.java index d4d11c7cc3b1..52155ebd5278 100644 --- a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/jta/mixingxaandnonxaconnections/primary/MyBean.java +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/jta/mixingxaandnonxaconnections/primary/MyBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,10 +20,8 @@ public class MyBean { - // tag::code[] public MyBean(ConnectionFactory connectionFactory) { // ... } - // end::code[] } diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/jta/mixingxaandnonxaconnections/xa/MyBean.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/jta/mixingxaandnonxaconnections/xa/MyBean.java index ab5c4f63a9bf..16a3326821d8 100644 --- a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/jta/mixingxaandnonxaconnections/xa/MyBean.java +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/jta/mixingxaandnonxaconnections/xa/MyBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,10 +22,8 @@ public class MyBean { - // tag::code[] public MyBean(@Qualifier("xaJmsConnectionFactory") ConnectionFactory connectionFactory) { // ... } - // end::code[] } diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/kafka/streams/MyKafkaStreamsConfiguration.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/kafka/streams/MyKafkaStreamsConfiguration.java index 23ea0b900871..f659a04dae05 100644 --- a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/kafka/streams/MyKafkaStreamsConfiguration.java +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/kafka/streams/MyKafkaStreamsConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,8 @@ package org.springframework.boot.docs.messaging.kafka.streams; +import java.util.Locale; + import org.apache.kafka.common.serialization.Serdes; import org.apache.kafka.streams.KeyValue; import org.apache.kafka.streams.StreamsBuilder; @@ -39,7 +41,7 @@ public KStream kStream(StreamsBuilder streamsBuilder) { } private KeyValue uppercaseValue(Integer key, String value) { - return new KeyValue<>(key, value.toUpperCase()); + return new KeyValue<>(key, value.toUpperCase(Locale.getDefault())); } } diff --git a/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/relaxedbinding/mapsfromenvironmentvariables/MyMapsProperties.kt b/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/relaxedbinding/mapsfromenvironmentvariables/MyMapsProperties.kt new file mode 100644 index 000000000000..f58251e968cb --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/relaxedbinding/mapsfromenvironmentvariables/MyMapsProperties.kt @@ -0,0 +1,10 @@ +package org.springframework.boot.docs.features.externalconfig.typesafeconfigurationproperties.relaxedbinding.mapsfromenvironmentvariables + +import org.springframework.boot.context.properties.ConfigurationProperties + +@ConfigurationProperties(prefix = "my.props") +class MyMapsProperties { + + val values: Map = HashMap() + +} diff --git a/spring-boot-project/spring-boot-parent/build.gradle b/spring-boot-project/spring-boot-parent/build.gradle index 72c981910180..78ab94e29f7f 100644 --- a/spring-boot-project/spring-boot-parent/build.gradle +++ b/spring-boot-project/spring-boot-parent/build.gradle @@ -1,6 +1,5 @@ plugins { id "org.springframework.boot.bom" - id "org.springframework.boot.conventions" id "org.springframework.boot.deployed" } diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-parent/build.gradle b/spring-boot-project/spring-boot-starters/spring-boot-starter-parent/build.gradle index 168b41e33ed9..4c85421b1404 100644 --- a/spring-boot-project/spring-boot-starters/spring-boot-starter-parent/build.gradle +++ b/spring-boot-project/spring-boot-starters/spring-boot-starter-parent/build.gradle @@ -1,5 +1,4 @@ plugins { - id "org.springframework.boot.conventions" id "org.springframework.boot.deployed" id "org.springframework.boot.maven-repository" } diff --git a/spring-boot-project/spring-boot-test-autoconfigure/build.gradle b/spring-boot-project/spring-boot-test-autoconfigure/build.gradle index fb0710bea9f9..0bd57c63e904 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/build.gradle +++ b/spring-boot-project/spring-boot-test-autoconfigure/build.gradle @@ -1,6 +1,5 @@ plugins { id "java-library" - id "org.springframework.boot.conventions" id "org.springframework.boot.deployed" id "org.springframework.boot.docker-test" id "org.springframework.boot.optional-dependencies" @@ -132,5 +131,5 @@ test { task testSliceMetadata(type: org.springframework.boot.build.test.autoconfigure.TestSliceMetadata) { sourceSet = sourceSets.main - outputFile = file("${buildDir}/test-slice-metadata.properties") + outputFile = layout.buildDirectory.file("test-slice-metadata.properties") } diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/ConditionReportApplicationContextFailureProcessor.java b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/ConditionReportApplicationContextFailureProcessor.java index b75649c16458..66e45638b8b4 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/ConditionReportApplicationContextFailureProcessor.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/ConditionReportApplicationContextFailureProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,7 +29,9 @@ * @author Phillip Webb * @author Scott Frederick * @since 3.0.0 + * @deprecated in 3.2.11 for removal in 3.6.0 */ +@Deprecated(since = "3.2.11", forRemoval = true) public class ConditionReportApplicationContextFailureProcessor implements ApplicationContextFailureProcessor { @Override diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/OnFailureConditionReportContextCustomizerFactory.java b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/OnFailureConditionReportContextCustomizerFactory.java new file mode 100644 index 000000000000..d95ff503887a --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/OnFailureConditionReportContextCustomizerFactory.java @@ -0,0 +1,92 @@ +/* + * Copyright 2012-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.test.autoconfigure; + +import java.util.List; +import java.util.function.Supplier; + +import org.springframework.boot.autoconfigure.condition.ConditionEvaluationReport; +import org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportMessage; +import org.springframework.boot.context.event.ApplicationFailedEvent; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationListener; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.test.context.ContextConfigurationAttributes; +import org.springframework.test.context.ContextCustomizer; +import org.springframework.test.context.ContextCustomizerFactory; +import org.springframework.test.context.MergedContextConfiguration; + +/** + * {@link ContextCustomizerFactory} that customizes the {@link ApplicationContext + * application context} such that a {@link ConditionEvaluationReport condition evaluation + * report} is output when the application under test {@link ApplicationFailedEvent fails + * to start}. + * + * @author Andy Wilkinson + */ +class OnFailureConditionReportContextCustomizerFactory implements ContextCustomizerFactory { + + @Override + public ContextCustomizer createContextCustomizer(Class testClass, + List configAttributes) { + return new OnFailureConditionReportContextCustomizer(); + } + + static class OnFailureConditionReportContextCustomizer implements ContextCustomizer { + + @Override + public void customizeContext(ConfigurableApplicationContext context, MergedContextConfiguration mergedConfig) { + Supplier reportSupplier; + if (context instanceof GenericApplicationContext) { + ConditionEvaluationReport report = ConditionEvaluationReport.get(context.getBeanFactory()); + reportSupplier = () -> report; + } + else { + reportSupplier = () -> ConditionEvaluationReport.get(context.getBeanFactory()); + } + context.addApplicationListener(new ApplicationFailureListener(reportSupplier)); + } + + @Override + public boolean equals(Object obj) { + return (obj != null) && (obj.getClass() == getClass()); + } + + @Override + public int hashCode() { + return getClass().hashCode(); + } + + } + + private static final class ApplicationFailureListener implements ApplicationListener { + + private final Supplier reportSupplier; + + private ApplicationFailureListener(Supplier reportSupplier) { + this.reportSupplier = reportSupplier; + } + + @Override + public void onApplicationEvent(ApplicationFailedEvent event) { + System.err.println(new ConditionEvaluationReportMessage(this.reportSupplier.get())); + } + + } + +} diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/filter/StandardAnnotationCustomizableTypeExcludeFilter.java b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/filter/StandardAnnotationCustomizableTypeExcludeFilter.java index 8a9f0da2cf95..ab407a71f59a 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/filter/StandardAnnotationCustomizableTypeExcludeFilter.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/filter/StandardAnnotationCustomizableTypeExcludeFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ import java.lang.annotation.Annotation; import java.util.Collections; +import java.util.Locale; import java.util.Set; import org.springframework.context.annotation.ComponentScan.Filter; @@ -45,7 +46,7 @@ public abstract class StandardAnnotationCustomizableTypeExcludeFilter new TestContextManager(FailingTests.class).getTestContext().getApplicationContext()); + assertThat(output).contains("JacksonAutoConfiguration matched"); + } + + @SpringBootTest + static class FailingTests { + + @Configuration(proxyBeanMethods = false) + @ImportAutoConfiguration(JacksonAutoConfiguration.class) + static class TestConfig { + + @Bean + String faultyBean() { + throw new IllegalStateException(); + } + + } + + } + +} diff --git a/spring-boot-project/spring-boot-test/build.gradle b/spring-boot-project/spring-boot-test/build.gradle index 93fc006fbb96..3cce752e2f5a 100644 --- a/spring-boot-project/spring-boot-test/build.gradle +++ b/spring-boot-project/spring-boot-test/build.gradle @@ -1,7 +1,7 @@ plugins { + id "dev.adamko.dokkatoo-html" id "java-library" id "org.jetbrains.kotlin.jvm" - id "org.springframework.boot.conventions" id "org.springframework.boot.deployed" id "org.springframework.boot.optional-dependencies" } diff --git a/spring-boot-project/spring-boot-testcontainers/build.gradle b/spring-boot-project/spring-boot-testcontainers/build.gradle index c78397114e96..091fcf0462c4 100644 --- a/spring-boot-project/spring-boot-testcontainers/build.gradle +++ b/spring-boot-project/spring-boot-testcontainers/build.gradle @@ -2,7 +2,6 @@ plugins { id "java-library" id "org.springframework.boot.auto-configuration" id "org.springframework.boot.configuration-properties" - id "org.springframework.boot.conventions" id "org.springframework.boot.deployed" id "org.springframework.boot.docker-test" id "org.springframework.boot.optional-dependencies" diff --git a/spring-boot-project/spring-boot-testcontainers/src/dockerTest/java/org/springframework/boot/testcontainers/service/connection/otlp/OpenTelemetryMetricsContainerConnectionDetailsFactoryIntegrationTests.java b/spring-boot-project/spring-boot-testcontainers/src/dockerTest/java/org/springframework/boot/testcontainers/service/connection/otlp/OpenTelemetryMetricsContainerConnectionDetailsFactoryIntegrationTests.java index c48d6a2135ce..50b558711ebc 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/dockerTest/java/org/springframework/boot/testcontainers/service/connection/otlp/OpenTelemetryMetricsContainerConnectionDetailsFactoryIntegrationTests.java +++ b/spring-boot-project/spring-boot-testcontainers/src/dockerTest/java/org/springframework/boot/testcontainers/service/connection/otlp/OpenTelemetryMetricsContainerConnectionDetailsFactoryIntegrationTests.java @@ -80,24 +80,20 @@ void connectionCanBeMadeToOpenTelemetryCollectorContainer() { Timer.builder("test.timer").register(this.meterRegistry).record(Duration.ofMillis(123)); DistributionSummary.builder("test.distributionsummary").register(this.meterRegistry).record(24); Awaitility.await() - .atMost(Duration.ofSeconds(5)) - .pollDelay(Duration.ofMillis(100)) - .pollInterval(Duration.ofMillis(100)) + .atMost(Duration.ofSeconds(30)) .untilAsserted(() -> whenPrometheusScraped().then() .statusCode(200) .contentType(OPENMETRICS_001) - .body(endsWith("# EOF\n"), containsString("service_name"))); - whenPrometheusScraped().then() - .body(containsString( - "{job=\"test\",service_name=\"test\",telemetry_sdk_language=\"java\",telemetry_sdk_name=\"io.micrometer\""), - matchesPattern("(?s)^.*test_counter\\{.+} 42\\.0\\n.*$"), - matchesPattern("(?s)^.*test_gauge\\{.+} 12\\.0\\n.*$"), - matchesPattern("(?s)^.*test_timer_count\\{.+} 1\\n.*$"), - matchesPattern("(?s)^.*test_timer_sum\\{.+} 123\\.0\\n.*$"), - matchesPattern("(?s)^.*test_timer_bucket\\{.+,le=\"\\+Inf\"} 1\\n.*$"), - matchesPattern("(?s)^.*test_distributionsummary_count\\{.+} 1\\n.*$"), - matchesPattern("(?s)^.*test_distributionsummary_sum\\{.+} 24\\.0\\n.*$"), - matchesPattern("(?s)^.*test_distributionsummary_bucket\\{.+,le=\"\\+Inf\"} 1\\n.*$")); + .body(endsWith("# EOF\n"), containsString( + "{job=\"test\",service_name=\"test\",telemetry_sdk_language=\"java\",telemetry_sdk_name=\"io.micrometer\""), + matchesPattern("(?s)^.*test_counter\\{.+} 42\\.0\\n.*$"), + matchesPattern("(?s)^.*test_gauge\\{.+} 12\\.0\\n.*$"), + matchesPattern("(?s)^.*test_timer_count\\{.+} 1\\n.*$"), + matchesPattern("(?s)^.*test_timer_sum\\{.+} 123\\.0\\n.*$"), + matchesPattern("(?s)^.*test_timer_bucket\\{.+,le=\"\\+Inf\"} 1\\n.*$"), + matchesPattern("(?s)^.*test_distributionsummary_count\\{.+} 1\\n.*$"), + matchesPattern("(?s)^.*test_distributionsummary_sum\\{.+} 24\\.0\\n.*$"), + matchesPattern("(?s)^.*test_distributionsummary_bucket\\{.+,le=\"\\+Inf\"} 1\\n.*$"))); } private Response whenPrometheusScraped() { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-antlib/build.gradle b/spring-boot-project/spring-boot-tools/spring-boot-antlib/build.gradle index 750604b01944..61dde9509f61 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-antlib/build.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-antlib/build.gradle @@ -1,6 +1,5 @@ plugins { id "java-library" - id "org.springframework.boot.conventions" id "org.springframework.boot.deployed" } @@ -26,9 +25,10 @@ dependencies { implementation("org.springframework:spring-core") } -task copyIntegrationTestSources(type: Copy) { +task syncIntegrationTestSources(type: Sync) { + destinationDir file(layout.buildDirectory.dir("it")) from file("src/it") - into "${buildDir}/it" + filter(springRepositoryTransformers.ant()) } processResources { @@ -39,8 +39,8 @@ processResources { } task integrationTest { - dependsOn copyIntegrationTestSources, jar - def resultsDir = file("${buildDir}/test-results/integrationTest") + dependsOn syncIntegrationTestSources, jar + def resultsDir = file(layout.buildDirectory.dir("test-results/integrationTest")) inputs.dir(file("src/it")).withPathSensitivity(PathSensitivity.RELATIVE).withPropertyName("source") inputs.files(sourceSets.main.runtimeClasspath).withNormalizer(ClasspathNormalizer).withPropertyName("classpath") outputs.dirs resultsDir @@ -62,9 +62,9 @@ task integrationTest { ant.propertyref(name: "ivy.class.path") } plainlistener() - file("${buildDir}/test-results/integrationTest").mkdirs() + file(layout.buildDirectory.dir("test-results/integrationTest")).mkdirs() xmllistener(toDir: resultsDir) - fileset(dir: "${buildDir}/it", includes: "**/build.xml") + fileset(dir: layout.buildDirectory.dir("it").get().asFile.toString(), includes: "**/build.xml") } } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-antlib/src/it/sample/ivysettings.xml b/spring-boot-project/spring-boot-tools/spring-boot-antlib/src/it/sample/ivysettings.xml index 67038fba9074..2d04a1ad21bd 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-antlib/src/it/sample/ivysettings.xml +++ b/spring-boot-project/spring-boot-tools/spring-boot-antlib/src/it/sample/ivysettings.xml @@ -8,8 +8,8 @@ - - + + \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-autoconfigure-processor/build.gradle b/spring-boot-project/spring-boot-tools/spring-boot-autoconfigure-processor/build.gradle index d9d8630ee5b3..b7dee1febf4b 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-autoconfigure-processor/build.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-autoconfigure-processor/build.gradle @@ -1,6 +1,5 @@ plugins { id "java-library" - id "org.springframework.boot.conventions" id "org.springframework.boot.deployed" id "org.springframework.boot.annotation-processor" } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/build.gradle b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/build.gradle index 20f33dca4cd7..43e107f410d2 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/build.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/build.gradle @@ -1,6 +1,5 @@ plugins { id "java-library" - id "org.springframework.boot.conventions" id "org.springframework.boot.deployed" id "org.springframework.boot.docker-test" } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/ImageReference.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/ImageReference.java index 983524827c55..c3c263e89bf8 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/ImageReference.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/ImageReference.java @@ -187,7 +187,7 @@ public ImageReference inTaggedOrDigestForm() { */ public static ImageReference forJarFile(File jarFile) { String filename = jarFile.getName(); - Assert.isTrue(filename.toLowerCase().endsWith(".jar"), () -> "File '" + jarFile + "' is not a JAR"); + Assert.isTrue(filename.toLowerCase(Locale.ROOT).endsWith(".jar"), () -> "File '" + jarFile + "' is not a JAR"); filename = filename.substring(0, filename.length() - 4); int firstDot = filename.indexOf('.'); if (firstDot == -1) { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cli/build.gradle b/spring-boot-project/spring-boot-tools/spring-boot-cli/build.gradle index bf42c6816213..312efac14343 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-cli/build.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-cli/build.gradle @@ -2,7 +2,6 @@ plugins { id "java" id "eclipse" id "org.springframework.boot.deployed" - id "org.springframework.boot.conventions" id "org.springframework.boot.integration-test" } @@ -82,11 +81,11 @@ def configureArchive(archive) { into "lib/" } archive.from(file("src/main/content")) { - dirMode = 0755 - fileMode = 0644 + dirPermissions { unix(0755) } + filePermissions { unix(0644) } } archive.from(file("src/main/executablecontent")) { - fileMode = 0755 + filePermissions { unix(0755) } } } @@ -107,12 +106,12 @@ task tar(type: Tar) { task homebrewFormula(type: org.springframework.boot.build.cli.HomebrewFormula) { dependsOn tar - outputDir = file("${buildDir}/homebrew") + outputDir = layout.buildDirectory.dir("homebrew") template = file("src/main/homebrew/spring-boot.rb") archive = tar.archiveFile } -def homebrewFormulaArtifact = artifacts.add("archives", file("${buildDir}/homebrew/spring-boot.rb")) { +def homebrewFormulaArtifact = artifacts.add("archives", file(layout.buildDirectory.file("homebrew/spring-boot.rb"))) { type "rb" classifier "homebrew" builtBy "homebrewFormula" diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cli/src/intTest/resources/settings.xml b/spring-boot-project/spring-boot-tools/spring-boot-cli/src/intTest/resources/settings.xml index b85b5c25ffd5..4e7332c0f77d 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-cli/src/intTest/resources/settings.xml +++ b/spring-boot-project/spring-boot-tools/spring-boot-cli/src/intTest/resources/settings.xml @@ -1,33 +1,34 @@ - ../../../../build/local-m2-repository - - - cli-test-repo - - true - - - - local.central - file:../../../../build/test-repository - - true - - - true - - - - thymeleaf-snapshot - https://oss.sonatype.org/content/repositories/snapshots - - true - - - true - - - - - + ../../../../build/local-m2-repository + + + + cli-test-repo + + true + + + + local.central + file:../../../../build/test-repository + + true + + + true + + + + thymeleaf-snapshot + https://oss.sonatype.org/content/repositories/snapshots + + true + + + true + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cli/src/test/resources/cli-tester/.m2/settings.xml b/spring-boot-project/spring-boot-tools/spring-boot-cli/src/test/resources/cli-tester/.m2/settings.xml deleted file mode 100644 index 504eb80a0555..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-cli/src/test/resources/cli-tester/.m2/settings.xml +++ /dev/null @@ -1,44 +0,0 @@ - - - build/local-m2-repository - - - cli-test-repo - - true - - - - local.central - file:build/test-repository - - true - - - true - - - - spring-snapshot - https://repo.spring.io/snapshot - - false - - - true - - - - spring-milestone - https://repo.spring.io/milestone - - true - - - false - - - - - - diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/build.gradle b/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/build.gradle index 186a2cff85a1..0041da9fad46 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/build.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/build.gradle @@ -1,6 +1,5 @@ plugins { id "java" - id "org.springframework.boot.conventions" } description = "Spring Boot Configuration Metadata Changelog Generator" diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata/build.gradle b/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata/build.gradle index 7b8fbb193c68..70ad03b867e9 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata/build.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata/build.gradle @@ -1,6 +1,5 @@ plugins { id "java-library" - id "org.springframework.boot.conventions" id "org.springframework.boot.deployed" } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/build.gradle b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/build.gradle index a7bde3691ba8..240281c2650e 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/build.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/build.gradle @@ -1,6 +1,5 @@ plugins { id "java-library" - id "org.springframework.boot.conventions" id "org.springframework.boot.deployed" id "org.springframework.boot.annotation-processor" } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/build.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/build.gradle index cb06c6e23a27..eb3cc5d8ec73 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/build.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/build.gradle @@ -6,7 +6,6 @@ plugins { id "java-gradle-plugin" id "maven-publish" id "org.antora" - id "org.springframework.boot.conventions" id "org.springframework.boot.docker-test" id "org.springframework.boot.maven-repository" id "org.springframework.boot.optional-dependencies" diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/examples/getting-started/apply-plugin-commercial.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/examples/getting-started/apply-plugin-commercial.gradle new file mode 100644 index 000000000000..eea03ac0f688 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/examples/getting-started/apply-plugin-commercial.gradle @@ -0,0 +1,3 @@ +plugins { + id 'org.springframework.boot' version '{gradle-project-version}' +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/examples/getting-started/apply-plugin-commercial.gradle.kts b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/examples/getting-started/apply-plugin-commercial.gradle.kts new file mode 100644 index 000000000000..fead5b05c83c --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/examples/getting-started/apply-plugin-commercial.gradle.kts @@ -0,0 +1,3 @@ +plugins { + id("org.springframework.boot") version "{gradle-project-version}" +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/examples/managing-dependencies/depend-on-plugin-commercial.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/examples/managing-dependencies/depend-on-plugin-commercial.gradle new file mode 100644 index 000000000000..88fba72d152b --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/examples/managing-dependencies/depend-on-plugin-commercial.gradle @@ -0,0 +1,3 @@ +plugins { + id 'org.springframework.boot' version '{gradle-project-version}' apply false +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/examples/managing-dependencies/depend-on-plugin-commercial.gradle.kts b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/examples/managing-dependencies/depend-on-plugin-commercial.gradle.kts new file mode 100644 index 000000000000..5bebec31c3f8 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/examples/managing-dependencies/depend-on-plugin-commercial.gradle.kts @@ -0,0 +1,3 @@ +plugins { + id("org.springframework.boot") version "{gradle-project-version}" apply false +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/pages/getting-started.adoc b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/pages/getting-started.adoc index bc55314e63f0..5e923ee0d1e2 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/pages/getting-started.adoc +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/pages/getting-started.adoc @@ -3,7 +3,34 @@ To get started with the plugin it needs to be applied to your project. -ifeval::["{artifact-release-type}" == "release"] +ifeval::["{build-type}" == "commercial"] +The plugin is published to the Spring Commercial repository. +You will have to configure your build to access this repository. +This is usual done through a local artifact repository that mirrors the content of the Spring Commercial repository. +Alternatively, while it is not recommended, the Spring Commercial repository can also be accessed directly. +In either case, see https://docs.vmware.com/en/Tanzu-Spring-Runtime/Commercial/Tanzu-Spring-Runtime/spring-enterprise-subscription.html[the Tanzu Spring Runtime documentation] for further details. + +With access to the Spring Commercial repository configured in `settings.gradle` or `settings.gradle.kts`, the plugin can be applied using the `plugins` block: + +[tabs] +====== +Groovy:: ++ +[source,groovy,indent=0,subs="verbatim,attributes"] +---- +include::example$getting-started/apply-plugin-commercial.gradle[] +---- +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,attributes"] +---- +include::example$getting-started/apply-plugin-commercial.gradle.kts[] +---- +====== +endif::[] + + +ifeval::["{build-and-artifact-release-type}" == "opensource-release"] The plugin is https://plugins.gradle.org/plugin/org.springframework.boot[published to Gradle's plugin portal] and can be applied using the `plugins` block: [tabs] @@ -23,7 +50,7 @@ include::example$getting-started/apply-plugin-release.gradle.kts[] ====== endif::[] -ifeval::["{artifact-release-type}" == "milestone"] +ifeval::["{build-and-artifact-release-type}" == "opensource-milestone"] The plugin is published to the Spring milestones repository. Gradle can be configured to use the milestones repository and the plugin can then be applied using the `plugins` block. To configure Gradle to use the milestones repository, add the following to your `settings.gradle` (Groovy) or `settings.gradle.kts` (Kotlin): @@ -63,7 +90,7 @@ include::example$getting-started/apply-plugin-release.gradle.kts[] ====== endif::[] -ifeval::["{artifact-release-type}" == "snapshot"] +ifeval::["{build-and-artifact-release-type}" == "opensource-snapshot"] The plugin is published to the Spring snapshots repository. Gradle can be configured to use the snapshots repository and the plugin can then be applied using the `plugins` block. To configure Gradle to use the snapshots repository, add the following to your `settings.gradle` (Groovy) or `settings.gradle.kts` (Kotlin): diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/pages/managing-dependencies.adoc b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/pages/managing-dependencies.adoc index d0c9cf033a46..1955207a68bd 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/pages/managing-dependencies.adoc +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/pages/managing-dependencies.adoc @@ -70,7 +70,25 @@ The `SpringBootPlugin` class provides a `BOM_COORDINATES` constant that can be u First, configure the project to depend on the Spring Boot plugin but do not apply it: -ifeval::["{artifact-release-type}" == "release"] +ifeval::["{build-type}" == "commercial"] +[tabs] +====== +Groovy:: ++ +[source,groovy,indent=0,subs="verbatim,attributes"] +---- +include::example$managing-dependencies/depend-on-plugin-commercial.gradle[] +---- +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,attributes"] +---- +include::example$managing-dependencies/depend-on-plugin-commercial.gradle.kts[] +---- +====== +endif::[] + +ifeval::["{build-and-artifact-release-type}" == "opensource-release"] [tabs] ====== Groovy:: @@ -88,7 +106,7 @@ include::example$managing-dependencies/depend-on-plugin-release.gradle.kts[] ====== endif::[] -ifeval::["{artifact-release-type}" == "milestone"] +ifeval::["{build-and-artifact-release-type}" == "opensource-milestone"] [tabs] ====== Groovy:: @@ -106,7 +124,7 @@ include::example$managing-dependencies/depend-on-plugin-release.gradle.kts[] ====== endif::[] -ifeval::["{artifact-release-type}" == "snapshot"] +ifeval::["{build-and-artifact-release-type}" == "opensource-snapshot"] [tabs] ====== Groovy:: diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/pages/reacting.adoc b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/pages/reacting.adoc index f283e27cb700..ddf0b0ed51a4 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/pages/reacting.adoc +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/pages/reacting.adoc @@ -92,3 +92,12 @@ When the {url-native-build-tools-docs-gradle-plugin}[GraalVM Native Image plugin . Configures the `bootBuildImage` task to use `paketobuildpacks/builder-jammy-tiny:latest` as its builder and to set `BP_NATIVE_IMAGE` to `true` in its environment. + +[[reacting-to-other-plugins.cyclonedx]] +== Reacting to the CycloneDX Plugin + +When the {url-cyclonedx-docs-gradle-plugin}[CycloneDX plugin] is applied to a project, the Spring Boot plugin: + +. Configures the `cyclonedxBom` task to use the `application` project type and output the SBOM to the `application.cdx` file in JSON format without full license texts. +. Adds the SBOM under `META-INF/sbom` in the generated jar or war file. +. Adds the `Sbom-Format` and `Sbom-Location` to the manifest of the jar or war file. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/ApplicationPluginAction.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/ApplicationPluginAction.java index f87b6fe5fce9..b767735ed3be 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/ApplicationPluginAction.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/ApplicationPluginAction.java @@ -19,10 +19,8 @@ import java.io.IOException; import java.io.InputStreamReader; import java.io.StringWriter; -import java.lang.reflect.Method; import java.util.concurrent.Callable; -import org.gradle.api.Action; import org.gradle.api.GradleException; import org.gradle.api.Plugin; import org.gradle.api.Project; @@ -128,32 +126,16 @@ private String loadResource(String name) { private void configureFilePermissions(CopySpec copySpec, int mode) { if (GradleVersion.current().compareTo(GradleVersion.version("8.3")) >= 0) { - try { - Method filePermissions = copySpec.getClass().getMethod("filePermissions", Action.class); - filePermissions.invoke(copySpec, new Action<>() { - - @Override - public void execute(Object filePermissions) { - String unixPermissions = Integer.toString(mode, 8); - try { - Method unix = filePermissions.getClass().getMethod("unix", String.class); - unix.invoke(filePermissions, unixPermissions); - } - catch (Exception ex) { - throw new GradleException("Failed to set file permissions to '" + unixPermissions + "'", - ex); - } - } - - }); - } - catch (Exception ex) { - throw new GradleException("Failed to set file permissions", ex); - } + copySpec.filePermissions((filePermissions) -> filePermissions.unix(Integer.toString(mode, 8))); } else { - copySpec.setFileMode(mode); + configureFileMode(copySpec, mode); } } + @SuppressWarnings("deprecation") + private void configureFileMode(CopySpec copySpec, int mode) { + copySpec.setFileMode(mode); + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootArchiveSupport.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootArchiveSupport.java index 6cdd36d5f2a4..437547bf9435 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootArchiveSupport.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootArchiveSupport.java @@ -26,9 +26,8 @@ import java.util.Set; import java.util.TreeMap; import java.util.function.Function; -import java.util.function.Supplier; -import org.gradle.api.GradleException; +import org.gradle.api.file.ConfigurableFilePermissions; import org.gradle.api.file.CopySpec; import org.gradle.api.file.FileCopyDetails; import org.gradle.api.file.FileTreeElement; @@ -133,8 +132,8 @@ CopyAction createCopyAction(Jar jar, ResolvedDependencies resolvedDependencies, File output = jar.getArchiveFile().get().getAsFile(); Manifest manifest = jar.getManifest(); boolean preserveFileTimestamps = jar.isPreserveFileTimestamps(); - Integer dirMode = getDirMode(jar); - Integer fileMode = getFileMode(jar); + Integer dirPermissions = getUnixNumericDirPermissions(jar); + Integer filePermissions = getUnixNumericFilePermissions(jar); boolean includeDefaultLoader = isUsingDefaultLoader(jar); Spec requiresUnpack = this.requiresUnpack.getAsSpec(); Spec exclusions = this.exclusions.getAsExcludeSpec(); @@ -142,35 +141,35 @@ CopyAction createCopyAction(Jar jar, ResolvedDependencies resolvedDependencies, Spec librarySpec = this.librarySpec; Function compressionResolver = this.compressionResolver; String encoding = jar.getMetadataCharset(); - CopyAction action = new BootZipCopyAction(output, manifest, preserveFileTimestamps, dirMode, fileMode, - includeDefaultLoader, jarmodeToolsLocation, requiresUnpack, exclusions, launchScript, librarySpec, - compressionResolver, encoding, resolvedDependencies, supportsSignatureFile, layerResolver, + CopyAction action = new BootZipCopyAction(output, manifest, preserveFileTimestamps, dirPermissions, + filePermissions, includeDefaultLoader, jarmodeToolsLocation, requiresUnpack, exclusions, launchScript, + librarySpec, compressionResolver, encoding, resolvedDependencies, supportsSignatureFile, layerResolver, loaderImplementation); return jar.isReproducibleFileOrder() ? new ReproducibleOrderingCopyAction(action) : action; } - private Integer getDirMode(CopySpec copySpec) { - return getMode(copySpec, "getDirPermissions", () -> copySpec.getDirMode()); + private Integer getUnixNumericDirPermissions(CopySpec copySpec) { + return (GradleVersion.current().compareTo(GradleVersion.version("8.3")) >= 0) + ? asUnixNumeric(copySpec.getDirPermissions()) : getDirMode(copySpec); } - private Integer getFileMode(CopySpec copySpec) { - return getMode(copySpec, "getFilePermissions", () -> copySpec.getFileMode()); + private Integer getUnixNumericFilePermissions(CopySpec copySpec) { + return (GradleVersion.current().compareTo(GradleVersion.version("8.3")) >= 0) + ? asUnixNumeric(copySpec.getFilePermissions()) : getFileMode(copySpec); } - @SuppressWarnings("unchecked") - private Integer getMode(CopySpec copySpec, String methodName, Supplier fallback) { - if (GradleVersion.current().compareTo(GradleVersion.version("8.3")) >= 0) { - try { - Object filePermissions = ((Property) copySpec.getClass().getMethod(methodName).invoke(copySpec)) - .getOrNull(); - return (filePermissions != null) - ? (int) filePermissions.getClass().getMethod("toUnixNumeric").invoke(filePermissions) : null; - } - catch (Exception ex) { - throw new GradleException("Failed to get permissions", ex); - } - } - return fallback.get(); + private Integer asUnixNumeric(Property permissions) { + return permissions.isPresent() ? permissions.get().toUnixNumeric() : null; + } + + @SuppressWarnings("deprecation") + private Integer getDirMode(CopySpec copySpec) { + return copySpec.getDirMode(); + } + + @SuppressWarnings("deprecation") + private Integer getFileMode(CopySpec copySpec) { + return copySpec.getFileMode(); } private boolean isUsingDefaultLoader(Jar jar) { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootZipCopyAction.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootZipCopyAction.java index 60bcebc04921..fdde482b7e2c 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootZipCopyAction.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootZipCopyAction.java @@ -22,7 +22,6 @@ import java.io.InputStream; import java.io.OutputStream; import java.io.OutputStreamWriter; -import java.lang.reflect.Method; import java.time.OffsetDateTime; import java.time.ZoneOffset; import java.util.Collection; @@ -488,17 +487,12 @@ private int getFileMode(FileCopyDetails details) { } private int getPermissions(FileCopyDetails details) { - if (GradleVersion.current().compareTo(GradleVersion.version("8.3")) >= 0) { - try { - Method getPermissionsMethod = details.getClass().getMethod("getPermissions"); - getPermissionsMethod.setAccessible(true); - Object permissions = getPermissionsMethod.invoke(details); - return (int) permissions.getClass().getMethod("toUnixNumeric").invoke(permissions); - } - catch (Exception ex) { - throw new GradleException("Failed to get permissions", ex); - } - } + return (GradleVersion.current().compareTo(GradleVersion.version("8.3")) >= 0) + ? details.getPermissions().toUnixNumeric() : getMode(details); + } + + @SuppressWarnings("deprecation") + private int getMode(FileCopyDetails details) { return details.getMode(); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/junit/GradleMultiDslExtension.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/junit/GradleMultiDslExtension.java index 41d82960c904..7683e044fecf 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/junit/GradleMultiDslExtension.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/junit/GradleMultiDslExtension.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/junit/GradleProjectBuilder.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/junit/GradleProjectBuilder.java index 9f7a01053abf..1f63d8677122 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/junit/GradleProjectBuilder.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/junit/GradleProjectBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,22 +18,17 @@ import java.io.File; -import org.gradle.api.JavaVersion; import org.gradle.api.Project; -import org.gradle.internal.nativeintegration.services.NativeServices; import org.gradle.testfixtures.ProjectBuilder; -import org.gradle.testfixtures.internal.ProjectBuilderImpl; import org.springframework.util.Assert; import org.springframework.util.StringUtils; /** * Helper class to build Gradle {@link Project Projects} for test fixtures. Wraps - * functionality of Gradle's own {@link ProjectBuilder} in order to work around an issue - * on JDK 17 and 18. + * functionality of Gradle's own {@link ProjectBuilder}. * * @author Christoph Dreis - * @see Gradle Support JDK 17 */ public final class GradleProjectBuilder { @@ -67,14 +62,6 @@ public Project build() { if (StringUtils.hasText(this.name)) { builder.withName(this.name); } - if (JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_17)) { - NativeServices.initializeOnClient(userHome); - try { - ProjectBuilderImpl.getGlobalServices(); - } - catch (Throwable ignore) { - } - } return builder.build(); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/KotlinPluginActionIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/KotlinPluginActionIntegrationTests.java index b3978b47f705..8afa6036f967 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/KotlinPluginActionIntegrationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/KotlinPluginActionIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -83,7 +83,7 @@ void taskConfigurationIsAvoided() throws IOException { configured.add(line.substring("Configuring :".length())); } } - assertThat(configured).containsExactlyInAnyOrder("help", "clean"); + assertThat(configured).containsExactlyInAnyOrder("help", "compileJava", "clean"); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/buildinfo/BuildInfoTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/buildinfo/BuildInfoTests.java index 538c385418c2..84eb1c1ca86c 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/buildinfo/BuildInfoTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/buildinfo/BuildInfoTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -176,7 +176,7 @@ private Project createProject(String projectName) { Project project = GradleProjectBuilder.builder().withProjectDir(projectDir).withName(projectName).build(); ((ProjectInternal) project).getServices() .get(GradlePropertiesController.class) - .loadGradlePropertiesFrom(projectDir); + .loadGradlePropertiesFrom(projectDir, false); return project; } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveIntegrationTests.java index 3dce1fed4c3a..df29ef1ef0f9 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveIntegrationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveIntegrationTests.java @@ -638,7 +638,7 @@ private void copyMainClassApplication() throws IOException { protected void copyApplication(String name) throws IOException { File output = new File(this.gradleBuild.getProjectDir(), - "src/main/java/com/example/" + this.taskName.toLowerCase() + "/" + name); + "src/main/java/com/example/" + this.taskName.toLowerCase(Locale.ROOT) + "/" + name); output.mkdirs(); FileSystemUtils.copyRecursively( new File("src/test/java/com/example/" + this.taskName.toLowerCase(Locale.ENGLISH) + "/" + name), diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-test-support/build.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-test-support/build.gradle index 48a3ac40f96d..f45f01ac8f78 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-test-support/build.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-test-support/build.gradle @@ -1,6 +1,5 @@ plugins { id "java-library" - id "org.springframework.boot.conventions" } description = "Spring Boot Gradle Testing Support" diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-test-support/src/main/java/org/springframework/boot/testsupport/gradle/testkit/GradleBuildExtension.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-test-support/src/main/java/org/springframework/boot/testsupport/gradle/testkit/GradleBuildExtension.java index 88c7b8b6d1fe..8f0a6956acb0 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-test-support/src/main/java/org/springframework/boot/testsupport/gradle/testkit/GradleBuildExtension.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-test-support/src/main/java/org/springframework/boot/testsupport/gradle/testkit/GradleBuildExtension.java @@ -16,6 +16,7 @@ package org.springframework.boot.testsupport.gradle.testkit; +import java.io.File; import java.lang.reflect.Field; import java.net.URL; import java.util.regex.Pattern; @@ -25,6 +26,7 @@ import org.junit.jupiter.api.extension.Extension; import org.junit.jupiter.api.extension.ExtensionContext; +import org.springframework.util.Assert; import org.springframework.util.ReflectionUtils; /** @@ -43,6 +45,7 @@ public class GradleBuildExtension implements BeforeEachCallback, AfterEachCallba @Override public void beforeEach(ExtensionContext context) throws Exception { GradleBuild gradleBuild = extractGradleBuild(context); + gradleBuild.scriptProperty("parentRootDir", findParentRootDir().getAbsolutePath()); URL scriptUrl = findDefaultScript(context); if (scriptUrl != null) { gradleBuild.script(scriptUrl.getFile()); @@ -54,6 +57,22 @@ public void beforeEach(ExtensionContext context) throws Exception { gradleBuild.before(); } + private File findParentRootDir() { + File dir = new File("").getAbsoluteFile(); + int depth = 0; + while (dir != null && !hasGradleBuildFiles(dir)) { + Assert.state(depth++ < 5, "Unable to find parent root"); + dir = dir.getParentFile(); + } + Assert.state(dir != null, "Unable to find parent root"); + return dir; + } + + private boolean hasGradleBuildFiles(File dir) { + return new File(dir, "settings.gradle").exists() && new File(dir, "build.gradle").exists() + && new File(dir, "gradle.properties").exists(); + } + private GradleBuild extractGradleBuild(ExtensionContext context) throws Exception { Object testInstance = context.getRequiredTestInstance(); Field gradleBuildField = ReflectionUtils.findField(testInstance.getClass(), "gradleBuild"); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-test-support/src/main/java/org/springframework/boot/testsupport/gradle/testkit/GradleVersions.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-test-support/src/main/java/org/springframework/boot/testsupport/gradle/testkit/GradleVersions.java index c21c84badac7..17998b002806 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-test-support/src/main/java/org/springframework/boot/testsupport/gradle/testkit/GradleVersions.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-test-support/src/main/java/org/springframework/boot/testsupport/gradle/testkit/GradleVersions.java @@ -33,10 +33,16 @@ private GradleVersions() { } public static List allCompatible() { - if (isJavaVersion(JavaVersion.VERSION_20)) { - return Arrays.asList("8.1.1", "8.10"); + if (isJavaVersion(JavaVersion.VERSION_23)) { + return Arrays.asList(GradleVersion.current().getVersion()); } - return Arrays.asList("7.5.1", GradleVersion.current().getVersion(), "8.0.2", "8.10"); + if (isJavaVersion(JavaVersion.VERSION_22)) { + return Arrays.asList("8.8", GradleVersion.current().getVersion()); + } + if (isJavaVersion(JavaVersion.VERSION_21)) { + return Arrays.asList("8.5", GradleVersion.current().getVersion()); + } + return Arrays.asList("7.6.4", "8.3", GradleVersion.current().getVersion()); } public static String minimumCompatible() { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/build.gradle b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/build.gradle index 7a0a9e911b26..167a619ff5c4 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/build.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/build.gradle @@ -1,6 +1,5 @@ plugins { id "java-library" - id "org.springframework.boot.conventions" id "org.springframework.boot.deployed" } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/Context.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/Context.java index 32f7976cddfe..a889808c31de 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/Context.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/Context.java @@ -25,6 +25,7 @@ import java.nio.file.Paths; import java.security.CodeSource; import java.security.ProtectionDomain; +import java.util.Locale; import java.util.jar.JarFile; import org.springframework.util.Assert; @@ -67,7 +68,7 @@ private boolean isExistingFile(File archiveFile) { } private boolean isJarOrWar(File jarFile) { - String name = jarFile.getName().toLowerCase(); + String name = jarFile.getName().toLowerCase(Locale.ROOT); return name.endsWith(".jar") || name.endsWith(".war"); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/build.gradle b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/build.gradle index d45e33698c5c..2511e5a4bb8a 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/build.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/build.gradle @@ -1,6 +1,5 @@ plugins { id "java-library" - id "org.springframework.boot.conventions" id "org.springframework.boot.deployed" } @@ -22,6 +21,8 @@ dependencies { testRuntimeOnly("org.springframework:spring-webmvc") } -tasks.named("checkArchitectureMain").configure { - prohibitObjectsRequireNonNull = false +tasks.configureEach { + if ("checkArchitectureMain".equals(it.name)) { + prohibitObjectsRequireNonNull = false + } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/build.gradle b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/build.gradle index 4033e3e8a0f6..9508cfc62548 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/build.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/build.gradle @@ -1,12 +1,11 @@ plugins { id "java-library" - id "org.springframework.boot.conventions" id "org.springframework.boot.deployed" } description = "Spring Boot Loader Tools" -def generatedResources = "${buildDir}/generated-resources/main" +Provider generatedResources = layout.buildDirectory.dir("generated-resources/main") configurations { loader { @@ -63,7 +62,7 @@ task reproducibleLoaderJar(type: Jar) { reproducibleFileOrder = true preserveFileTimestamps = false archiveFileName = "spring-boot-loader.jar" - destinationDirectory = file("${generatedResources}/META-INF/loader") + destinationDirectory = file(generatedResources.map {it.dir("META-INF/loader") }) } task reproducibleLoaderClassicJar(type: Jar) { @@ -78,7 +77,7 @@ task reproducibleLoaderClassicJar(type: Jar) { reproducibleFileOrder = true preserveFileTimestamps = false archiveFileName = "spring-boot-loader-classic.jar" - destinationDirectory = file("${generatedResources}/META-INF/loader") + destinationDirectory = file(generatedResources.map { it.dir("META-INF/loader") }) } task toolsJar(type: Sync) { @@ -87,7 +86,7 @@ task toolsJar(type: Sync) { file(configurations.jarmode.incoming.files.singleFile) } rename({ "spring-boot-jarmode-tools.jar" }) - into(file("${generatedResources}/META-INF/jarmode")) + into(file(generatedResources.map { it.dir("META-INF/jarmode") })) } sourceSets { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/FileUtils.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/FileUtils.java index 0347e1cbe6f4..ee60fbafdf30 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/FileUtils.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/FileUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ import java.io.File; import java.io.IOException; +import java.util.Locale; import java.util.jar.Attributes; import java.util.jar.JarFile; import java.util.jar.Manifest; @@ -88,7 +89,7 @@ private static boolean hasDigestName(Attributes attributes) { } private static boolean isDigestName(Object name) { - return String.valueOf(name).toUpperCase().endsWith("-DIGEST"); + return String.valueOf(name).toUpperCase(Locale.ROOT).endsWith("-DIGEST"); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Layer.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Layer.java index 484bc2286c6a..544a7ec772c3 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Layer.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Layer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package org.springframework.boot.loader.tools; +import java.util.Locale; import java.util.regex.Pattern; import org.springframework.util.Assert; @@ -41,7 +42,7 @@ public class Layer { public Layer(String name) { Assert.hasText(name, "Name must not be empty"); Assert.isTrue(PATTERN.matcher(name).matches(), () -> "Malformed layer name '" + name + "'"); - Assert.isTrue(!name.equalsIgnoreCase("ext") && !name.toLowerCase().startsWith("springboot"), + Assert.isTrue(!name.equalsIgnoreCase("ext") && !name.toLowerCase(Locale.ROOT).startsWith("springboot"), () -> "Layer name '" + name + "' is reserved"); this.name = name; } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Repackager.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Repackager.java index 764c84f9fde8..c2391194c19f 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Repackager.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Repackager.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ import java.io.File; import java.io.IOException; import java.nio.file.attribute.FileTime; +import java.util.Locale; import java.util.Map; import java.util.jar.JarFile; @@ -50,7 +51,7 @@ public Repackager(File source) { @Override protected void writeSignatureFileIfNecessary(Map writtenLibraries, AbstractJarWriter writer) throws IOException { - if (getSource().getName().toLowerCase().endsWith(".jar") && hasSignedLibrary(writtenLibraries)) { + if (getSource().getName().toLowerCase(Locale.ROOT).endsWith(".jar") && hasSignedLibrary(writtenLibraries)) { writer.writeEntry("META-INF/BOOT.SF", (entryWriter) -> { }); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/build.gradle b/spring-boot-project/spring-boot-tools/spring-boot-loader/build.gradle index 0784082e601b..4a6e6dd8bb96 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/build.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/build.gradle @@ -1,6 +1,5 @@ plugins { id "java-library" - id "org.springframework.boot.conventions" id "org.springframework.boot.deployed" } @@ -22,6 +21,8 @@ dependencies { testRuntimeOnly("org.springframework:spring-webmvc") } -tasks.named("checkArchitectureMain").configure { - prohibitObjectsRequireNonNull = false +tasks.configureEach { + if ("checkArchitectureMain".equals(it.name)) { + prohibitObjectsRequireNonNull = false + } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntriesStream.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntriesStream.java index 35d9421874b4..3bb8419fb952 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntriesStream.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntriesStream.java @@ -54,9 +54,6 @@ class JarEntriesStream implements Closeable { JarEntry getNextEntry() throws IOException { this.entry = this.in.getNextJarEntry(); - if (this.entry != null) { - this.entry.getSize(); - } this.inflater.reset(); return this.entry; } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/JarFileUrlKey.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/JarFileUrlKey.java index e8ce0f503db1..d1ba28fa087a 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/JarFileUrlKey.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/JarFileUrlKey.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ import java.lang.ref.SoftReference; import java.net.URL; +import java.util.Locale; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -54,10 +55,10 @@ private static String create(URL url) { String host = url.getHost(); int port = (url.getPort() != -1) ? url.getPort() : url.getDefaultPort(); String file = url.getFile(); - value.append(protocol.toLowerCase()); + value.append(protocol.toLowerCase(Locale.ROOT)); value.append(":"); if (host != null && !host.isEmpty()) { - value.append(host.toLowerCase()); + value.append(host.toLowerCase(Locale.ROOT)); value.append((port != -1) ? ":" + port : ""); } value.append((file != null) ? file : ""); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/JarUrlConnection.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/JarUrlConnection.java index b188b0a8ddfa..038751f43281 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/JarUrlConnection.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/JarUrlConnection.java @@ -211,9 +211,9 @@ public boolean getAllowUserInteraction() { } @Override - public void setAllowUserInteraction(boolean allowuserinteraction) { + public void setAllowUserInteraction(boolean allowUserInteraction) { if (this.jarFileConnection != null) { - this.jarFileConnection.setAllowUserInteraction(allowuserinteraction); + this.jarFileConnection.setAllowUserInteraction(allowUserInteraction); } } @@ -223,9 +223,9 @@ public boolean getUseCaches() { } @Override - public void setUseCaches(boolean usecaches) { + public void setUseCaches(boolean useCaches) { if (this.jarFileConnection != null) { - this.jarFileConnection.setUseCaches(usecaches); + this.jarFileConnection.setUseCaches(useCaches); } } @@ -235,9 +235,9 @@ public boolean getDefaultUseCaches() { } @Override - public void setDefaultUseCaches(boolean defaultusecaches) { + public void setDefaultUseCaches(boolean defaultUseCaches) { if (this.jarFileConnection != null) { - this.jarFileConnection.setDefaultUseCaches(defaultusecaches); + this.jarFileConnection.setDefaultUseCaches(defaultUseCaches); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/LazyDelegatingInputStream.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/LazyDelegatingInputStream.java index 95e5cc3c14a7..ff47aa2debd6 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/LazyDelegatingInputStream.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/LazyDelegatingInputStream.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -64,9 +64,9 @@ public boolean markSupported() { } @Override - public synchronized void mark(int readlimit) { + public synchronized void mark(int readLimit) { try { - in().mark(readlimit); + in().mark(readLimit); } catch (IOException ex) { // Ignore diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/nio/file/NestedFileSystem.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/nio/file/NestedFileSystem.java index 1cbdf94d7644..544020595414 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/nio/file/NestedFileSystem.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/nio/file/NestedFileSystem.java @@ -98,11 +98,9 @@ private boolean hasFileSystem(URI uri) { private boolean isCreatingNewFileSystem() { StackTraceElement[] stack = Thread.currentThread().getStackTrace(); - if (stack != null) { - for (StackTraceElement element : stack) { - if (FILE_SYSTEMS_CLASS_NAME.equals(element.getClassName())) { - return "newFileSystem".equals(element.getMethodName()); - } + for (StackTraceElement element : stack) { + if (FILE_SYSTEMS_CLASS_NAME.equals(element.getClassName())) { + return "newFileSystem".equals(element.getMethodName()); } } return false; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/nio/file/NestedFileSystemProvider.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/nio/file/NestedFileSystemProvider.java index ca136748df8c..fce7e8c56580 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/nio/file/NestedFileSystemProvider.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/nio/file/NestedFileSystemProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -50,7 +50,7 @@ */ public class NestedFileSystemProvider extends FileSystemProvider { - private Map fileSystems = new HashMap<>(); + private final Map fileSystems = new HashMap<>(); @Override public String getScheme() { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/nio/file/UriPathEncoder.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/nio/file/UriPathEncoder.java index a58f252a6f42..9b96ef3c93e2 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/nio/file/UriPathEncoder.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/nio/file/UriPathEncoder.java @@ -28,7 +28,7 @@ final class UriPathEncoder { // Based on org.springframework.web.util.UriUtils - private static char[] ALLOWED = "/:@-._~!$&\'()*+,;=".toCharArray(); + private static final char[] ALLOWED = "/:@-._~!$&\'()*+,;=".toCharArray(); private UriPathEncoder() { } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/build.gradle b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/build.gradle index 75fc3390d6c5..0a968ca214a1 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/build.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/build.gradle @@ -1,6 +1,5 @@ plugins { id "org.antora" - id "org.springframework.boot.conventions" id "org.springframework.boot.maven-plugin" id "org.springframework.boot.optional-dependencies" id "org.springframework.boot.docker-test" @@ -92,15 +91,21 @@ ext { xsdVersion = versionElements[0] + "." + versionElements[1] } +task copySettingsXml(type: Copy) { + from file("src/intTest/projects/settings.xml") + into layout.buildDirectory.dir("generated-resources/settings") + filter(springRepositoryTransformers.mavenSettings()) +} + sourceSets { main { - output.dir("${buildDir}/generated/resources/xsd", builtBy: "xsdResources") + output.dir(layout.buildDirectory.dir("generated/resources/xsd"), builtBy: "xsdResources") } intTest { - output.dir("${buildDir}/generated-resources", builtBy: "extractVersionProperties") + output.dir(layout.buildDirectory.dir("generated-resources"), builtBy: ["extractVersionProperties", "copySettingsXml"]) } dockerTest { - output.dir("${buildDir}/generated-resources", builtBy: "extractVersionProperties") + output.dir(layout.buildDirectory.dir("generated-resources"), builtBy: "extractVersionProperties") } } @@ -119,7 +124,7 @@ javadoc { task xsdResources(type: Sync) { from "src/main/xsd/layers-${project.ext.xsdVersion}.xsd" - into "${buildDir}/generated/resources/xsd/org/springframework/boot/maven" + into layout.buildDirectory.dir("generated/resources/xsd/org/springframework/boot/maven") rename { fileName -> "layers.xsd" } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/antora/modules/maven-plugin/pages/aot.adoc b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/antora/modules/maven-plugin/pages/aot.adoc index c812002e7b18..b3e53cecef4a 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/antora/modules/maven-plugin/pages/aot.adoc +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/antora/modules/maven-plugin/pages/aot.adoc @@ -41,6 +41,12 @@ The `native` profile configures the following: ** Validate that a suitable GraalVM version is available. ** Download third-party reachability metadata. +[WARNING] +==== +The use of the raw classpath means that native image does not know about the generated `MANIFEST`. +If you need to read the content of the manifest in a native image, for instance to get the implementation version of your application, configure the `classesDirectory` option to use the regular jar. +==== + To benefit from the `native` profile, a module that represents an application should define two plugins, as shown in the following example: [source,xml,indent=0,subs="verbatim,attributes"] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/antora/modules/maven-plugin/pages/getting-started.adoc b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/antora/modules/maven-plugin/pages/getting-started.adoc index 61a577d430ed..4d74580e3353 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/antora/modules/maven-plugin/pages/getting-started.adoc +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/antora/modules/maven-plugin/pages/getting-started.adoc @@ -8,9 +8,19 @@ To use the Spring Boot Maven Plugin, include the appropriate XML in the `plugins include::example$getting-started/pom.xml[tags=getting-started] ---- +ifeval::["{build-type}" == "commercial"] +The plugin is published to the Spring Commercial repository. +You will have to configure your build to access this repository. +This is usually done through a local artifact repository that mirrors the content of the Spring Commercial repository. +Alternatively, while it is not recommended, the Spring Commercial repository can also be accessed directly. +In either case, see https://docs.vmware.com/en/Tanzu-Spring-Runtime/Commercial/Tanzu-Spring-Runtime/spring-enterprise-subscription.html[the Tanzu Spring Runtime documentation] for further details. +endif::[] + +ifeval::["{build-type}" == "opensource"] If you use a milestone or snapshot release, you also need to add the appropriate `pluginRepository` elements, as shown in the following listing: [source,xml,indent=0,subs="verbatim,attributes"] ---- include::example$getting-started/plugin-repositories-pom.xml[tags=plugin-repositories] ---- +endif::[] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/MavenBuild.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/MavenBuild.java index 0c64502f236c..7393ef9233d4 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/MavenBuild.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/MavenBuild.java @@ -160,7 +160,7 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IO } }); - String settingsXml = Files.readString(Paths.get("src", "intTest", "projects", "settings.xml")) + String settingsXml = Files.readString(Paths.get("build", "generated-resources", "settings", "settings.xml")) .replace("@localCentralUrl@", new File("build/test-maven-repository").toURI().toURL().toString()) .replace("@localRepositoryPath@", new File("build/local-maven-repository").getAbsolutePath()); Files.writeString(destination.resolve("settings.xml"), settingsXml, StandardOpenOption.CREATE_NEW); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/settings.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/settings.xml index e4aacb2648a7..527bb28dd210 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/settings.xml +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/settings.xml @@ -18,19 +18,7 @@ true - - spring-milestones - Spring Milestones - https://repo.spring.io/milestone - - - spring-snapshots - Spring Snapshots - https://repo.spring.io/snapshot - - true - - + @@ -43,11 +31,7 @@ true - - spring-milestones - Spring Milestones - https://repo.spring.io/milestone - + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractRunMojo.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractRunMojo.java index f21d6c8308ce..b9e2685b9b7c 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractRunMojo.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractRunMojo.java @@ -477,7 +477,9 @@ private String escape(CharSequence content) { } static ArgFile create(CharSequence content) throws IOException { - ArgFile argFile = new ArgFile(Files.createTempFile("spring-boot-", ".argfile")); + Path tempFile = Files.createTempFile("spring-boot-", ".argfile"); + tempFile.toFile().deleteOnExit(); + ArgFile argFile = new ArgFile(tempFile); argFile.write(content); return argFile; } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-properties-migrator/build.gradle b/spring-boot-project/spring-boot-tools/spring-boot-properties-migrator/build.gradle index c722881e9aef..8393d4a3a40a 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-properties-migrator/build.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-properties-migrator/build.gradle @@ -1,6 +1,5 @@ plugins { id "java" - id "org.springframework.boot.conventions" id "org.springframework.boot.deployed" } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support-docker/build.gradle b/spring-boot-project/spring-boot-tools/spring-boot-test-support-docker/build.gradle index 41a47c555a6c..a7b7014194a3 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-test-support-docker/build.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-test-support-docker/build.gradle @@ -1,6 +1,5 @@ plugins { id "java-library" - id "org.springframework.boot.conventions" id "org.springframework.boot.optional-dependencies" } @@ -17,7 +16,6 @@ dependencies { optional("org.testcontainers:activemq") optional("org.testcontainers:cassandra") - optional("org.testcontainers:cassandra") optional("org.testcontainers:couchbase") optional("org.testcontainers:elasticsearch") optional("org.testcontainers:junit-jupiter") diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/build.gradle b/spring-boot-project/spring-boot-tools/spring-boot-test-support/build.gradle index 47233a46f9be..7b22be3960a9 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-test-support/build.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-test-support/build.gradle @@ -1,6 +1,5 @@ plugins { id "java-library" - id "org.springframework.boot.conventions" id "org.springframework.boot.optional-dependencies" } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/process/DisabledIfProcessUnavailableCondition.java b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/process/DisabledIfProcessUnavailableCondition.java index 0e48896d981f..612124659975 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/process/DisabledIfProcessUnavailableCondition.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/process/DisabledIfProcessUnavailableCondition.java @@ -19,6 +19,7 @@ import java.lang.reflect.AnnotatedElement; import java.util.ArrayList; import java.util.List; +import java.util.Locale; import java.util.concurrent.TimeUnit; import java.util.stream.Stream; @@ -42,7 +43,7 @@ class DisabledIfProcessUnavailableCondition implements ExecutionCondition { private static final String USR_LOCAL_BIN = "/usr/local/bin"; - private static final boolean MAC_OS = System.getProperty("os.name").toLowerCase().contains("mac"); + private static final boolean MAC_OS = System.getProperty("os.name").toLowerCase(Locale.ROOT).contains("mac"); @Override public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) { diff --git a/spring-boot-project/spring-boot/build.gradle b/spring-boot-project/spring-boot/build.gradle index 0e2f235e55b7..1af141ef0b30 100644 --- a/spring-boot-project/spring-boot/build.gradle +++ b/spring-boot-project/spring-boot/build.gradle @@ -1,7 +1,7 @@ plugins { + id "dev.adamko.dokkatoo-html" id "java-library" id "org.jetbrains.kotlin.jvm" - id "org.springframework.boot.conventions" id "org.springframework.boot.configuration-properties" id "org.springframework.boot.deployed" id "org.springframework.boot.optional-dependencies" @@ -9,7 +9,7 @@ plugins { description = "Spring Boot" -def tomcatConfigProperties = "$buildDir/tomcat-config-properties" +def tomcatConfigProperties = layout.buildDirectory.dir("tomcat-config-properties") configurations { tomcatDistribution diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBinder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBinder.java index 673f0dcadaff..ede0f6bf6eca 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBinder.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBinder.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -156,7 +156,7 @@ private Validator getSelfValidator(Bindable target) { return (value instanceof Validator validator) ? validator : null; } Class type = target.getType().resolve(); - if (Validator.class.isAssignableFrom(type)) { + if (type != null && Validator.class.isAssignableFrom(type)) { return new SelfValidatingConstructorBoundBindableValidator(type); } return null; @@ -208,7 +208,7 @@ static void register(BeanDefinitionRegistry registry) { .rootBeanDefinition(ConfigurationPropertiesBinderFactory.class) .getBeanDefinition(); definition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); - registry.registerBeanDefinition(ConfigurationPropertiesBinder.BEAN_NAME, definition); + registry.registerBeanDefinition(BEAN_NAME, definition); } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Binder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Binder.java index 4d8fbf0133d2..1630d2d8997a 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Binder.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Binder.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -272,8 +272,8 @@ public BindResult bind(ConfigurationPropertyName name, Bindable target /** * Bind the specified target {@link Class} using this binder's - * {@link ConfigurationPropertySource property sources} or create a new instance using - * the type of the {@link Bindable} if the result of the binding is {@code null}. + * {@link ConfigurationPropertySource property sources} or create a new instance of + * the specified target {@link Class} if the result of the binding is {@code null}. * @param name the configuration property name to bind * @param target the target class * @param the bound type diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/env/ConfigTreePropertySource.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/env/ConfigTreePropertySource.java index ae3737c547b3..becea918c401 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/env/ConfigTreePropertySource.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/env/ConfigTreePropertySource.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,6 +26,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.EnumSet; +import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.TreeMap; @@ -212,7 +213,7 @@ static Map findAll(Path sourceDirectory, Set