diff --git a/.github/actions/await-http-resource/action.yml b/.github/actions/await-http-resource/action.yml new file mode 100644 index 000000000000..7d2b3462b537 --- /dev/null +++ b/.github/actions/await-http-resource/action.yml @@ -0,0 +1,20 @@ +name: Await HTTP Resource +description: Waits for an HTTP resource to be available (a HEAD request succeeds) +inputs: + url: + description: 'The URL of the resource to await' + required: true +runs: + using: composite + steps: + - name: Await HTTP resource + shell: bash + run: | + url=${{ inputs.url }} + echo "Waiting for $url" + until curl --fail --head --silent ${{ inputs.url }} > /dev/null + do + echo "." + sleep 60 + done + echo "$url is available" diff --git a/.github/actions/build/action.yml b/.github/actions/build/action.yml new file mode 100644 index 000000000000..87fbbe8ddac3 --- /dev/null +++ b/.github/actions/build/action.yml @@ -0,0 +1,56 @@ +name: 'Build' +description: 'Builds the project, optionally publishing it to a local deployment repository' +inputs: + java-version: + required: false + default: '8' + description: 'The Java version to compile and test with' + java-early-access: + required: false + default: 'false' + description: 'Whether the Java version is in early access' + java-toolchain: + required: false + default: 'false' + description: 'Whether a Java toolchain should be used' + 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' +outputs: + build-scan-url: + description: 'The 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' + value: ${{ steps.read-version.outputs.version }} +runs: + using: composite + steps: + - name: Prepare Gradle Build + uses: ./.github/actions/prepare-gradle-build + with: + develocity-access-key: ${{ inputs.develocity-access-key }} + java-version: ${{ inputs.java-version }} + java-early-access: ${{ inputs.java-early-access }} + java-toolchain: ${{ inputs.java-toolchain }} + - name: Build + id: build + if: ${{ inputs.publish == 'false' }} + shell: bash + run: ./gradlew check + - name: Publish + id: publish + if: ${{ inputs.publish == 'true' }} + shell: bash + run: ./gradlew -PdeploymentRepository=$(pwd)/deployment-repository build publishAllPublicationsToDeploymentRepository + - name: Read Version From gradle.properties + id: read-version + shell: bash + run: | + version=$(sed -n 's/version=\(.*\)/\1/p' gradle.properties) + echo "Version is $version" + echo "version=$version" >> $GITHUB_OUTPUT diff --git a/.github/actions/create-github-release/action.yml b/.github/actions/create-github-release/action.yml new file mode 100644 index 000000000000..e0120764f1e4 --- /dev/null +++ b/.github/actions/create-github-release/action.yml @@ -0,0 +1,23 @@ +name: Create GitHub Release +description: Create the release on GitHub with a changelog +inputs: + milestone: + description: Name of the GitHub milestone for which a release will be created + required: true + token: + description: Token to use for authentication with GitHub + required: true +runs: + using: composite + steps: + - name: Generate Changelog + uses: spring-io/github-changelog-generator@185319ad7eaa75b0e8e72e4b6db19c8b2cb8c4c1 #v0.0.11 + with: + milestone: ${{ inputs.milestone }} + token: ${{ inputs.token }} + config-file: .github/actions/create-github-release/changelog-generator.yml + - name: Create GitHub Release + env: + GITHUB_TOKEN: ${{ inputs.token }} + shell: bash + run: gh release create ${{ format('v{0}', inputs.milestone) }} --notes-file changelog.md diff --git a/ci/config/changelog-generator.yml b/.github/actions/create-github-release/changelog-generator.yml similarity index 73% rename from ci/config/changelog-generator.yml rename to .github/actions/create-github-release/changelog-generator.yml index fce2a3a860b1..725c40966679 100644 --- a/ci/config/changelog-generator.yml +++ b/.github/actions/create-github-release/changelog-generator.yml @@ -17,4 +17,12 @@ changelog: - "type: dependency-upgrade" contributors: exclude: - names: ["bclozel", "jhoeller", "poutsma", "rstoyanchev", "sbrannen", "sdeleuze", "snicoll"] + names: + - "bclozel" + - "jhoeller" + - "poutsma" + - "rstoyanchev" + - "sbrannen" + - "sdeleuze" + - "simonbasle" + - "snicoll" diff --git a/.github/actions/prepare-gradle-build/action.yml b/.github/actions/prepare-gradle-build/action.yml new file mode 100644 index 000000000000..ca8ef69b790c --- /dev/null +++ b/.github/actions/prepare-gradle-build/action.yml @@ -0,0 +1,49 @@ +name: 'Prepare Gradle Build' +description: 'Prepares a Gradle build. Sets up Java and Gradle and configures Gradle properties' +inputs: + java-version: + required: false + default: '8' + description: 'The Java version to use for the build' + java-early-access: + required: false + default: 'false' + description: 'Whether the Java version is in early access' + java-toolchain: + required: false + default: 'false' + description: 'Whether a Java toolchain should be used' + develocity-access-key: + required: false + description: 'The access key for authentication with ge.spring.io' +runs: + using: composite + steps: + - name: Set Up Java + uses: actions/setup-java@v4 + with: + distribution: ${{ inputs.java-early-access == 'true' && 'temurin' || 'liberica' }} + java-version: | + ${{ inputs.java-early-access == 'true' && format('{0}-ea', inputs.java-version) || inputs.java-version }} + ${{ inputs.java-toolchain == 'true' && '8' || '' }} + - name: Set Up Gradle + uses: gradle/actions/setup-gradle@d9c87d481d55275bb5441eef3fe0e46805f9ef70 # v3.5.0 + with: + cache-read-only: false + develocity-access-key: ${{ inputs.develocity-access-key }} + - name: Configure Gradle Properties + shell: bash + run: | + mkdir -p $HOME/.gradle + echo 'systemProp.user.name=spring-builds+github' >> $HOME/.gradle/gradle.properties + echo 'systemProp.org.gradle.internal.launcher.welcomeMessageEnabled=false' >> $HOME/.gradle/gradle.properties + echo 'org.gradle.daemon=false' >> $HOME/.gradle/gradle.properties + echo 'org.gradle.daemon=4' >> $HOME/.gradle/gradle.properties + - name: Configure Toolchain Properties + if: ${{ inputs.java-toolchain == 'true' }} + shell: bash + run: | + echo toolchainVersion=${{ inputs.java-version }} >> $HOME/.gradle/gradle.properties + echo systemProp.org.gradle.java.installations.auto-detect=false >> $HOME/.gradle/gradle.properties + echo systemProp.org.gradle.java.installations.auto-download=false >> $HOME/.gradle/gradle.properties + echo systemProp.org.gradle.java.installations.paths=${{ format('$JAVA_HOME_{0}_X64', inputs.java-version) }} >> $HOME/.gradle/gradle.properties diff --git a/.github/actions/send-notification/action.yml b/.github/actions/send-notification/action.yml new file mode 100644 index 000000000000..d1389776397a --- /dev/null +++ b/.github/actions/send-notification/action.yml @@ -0,0 +1,33 @@ +name: Send Notification +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' + run-name: + description: 'Name of the run to include in the notification' + default: ${{ format('{0} {1}', github.ref_name, github.job) }} +runs: + using: composite + steps: + - 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 + if: ${{ inputs.status == 'success' }} + 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 + if: ${{ inputs.status == 'failure' }} + 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 + if: ${{ inputs.status == 'cancelled' }} + 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 new file mode 100644 index 000000000000..d4e86caf1196 --- /dev/null +++ b/.github/actions/sync-to-maven-central/action.yml @@ -0,0 +1,43 @@ +name: Sync to Maven Central +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-framework-version: + description: 'The version of Spring Framework that is being synced to Central' + required: true + ossrh-s01-token-username: + description: 'Username for authentication with s01.oss.sonatype.org' + 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' + required: true +runs: + using: composite + steps: + - name: Set Up JFrog CLI + uses: jfrog/setup-jfrog-cli@105617d23456a69a92485207c4f28ae12297581d # v4.2.1 + env: + JF_ENV_SPRING: ${{ inputs.jfrog-cli-config-token }} + - name: Download Release Artifacts + shell: bash + run: jf rt download --spec ${{ format('{0}/artifacts.spec', github.action_path) }} --spec-vars 'buildName=${{ format('spring-framework-{0}', inputs.spring-framework-version) }};buildNumber=${{ github.run_number }}' + - name: Sync + uses: spring-io/nexus-sync-action@42477a2230a2f694f9eaa4643fa9e76b99b7ab84 # v0.0.1 + with: + username: ${{ inputs.ossrh-s01-token-username }} + password: ${{ inputs.ossrh-s01-token-password }} + staging-profile-name: ${{ inputs.ossrh-s01-staging-profile }} + create: true + upload: true + close: true + release: true + generate-checksums: true + - name: Await + uses: ./.github/actions/await-http-resource + with: + url: ${{ format('https://repo.maven.apache.org/maven2/org/springframework/spring-context/{0}/spring-context-{0}.jar', inputs.spring-framework-version) }} diff --git a/.github/actions/sync-to-maven-central/artifacts.spec b/.github/actions/sync-to-maven-central/artifacts.spec new file mode 100644 index 000000000000..b9b7b53d20d3 --- /dev/null +++ b/.github/actions/sync-to-maven-central/artifacts.spec @@ -0,0 +1,20 @@ +{ + "files": [ + { + "aql": { + "items.find": { + "$and": [ + { + "@build.name": "${buildName}", + "@build.number": "${buildNumber}", + "path": { + "$nmatch": "org/springframework/spring-*.zip" + } + } + ] + } + }, + "target": "nexus/" + } + ] +} diff --git a/.github/workflows/backport-bot.yml b/.github/workflows/backport-bot.yml new file mode 100644 index 000000000000..4d025ece2ceb --- /dev/null +++ b/.github/workflows/backport-bot.yml @@ -0,0 +1,34 @@ +name: Backport Bot + +on: + issues: + types: [labeled] + pull_request: + types: [labeled] + push: + branches: + - '*.x' +permissions: + contents: read +jobs: + build: + permissions: + contents: read + issues: write + pull-requests: write + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@v4 + - name: Set up Java + uses: actions/setup-java@v4 + with: + distribution: 'liberica' + java-version: 17 + - name: Download BackportBot + run: wget https://github.com/spring-io/backport-bot/releases/download/latest/backport-bot-0.0.1-SNAPSHOT.jar + - name: Backport + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_EVENT: ${{ toJSON(github.event) }} + run: java -jar backport-bot-0.0.1-SNAPSHOT.jar --github.accessToken="$GITHUB_TOKEN" --github.event_name "$GITHUB_EVENT_NAME" --github.event "$GITHUB_EVENT" diff --git a/.github/workflows/build-and-deploy-snapshot.yml b/.github/workflows/build-and-deploy-snapshot.yml new file mode 100644 index 000000000000..0d475f558700 --- /dev/null +++ b/.github/workflows/build-and-deploy-snapshot.yml @@ -0,0 +1,59 @@ +name: Build and Deploy Snapshot +on: + push: + branches: + - 5.3.x +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} +jobs: + build-and-deploy-snapshot: + name: Build and Deploy Snapshot + runs-on: ubuntu-latest + timeout-minutes: 60 + if: ${{ github.repository == 'spring-projects/spring-framework' }} + steps: + - name: Check Out Code + uses: actions/checkout@v4 + - name: Build and Publish + id: build-and-publish + uses: ./.github/actions/build + with: + develocity-access-key: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }} + 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-framework-5.3.x' + repository: 'libs-snapshot-local' + folder: 'deployment-repository' + signing-key: ${{ secrets.GPG_PRIVATE_KEY }} + signing-passphrase: ${{ secrets.GPG_PASSPHRASE }} + artifact-properties: | + /**/spring-*.zip::zip.name=spring-framework,zip.deployed=false + /**/spring-*-docs.zip::zip.type=docs + /**/spring-*-dist.zip::zip.type=dist + /**/spring-*-schema.zip::zip.type=schema + - name: Send Notification + uses: ./.github/actions/send-notification + if: always() + 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 8', github.ref_name) }} + outputs: + version: ${{ steps.build-and-publish.outputs.version }} + verify: + name: Verify + needs: build-and-deploy-snapshot + uses: ./.github/workflows/verify.yml + secrets: + google-chat-webhook-url: ${{ secrets.GOOGLE_CHAT_WEBHOOK_URL }} + repository-password: ${{ secrets.ARTIFACTORY_PASSWORD }} + 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/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000000..5464966e9820 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,55 @@ +name: CI +on: + push: + branches: + - 5.3.x +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} +jobs: + ci: + name: '${{ matrix.os.name}} | Java ${{ matrix.java.version}}' + runs-on: ${{ matrix.os.id }} + timeout-minutes: 60 + if: ${{ github.repository == 'spring-projects/spring-framework' }} + strategy: + matrix: + os: + - id: ubuntu-latest + name: Linux + java: + - version: 8 + toolchain: false + - version: 17 + toolchain: true + - version: 21 + toolchain: true + exclude: + - os: + name: Linux + java: + version: 8 + steps: + - name: Prepare Windows runner + if: ${{ runner.os == 'Windows' }} + run: | + git config --global core.autocrlf true + git config --global core.longPaths true + Stop-Service -name Docker + - name: Check Out Code + uses: actions/checkout@v4 + - name: Build + id: build + uses: ./.github/actions/build + with: + java-version: ${{ matrix.java.version }} + java-early-access: ${{ matrix.java.early-access || 'false' }} + java-toolchain: ${{ matrix.java.toolchain }} + develocity-access-key: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }} + - name: Send Notification + uses: ./.github/actions/send-notification + if: always() + 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) }} diff --git a/.github/workflows/gradle-wrapper-validation.yml b/.github/workflows/gradle-wrapper-validation.yml deleted file mode 100644 index c80a7e5278d0..000000000000 --- a/.github/workflows/gradle-wrapper-validation.yml +++ /dev/null @@ -1,13 +0,0 @@ -name: "Validate Gradle Wrapper" -on: [push, pull_request] - -permissions: - contents: read - -jobs: - validation: - name: "Validation" - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: gradle/wrapper-validation-action@v1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000000..56c1bfbfd32e --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,95 @@ +name: Release +on: + push: + tags: + - v5.3.[0-9]+ +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} +jobs: + build-and-stage-release: + if: ${{ github.repository == 'spring-projects/spring-framework' }} + name: Build and Stage Release + runs-on: ubuntu-latest + steps: + - name: Check Out Code + uses: actions/checkout@v4 + - name: Build and Publish + id: build-and-publish + uses: ./.github/actions/build + with: + develocity-access-key: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }} + 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-framework-{0}', steps.build-and-publish.outputs.version)}} + repository: 'libs-staging-local' + folder: 'deployment-repository' + signing-key: ${{ secrets.GPG_PRIVATE_KEY }} + signing-passphrase: ${{ secrets.GPG_PASSPHRASE }} + artifact-properties: | + /**/spring-*.zip::zip.name=spring-framework,zip.deployed=false + /**/spring-*-docs.zip::zip.type=docs + /**/spring-*-dist.zip::zip.type=dist + /**/spring-*-schema.zip::zip.type=schema + 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: + google-chat-webhook-url: ${{ secrets.GOOGLE_CHAT_WEBHOOK_URL }} + repository-password: ${{ secrets.ARTIFACTORY_PASSWORD }} + repository-username: ${{ secrets.ARTIFACTORY_USERNAME }} + token: ${{ secrets.GH_ACTIONS_REPO_TOKEN }} + sync-to-maven-central: + name: Sync to Maven Central + needs: + - build-and-stage-release + - verify + runs-on: ubuntu-latest + steps: + - name: Check Out Code + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - name: Sync to Maven Central + uses: ./.github/actions/sync-to-maven-central + with: + jfrog-cli-config-token: ${{ secrets.JF_ARTIFACTORY_SPRING }} + ossrh-s01-staging-profile: ${{ secrets.OSSRH_S01_STAGING_PROFILE }} + ossrh-s01-token-password: ${{ secrets.OSSRH_S01_TOKEN_PASSWORD }} + ossrh-s01-token-username: ${{ secrets.OSSRH_S01_TOKEN_USERNAME }} + spring-framework-version: ${{ needs.build-and-stage-release.outputs.version }} + promote-release: + name: Promote Release + needs: + - build-and-stage-release + - sync-to-maven-central + runs-on: ubuntu-latest + steps: + - name: Set up JFrog CLI + uses: jfrog/setup-jfrog-cli@105617d23456a69a92485207c4f28ae12297581d # v4.2.1 + env: + JF_ENV_SPRING: ${{ secrets.JF_ARTIFACTORY_SPRING }} + - name: Promote build + run: jfrog rt build-promote ${{ format('spring-framework-{0}', needs.build-and-stage-release.outputs.version)}} ${{ github.run_number }} libs-release-local + create-github-release: + name: Create GitHub Release + needs: + - build-and-stage-release + - promote-release + runs-on: ubuntu-latest + steps: + - name: Check Out Code + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - name: Create GitHub Release + uses: ./.github/actions/create-github-release + with: + milestone: ${{ needs.build-and-stage-release.outputs.version }} + token: ${{ secrets.GH_ACTIONS_REPO_TOKEN }} diff --git a/.github/workflows/validate-gradle-wrapper.yml b/.github/workflows/validate-gradle-wrapper.yml new file mode 100644 index 000000000000..7a473b3afe72 --- /dev/null +++ b/.github/workflows/validate-gradle-wrapper.yml @@ -0,0 +1,11 @@ +name: "Validate Gradle Wrapper" +on: [push, pull_request] +permissions: + contents: read +jobs: + validation: + name: "Validate Gradle Wrapper" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: gradle/actions/wrapper-validation@d9c87d481d55275bb5441eef3fe0e46805f9ef70 # v3.5.0 diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml new file mode 100644 index 000000000000..86aaec7819cd --- /dev/null +++ b/.github/workflows/verify.yml @@ -0,0 +1,71 @@ +name: Verify +on: + workflow_call: + inputs: + version: + required: true + type: string + staging: + required: false + default: false + type: boolean + secrets: + repository-username: + required: false + repository-password: + required: false + google-chat-webhook-url: + required: true + token: + required: true +jobs: + verify: + name: Verify + runs-on: ubuntu-latest + steps: + - name: Check Out Release Verification Tests + uses: actions/checkout@v4 + with: + repository: spring-projects/spring-framework-release-verification + ref: 'v0.0.2' + token: ${{ secrets.token }} + - name: Check Out Send Notification Action + uses: actions/checkout@v4 + with: + path: spring-framework + sparse-checkout: .github/actions/send-notification + - name: Set Up Java + uses: actions/setup-java@v4 + with: + distribution: 'liberica' + java-version: 8 + - name: Set Up Gradle + uses: gradle/actions/setup-gradle@dbbdc275be76ac10734476cc723d82dfe7ec6eda # v3.4.2 + with: + cache-read-only: false + - name: Configure Gradle Properties + shell: bash + run: | + mkdir -p $HOME/.gradle + echo 'org.gradle.daemon=false' >> $HOME/.gradle/gradle.properties + - name: Run Release Verification Tests + env: + RVT_VERSION: ${{ inputs.version }} + RVT_RELEASE_TYPE: oss + RVT_STAGING: ${{ inputs.staging }} + RVT_OSS_REPOSITORY_USERNAME: ${{ secrets.repository-username }} + RVT_OSS_REPOSITORY_PASSWORD: ${{ secrets.repository-password }} + run: ./gradlew spring-framework-release-verification-tests:test + - name: Upload Build Reports on Failure + uses: actions/upload-artifact@v4 + if: failure() + with: + name: build-reports + path: '**/build/reports/' + - name: Send Notification + uses: ./spring-framework/.github/actions/send-notification + if: failure() + with: + webhook-url: ${{ secrets.google-chat-webhook-url }} + status: ${{ job.status }} + run-name: ${{ format('{0} | Verification | {1}', github.ref_name, inputs.version) }} diff --git a/.gitignore b/.gitignore index c0d1d6dd9e36..7d181d59f98d 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,7 @@ buildSrc/build /integration-tests/build /src/asciidoc/build target/ +/target/ # Eclipse artifacts, including WTP generated manifests .classpath @@ -45,4 +46,6 @@ atlassian-ide-plugin.xml .gradletasknamecache # VS Code -.vscode/ \ No newline at end of file +.vscode/ + +cached-antora-playbook.yml diff --git a/.sdkmanrc b/.sdkmanrc index 7b85e08a2ade..7a4fc046f16a 100644 --- a/.sdkmanrc +++ b/.sdkmanrc @@ -1,3 +1,3 @@ # Enable auto-env through the sdkman_auto_env config # Add key=value pairs of SDKs to use below -java=8.0.352-librca +java=8.0.382-librca diff --git a/README.md b/README.md index 1a1cb7f6f761..f076adcd1101 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Spring Framework [![Build Status](https://ci.spring.io/api/v1/teams/spring-framework/pipelines/spring-framework-5.3.x/jobs/build/badge)](https://ci.spring.io/teams/spring-framework/pipelines/spring-framework-5.3.x?groups=Build") [![Revved up by Gradle Enterprise](https://img.shields.io/badge/Revved%20up%20by-Gradle%20Enterprise-06A0CE?logo=Gradle&labelColor=02303A)](https://ge.spring.io/scans?search.rootProjectNames=spring) +# Spring Framework [![Build Status](https://github.com/spring-projects/spring-framework/actions/workflows/build-and-deploy-snapshot.yml/badge.svg?branch=5.3.x)](https://github.com/spring-projects/spring-framework/actions/workflows/build-and-deploy-snapshot.yml?query=branch%3A5.3.x) [![Revved up by Develocity](https://img.shields.io/badge/Revved%20up%20by-Develocity-06A0CE?logo=Gradle&labelColor=02303A)](https://ge.spring.io/scans?search.rootProjectNames=spring) This is the home of the Spring Framework: the foundation for all [Spring projects](https://spring.io/projects). Collectively the Spring Framework and the family of Spring projects are often referred to simply as "Spring". diff --git a/build.gradle b/build.gradle index dbc25c94dca7..a0d87c6514ed 100644 --- a/build.gradle +++ b/build.gradle @@ -28,18 +28,18 @@ configure(allprojects) { project -> dependencyManagement { imports { mavenBom "com.fasterxml.jackson:jackson-bom:2.12.7" - mavenBom "io.netty:netty-bom:4.1.85.Final" - mavenBom "io.projectreactor:reactor-bom:2020.0.25" + mavenBom "io.netty:netty-bom:4.1.112.Final" + mavenBom "io.projectreactor:reactor-bom:2020.0.47" mavenBom "io.r2dbc:r2dbc-bom:Arabba-SR13" mavenBom "io.rsocket:rsocket-bom:1.1.3" - mavenBom "org.eclipse.jetty:jetty-bom:9.4.49.v20220914" + mavenBom "org.eclipse.jetty:jetty-bom:9.4.54.v20240208" mavenBom "org.jetbrains.kotlin:kotlin-bom:1.5.32" mavenBom "org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.5.2" mavenBom "org.jetbrains.kotlinx:kotlinx-serialization-bom:1.2.2" mavenBom "org.junit:junit-bom:5.8.2" } dependencies { - dependencySet(group: 'org.apache.logging.log4j', version: '2.19.0') { + dependencySet(group: 'org.apache.logging.log4j', version: '2.21.1') { entry 'log4j-api' entry 'log4j-core' entry 'log4j-jul' @@ -67,9 +67,9 @@ configure(allprojects) { project -> dependency "io.reactivex:rxjava:1.3.8" dependency "io.reactivex:rxjava-reactive-streams:1.2.1" dependency "io.reactivex.rxjava2:rxjava:2.2.21" - dependency "io.reactivex.rxjava3:rxjava:3.1.5" - dependency "io.smallrye.reactive:mutiny:1.8.0" - dependency "io.projectreactor.tools:blockhound:1.0.6.RELEASE" + dependency "io.reactivex.rxjava3:rxjava:3.1.8" + dependency "io.smallrye.reactive:mutiny:1.9.0" + dependency "io.projectreactor.tools:blockhound:1.0.8.RELEASE" dependency "com.caucho:hessian:4.0.63" dependency "com.fasterxml:aalto-xml:1.3.1" @@ -92,11 +92,11 @@ configure(allprojects) { project -> entry 'jibx-run' } dependency "org.ogce:xpp3:1.1.6" - dependency "org.yaml:snakeyaml:1.30" + dependency "org.yaml:snakeyaml:1.33" dependency "com.h2database:h2:2.1.214" dependency "com.github.ben-manes.caffeine:caffeine:2.9.3" - dependency "com.github.librepdf:openpdf:1.3.30" + dependency "com.github.librepdf:openpdf:1.3.33" dependency "com.rometools:rome:1.18.0" dependency "commons-io:commons-io:2.5" dependency "io.vavr:vavr:0.10.4" @@ -128,18 +128,18 @@ configure(allprojects) { project -> dependency "org.webjars:webjars-locator-core:0.48" dependency "org.webjars:underscorejs:1.8.3" - dependencySet(group: 'org.apache.tomcat', version: '9.0.68') { + dependencySet(group: 'org.apache.tomcat', version: '9.0.82') { entry 'tomcat-util' entry('tomcat-websocket') { exclude group: "org.apache.tomcat", name: "tomcat-servlet-api" exclude group: "org.apache.tomcat", name: "tomcat-websocket-api" } } - dependencySet(group: 'org.apache.tomcat.embed', version: '9.0.68') { + dependencySet(group: 'org.apache.tomcat.embed', version: '9.0.82') { entry 'tomcat-embed-core' entry 'tomcat-embed-websocket' } - dependencySet(group: 'io.undertow', version: '2.2.21.Final') { + dependencySet(group: 'io.undertow', version: '2.2.29.Final') { entry 'undertow-core' entry('undertow-servlet') { exclude group: "org.jboss.spec.javax.servlet", name: "jboss-servlet-api_4.0_spec" @@ -191,14 +191,14 @@ configure(allprojects) { project -> dependency "org.junit.support:testng-engine:1.0.4" dependency "org.hamcrest:hamcrest:2.2" dependency "org.awaitility:awaitility:3.1.6" - dependency "org.assertj:assertj-core:3.23.0" + dependency "org.assertj:assertj-core:3.24.2" dependencySet(group: 'org.xmlunit', version: '2.9.0') { entry 'xmlunit-assertj' entry('xmlunit-matchers') { exclude group: "org.hamcrest", name: "hamcrest-core" } } - dependencySet(group: 'org.mockito', version: '4.8.1') { + dependencySet(group: 'org.mockito', version: '4.9.0') { // spring-beans tests fail with 4.10+ entry('mockito-core') { exclude group: "org.hamcrest", name: "hamcrest-core" } @@ -206,10 +206,10 @@ configure(allprojects) { project -> } dependency "io.mockk:mockk:1.12.1" - dependency("net.sourceforge.htmlunit:htmlunit:2.66.0") { + dependency("net.sourceforge.htmlunit:htmlunit:2.70.0") { exclude group: "commons-logging", name: "commons-logging" } - dependency("org.seleniumhq.selenium:htmlunit-driver:2.66.0") { + dependency("org.seleniumhq.selenium:htmlunit-driver:2.70.0") { exclude group: "commons-logging", name: "commons-logging" } dependency("org.seleniumhq.selenium:selenium-java:3.141.59") { @@ -238,7 +238,7 @@ configure(allprojects) { project -> dependency "com.ibm.websphere:uow:6.0.2.17" dependency "com.jamonapi:jamon:2.82" dependency "joda-time:joda-time:2.10.13" - dependency "org.eclipse.persistence:org.eclipse.persistence.jpa:2.7.10" + dependency "org.eclipse.persistence:org.eclipse.persistence.jpa:2.7.12" dependency "org.javamoney:moneta:1.3" dependency "com.sun.activation:javax.activation:1.2.0" @@ -340,7 +340,7 @@ configure([rootProject] + javaProjects) { project -> } checkstyle { - toolVersion = "10.4" + toolVersion = "10.12.7" configDirectory.set(rootProject.file("src/checkstyle")) } @@ -362,7 +362,7 @@ configure([rootProject] + javaProjects) { project -> // JSR-305 only used for non-required meta-annotations compileOnly("com.google.code.findbugs:jsr305") testCompileOnly("com.google.code.findbugs:jsr305") - checkstyle("io.spring.javaformat:spring-javaformat-checkstyle:0.0.31") + checkstyle("io.spring.javaformat:spring-javaformat-checkstyle:0.0.42") } ext.javadocLinks = [ @@ -375,11 +375,12 @@ configure([rootProject] + javaProjects) { project -> "https://tiles.apache.org/tiles-request/apidocs/", "https://tiles.apache.org/framework/apidocs/", "https://www.eclipse.org/aspectj/doc/released/aspectj5rt-api/", - "https://www.ehcache.org/apidocs/2.10.4/", - "https://www.quartz-scheduler.org/api/2.3.0/", - "https://fasterxml.github.io/jackson-core/javadoc/2.10/", - "https://fasterxml.github.io/jackson-databind/javadoc/2.10/", - "https://fasterxml.github.io/jackson-dataformat-xml/javadoc/2.10/", + // Temporarily commenting out Ehcache and Quartz since javadoc on JDK 8 cannot access them. + // "https://www.ehcache.org/apidocs/2.10.4/", + // "https://www.quartz-scheduler.org/api/2.3.0/", + "https://www.javadoc.io/doc/com.fasterxml.jackson.core/jackson-core/2.12.7/", + "https://www.javadoc.io/doc/com.fasterxml.jackson.core/jackson-databind/2.12.7/", + "https://www.javadoc.io/doc/com.fasterxml.jackson.dataformat/jackson-dataformat-xml/2.12.7/", "https://hc.apache.org/httpcomponents-client-5.1.x/current/httpclient5/apidocs/", "https://projectreactor.io/docs/test/release/api/", "https://junit.org/junit4/javadoc/4.13.2/", @@ -388,7 +389,8 @@ configure([rootProject] + javaProjects) { project -> // "https://junit.org/junit5/docs/5.8.2/api/", "https://www.reactive-streams.org/reactive-streams-1.0.3-javadoc/", "https://javadoc.io/static/io.rsocket/rsocket-core/1.1.1/", - "https://r2dbc.io/spec/0.8.5.RELEASE/api/", + // Temporarily commenting out R2DBC since javadoc on JDK 8 cannot access it. + // "https://r2dbc.io/spec/0.8.5.RELEASE/api/", // The external Javadoc link for JSR 305 must come last to ensure that types from // JSR 250 (such as @PostConstruct) are still supported. This is due to the fact // that JSR 250 and JSR 305 both define types in javax.annotation, which results diff --git a/ci/README.adoc b/ci/README.adoc deleted file mode 100644 index cb617637d9b4..000000000000 --- a/ci/README.adoc +++ /dev/null @@ -1,57 +0,0 @@ -== Spring Framework Concourse pipeline - -The Spring Framework uses https://concourse-ci.org/[Concourse] for its CI build and other automated tasks. -The Spring team has a dedicated Concourse instance available at https://ci.spring.io with a build pipeline -for https://ci.spring.io/teams/spring-framework/pipelines/spring-framework-5.3.x[Spring Framework 5.3.x]. - -=== Setting up your development environment - -If you're part of the Spring Framework project on GitHub, you can get access to CI management features. -First, you need to go to https://ci.spring.io and install the client CLI for your platform (see bottom right of the screen). - -You can then login with the instance using: - -[source] ----- -$ fly -t spring login -n spring-framework -c https://ci.spring.io ----- - -Once logged in, you should get something like: - -[source] ----- -$ fly ts -name url team expiry -spring https://ci.spring.io spring-framework Wed, 25 Mar 2020 17:45:26 UTC ----- - -=== Pipeline configuration and structure - -The build pipelines are described in `pipeline.yml` file. - -This file is listing Concourse resources, i.e. build inputs and outputs such as container images, artifact repositories, source repositories, notification services, etc. - -It also describes jobs (a job is a sequence of inputs, tasks and outputs); jobs are organized by groups. - -The `pipeline.yml` definition contains `((parameters))` which are loaded from the `parameters.yml` file or from our https://docs.cloudfoundry.org/credhub/[credhub instance]. - -You'll find in this folder the following resources: - -* `pipeline.yml` the build pipeline -* `parameters.yml` the build parameters used for the pipeline -* `images/` holds the container images definitions used in this pipeline -* `scripts/` holds the build scripts that ship within the CI container images -* `tasks` contains the task definitions used in the main `pipeline.yml` - -=== Updating the build pipeline - -Updating files on the repository is not enough to update the build pipeline, as changes need to be applied. - -The pipeline can be deployed using the following command: - -[source] ----- -$ fly -t spring set-pipeline -p spring-framework-5.3.x -c ci/pipeline.yml -l ci/parameters.yml ----- - -NOTE: This assumes that you have credhub integration configured with the appropriate secrets. diff --git a/ci/config/release-scripts.yml b/ci/config/release-scripts.yml deleted file mode 100644 index d31f8cba00dc..000000000000 --- a/ci/config/release-scripts.yml +++ /dev/null @@ -1,10 +0,0 @@ -logging: - level: - io.spring.concourse: DEBUG -spring: - main: - banner-mode: off -sonatype: - exclude: - - 'build-info\.json' - - '.*\.zip' diff --git a/ci/images/README.adoc b/ci/images/README.adoc deleted file mode 100644 index 6da9addd9ca5..000000000000 --- a/ci/images/README.adoc +++ /dev/null @@ -1,21 +0,0 @@ -== CI Images - -These images are used by CI to run the actual builds. - -To build the image locally run the following from this directory: - ----- -$ docker build --no-cache -f /Dockerfile . ----- - -For example - ----- -$ docker build --no-cache -f spring-framework-ci-image/Dockerfile . ----- - -To test run: - ----- -$ docker run -it --entrypoint /bin/bash ----- diff --git a/ci/images/ci-image/Dockerfile b/ci/images/ci-image/Dockerfile deleted file mode 100644 index 0548a7c6b230..000000000000 --- a/ci/images/ci-image/Dockerfile +++ /dev/null @@ -1,11 +0,0 @@ -FROM ubuntu:focal-20220922 - -ADD setup.sh /setup.sh -ADD get-jdk-url.sh /get-jdk-url.sh -RUN ./setup.sh java8 - -ENV JAVA_HOME /opt/openjdk/java8 -ENV JDK11 /opt/openjdk/java11 -ENV JDK17 /opt/openjdk/java17 - -ENV PATH $JAVA_HOME/bin:$PATH diff --git a/ci/images/get-jdk-url.sh b/ci/images/get-jdk-url.sh deleted file mode 100755 index ecf692e51726..000000000000 --- a/ci/images/get-jdk-url.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/bin/bash -set -e - -case "$1" in - java8) - echo "https://github.com/bell-sw/Liberica/releases/download/8u345+1/bellsoft-jdk8u345+1-linux-amd64.tar.gz" - ;; - java11) - echo "https://github.com/bell-sw/Liberica/releases/download/11.0.16+8/bellsoft-jdk11.0.16+8-linux-amd64.tar.gz" - ;; - java17) - echo "https://github.com/bell-sw/Liberica/releases/download/17.0.4+8/bellsoft-jdk17.0.4+8-linux-amd64.tar.gz" - ;; - java18) - echo "https://github.com/bell-sw/Liberica/releases/download/18.0.2+10/bellsoft-jdk18.0.2+10-linux-amd64.tar.gz" - ;; - *) - echo $"Unknown java version" - exit 1 -esac diff --git a/ci/images/setup.sh b/ci/images/setup.sh deleted file mode 100755 index f7add2e15cb0..000000000000 --- a/ci/images/setup.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/bin/bash -set -ex - -########################################################### -# UTILS -########################################################### - -export DEBIAN_FRONTEND=noninteractive -apt-get update -apt-get install --no-install-recommends -y tzdata ca-certificates net-tools libxml2-utils git curl libudev1 libxml2-utils iptables iproute2 jq fontconfig -ln -fs /usr/share/zoneinfo/UTC /etc/localtime -dpkg-reconfigure --frontend noninteractive tzdata -rm -rf /var/lib/apt/lists/* - -curl https://raw.githubusercontent.com/spring-io/concourse-java-scripts/v0.0.4/concourse-java.sh > /opt/concourse-java.sh - -########################################################### -# JAVA -########################################################### - -mkdir -p /opt/openjdk -pushd /opt/openjdk > /dev/null -for jdk in java8 java11 java17 -do - JDK_URL=$( /get-jdk-url.sh $jdk ) - mkdir $jdk - pushd $jdk > /dev/null - curl -L ${JDK_URL} | tar zx --strip-components=1 - test -f bin/java - test -f bin/javac - popd > /dev/null -done -popd - -########################################################### -# GRADLE ENTERPRISE -########################################################### -cd / -mkdir ~/.gradle -echo 'systemProp.user.name=concourse' > ~/.gradle/gradle.properties diff --git a/ci/parameters.yml b/ci/parameters.yml deleted file mode 100644 index 2f970570a230..000000000000 --- a/ci/parameters.yml +++ /dev/null @@ -1,14 +0,0 @@ -github-repo: "https://github.com/spring-projects/spring-framework.git" -github-repo-name: "spring-projects/spring-framework" -sonatype-staging-profile: "org.springframework" -docker-hub-organization: "springci" -artifactory-server: "https://repo.spring.io" -branch: "5.3.x" -milestone: "5.3.x" -build-name: "spring-framework" -pipeline-name: "spring-framework" -concourse-url: "https://ci.spring.io" -registry-mirror-host: docker.repo.spring.io -registry-mirror-username: ((artifactory-username)) -registry-mirror-password: ((artifactory-password)) -task-timeout: 1h00m diff --git a/ci/pipeline.yml b/ci/pipeline.yml deleted file mode 100644 index 5f7e91ba4661..000000000000 --- a/ci/pipeline.yml +++ /dev/null @@ -1,480 +0,0 @@ -anchors: - git-repo-resource-source: &git-repo-resource-source - uri: ((github-repo)) - username: ((github-username)) - password: ((github-ci-release-token)) - branch: ((branch)) - gradle-enterprise-task-params: &gradle-enterprise-task-params - GRADLE_ENTERPRISE_ACCESS_KEY: ((gradle_enterprise_secret_access_key)) - GRADLE_ENTERPRISE_CACHE_USERNAME: ((gradle_enterprise_cache_user.username)) - GRADLE_ENTERPRISE_CACHE_PASSWORD: ((gradle_enterprise_cache_user.password)) - sonatype-task-params: &sonatype-task-params - SONATYPE_USERNAME: ((sonatype-username)) - SONATYPE_PASSWORD: ((sonatype-password)) - SONATYPE_URL: ((sonatype-url)) - SONATYPE_STAGING_PROFILE: ((sonatype-staging-profile)) - artifactory-task-params: &artifactory-task-params - ARTIFACTORY_SERVER: ((artifactory-server)) - ARTIFACTORY_USERNAME: ((artifactory-username)) - ARTIFACTORY_PASSWORD: ((artifactory-password)) - build-project-task-params: &build-project-task-params - BRANCH: ((branch)) - <<: *gradle-enterprise-task-params - docker-resource-source: &docker-resource-source - username: ((docker-hub-username)) - password: ((docker-hub-password)) - tag: ((milestone)) - registry-mirror-vars: ®istry-mirror-vars - registry-mirror-host: ((registry-mirror-host)) - registry-mirror-username: ((registry-mirror-username)) - registry-mirror-password: ((registry-mirror-password)) - slack-fail-params: &slack-fail-params - text: > - :concourse-failed: - [$TEXT_FILE_CONTENT] - text_file: git-repo/build/build-scan-uri.txt - silent: true - icon_emoji: ":concourse:" - username: concourse-ci - changelog-task-params: &changelog-task-params - name: generated-changelog/tag - tag: generated-changelog/tag - body: generated-changelog/changelog.md - github-task-params: &github-task-params - GITHUB_USERNAME: ((github-username)) - GITHUB_TOKEN: ((github-ci-release-token)) - -resource_types: -- name: registry-image - type: registry-image - source: - repository: concourse/registry-image-resource - tag: 1.5.0 -- name: artifactory-resource - type: registry-image - source: - repository: springio/artifactory-resource - tag: 0.0.17 -- name: github-release - type: registry-image - source: - repository: concourse/github-release-resource - tag: 1.5.5 -- name: github-status-resource - type: registry-image - source: - repository: dpb587/github-status-resource - tag: master -- name: pull-request - type: registry-image - source: - repository: teliaoss/github-pr-resource - tag: v0.23.0 -- name: slack-notification - type: registry-image - source: - repository: cfcommunity/slack-notification-resource - tag: latest -resources: -- name: git-repo - type: git - icon: github - source: - <<: *git-repo-resource-source -- name: every-morning - type: time - icon: alarm - source: - start: 8:00 AM - stop: 9:00 AM - location: Europe/Vienna -- name: ci-images-git-repo - type: git - icon: github - source: - uri: ((github-repo)) - branch: ((branch)) - paths: ["ci/images/*"] -- name: ci-image - type: registry-image - icon: docker - source: - <<: *docker-resource-source - repository: ((docker-hub-organization))/spring-framework-ci -- name: artifactory-repo - type: artifactory-resource - icon: package-variant - source: - uri: ((artifactory-server)) - username: ((artifactory-username)) - password: ((artifactory-password)) - build_name: ((build-name)) -- name: git-pull-request - type: pull-request - icon: source-pull - source: - access_token: ((github-ci-pull-request-token)) - repository: ((github-repo-name)) - base_branch: ((branch)) - ignore_paths: ["ci/*"] -- name: repo-status-build - type: github-status-resource - icon: eye-check-outline - source: - repository: ((github-repo-name)) - access_token: ((github-ci-status-token)) - branch: ((branch)) - context: build -- name: repo-status-jdk11-build - type: github-status-resource - icon: eye-check-outline - source: - repository: ((github-repo-name)) - access_token: ((github-ci-status-token)) - branch: ((branch)) - context: jdk11-build -- name: repo-status-jdk17-build - type: github-status-resource - icon: eye-check-outline - source: - repository: ((github-repo-name)) - access_token: ((github-ci-status-token)) - branch: ((branch)) - context: jdk17-build -- name: slack-alert - type: slack-notification - icon: slack - source: - url: ((slack-webhook-url)) -- name: github-pre-release - type: github-release - icon: briefcase-download-outline - source: - owner: spring-projects - repository: spring-framework - access_token: ((github-ci-release-token)) - pre_release: true - release: false -- name: github-release - type: github-release - icon: briefcase-download - source: - owner: spring-projects - repository: spring-framework - access_token: ((github-ci-release-token)) - pre_release: false -jobs: -- name: build-ci-images - plan: - - get: git-repo - - get: ci-images-git-repo - trigger: true - - task: build-ci-image - privileged: true - file: git-repo/ci/tasks/build-ci-image.yml - output_mapping: - image: ci-image - vars: - ci-image-name: ci-image - <<: *registry-mirror-vars - - put: ci-image - params: - image: ci-image/image.tar -- name: build - serial: true - public: true - plan: - - get: ci-image - - get: git-repo - trigger: true - - put: repo-status-build - params: { state: "pending", commit: "git-repo" } - - do: - - task: build-project - image: ci-image - file: git-repo/ci/tasks/build-project.yml - privileged: true - timeout: ((task-timeout)) - params: - <<: *build-project-task-params - on_failure: - do: - - put: repo-status-build - params: { state: "failure", commit: "git-repo" } - - put: slack-alert - params: - <<: *slack-fail-params - - put: repo-status-build - params: { state: "success", commit: "git-repo" } - - put: artifactory-repo - params: &artifactory-params - signing_key: ((signing-key)) - signing_passphrase: ((signing-passphrase)) - repo: libs-snapshot-local - folder: distribution-repository - build_uri: "https://ci.spring.io/teams/${BUILD_TEAM_NAME}/pipelines/${BUILD_PIPELINE_NAME}/jobs/${BUILD_JOB_NAME}/builds/${BUILD_NAME}" - build_number: "${BUILD_PIPELINE_NAME}-${BUILD_JOB_NAME}-${BUILD_NAME}" - disable_checksum_uploads: true - threads: 8 - artifact_set: - - include: - - "/**/spring-*.zip" - properties: - "zip.name": "spring-framework" - "zip.displayname": "Spring Framework" - "zip.deployed": "false" - - include: - - "/**/spring-*-docs.zip" - properties: - "zip.type": "docs" - - include: - - "/**/spring-*-dist.zip" - properties: - "zip.type": "dist" - - include: - - "/**/spring-*-schema.zip" - properties: - "zip.type": "schema" - get_params: - threads: 8 -- name: jdk11-build - serial: true - public: true - plan: - - get: ci-image - - get: git-repo - - get: every-morning - trigger: true - - put: repo-status-jdk11-build - params: { state: "pending", commit: "git-repo" } - - do: - - task: check-project - image: ci-image - file: git-repo/ci/tasks/check-project.yml - privileged: true - timeout: ((task-timeout)) - params: - TEST_TOOLCHAIN: 11 - <<: *build-project-task-params - on_failure: - do: - - put: repo-status-jdk11-build - params: { state: "failure", commit: "git-repo" } - - put: slack-alert - params: - <<: *slack-fail-params - - put: repo-status-jdk11-build - params: { state: "success", commit: "git-repo" } -- name: jdk17-build - serial: true - public: true - plan: - - get: ci-image - - get: git-repo - - get: every-morning - trigger: true - - put: repo-status-jdk17-build - params: { state: "pending", commit: "git-repo" } - - do: - - task: check-project - image: ci-image - file: git-repo/ci/tasks/check-project.yml - privileged: true - timeout: ((task-timeout)) - params: - TEST_TOOLCHAIN: 15 - <<: *build-project-task-params - on_failure: - do: - - put: repo-status-jdk17-build - params: { state: "failure", commit: "git-repo" } - - put: slack-alert - params: - <<: *slack-fail-params - - put: repo-status-jdk17-build - params: { state: "success", commit: "git-repo" } -- name: build-pull-requests - serial: true - public: true - plan: - - get: ci-image - - get: git-repo - resource: git-pull-request - trigger: true - version: every - - do: - - put: git-pull-request - params: - path: git-repo - status: pending - - task: build-pr - image: ci-image - file: git-repo/ci/tasks/build-pr.yml - privileged: true - timeout: ((task-timeout)) - params: - BRANCH: ((branch)) - on_success: - put: git-pull-request - params: - path: git-repo - status: success - on_failure: - put: git-pull-request - params: - path: git-repo - status: failure -- name: stage-milestone - serial: true - plan: - - get: ci-image - - get: git-repo - trigger: false - - task: stage - image: ci-image - file: git-repo/ci/tasks/stage-version.yml - params: - RELEASE_TYPE: M - <<: *gradle-enterprise-task-params - - put: artifactory-repo - params: - <<: *artifactory-params - repo: libs-staging-local - - put: git-repo - params: - repository: stage-git-repo -- name: promote-milestone - serial: true - plan: - - get: ci-image - - get: git-repo - trigger: false - - get: artifactory-repo - trigger: false - passed: [stage-milestone] - params: - download_artifacts: false - save_build_info: true - - task: promote - file: git-repo/ci/tasks/promote-version.yml - params: - RELEASE_TYPE: M - <<: *artifactory-task-params - - task: generate-changelog - file: git-repo/ci/tasks/generate-changelog.yml - params: - RELEASE_TYPE: M - <<: *github-task-params - - put: github-pre-release - params: - <<: *changelog-task-params -- name: stage-rc - serial: true - plan: - - get: ci-image - - get: git-repo - trigger: false - - task: stage - image: ci-image - file: git-repo/ci/tasks/stage-version.yml - params: - RELEASE_TYPE: RC - <<: *gradle-enterprise-task-params - - put: artifactory-repo - params: - <<: *artifactory-params - repo: libs-staging-local - - put: git-repo - params: - repository: stage-git-repo -- name: promote-rc - serial: true - plan: - - get: ci-image - - get: git-repo - trigger: false - - get: artifactory-repo - trigger: false - passed: [stage-rc] - params: - download_artifacts: false - save_build_info: true - - task: promote - file: git-repo/ci/tasks/promote-version.yml - params: - RELEASE_TYPE: RC - <<: *artifactory-task-params - - task: generate-changelog - file: git-repo/ci/tasks/generate-changelog.yml - params: - RELEASE_TYPE: RC - <<: *github-task-params - - put: github-pre-release - params: - <<: *changelog-task-params -- name: stage-release - serial: true - plan: - - get: ci-image - - get: git-repo - trigger: false - - task: stage - image: ci-image - file: git-repo/ci/tasks/stage-version.yml - params: - RELEASE_TYPE: RELEASE - <<: *gradle-enterprise-task-params - - put: artifactory-repo - params: - <<: *artifactory-params - repo: libs-staging-local - - put: git-repo - params: - repository: stage-git-repo -- name: promote-release - serial: true - plan: - - get: ci-image - - get: git-repo - trigger: false - - get: artifactory-repo - trigger: false - passed: [stage-release] - params: - download_artifacts: true - save_build_info: true - - task: promote - file: git-repo/ci/tasks/promote-version.yml - params: - RELEASE_TYPE: RELEASE - <<: *artifactory-task-params - <<: *sonatype-task-params -- name: create-github-release - serial: true - plan: - - get: ci-image - - get: git-repo - - get: artifactory-repo - trigger: true - passed: [promote-release] - params: - download_artifacts: false - save_build_info: true - - task: generate-changelog - file: git-repo/ci/tasks/generate-changelog.yml - params: - RELEASE_TYPE: RELEASE - <<: *github-task-params - - put: github-release - params: - <<: *changelog-task-params - -groups: -- name: "builds" - jobs: ["build", "jdk11-build", "jdk17-build"] -- name: "releases" - jobs: ["stage-milestone", "stage-rc", "stage-release", "promote-milestone", "promote-rc", "promote-release", "create-github-release"] -- name: "ci-images" - jobs: ["build-ci-images"] -- name: "pull-requests" - jobs: [ "build-pull-requests" ] diff --git a/ci/scripts/build-pr.sh b/ci/scripts/build-pr.sh deleted file mode 100755 index 94c4e8df65b4..000000000000 --- a/ci/scripts/build-pr.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash -set -e - -source $(dirname $0)/common.sh - -pushd git-repo > /dev/null -./gradlew -Dorg.gradle.internal.launcher.welcomeMessageEnabled=false --no-daemon --max-workers=4 check -popd > /dev/null diff --git a/ci/scripts/build-project.sh b/ci/scripts/build-project.sh deleted file mode 100755 index 3844d1a3ddb4..000000000000 --- a/ci/scripts/build-project.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash -set -e - -source $(dirname $0)/common.sh -repository=$(pwd)/distribution-repository - -pushd git-repo > /dev/null -./gradlew -Dorg.gradle.internal.launcher.welcomeMessageEnabled=false --no-daemon --max-workers=4 -PdeploymentRepository=${repository} build publishAllPublicationsToDeploymentRepository -popd > /dev/null diff --git a/ci/scripts/check-project.sh b/ci/scripts/check-project.sh deleted file mode 100755 index 7f6ca04cea9b..000000000000 --- a/ci/scripts/check-project.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash -set -e - -source $(dirname $0)/common.sh - -pushd git-repo > /dev/null -./gradlew -Dorg.gradle.internal.launcher.welcomeMessageEnabled=false -Porg.gradle.java.installations.fromEnv=JDK11,JDK15 \ - -PmainToolchain=${MAIN_TOOLCHAIN} -PtestToolchain=${TEST_TOOLCHAIN} --no-daemon --max-workers=4 check -popd > /dev/null diff --git a/ci/scripts/common.sh b/ci/scripts/common.sh deleted file mode 100644 index 1accaa616732..000000000000 --- a/ci/scripts/common.sh +++ /dev/null @@ -1,2 +0,0 @@ -source /opt/concourse-java.sh -setup_symlinks \ No newline at end of file diff --git a/ci/scripts/generate-changelog.sh b/ci/scripts/generate-changelog.sh deleted file mode 100755 index d3d2b97e5dba..000000000000 --- a/ci/scripts/generate-changelog.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash -set -e - -CONFIG_DIR=git-repo/ci/config -version=$( cat artifactory-repo/build-info.json | jq -r '.buildInfo.modules[0].id' | sed 's/.*:.*:\(.*\)/\1/' ) - -java -jar /github-changelog-generator.jar \ - --spring.config.location=${CONFIG_DIR}/changelog-generator.yml \ - ${version} generated-changelog/changelog.md - -echo ${version} > generated-changelog/version -echo v${version} > generated-changelog/tag diff --git a/ci/scripts/promote-version.sh b/ci/scripts/promote-version.sh deleted file mode 100755 index 2b932f5f20f9..000000000000 --- a/ci/scripts/promote-version.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/bash - -source $(dirname $0)/common.sh -CONFIG_DIR=git-repo/ci/config - -version=$( cat artifactory-repo/build-info.json | jq -r '.buildInfo.modules[0].id' | sed 's/.*:.*:\(.*\)/\1/' ) -export BUILD_INFO_LOCATION=$(pwd)/artifactory-repo/build-info.json - -java -jar /concourse-release-scripts.jar \ - --spring.config.location=${CONFIG_DIR}/release-scripts.yml \ - publishToCentral $RELEASE_TYPE $BUILD_INFO_LOCATION artifactory-repo || { exit 1; } - -java -jar /concourse-release-scripts.jar \ - --spring.config.location=${CONFIG_DIR}/release-scripts.yml \ - promote $RELEASE_TYPE $BUILD_INFO_LOCATION || { exit 1; } - -echo "Promotion complete" -echo $version > version/version diff --git a/ci/scripts/stage-version.sh b/ci/scripts/stage-version.sh deleted file mode 100755 index 73c57755451c..000000000000 --- a/ci/scripts/stage-version.sh +++ /dev/null @@ -1,50 +0,0 @@ -#!/bin/bash -set -e - -source $(dirname $0)/common.sh -repository=$(pwd)/distribution-repository - -pushd git-repo > /dev/null -git fetch --tags --all > /dev/null -popd > /dev/null - -git clone git-repo stage-git-repo > /dev/null - -pushd stage-git-repo > /dev/null - -snapshotVersion=$( awk -F '=' '$1 == "version" { print $2 }' gradle.properties ) -if [[ $RELEASE_TYPE = "M" ]]; then - stageVersion=$( get_next_milestone_release $snapshotVersion) - nextVersion=$snapshotVersion -elif [[ $RELEASE_TYPE = "RC" ]]; then - stageVersion=$( get_next_rc_release $snapshotVersion) - nextVersion=$snapshotVersion -elif [[ $RELEASE_TYPE = "RELEASE" ]]; then - stageVersion=$( get_next_release $snapshotVersion) - nextVersion=$( bump_version_number $snapshotVersion) -else - echo "Unknown release type $RELEASE_TYPE" >&2; exit 1; -fi - -echo "Staging $stageVersion (next version will be $nextVersion)" -sed -i "s/version=$snapshotVersion/version=$stageVersion/" gradle.properties - -git config user.name "Spring Builds" > /dev/null -git config user.email "spring-builds@users.noreply.github.com" > /dev/null -git add gradle.properties > /dev/null -git commit -m"Release v$stageVersion" > /dev/null -git tag -a "v$stageVersion" -m"Release v$stageVersion" > /dev/null - -./gradlew --no-daemon --max-workers=4 -PdeploymentRepository=${repository} build publishAllPublicationsToDeploymentRepository - -git reset --hard HEAD^ > /dev/null -if [[ $nextVersion != $snapshotVersion ]]; then - echo "Setting next development version (v$nextVersion)" - sed -i "s/version=$snapshotVersion/version=$nextVersion/" gradle.properties - git add gradle.properties > /dev/null - git commit -m"Next development version (v$nextVersion)" > /dev/null -fi; - -echo "Staging Complete" - -popd > /dev/null diff --git a/ci/tasks/build-ci-image.yml b/ci/tasks/build-ci-image.yml deleted file mode 100644 index 2392595349e9..000000000000 --- a/ci/tasks/build-ci-image.yml +++ /dev/null @@ -1,31 +0,0 @@ ---- -platform: linux -image_resource: - type: registry-image - source: - repository: concourse/oci-build-task - tag: 0.9.1 - registry_mirror: - host: ((registry-mirror-host)) - username: ((registry-mirror-username)) - password: ((registry-mirror-password)) -inputs: - - name: ci-images-git-repo -outputs: - - name: image -caches: - - path: ci-image-cache -params: - CONTEXT: ci-images-git-repo/ci/images - DOCKERFILE: ci-images-git-repo/ci/images/ci-image/Dockerfile - DOCKER_HUB_AUTH: ((docker-hub-auth)) -run: - path: /bin/sh - args: - - "-c" - - | - mkdir -p /root/.docker - cat > /root/.docker/config.json < } settings.gradle.projectsLoaded { - gradleEnterprise { + develocity { buildScan { File buildDir = settings.gradle.rootProject.getBuildDir() buildDir.mkdirs() diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJAdviceParameterNameDiscoverer.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJAdviceParameterNameDiscoverer.java index 60a45977513e..b9b1f463e325 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJAdviceParameterNameDiscoverer.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJAdviceParameterNameDiscoverer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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. @@ -38,6 +38,14 @@ * for an advice method from the pointcut expression, returning, and throwing clauses. * If an unambiguous interpretation is not available, it returns {@code null}. * + *

Algorithm Summary

+ *

If an unambiguous binding can be deduced, then it is. + * If the advice requirements cannot possibly be satisfied, then {@code null} + * is returned. By setting the {@link #setRaiseExceptions(boolean) raiseExceptions} + * property to {@code true}, descriptive exceptions will be thrown instead of + * returning {@code null} in the case that the parameter names cannot be discovered. + * + *

Algorithm Details

*

This class interprets arguments in the following way: *

    *
  1. If the first parameter of the method is of type {@link JoinPoint} @@ -65,15 +73,15 @@ * zero we proceed to the next stage. If {@code a} > 1 then an * {@code AmbiguousBindingException} is raised. If {@code a} == 1, * and there are no unbound arguments of type {@code Annotation+}, - * then an {@code IllegalArgumentException} is raised. if there is + * then an {@code IllegalArgumentException} is raised. If there is * exactly one such argument, then the corresponding parameter name is * assigned the value from the pointcut expression.
  2. - *
  3. If a returningName has been set, and there are no unbound arguments + *
  4. If a {@code returningName} has been set, and there are no unbound arguments * then an {@code IllegalArgumentException} is raised. If there is * more than one unbound argument then an * {@code AmbiguousBindingException} is raised. If there is exactly * one unbound argument then the corresponding parameter name is assigned - * the value <returningName>.
  5. + * the value of the {@code returningName}. *
  6. If there remain unbound arguments, then the pointcut expression is * examined once more for {@code this}, {@code target}, and * {@code args} pointcut expressions used in the binding form (binding @@ -99,20 +107,12 @@ *

    The behavior on raising an {@code IllegalArgumentException} or * {@code AmbiguousBindingException} is configurable to allow this discoverer * to be used as part of a chain-of-responsibility. By default the condition will - * be logged and the {@code getParameterNames(..)} method will simply return + * be logged and the {@link #getParameterNames(Method)} method will simply return * {@code null}. If the {@link #setRaiseExceptions(boolean) raiseExceptions} * property is set to {@code true}, the conditions will be thrown as * {@code IllegalArgumentException} and {@code AmbiguousBindingException}, * respectively. * - *

    Was that perfectly clear? ;) - * - *

    Short version: If an unambiguous binding can be deduced, then it is. - * If the advice requirements cannot possibly be satisfied, then {@code null} - * is returned. By setting the {@link #setRaiseExceptions(boolean) raiseExceptions} - * property to {@code true}, descriptive exceptions will be thrown instead of - * returning {@code null} in the case that the parameter names cannot be discovered. - * * @author Adrian Colyer * @author Juergen Hoeller * @since 2.0 @@ -158,7 +158,7 @@ public class AspectJAdviceParameterNameDiscoverer implements ParameterNameDiscov /** The pointcut expression associated with the advice, as a simple String. */ @Nullable - private String pointcutExpression; + private final String pointcutExpression; private boolean raiseExceptions; @@ -197,7 +197,7 @@ public void setRaiseExceptions(boolean raiseExceptions) { /** * If {@code afterReturning} advice binds the return value, the - * returning variable name must be specified. + * {@code returning} variable name must be specified. * @param returningName the name of the returning variable */ public void setReturningName(@Nullable String returningName) { @@ -206,18 +206,17 @@ public void setReturningName(@Nullable String returningName) { /** * If {@code afterThrowing} advice binds the thrown value, the - * throwing variable name must be specified. + * {@code throwing} variable name must be specified. * @param throwingName the name of the throwing variable */ public void setThrowingName(@Nullable String throwingName) { this.throwingName = throwingName; } - /** * Deduce the parameter names for an advice method. - *

    See the {@link AspectJAdviceParameterNameDiscoverer class level javadoc} - * for this class for details of the algorithm used. + *

    See the {@link AspectJAdviceParameterNameDiscoverer class-level javadoc} + * for this class for details on the algorithm used. * @param method the target {@link Method} * @return the parameter names */ @@ -316,13 +315,13 @@ public String[] getParameterNames(Constructor ctor) { } - private void bindParameterName(int index, String name) { + private void bindParameterName(int index, @Nullable String name) { this.parameterNameBindings[index] = name; this.numberOfRemainingUnboundArguments--; } /** - * If the first parameter is of type JoinPoint or ProceedingJoinPoint,bind "thisJoinPoint" as + * If the first parameter is of type JoinPoint or ProceedingJoinPoint, bind "thisJoinPoint" as * parameter name and return true, else return false. */ private boolean maybeBindThisJoinPoint() { @@ -367,8 +366,8 @@ private void maybeBindThrowingVariable() { } if (throwableIndex == -1) { - throw new IllegalStateException("Binding of throwing parameter '" + this.throwingName - + "' could not be completed as no available arguments are a subtype of Throwable"); + throw new IllegalStateException("Binding of throwing parameter '" + this.throwingName + + "' could not be completed as no available arguments are a subtype of Throwable"); } else { bindParameterName(throwableIndex, this.throwingName); @@ -400,7 +399,6 @@ private void maybeBindReturningVariable() { } } - /** * Parse the string pointcut expression looking for: * @this, @target, @args, @within, @withincode, @annotation. @@ -465,7 +463,7 @@ else if (numAnnotationSlots == 1) { } } - /* + /** * If the token starts meets Java identifier conventions, it's in. */ @Nullable @@ -533,7 +531,6 @@ else if (tokens[i].equals("args") || tokens[i].startsWith("args(")) { } } - if (varNames.size() > 1) { throw new AmbiguousBindingException("Found " + varNames.size() + " candidate this(), target() or args() variables but only one unbound argument slot"); @@ -609,7 +606,7 @@ else if (varNames.size() == 1) { // else varNames.size must be 0 and we have nothing to bind. } - /* + /** * We've found the start of a binding pointcut at the given index into the * token array. Now we need to extract the pointcut body and return it. */ @@ -709,7 +706,7 @@ private boolean alreadyBound(String varName) { return false; } - /* + /** * Return {@code true} if the given argument type is a subclass * of the given supertype. */ @@ -737,7 +734,7 @@ private int countNumberOfUnboundPrimitiveArguments() { return count; } - /* + /** * Find the argument index with the given type, and bind the given * {@code varName} in that position. */ diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJExpressionPointcut.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJExpressionPointcut.java index b2efee6caa2c..0dd07c08af99 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJExpressionPointcut.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJExpressionPointcut.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-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.IOException; import java.io.ObjectInputStream; +import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.Arrays; @@ -42,6 +43,7 @@ import org.aspectj.weaver.tools.PointcutParser; import org.aspectj.weaver.tools.PointcutPrimitive; import org.aspectj.weaver.tools.ShadowMatch; +import org.aspectj.weaver.tools.UnsupportedPointcutPrimitiveException; import org.springframework.aop.ClassFilter; import org.springframework.aop.IntroductionAwareMethodMatcher; @@ -85,6 +87,8 @@ public class AspectJExpressionPointcut extends AbstractExpressionPointcut implements ClassFilter, IntroductionAwareMethodMatcher, BeanFactoryAware { + private static final String AJC_MAGIC = "ajc$"; + private static final Set SUPPORTED_PRIMITIVES = new HashSet<>(); static { @@ -106,6 +110,8 @@ public class AspectJExpressionPointcut extends AbstractExpressionPointcut @Nullable private Class pointcutDeclarationScope; + private boolean aspectCompiledByAjc; + private String[] pointcutParameterNames = new String[0]; private Class[] pointcutParameterTypes = new Class[0]; @@ -119,6 +125,8 @@ public class AspectJExpressionPointcut extends AbstractExpressionPointcut @Nullable private transient PointcutExpression pointcutExpression; + private transient boolean pointcutParsingFailed = false; + private transient Map shadowMatchCache = new ConcurrentHashMap<>(32); @@ -135,7 +143,7 @@ public AspectJExpressionPointcut() { * @param paramTypes the parameter types for the pointcut */ public AspectJExpressionPointcut(Class declarationScope, String[] paramNames, Class[] paramTypes) { - this.pointcutDeclarationScope = declarationScope; + setPointcutDeclarationScope(declarationScope); if (paramNames.length != paramTypes.length) { throw new IllegalStateException( "Number of pointcut parameter names must match number of pointcut parameter types"); @@ -150,6 +158,7 @@ public AspectJExpressionPointcut(Class declarationScope, String[] paramNames, */ public void setPointcutDeclarationScope(Class pointcutDeclarationScope) { this.pointcutDeclarationScope = pointcutDeclarationScope; + this.aspectCompiledByAjc = compiledByAjc(pointcutDeclarationScope); } /** @@ -174,25 +183,30 @@ public void setBeanFactory(BeanFactory beanFactory) { @Override public ClassFilter getClassFilter() { - obtainPointcutExpression(); + checkExpression(); return this; } @Override public MethodMatcher getMethodMatcher() { - obtainPointcutExpression(); + checkExpression(); return this; } /** - * Check whether this pointcut is ready to match, - * lazily building the underlying AspectJ pointcut expression. + * Check whether this pointcut is ready to match. */ - private PointcutExpression obtainPointcutExpression() { + private void checkExpression() { if (getExpression() == null) { throw new IllegalStateException("Must set property 'expression' before attempting to match"); } + } + + /** + * Lazily build the underlying AspectJ pointcut expression. + */ + private PointcutExpression obtainPointcutExpression() { if (this.pointcutExpression == null) { this.pointcutClassLoader = determinePointcutClassLoader(); this.pointcutExpression = buildPointcutExpression(this.pointcutClassLoader); @@ -248,8 +262,8 @@ private PointcutParser initializePointcutParser(@Nullable ClassLoader classLoade /** * If a pointcut expression has been specified in XML, the user cannot - * write {@code and} as "&&" (though && will work). - * We also allow {@code and} between two pointcut sub-expressions. + * write "and" as "&&" (though {@code &&} will work). + *

    We also allow "and" between two pointcut sub-expressions. *

    This method converts back to {@code &&} for the AspectJ pointcut parser. */ private String replaceBooleanOperators(String pcExpr) { @@ -269,10 +283,18 @@ public PointcutExpression getPointcutExpression() { @Override public boolean matches(Class targetClass) { - PointcutExpression pointcutExpression = obtainPointcutExpression(); + if (this.pointcutParsingFailed) { + // Pointcut parsing failed before below -> avoid trying again. + return false; + } + if (this.aspectCompiledByAjc && compiledByAjc(targetClass)) { + // ajc-compiled aspect class for ajc-compiled target class -> already weaved. + return false; + } + try { try { - return pointcutExpression.couldMatchJoinPointsInType(targetClass); + return obtainPointcutExpression().couldMatchJoinPointsInType(targetClass); } catch (ReflectionWorldException ex) { logger.debug("PointcutExpression matching rejected target class - trying fallback expression", ex); @@ -283,6 +305,12 @@ public boolean matches(Class targetClass) { } } } + catch (IllegalArgumentException | IllegalStateException | UnsupportedPointcutPrimitiveException ex) { + this.pointcutParsingFailed = true; + if (logger.isDebugEnabled()) { + logger.debug("Pointcut parser rejected expression [" + getExpression() + "]: " + ex); + } + } catch (Throwable ex) { logger.debug("PointcutExpression matching rejected target class", ex); } @@ -291,7 +319,6 @@ public boolean matches(Class targetClass) { @Override public boolean matches(Method method, Class targetClass, boolean hasIntroductions) { - obtainPointcutExpression(); ShadowMatch shadowMatch = getTargetShadowMatch(method, targetClass); // Special handling for this, target, @this, @target, @annotation @@ -329,7 +356,6 @@ public boolean isRuntime() { @Override public boolean matches(Method method, Class targetClass, Object... args) { - obtainPointcutExpression(); ShadowMatch shadowMatch = getTargetShadowMatch(method, targetClass); // Bind Spring AOP proxy to AspectJ "this" and Spring AOP target to AspectJ target, @@ -517,6 +543,16 @@ else if (shadowMatch.maybeMatches() && fallbackExpression != null) { return shadowMatch; } + private static boolean compiledByAjc(Class clazz) { + for (Field field : clazz.getDeclaredFields()) { + if (field.getName().startsWith(AJC_MAGIC)) { + return true; + } + } + Class superclass = clazz.getSuperclass(); + return (superclass != null && compiledByAjc(superclass)); + } + @Override public boolean equals(@Nullable Object other) { @@ -527,7 +563,7 @@ public boolean equals(@Nullable Object other) { return false; } AspectJExpressionPointcut otherPc = (AspectJExpressionPointcut) other; - return ObjectUtils.nullSafeEquals(this.getExpression(), otherPc.getExpression()) && + return ObjectUtils.nullSafeEquals(getExpression(), otherPc.getExpression()) && ObjectUtils.nullSafeEquals(this.pointcutDeclarationScope, otherPc.pointcutDeclarationScope) && ObjectUtils.nullSafeEquals(this.pointcutParameterNames, otherPc.pointcutParameterNames) && ObjectUtils.nullSafeEquals(this.pointcutParameterTypes, otherPc.pointcutParameterTypes); @@ -535,7 +571,7 @@ public boolean equals(@Nullable Object other) { @Override public int hashCode() { - int hashCode = ObjectUtils.nullSafeHashCode(this.getExpression()); + int hashCode = ObjectUtils.nullSafeHashCode(getExpression()); hashCode = 31 * hashCode + ObjectUtils.nullSafeHashCode(this.pointcutDeclarationScope); hashCode = 31 * hashCode + ObjectUtils.nullSafeHashCode(this.pointcutParameterNames); hashCode = 31 * hashCode + ObjectUtils.nullSafeHashCode(this.pointcutParameterTypes); diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AbstractAspectJAdvisorFactory.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AbstractAspectJAdvisorFactory.java index 58f3c23b459f..efdd136628b2 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AbstractAspectJAdvisorFactory.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AbstractAspectJAdvisorFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-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,7 +18,6 @@ import java.lang.annotation.Annotation; import java.lang.reflect.Constructor; -import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.HashMap; @@ -57,8 +56,6 @@ */ public abstract class AbstractAspectJAdvisorFactory implements AspectJAdvisorFactory { - private static final String AJC_MAGIC = "ajc$"; - private static final Class[] ASPECTJ_ANNOTATION_CLASSES = new Class[] { Pointcut.class, Around.class, Before.class, After.class, AfterReturning.class, AfterThrowing.class}; @@ -69,37 +66,11 @@ public abstract class AbstractAspectJAdvisorFactory implements AspectJAdvisorFac protected final ParameterNameDiscoverer parameterNameDiscoverer = new AspectJAnnotationParameterNameDiscoverer(); - /** - * We consider something to be an AspectJ aspect suitable for use by the Spring AOP system - * if it has the @Aspect annotation, and was not compiled by ajc. The reason for this latter test - * is that aspects written in the code-style (AspectJ language) also have the annotation present - * when compiled by ajc with the -1.5 flag, yet they cannot be consumed by Spring AOP. - */ @Override public boolean isAspect(Class clazz) { - return (hasAspectAnnotation(clazz) && !compiledByAjc(clazz)); - } - - private boolean hasAspectAnnotation(Class clazz) { return (AnnotationUtils.findAnnotation(clazz, Aspect.class) != null); } - /** - * We need to detect this as "code-style" AspectJ aspects should not be - * interpreted by Spring AOP. - */ - private boolean compiledByAjc(Class clazz) { - // The AJTypeSystem goes to great lengths to provide a uniform appearance between code-style and - // annotation-style aspects. Therefore there is no 'clean' way to tell them apart. Here we rely on - // an implementation detail of the AspectJ compiler. - for (Field field : clazz.getDeclaredFields()) { - if (field.getName().startsWith(AJC_MAGIC)) { - return true; - } - } - return false; - } - @Override public void validate(Class aspectClass) throws AopConfigException { // If the parent has the annotation and isn't abstract it's an error @@ -124,6 +95,7 @@ public void validate(Class aspectClass) throws AopConfigException { } } + /** * Find and return the first AspectJ annotation on the given method * (there should only be one anyway...). @@ -163,7 +135,7 @@ protected enum AspectJAnnotationType { /** - * Class modelling an AspectJ annotation, exposing its type enumeration and + * Class modeling an AspectJ annotation, exposing its type enumeration and * pointcut String. * @param the annotation type */ diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AspectMetadata.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AspectMetadata.java index 1617e2267e9d..c2151741d597 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AspectMetadata.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AspectMetadata.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-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. @@ -126,10 +126,16 @@ public AspectMetadata(Class aspectClass, String aspectName) { * Extract contents from String of form {@code pertarget(contents)}. */ private String findPerClause(Class aspectClass) { - String str = aspectClass.getAnnotation(Aspect.class).value(); - int beginIndex = str.indexOf('(') + 1; - int endIndex = str.length() - 1; - return str.substring(beginIndex, endIndex); + Aspect ann = aspectClass.getAnnotation(Aspect.class); + if (ann == null) { + return ""; + } + String value = ann.value(); + int beginIndex = value.indexOf('('); + if (beginIndex < 0) { + return ""; + } + return value.substring(beginIndex + 1, value.length() - 1); } diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/BeanFactoryAspectJAdvisorsBuilder.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/BeanFactoryAspectJAdvisorsBuilder.java index 8896f990ecbb..a318ea56bb49 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/BeanFactoryAspectJAdvisorsBuilder.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/BeanFactoryAspectJAdvisorsBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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,9 +22,12 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.aspectj.lang.reflect.PerClauseKind; import org.springframework.aop.Advisor; +import org.springframework.aop.framework.AopConfigException; import org.springframework.beans.factory.BeanFactoryUtils; import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.lang.Nullable; @@ -40,6 +43,8 @@ */ public class BeanFactoryAspectJAdvisorsBuilder { + private static final Log logger = LogFactory.getLog(BeanFactoryAspectJAdvisorsBuilder.class); + private final ListableBeanFactory beanFactory; private final AspectJAdvisorFactory advisorFactory; @@ -102,30 +107,37 @@ public List buildAspectJAdvisors() { continue; } if (this.advisorFactory.isAspect(beanType)) { - aspectNames.add(beanName); - AspectMetadata amd = new AspectMetadata(beanType, beanName); - if (amd.getAjType().getPerClause().getKind() == PerClauseKind.SINGLETON) { - MetadataAwareAspectInstanceFactory factory = - new BeanFactoryAspectInstanceFactory(this.beanFactory, beanName); - List classAdvisors = this.advisorFactory.getAdvisors(factory); - if (this.beanFactory.isSingleton(beanName)) { - this.advisorsCache.put(beanName, classAdvisors); + try { + AspectMetadata amd = new AspectMetadata(beanType, beanName); + if (amd.getAjType().getPerClause().getKind() == PerClauseKind.SINGLETON) { + MetadataAwareAspectInstanceFactory factory = + new BeanFactoryAspectInstanceFactory(this.beanFactory, beanName); + List classAdvisors = this.advisorFactory.getAdvisors(factory); + if (this.beanFactory.isSingleton(beanName)) { + this.advisorsCache.put(beanName, classAdvisors); + } + else { + this.aspectFactoryCache.put(beanName, factory); + } + advisors.addAll(classAdvisors); } else { + // Per target or per this. + if (this.beanFactory.isSingleton(beanName)) { + throw new IllegalArgumentException("Bean with name '" + beanName + + "' is a singleton, but aspect instantiation model is not singleton"); + } + MetadataAwareAspectInstanceFactory factory = + new PrototypeAspectInstanceFactory(this.beanFactory, beanName); this.aspectFactoryCache.put(beanName, factory); + advisors.addAll(this.advisorFactory.getAdvisors(factory)); } - advisors.addAll(classAdvisors); + aspectNames.add(beanName); } - else { - // Per target or per this. - if (this.beanFactory.isSingleton(beanName)) { - throw new IllegalArgumentException("Bean with name '" + beanName + - "' is a singleton, but aspect instantiation model is not singleton"); + catch (IllegalArgumentException | IllegalStateException | AopConfigException ex) { + if (logger.isDebugEnabled()) { + logger.debug("Ignoring incompatible aspect [" + beanType.getName() + "]: " + ex); } - MetadataAwareAspectInstanceFactory factory = - new PrototypeAspectInstanceFactory(this.beanFactory, beanName); - this.aspectFactoryCache.put(beanName, factory); - advisors.addAll(this.advisorFactory.getAdvisors(factory)); } } } diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/InstantiationModelAwarePointcutAdvisorImpl.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/InstantiationModelAwarePointcutAdvisorImpl.java index 7d89175c8c2d..149cb3b0b0d1 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/InstantiationModelAwarePointcutAdvisorImpl.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/InstantiationModelAwarePointcutAdvisorImpl.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-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. @@ -35,7 +35,8 @@ /** * Internal implementation of AspectJPointcutAdvisor. - * Note that there will be one instance of this advisor for each target method. + * + *

    Note that there will be one instance of this advisor for each target method. * * @author Rod Johnson * @author Juergen Hoeller @@ -293,7 +294,7 @@ public boolean matches(Method method, Class targetClass) { @Override public boolean matches(Method method, Class targetClass, Object... args) { // This can match only on declared pointcut. - return (isAspectMaterialized() && this.declaredPointcut.matches(method, targetClass)); + return (isAspectMaterialized() && this.declaredPointcut.matches(method, targetClass, args)); } private boolean isAspectMaterialized() { diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/MetadataAwareAspectInstanceFactory.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/MetadataAwareAspectInstanceFactory.java index 5a67f7fbac93..cb3e29baf49a 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/MetadataAwareAspectInstanceFactory.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/MetadataAwareAspectInstanceFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2023 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,10 +23,6 @@ * Subinterface of {@link org.springframework.aop.aspectj.AspectInstanceFactory} * that returns {@link AspectMetadata} associated with AspectJ-annotated classes. * - *

    Ideally, AspectInstanceFactory would include this method itself, but because - * AspectMetadata uses Java-5-only {@link org.aspectj.lang.reflect.AjType}, - * we need to split out this subinterface. - * * @author Rod Johnson * @since 2.0 * @see AspectMetadata @@ -35,13 +31,13 @@ public interface MetadataAwareAspectInstanceFactory extends AspectInstanceFactory { /** - * Return the AspectJ AspectMetadata for this factory's aspect. + * Get the AspectJ AspectMetadata for this factory's aspect. * @return the aspect metadata */ AspectMetadata getAspectMetadata(); /** - * Return the best possible creation mutex for this factory. + * Get the best possible creation mutex for this factory. * @return the mutex object (may be {@code null} for no mutex to use) * @since 4.3 */ diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/ReflectiveAspectJAdvisorFactory.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/ReflectiveAspectJAdvisorFactory.java index c1c10c946ed6..63f95c17aea3 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/ReflectiveAspectJAdvisorFactory.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/ReflectiveAspectJAdvisorFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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,6 +50,7 @@ import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.converter.ConvertingComparator; import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; import org.springframework.util.ReflectionUtils; import org.springframework.util.ReflectionUtils.MethodFilter; import org.springframework.util.StringUtils; @@ -133,17 +134,19 @@ public List getAdvisors(MetadataAwareAspectInstanceFactory aspectInstan List advisors = new ArrayList<>(); for (Method method : getAdvisorMethods(aspectClass)) { - // Prior to Spring Framework 5.2.7, advisors.size() was supplied as the declarationOrderInAspect - // to getAdvisor(...) to represent the "current position" in the declared methods list. - // However, since Java 7 the "current position" is not valid since the JDK no longer - // returns declared methods in the order in which they are declared in the source code. - // Thus, we now hard code the declarationOrderInAspect to 0 for all advice methods - // discovered via reflection in order to support reliable advice ordering across JVM launches. - // Specifically, a value of 0 aligns with the default value used in - // AspectJPrecedenceComparator.getAspectDeclarationOrder(Advisor). - Advisor advisor = getAdvisor(method, lazySingletonAspectInstanceFactory, 0, aspectName); - if (advisor != null) { - advisors.add(advisor); + if (method.equals(ClassUtils.getMostSpecificMethod(method, aspectClass))) { + // Prior to Spring Framework 5.2.7, advisors.size() was supplied as the declarationOrderInAspect + // to getAdvisor(...) to represent the "current position" in the declared methods list. + // However, since Java 7 the "current position" is not valid since the JDK no longer + // returns declared methods in the order in which they are declared in the source code. + // Thus, we now hard code the declarationOrderInAspect to 0 for all advice methods + // discovered via reflection in order to support reliable advice ordering across JVM launches. + // Specifically, a value of 0 aligns with the default value used in + // AspectJPrecedenceComparator.getAspectDeclarationOrder(Advisor). + Advisor advisor = getAdvisor(method, lazySingletonAspectInstanceFactory, 0, aspectName); + if (advisor != null) { + advisors.add(advisor); + } } } @@ -210,8 +213,16 @@ public Advisor getAdvisor(Method candidateAdviceMethod, MetadataAwareAspectInsta return null; } - return new InstantiationModelAwarePointcutAdvisorImpl(expressionPointcut, candidateAdviceMethod, - this, aspectInstanceFactory, declarationOrderInAspect, aspectName); + try { + return new InstantiationModelAwarePointcutAdvisorImpl(expressionPointcut, candidateAdviceMethod, + this, aspectInstanceFactory, declarationOrderInAspect, aspectName); + } + catch (IllegalArgumentException | IllegalStateException ex) { + if (logger.isDebugEnabled()) { + logger.debug("Ignoring incompatible advice method: " + candidateAdviceMethod, ex); + } + return null; + } } @Nullable diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/AdvisedSupport.java b/spring-aop/src/main/java/org/springframework/aop/framework/AdvisedSupport.java index 3de330c108ce..1af9f25bc888 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/AdvisedSupport.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/AdvisedSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -44,7 +44,8 @@ /** * Base class for AOP proxy configuration managers. - * These are not themselves AOP proxies, but subclasses of this class are + * + *

    These are not themselves AOP proxies, but subclasses of this class are * normally factories from which AOP proxy instances are obtained directly. * *

    This class frees subclasses of the housekeeping of Advices @@ -52,7 +53,8 @@ * methods, which are provided by subclasses. * *

    This class is serializable; subclasses need not be. - * This class is used to hold snapshots of proxies. + * + *

    This class is used to hold snapshots of proxies. * * @author Rod Johnson * @author Juergen Hoeller @@ -104,7 +106,7 @@ public AdvisedSupport() { } /** - * Create a AdvisedSupport instance with the given parameters. + * Create an {@code AdvisedSupport} instance with the given parameters. * @param interfaces the proxied interfaces */ public AdvisedSupport(Class... interfaces) { @@ -115,7 +117,7 @@ public AdvisedSupport(Class... interfaces) { /** * Set the given object as target. - * Will create a SingletonTargetSource for the object. + *

    Will create a SingletonTargetSource for the object. * @see #setTargetSource * @see org.springframework.aop.target.SingletonTargetSource */ @@ -344,8 +346,7 @@ public void addAdvisors(Collection advisors) { private void validateIntroductionAdvisor(IntroductionAdvisor advisor) { advisor.validateInterfaces(); // If the advisor passed validation, we can make the change. - Class[] ifcs = advisor.getInterfaces(); - for (Class ifc : ifcs) { + for (Class ifc : advisor.getInterfaces()) { addInterface(ifc); } } @@ -491,9 +492,9 @@ protected void copyConfigurationFrom(AdvisedSupport other) { } /** - * Copy the AOP configuration from the given AdvisedSupport object, - * but allow substitution of a fresh TargetSource and a given interceptor chain. - * @param other the AdvisedSupport object to take proxy configuration from + * Copy the AOP configuration from the given {@link AdvisedSupport} object, + * but allow substitution of a fresh {@link TargetSource} and a given interceptor chain. + * @param other the {@code AdvisedSupport} object to take proxy configuration from * @param targetSource the new TargetSource * @param advisors the Advisors for the chain */ @@ -513,14 +514,15 @@ protected void copyConfigurationFrom(AdvisedSupport other, TargetSource targetSo } /** - * Build a configuration-only copy of this AdvisedSupport, - * replacing the TargetSource. + * Build a configuration-only copy of this {@link AdvisedSupport}, + * replacing the {@link TargetSource}. */ AdvisedSupport getConfigurationOnlyCopy() { AdvisedSupport copy = new AdvisedSupport(); copy.copyFrom(this); copy.targetSource = EmptyTargetSource.forClass(getTargetClass(), getTargetSource().isStatic()); copy.advisorChainFactory = this.advisorChainFactory; + copy.methodCache = this.methodCache; copy.interfaces = new ArrayList<>(this.interfaces); copy.advisors = new ArrayList<>(this.advisors); return copy; diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/CglibAopProxy.java b/spring-aop/src/main/java/org/springframework/aop/framework/CglibAopProxy.java index 87fa84d6b98a..a172c86c2472 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/CglibAopProxy.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/CglibAopProxy.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -283,8 +283,8 @@ else if (logger.isDebugEnabled() && !Modifier.isPublic(mod) && !Modifier.isProte private Callback[] getCallbacks(Class rootClass) throws Exception { // Parameters used for optimization choices... - boolean exposeProxy = this.advised.isExposeProxy(); boolean isFrozen = this.advised.isFrozen(); + boolean exposeProxy = this.advised.isExposeProxy(); boolean isStatic = this.advised.getTargetSource().isStatic(); // Choose an "aop" interceptor (used for AOP calls). @@ -424,10 +424,9 @@ public static class SerializableNoOp implements NoOp, Serializable { /** - * Method interceptor used for static targets with no advice chain. The call - * is passed directly back to the target. Used when the proxy needs to be - * exposed and it can't be determined that the method won't return - * {@code this}. + * Method interceptor used for static targets with no advice chain. The call is + * passed directly back to the target. Used when the proxy needs to be exposed + * and it can't be determined that the method won't return {@code this}. */ private static class StaticUnadvisedInterceptor implements MethodInterceptor, Serializable { @@ -899,9 +898,9 @@ public int accept(Method method) { // Proxy is not yet available, but that shouldn't matter. List chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass); boolean haveAdvice = !chain.isEmpty(); + boolean isFrozen = this.advised.isFrozen(); boolean exposeProxy = this.advised.isExposeProxy(); boolean isStatic = this.advised.getTargetSource().isStatic(); - boolean isFrozen = this.advised.isFrozen(); if (haveAdvice || !isFrozen) { // If exposing the proxy, then AOP_PROXY must be used. if (exposeProxy) { @@ -970,6 +969,9 @@ public boolean equals(@Nullable Object other) { if (this.advised.isExposeProxy() != otherAdvised.isExposeProxy()) { return false; } + if (this.advised.isOpaque() != otherAdvised.isOpaque()) { + return false; + } if (this.advised.getTargetSource().isStatic() != otherAdvised.getTargetSource().isStatic()) { return false; } @@ -1016,10 +1018,6 @@ public int hashCode() { Advice advice = advisor.getAdvice(); hashCode = 13 * hashCode + advice.getClass().hashCode(); } - hashCode = 13 * hashCode + (this.advised.isFrozen() ? 1 : 0); - hashCode = 13 * hashCode + (this.advised.isExposeProxy() ? 1 : 0); - hashCode = 13 * hashCode + (this.advised.isOptimize() ? 1 : 0); - hashCode = 13 * hashCode + (this.advised.isOpaque() ? 1 : 0); return hashCode; } } diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/JdkDynamicAopProxy.java b/spring-aop/src/main/java/org/springframework/aop/framework/JdkDynamicAopProxy.java index 3aeb80cd1824..10cd8acda1bc 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/JdkDynamicAopProxy.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/JdkDynamicAopProxy.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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. @@ -123,7 +123,33 @@ public Object getProxy(@Nullable ClassLoader classLoader) { if (logger.isTraceEnabled()) { logger.trace("Creating JDK dynamic proxy: " + this.advised.getTargetSource()); } - return Proxy.newProxyInstance(classLoader, this.proxiedInterfaces, this); + return Proxy.newProxyInstance(determineClassLoader(classLoader), this.proxiedInterfaces, this); + } + + /** + * Determine whether the JDK bootstrap or platform loader has been suggested -> + * use higher-level loader which can see Spring infrastructure classes instead. + */ + private ClassLoader determineClassLoader(@Nullable ClassLoader classLoader) { + if (classLoader == null) { + // JDK bootstrap loader -> use spring-aop ClassLoader instead. + return getClass().getClassLoader(); + } + if (classLoader.getParent() == null) { + // Potentially the JDK platform loader on JDK 9+ + ClassLoader aopClassLoader = getClass().getClassLoader(); + ClassLoader aopParent = aopClassLoader.getParent(); + while (aopParent != null) { + if (classLoader == aopParent) { + // Suggested ClassLoader is ancestor of spring-aop ClassLoader + // -> use spring-aop ClassLoader itself instead. + return aopClassLoader; + } + aopParent = aopParent.getParent(); + } + } + // Regular case: use suggested ClassLoader as-is. + return classLoader; } /** diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/target/AbstractBeanFactoryBasedTargetSourceCreator.java b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/target/AbstractBeanFactoryBasedTargetSourceCreator.java index fcef74b56ef4..6bb3f913db1d 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/target/AbstractBeanFactoryBasedTargetSourceCreator.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/target/AbstractBeanFactoryBasedTargetSourceCreator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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. @@ -34,6 +34,7 @@ import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.beans.factory.support.GenericBeanDefinition; import org.springframework.lang.Nullable; +import org.springframework.util.Assert; /** * Convenient superclass for @@ -81,6 +82,11 @@ protected final BeanFactory getBeanFactory() { return this.beanFactory; } + private ConfigurableBeanFactory getConfigurableBeanFactory() { + Assert.state(this.beanFactory != null, "BeanFactory not set"); + return this.beanFactory; + } + //--------------------------------------------------------------------- // Implementation of the TargetSourceCreator interface @@ -104,7 +110,7 @@ public final TargetSource getTargetSource(Class beanClass, String beanName) { // We need to override just this bean definition, as it may reference other beans // and we're happy to take the parent's definition for those. // Always use prototype scope if demanded. - BeanDefinition bd = this.beanFactory.getMergedBeanDefinition(beanName); + BeanDefinition bd = getConfigurableBeanFactory().getMergedBeanDefinition(beanName); GenericBeanDefinition bdCopy = new GenericBeanDefinition(bd); if (isPrototypeBased()) { bdCopy.setScope(BeanDefinition.SCOPE_PROTOTYPE); @@ -126,7 +132,7 @@ public final TargetSource getTargetSource(Class beanClass, String beanName) { protected DefaultListableBeanFactory getInternalBeanFactoryForBean(String beanName) { synchronized (this.internalBeanFactories) { return this.internalBeanFactories.computeIfAbsent(beanName, - name -> buildInternalBeanFactory(this.beanFactory)); + name -> buildInternalBeanFactory(getConfigurableBeanFactory())); } } diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/target/package-info.java b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/target/package-info.java new file mode 100644 index 000000000000..2e0608db9d2c --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/target/package-info.java @@ -0,0 +1,10 @@ +/** + * Various {@link org.springframework.aop.framework.autoproxy.TargetSourceCreator} + * implementations for use with Spring's AOP auto-proxying support. + */ +@NonNullApi +@NonNullFields +package org.springframework.aop.framework.autoproxy.target; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-aop/src/main/java/org/springframework/aop/support/AopUtils.java b/spring-aop/src/main/java/org/springframework/aop/support/AopUtils.java index e3001ec4975d..aced157280dd 100644 --- a/spring-aop/src/main/java/org/springframework/aop/support/AopUtils.java +++ b/spring-aop/src/main/java/org/springframework/aop/support/AopUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-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. @@ -186,10 +186,10 @@ public static boolean isFinalizeMethod(@Nullable Method method) { * this method resolves bridge methods in order to retrieve attributes from * the original method definition. * @param method the method to be invoked, which may come from an interface - * @param targetClass the target class for the current invocation. - * May be {@code null} or may not even implement the method. + * @param targetClass the target class for the current invocation + * (can be {@code null} or may not even implement the method) * @return the specific target method, or the original method if the - * {@code targetClass} doesn't implement it or is {@code null} + * {@code targetClass} does not implement it * @see org.springframework.util.ClassUtils#getMostSpecificMethod */ public static Method getMostSpecificMethod(Method method, @Nullable Class targetClass) { diff --git a/spring-aop/src/main/java/org/springframework/aop/target/AbstractBeanFactoryBasedTargetSource.java b/spring-aop/src/main/java/org/springframework/aop/target/AbstractBeanFactoryBasedTargetSource.java index 78c6d3abf6d6..9b637a59b689 100644 --- a/spring-aop/src/main/java/org/springframework/aop/target/AbstractBeanFactoryBasedTargetSource.java +++ b/spring-aop/src/main/java/org/springframework/aop/target/AbstractBeanFactoryBasedTargetSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2023 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,8 @@ import org.springframework.aop.TargetSource; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; /** @@ -57,15 +59,18 @@ public abstract class AbstractBeanFactoryBasedTargetSource implements TargetSour protected final Log logger = LogFactory.getLog(getClass()); /** Name of the target bean we will create on each invocation. */ + @Nullable private String targetBeanName; /** Class of the target. */ + @Nullable private volatile Class targetClass; /** * BeanFactory that owns this TargetSource. We need to hold onto this * reference so that we can create new prototype instances as necessary. */ + @Nullable private BeanFactory beanFactory; @@ -86,6 +91,7 @@ public void setTargetBeanName(String targetBeanName) { * Return the name of the target bean in the factory. */ public String getTargetBeanName() { + Assert.state(this.targetBeanName != null, "Target bean name not set"); return this.targetBeanName; } @@ -115,11 +121,13 @@ public void setBeanFactory(BeanFactory beanFactory) { * Return the owning BeanFactory. */ public BeanFactory getBeanFactory() { + Assert.state(this.beanFactory != null, "BeanFactory not set"); return this.beanFactory; } @Override + @Nullable public Class getTargetClass() { Class targetClass = this.targetClass; if (targetClass != null) { @@ -128,7 +136,7 @@ public Class getTargetClass() { synchronized (this) { // Full check within synchronization, entering the BeanFactory interaction algorithm only once... targetClass = this.targetClass; - if (targetClass == null && this.beanFactory != null) { + if (targetClass == null && this.beanFactory != null && this.targetBeanName != null) { // Determine type of the target bean. targetClass = this.beanFactory.getType(this.targetBeanName); if (targetClass == null) { @@ -182,18 +190,16 @@ public boolean equals(Object other) { @Override public int hashCode() { - int hashCode = getClass().hashCode(); - hashCode = 13 * hashCode + ObjectUtils.nullSafeHashCode(this.beanFactory); - hashCode = 13 * hashCode + ObjectUtils.nullSafeHashCode(this.targetBeanName); - return hashCode; + return getClass().hashCode() * 13 + ObjectUtils.nullSafeHashCode(this.targetBeanName); } @Override public String toString() { StringBuilder sb = new StringBuilder(getClass().getSimpleName()); sb.append(" for target bean '").append(this.targetBeanName).append('\''); - if (this.targetClass != null) { - sb.append(" of type [").append(this.targetClass.getName()).append(']'); + Class targetClass = this.targetClass; + if (targetClass != null) { + sb.append(" of type [").append(targetClass.getName()).append(']'); } return sb.toString(); } diff --git a/spring-aop/src/main/java/org/springframework/aop/target/AbstractLazyCreationTargetSource.java b/spring-aop/src/main/java/org/springframework/aop/target/AbstractLazyCreationTargetSource.java index f742a106bab8..8246e1e6268f 100644 --- a/spring-aop/src/main/java/org/springframework/aop/target/AbstractLazyCreationTargetSource.java +++ b/spring-aop/src/main/java/org/springframework/aop/target/AbstractLazyCreationTargetSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2023 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. @@ -46,6 +46,7 @@ public abstract class AbstractLazyCreationTargetSource implements TargetSource { protected final Log logger = LogFactory.getLog(getClass()); /** The lazily initialized target object. */ + @Nullable private Object lazyTarget; diff --git a/spring-aop/src/main/java/org/springframework/aop/target/EmptyTargetSource.java b/spring-aop/src/main/java/org/springframework/aop/target/EmptyTargetSource.java index 2ebcc216dce7..22785ce0fa7b 100644 --- a/spring-aop/src/main/java/org/springframework/aop/target/EmptyTargetSource.java +++ b/spring-aop/src/main/java/org/springframework/aop/target/EmptyTargetSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2023 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. @@ -70,6 +70,7 @@ public static EmptyTargetSource forClass(@Nullable Class targetClass, boolean // Instance implementation //--------------------------------------------------------------------- + @Nullable private final Class targetClass; private final boolean isStatic; diff --git a/spring-aop/src/main/java/org/springframework/aop/target/HotSwappableTargetSource.java b/spring-aop/src/main/java/org/springframework/aop/target/HotSwappableTargetSource.java index 5d85f5043863..18cc5d22e3c3 100644 --- a/spring-aop/src/main/java/org/springframework/aop/target/HotSwappableTargetSource.java +++ b/spring-aop/src/main/java/org/springframework/aop/target/HotSwappableTargetSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2023 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. @@ -95,8 +95,7 @@ public synchronized Object swap(Object newTarget) throws IllegalArgumentExceptio /** - * Two HotSwappableTargetSources are equal if the current target - * objects are equal. + * Two HotSwappableTargetSources are equal if the current target objects are equal. */ @Override public boolean equals(Object other) { diff --git a/spring-aop/src/main/java/org/springframework/aop/target/SingletonTargetSource.java b/spring-aop/src/main/java/org/springframework/aop/target/SingletonTargetSource.java index 24ae9865a6db..85f84128a096 100644 --- a/spring-aop/src/main/java/org/springframework/aop/target/SingletonTargetSource.java +++ b/spring-aop/src/main/java/org/springframework/aop/target/SingletonTargetSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2023 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. @@ -82,14 +82,8 @@ public boolean isStatic() { */ @Override public boolean equals(Object other) { - if (this == other) { - return true; - } - if (!(other instanceof SingletonTargetSource)) { - return false; - } - SingletonTargetSource otherTargetSource = (SingletonTargetSource) other; - return this.target.equals(otherTargetSource.target); + return (this == other || (other instanceof SingletonTargetSource && + this.target.equals(((SingletonTargetSource) other).target))); } /** diff --git a/spring-aop/src/main/java/org/springframework/aop/target/ThreadLocalTargetSource.java b/spring-aop/src/main/java/org/springframework/aop/target/ThreadLocalTargetSource.java index 5c4b03c78495..dc585654b95e 100644 --- a/spring-aop/src/main/java/org/springframework/aop/target/ThreadLocalTargetSource.java +++ b/spring-aop/src/main/java/org/springframework/aop/target/ThreadLocalTargetSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2023 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,7 +58,12 @@ public class ThreadLocalTargetSource extends AbstractPrototypeBasedTargetSource * is meant to be per thread per instance of the ThreadLocalTargetSource class. */ private final ThreadLocal targetInThread = - new NamedThreadLocal<>("Thread-local instance of bean '" + getTargetBeanName() + "'"); + new NamedThreadLocal("Thread-local instance of bean") { + @Override + public String toString() { + return super.toString() + " '" + getTargetBeanName() + "'"; + } + }; /** * Set of managed targets, enabling us to keep track of the targets we've created. diff --git a/spring-aop/src/test/java/org/springframework/aop/aspectj/AspectJExpressionPointcutTests.java b/spring-aop/src/test/java/org/springframework/aop/aspectj/AspectJExpressionPointcutTests.java index e1b2a93b9589..c754f3d8a85a 100644 --- a/spring-aop/src/test/java/org/springframework/aop/aspectj/AspectJExpressionPointcutTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/aspectj/AspectJExpressionPointcutTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-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,9 +23,6 @@ import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; -import org.aspectj.weaver.tools.PointcutExpression; -import org.aspectj.weaver.tools.PointcutPrimitive; -import org.aspectj.weaver.tools.UnsupportedPointcutPrimitiveException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import test.annotation.EmptySpringAnnotation; @@ -42,14 +39,13 @@ import org.springframework.beans.testfixture.beans.subpkg.DeepBean; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; /** * @author Rob Harrop * @author Rod Johnson * @author Chris Beams + * @author Juergen Hoeller */ public class AspectJExpressionPointcutTests { @@ -65,7 +61,7 @@ public class AspectJExpressionPointcutTests { @BeforeEach - public void setUp() throws NoSuchMethodException { + public void setup() throws NoSuchMethodException { getAge = TestBean.class.getMethod("getAge"); setAge = TestBean.class.getMethod("setAge", int.class); setSomeNumber = TestBean.class.getMethod("setSomeNumber", Number.class); @@ -175,25 +171,25 @@ private void testWithinPackage(boolean matchSubpackages) throws SecurityExceptio @Test public void testFriendlyErrorOnNoLocationClassMatching() { AspectJExpressionPointcut pc = new AspectJExpressionPointcut(); - assertThatIllegalStateException().isThrownBy(() -> - pc.matches(ITestBean.class)) - .withMessageContaining("expression"); + assertThatIllegalStateException() + .isThrownBy(() -> pc.getClassFilter().matches(ITestBean.class)) + .withMessageContaining("expression"); } @Test public void testFriendlyErrorOnNoLocation2ArgMatching() { AspectJExpressionPointcut pc = new AspectJExpressionPointcut(); - assertThatIllegalStateException().isThrownBy(() -> - pc.matches(getAge, ITestBean.class)) - .withMessageContaining("expression"); + assertThatIllegalStateException() + .isThrownBy(() -> pc.getMethodMatcher().matches(getAge, ITestBean.class)) + .withMessageContaining("expression"); } @Test public void testFriendlyErrorOnNoLocation3ArgMatching() { AspectJExpressionPointcut pc = new AspectJExpressionPointcut(); - assertThatIllegalStateException().isThrownBy(() -> - pc.matches(getAge, ITestBean.class, (Object[]) null)) - .withMessageContaining("expression"); + assertThatIllegalStateException() + .isThrownBy(() -> pc.getMethodMatcher().matches(getAge, ITestBean.class, (Object[]) null)) + .withMessageContaining("expression"); } @@ -210,8 +206,10 @@ public void testMatchWithArgs() throws Exception { // not currently testable in a reliable fashion //assertDoesNotMatchStringClass(classFilter); - assertThat(methodMatcher.matches(setSomeNumber, TestBean.class, 12D)).as("Should match with setSomeNumber with Double input").isTrue(); - assertThat(methodMatcher.matches(setSomeNumber, TestBean.class, 11)).as("Should not match setSomeNumber with Integer input").isFalse(); + assertThat(methodMatcher.matches(setSomeNumber, TestBean.class, 12D)) + .as("Should match with setSomeNumber with Double input").isTrue(); + assertThat(methodMatcher.matches(setSomeNumber, TestBean.class, 11)) + .as("Should not match setSomeNumber with Integer input").isFalse(); assertThat(methodMatcher.matches(getAge, TestBean.class)).as("Should not match getAge").isFalse(); assertThat(methodMatcher.isRuntime()).as("Should be a runtime match").isTrue(); } @@ -246,14 +244,13 @@ public void testDynamicMatchingProxy() { @Test public void testInvalidExpression() { String expression = "execution(void org.springframework.beans.testfixture.beans.TestBean.setSomeNumber(Number) && args(Double)"; - assertThatIllegalArgumentException().isThrownBy( - getPointcut(expression)::getClassFilter); // call to getClassFilter forces resolution + assertThat(getPointcut(expression).getClassFilter().matches(Object.class)).isFalse(); } private TestBean getAdvisedProxy(String pointcutExpression, CallCountingInterceptor interceptor) { TestBean target = new TestBean(); - Pointcut pointcut = getPointcut(pointcutExpression); + AspectJExpressionPointcut pointcut = getPointcut(pointcutExpression); DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(); advisor.setAdvice(interceptor); @@ -277,40 +274,29 @@ private void assertMatchesTestBeanClass(ClassFilter classFilter) { @Test public void testWithUnsupportedPointcutPrimitive() { String expression = "call(int org.springframework.beans.testfixture.beans.TestBean.getAge())"; - assertThatExceptionOfType(UnsupportedPointcutPrimitiveException.class).isThrownBy(() -> - getPointcut(expression).getClassFilter()) // call to getClassFilter forces resolution... - .satisfies(ex -> assertThat(ex.getUnsupportedPrimitive()).isEqualTo(PointcutPrimitive.CALL)); + assertThat(getPointcut(expression).getClassFilter().matches(Object.class)).isFalse(); } @Test public void testAndSubstitution() { - Pointcut pc = getPointcut("execution(* *(..)) and args(String)"); - PointcutExpression expr = ((AspectJExpressionPointcut) pc).getPointcutExpression(); - assertThat(expr.getPointcutExpression()).isEqualTo("execution(* *(..)) && args(String)"); + AspectJExpressionPointcut pc = getPointcut("execution(* *(..)) and args(String)"); + String expr = pc.getPointcutExpression().getPointcutExpression(); + assertThat(expr).isEqualTo("execution(* *(..)) && args(String)"); } @Test public void testMultipleAndSubstitutions() { - Pointcut pc = getPointcut("execution(* *(..)) and args(String) and this(Object)"); - PointcutExpression expr = ((AspectJExpressionPointcut) pc).getPointcutExpression(); - assertThat(expr.getPointcutExpression()).isEqualTo("execution(* *(..)) && args(String) && this(Object)"); + AspectJExpressionPointcut pc = getPointcut("execution(* *(..)) and args(String) and this(Object)"); + String expr = pc.getPointcutExpression().getPointcutExpression(); + assertThat(expr).isEqualTo("execution(* *(..)) && args(String) && this(Object)"); } - private Pointcut getPointcut(String expression) { + private AspectJExpressionPointcut getPointcut(String expression) { AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut(); pointcut.setExpression(expression); return pointcut; } - - public static class OtherIOther implements IOther { - - @Override - public void absquatulate() { - // Empty - } - } - @Test public void testMatchGenericArgument() { String expression = "execution(* set*(java.util.List) )"; @@ -505,6 +491,15 @@ public void testAnnotationOnMethodArgumentsWithWildcards() throws Exception { } + public static class OtherIOther implements IOther { + + @Override + public void absquatulate() { + // Empty + } + } + + public static class HasGeneric { public void setFriends(List friends) { diff --git a/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/AbstractAspectJAdvisorFactoryTests.java b/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/AbstractAspectJAdvisorFactoryTests.java index 847ef4f38044..97d74dd6f4fe 100644 --- a/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/AbstractAspectJAdvisorFactoryTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/AbstractAspectJAdvisorFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-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. @@ -37,7 +37,6 @@ import org.aspectj.lang.annotation.DeclarePrecedence; import org.aspectj.lang.annotation.Pointcut; import org.aspectj.lang.reflect.MethodSignature; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import test.aop.DefaultLockable; import test.aop.Lockable; @@ -76,25 +75,24 @@ abstract class AbstractAspectJAdvisorFactoryTests { /** * To be overridden by concrete test subclasses. - * @return the fixture */ protected abstract AspectJAdvisorFactory getFixture(); @Test void rejectsPerCflowAspect() { - assertThatExceptionOfType(AopConfigException.class).isThrownBy(() -> - getFixture().getAdvisors( + assertThatExceptionOfType(AopConfigException.class) + .isThrownBy(() -> getFixture().getAdvisors( new SingletonMetadataAwareAspectInstanceFactory(new PerCflowAspect(), "someBean"))) - .withMessageContaining("PERCFLOW"); + .withMessageContaining("PERCFLOW"); } @Test void rejectsPerCflowBelowAspect() { - assertThatExceptionOfType(AopConfigException.class).isThrownBy(() -> - getFixture().getAdvisors( - new SingletonMetadataAwareAspectInstanceFactory(new PerCflowBelowAspect(), "someBean"))) - .withMessageContaining("PERCFLOWBELOW"); + assertThatExceptionOfType(AopConfigException.class) + .isThrownBy(() -> getFixture().getAdvisors( + new SingletonMetadataAwareAspectInstanceFactory(new PerCflowBelowAspect(), "someBean"))) + .withMessageContaining("PERCFLOWBELOW"); } @Test @@ -385,8 +383,7 @@ void introductionOnTargetImplementingInterface() { assertThat(lockable.locked()).as("Already locked").isTrue(); lockable.lock(); assertThat(lockable.locked()).as("Real target ignores locking").isTrue(); - assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> - lockable.unlock()); + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(lockable::unlock); } @Test @@ -413,9 +410,7 @@ void introductionBasedOnAnnotationMatch_SPR5307() { lockable.locked(); } - // TODO: Why does this test fail? It hasn't been run before, so it maybe never actually passed... @Test - @Disabled void introductionWithArgumentBinding() { TestBean target = new TestBean(); @@ -523,6 +518,16 @@ void afterAdviceTypes() throws Exception { assertThat(aspect.invocations).containsExactly("around - start", "before", "after throwing", "after", "around - end"); } + @Test + void parentAspect() { + TestBean target = new TestBean("Jane", 42); + MetadataAwareAspectInstanceFactory aspectInstanceFactory = new SingletonMetadataAwareAspectInstanceFactory( + new IncrementingAspect(), "incrementingAspect"); + ITestBean proxy = (ITestBean) createProxy(target, + getFixture().getAdvisors(aspectInstanceFactory), ITestBean.class); + assertThat(proxy.getAge()).isEqualTo(86); // (42 + 1) * 2 + } + @Test void failureWithoutExplicitDeclarePrecedence() { TestBean target = new TestBean(); @@ -647,7 +652,7 @@ public int getOrder() { static class NamedPointcutAspectWithFQN { @SuppressWarnings("unused") - private ITestBean fieldThatShouldBeIgnoredBySpringAtAspectJProcessing = new TestBean(); + private final ITestBean fieldThatShouldBeIgnoredBySpringAtAspectJProcessing = new TestBean(); @Pointcut("execution(* getAge())") void getAge() { @@ -767,6 +772,31 @@ Object echo(Object o) throws Exception { } + @Aspect + abstract static class DoublingAspect { + + @Around("execution(* getAge())") + public Object doubleAge(ProceedingJoinPoint pjp) throws Throwable { + return ((int) pjp.proceed()) * 2; + } + } + + + @Aspect + static class IncrementingAspect extends DoublingAspect { + + @Override + public Object doubleAge(ProceedingJoinPoint pjp) throws Throwable { + return ((int) pjp.proceed()) * 2; + } + + @Around("execution(* getAge())") + public int incrementAge(ProceedingJoinPoint pjp) throws Throwable { + return ((int) pjp.proceed()) + 1; + } + } + + @Aspect private static class InvocationTrackingAspect { @@ -824,7 +854,7 @@ void blowUpButDoesntMatterBecauseAroundAdviceWontLetThisBeInvoked() { @Around("getAge()") int preventExecution(ProceedingJoinPoint pjp) { - return 666; + return 42; } } @@ -844,7 +874,7 @@ void blowUpButDoesntMatterBecauseAroundAdviceWontLetThisBeInvoked() { @Around("getAge()") int preventExecution(ProceedingJoinPoint pjp) { - return 666; + return 42; } } @@ -1066,7 +1096,7 @@ class PerThisAspect { // Just to check that this doesn't cause problems with introduction processing @SuppressWarnings("unused") - private ITestBean fieldThatShouldBeIgnoredBySpringAtAspectJProcessing = new TestBean(); + private final ITestBean fieldThatShouldBeIgnoredBySpringAtAspectJProcessing = new TestBean(); @Around("execution(int *.getAge())") int returnCountAsAge() { diff --git a/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/ArgumentBindingTests.java b/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/ArgumentBindingTests.java index 835f4a4f95c3..4bb8f1c984aa 100644 --- a/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/ArgumentBindingTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/ArgumentBindingTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -38,56 +38,67 @@ * @author Adrian Colyer * @author Juergen Hoeller * @author Chris Beams + * @author Sam Brannen */ -public class ArgumentBindingTests { +class ArgumentBindingTests { @Test - public void testBindingInPointcutUsedByAdvice() { - TestBean tb = new TestBean(); - AspectJProxyFactory proxyFactory = new AspectJProxyFactory(tb); - proxyFactory.addAspect(NamedPointcutWithArgs.class); + void annotationArgumentNameBinding() { + AspectJProxyFactory proxyFactory = new AspectJProxyFactory(new TransactionalBean()); + proxyFactory.addAspect(PointcutWithAnnotationArgument.class); + ITransactionalBean proxiedTestBean = proxyFactory.getProxy(); + assertThatIllegalStateException() + .isThrownBy(proxiedTestBean::doInTransaction) + .withMessage("Invoked with @Transactional"); + } + + @Test + void bindingInPointcutUsedByAdvice() { + AspectJProxyFactory proxyFactory = new AspectJProxyFactory(new TestBean()); + proxyFactory.addAspect(NamedPointcutWithArgs.class); ITestBean proxiedTestBean = proxyFactory.getProxy(); - assertThatIllegalArgumentException().isThrownBy(() -> - proxiedTestBean.setName("Supercalifragalisticexpialidocious")); + + assertThatIllegalArgumentException() + .isThrownBy(() -> proxiedTestBean.setName("enigma")) + .withMessage("enigma"); } @Test - public void testAnnotationArgumentNameBinding() { - TransactionalBean tb = new TransactionalBean(); - AspectJProxyFactory proxyFactory = new AspectJProxyFactory(tb); - proxyFactory.addAspect(PointcutWithAnnotationArgument.class); + void bindingWithDynamicAdvice() { + AspectJProxyFactory proxyFactory = new AspectJProxyFactory(new TestBean()); + proxyFactory.addAspect(DynamicPointcutWithArgs.class); + ITestBean proxiedTestBean = proxyFactory.getProxy(); - ITransactionalBean proxiedTestBean = proxyFactory.getProxy(); - assertThatIllegalStateException().isThrownBy( - proxiedTestBean::doInTransaction); + proxiedTestBean.applyName(1); + assertThatIllegalArgumentException() + .isThrownBy(() -> proxiedTestBean.applyName("enigma")) + .withMessage("enigma"); } @Test - public void testParameterNameDiscoverWithReferencePointcut() throws Exception { + void parameterNameDiscoverWithReferencePointcut() throws Exception { AspectJAdviceParameterNameDiscoverer discoverer = new AspectJAdviceParameterNameDiscoverer("somepc(formal) && set(* *)"); discoverer.setRaiseExceptions(true); - Method methodUsedForParameterTypeDiscovery = - getClass().getMethod("methodWithOneParam", String.class); - String[] pnames = discoverer.getParameterNames(methodUsedForParameterTypeDiscovery); - assertThat(pnames.length).as("one parameter name").isEqualTo(1); - assertThat(pnames[0]).isEqualTo("formal"); + Method method = getClass().getDeclaredMethod("methodWithOneParam", String.class); + assertThat(discoverer.getParameterNames(method)).containsExactly("formal"); } - public void methodWithOneParam(String aParam) { + @SuppressWarnings("unused") + private void methodWithOneParam(String aParam) { } - public interface ITransactionalBean { + interface ITransactionalBean { @Transactional void doInTransaction(); } - public static class TransactionalBean implements ITransactionalBean { + static class TransactionalBean implements ITransactionalBean { @Override @Transactional @@ -95,38 +106,46 @@ public void doInTransaction() { } } -} -/** - * Represents Spring's Transactional annotation without actually introducing the dependency - */ -@Retention(RetentionPolicy.RUNTIME) -@interface Transactional { -} + /** + * Mimics Spring's @Transactional annotation without actually introducing the dependency. + */ + @Retention(RetentionPolicy.RUNTIME) + @interface Transactional { + } -@Aspect -class PointcutWithAnnotationArgument { + @Aspect + static class PointcutWithAnnotationArgument { - @Around(value = "execution(* org.springframework..*.*(..)) && @annotation(transaction)") - public Object around(ProceedingJoinPoint pjp, Transactional transaction) throws Throwable { - System.out.println("Invoked with transaction " + transaction); - throw new IllegalStateException(); + @Around("execution(* org.springframework..*.*(..)) && @annotation(transactional)") + public Object around(ProceedingJoinPoint pjp, Transactional transactional) throws Throwable { + throw new IllegalStateException("Invoked with @Transactional"); + } } -} + @Aspect + static class NamedPointcutWithArgs { -@Aspect -class NamedPointcutWithArgs { + @Pointcut("execution(* *(..)) && args(s,..)") + public void pointcutWithArgs(String s) {} + + @Around("pointcutWithArgs(aString)") + public Object doAround(ProceedingJoinPoint pjp, String aString) throws Throwable { + throw new IllegalArgumentException(aString); + } + } - @Pointcut("execution(* *(..)) && args(s,..)") - public void pointcutWithArgs(String s) {} - @Around("pointcutWithArgs(aString)") - public Object doAround(ProceedingJoinPoint pjp, String aString) throws Throwable { - System.out.println("got '" + aString + "' at '" + pjp + "'"); - throw new IllegalArgumentException(aString); + @Aspect("pertarget(execution(* *(..)))") + static class DynamicPointcutWithArgs { + + @Around("execution(* *(..)) && args(java.lang.String)") + public Object doAround(ProceedingJoinPoint pjp) throws Throwable { + throw new IllegalArgumentException(String.valueOf(pjp.getArgs()[0])); + } } } + diff --git a/spring-aop/src/test/java/org/springframework/aop/framework/ProxyFactoryTests.java b/spring-aop/src/test/java/org/springframework/aop/framework/ProxyFactoryTests.java index 88da7bf92738..0823151c294c 100644 --- a/spring-aop/src/test/java/org/springframework/aop/framework/ProxyFactoryTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/framework/ProxyFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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,10 @@ package org.springframework.aop.framework; +import java.sql.SQLException; +import java.sql.Savepoint; import java.util.ArrayList; +import java.util.Date; import java.util.List; import javax.accessibility.Accessible; @@ -380,6 +383,43 @@ public void testInterceptorWithoutJoinpoint() { assertThat(proxy.getName()).isEqualTo("tb"); } + @Test + public void testCharSequenceProxy() { + CharSequence target = "test"; + ProxyFactory pf = new ProxyFactory(target); + ClassLoader cl = target.getClass().getClassLoader(); + CharSequence proxy = (CharSequence) pf.getProxy(cl); + assertThat(proxy.toString()).isEqualTo(target); + } + + @Test + public void testDateProxy() { + Date target = new Date(); + ProxyFactory pf = new ProxyFactory(target); + pf.setProxyTargetClass(true); + ClassLoader cl = target.getClass().getClassLoader(); + Date proxy = (Date) pf.getProxy(cl); + assertThat(proxy.getTime()).isEqualTo(target.getTime()); + } + + @Test + public void testJdbcSavepointProxy() throws SQLException { + Savepoint target = new Savepoint() { + @Override + public int getSavepointId() throws SQLException { + return 1; + } + @Override + public String getSavepointName() throws SQLException { + return "sp"; + } + }; + ProxyFactory pf = new ProxyFactory(target); + ClassLoader cl = Savepoint.class.getClassLoader(); + Savepoint proxy = (Savepoint) pf.getProxy(cl); + assertThat(proxy.getSavepointName()).isEqualTo("sp"); + } + @Order(2) public static class A implements Runnable { @@ -391,7 +431,7 @@ public void run() { @Order(1) - public static class B implements Runnable{ + public static class B implements Runnable { @Override public void run() { diff --git a/spring-aop/src/test/java/org/springframework/aop/framework/adapter/ThrowsAdviceInterceptorTests.java b/spring-aop/src/test/java/org/springframework/aop/framework/adapter/ThrowsAdviceInterceptorTests.java index 40b038b457ef..d1bfc13af5d5 100644 --- a/spring-aop/src/test/java/org/springframework/aop/framework/adapter/ThrowsAdviceInterceptorTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/framework/adapter/ThrowsAdviceInterceptorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -77,9 +77,7 @@ public void testCorrectHandlerUsed() throws Throwable { given(mi.getMethod()).willReturn(Object.class.getMethod("hashCode")); given(mi.getThis()).willReturn(new Object()); given(mi.proceed()).willThrow(ex); - assertThatExceptionOfType(FileNotFoundException.class).isThrownBy(() -> - ti.invoke(mi)) - .isSameAs(ex); + assertThatExceptionOfType(FileNotFoundException.class).isThrownBy(() -> ti.invoke(mi)).isSameAs(ex); assertThat(th.getCalls()).isEqualTo(1); assertThat(th.getCalls("ioException")).isEqualTo(1); } @@ -92,9 +90,7 @@ public void testCorrectHandlerUsedForSubclass() throws Throwable { ConnectException ex = new ConnectException(""); MethodInvocation mi = mock(MethodInvocation.class); given(mi.proceed()).willThrow(ex); - assertThatExceptionOfType(ConnectException.class).isThrownBy(() -> - ti.invoke(mi)) - .isSameAs(ex); + assertThatExceptionOfType(ConnectException.class).isThrownBy(() -> ti.invoke(mi)).isSameAs(ex); assertThat(th.getCalls()).isEqualTo(1); assertThat(th.getCalls("remoteException")).isEqualTo(1); } @@ -117,9 +113,7 @@ public void afterThrowing(RemoteException ex) throws Throwable { ConnectException ex = new ConnectException(""); MethodInvocation mi = mock(MethodInvocation.class); given(mi.proceed()).willThrow(ex); - assertThatExceptionOfType(Throwable.class).isThrownBy(() -> - ti.invoke(mi)) - .isSameAs(t); + assertThatExceptionOfType(Throwable.class).isThrownBy(() -> ti.invoke(mi)).isSameAs(t); assertThat(th.getCalls()).isEqualTo(1); assertThat(th.getCalls("remoteException")).isEqualTo(1); } diff --git a/spring-aop/src/test/java/org/springframework/aop/support/AopUtilsTests.java b/spring-aop/src/test/java/org/springframework/aop/support/AopUtilsTests.java index dc5437e6cd67..8798428aff35 100644 --- a/spring-aop/src/test/java/org/springframework/aop/support/AopUtilsTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/support/AopUtilsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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,29 +17,35 @@ package org.springframework.aop.support; import java.lang.reflect.Method; +import java.util.List; import org.junit.jupiter.api.Test; import org.springframework.aop.ClassFilter; import org.springframework.aop.MethodMatcher; import org.springframework.aop.Pointcut; +import org.springframework.aop.framework.ProxyFactory; import org.springframework.aop.interceptor.ExposeInvocationInterceptor; import org.springframework.aop.target.EmptyTargetSource; import org.springframework.aop.testfixture.interceptor.NopInterceptor; import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.core.ResolvableType; import org.springframework.core.testfixture.io.SerializationTestUtils; import org.springframework.lang.Nullable; +import org.springframework.util.ReflectionUtils; import static org.assertj.core.api.Assertions.assertThat; /** * @author Rod Johnson * @author Chris Beams + * @author Sebastien Deleuze + * @author Juergen Hoeller */ -public class AopUtilsTests { +class AopUtilsTests { @Test - public void testPointcutCanNeverApply() { + void testPointcutCanNeverApply() { class TestPointcut extends StaticMethodMatcherPointcut { @Override public boolean matches(Method method, @Nullable Class clazzy) { @@ -52,13 +58,13 @@ public boolean matches(Method method, @Nullable Class clazzy) { } @Test - public void testPointcutAlwaysApplies() { + void testPointcutAlwaysApplies() { assertThat(AopUtils.canApply(new DefaultPointcutAdvisor(new NopInterceptor()), Object.class)).isTrue(); assertThat(AopUtils.canApply(new DefaultPointcutAdvisor(new NopInterceptor()), TestBean.class)).isTrue(); } @Test - public void testPointcutAppliesToOneMethodOnObject() { + void testPointcutAppliesToOneMethodOnObject() { class TestPointcut extends StaticMethodMatcherPointcut { @Override public boolean matches(Method method, @Nullable Class clazz) { @@ -78,7 +84,7 @@ public boolean matches(Method method, @Nullable Class clazz) { * that's subverted the singleton construction limitation. */ @Test - public void testCanonicalFrameworkClassesStillCanonicalOnDeserialization() throws Exception { + void testCanonicalFrameworkClassesStillCanonicalOnDeserialization() throws Exception { assertThat(SerializationTestUtils.serializeAndDeserialize(MethodMatcher.TRUE)).isSameAs(MethodMatcher.TRUE); assertThat(SerializationTestUtils.serializeAndDeserialize(ClassFilter.TRUE)).isSameAs(ClassFilter.TRUE); assertThat(SerializationTestUtils.serializeAndDeserialize(Pointcut.TRUE)).isSameAs(Pointcut.TRUE); @@ -88,4 +94,45 @@ public void testCanonicalFrameworkClassesStillCanonicalOnDeserialization() throw assertThat(SerializationTestUtils.serializeAndDeserialize(ExposeInvocationInterceptor.INSTANCE)).isSameAs(ExposeInvocationInterceptor.INSTANCE); } + @Test + void testInvokeJoinpointUsingReflection() throws Throwable { + String name = "foo"; + TestBean testBean = new TestBean(name); + Method method = ReflectionUtils.findMethod(TestBean.class, "getName"); + Object result = AopUtils.invokeJoinpointUsingReflection(testBean, method, new Object[0]); + assertThat(result).isEqualTo(name); + } + + @Test // gh-32365 + void mostSpecificMethodBetweenJdkProxyAndTarget() throws Exception { + Class proxyClass = new ProxyFactory(new WithInterface()).getProxy(getClass().getClassLoader()).getClass(); + Method specificMethod = AopUtils.getMostSpecificMethod(proxyClass.getMethod("handle", List.class), WithInterface.class); + assertThat(ResolvableType.forMethodParameter(specificMethod, 0).getGeneric().toClass()).isEqualTo(String.class); + } + + @Test // gh-32365 + void mostSpecificMethodBetweenCglibProxyAndTarget() throws Exception { + Class proxyClass = new ProxyFactory(new WithoutInterface()).getProxy(getClass().getClassLoader()).getClass(); + Method specificMethod = AopUtils.getMostSpecificMethod(proxyClass.getMethod("handle", List.class), WithoutInterface.class); + assertThat(ResolvableType.forMethodParameter(specificMethod, 0).getGeneric().toClass()).isEqualTo(String.class); + } + + + interface ProxyInterface { + + void handle(List list); + } + + static class WithInterface implements ProxyInterface { + + public void handle(List list) { + } + } + + static class WithoutInterface { + + public void handle(List list) { + } + } + } diff --git a/spring-aspects/src/test/java/org/springframework/aop/aspectj/autoproxy/AutoProxyWithCodeStyleAspectsTests.java b/spring-aspects/src/test/java/org/springframework/aop/aspectj/autoproxy/AutoProxyWithCodeStyleAspectsTests.java index 43947fa29c29..0e62726089d2 100644 --- a/spring-aspects/src/test/java/org/springframework/aop/aspectj/autoproxy/AutoProxyWithCodeStyleAspectsTests.java +++ b/spring-aspects/src/test/java/org/springframework/aop/aspectj/autoproxy/AutoProxyWithCodeStyleAspectsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-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,12 +22,13 @@ /** * @author Adrian Colyer + * @author Juergen Hoeller */ public class AutoProxyWithCodeStyleAspectsTests { @Test @SuppressWarnings("resource") - public void noAutoproxyingOfAjcCompiledAspects() { + public void noAutoProxyingOfAjcCompiledAspects() { new ClassPathXmlApplicationContext("org/springframework/aop/aspectj/autoproxy/ajcAutoproxyTests.xml"); } diff --git a/spring-aspects/src/test/java/org/springframework/beans/factory/aspectj/SpringConfiguredWithAutoProxyingTests.java b/spring-aspects/src/test/java/org/springframework/beans/factory/aspectj/SpringConfiguredWithAutoProxyingTests.java index a02bcad6793b..220780971182 100644 --- a/spring-aspects/src/test/java/org/springframework/beans/factory/aspectj/SpringConfiguredWithAutoProxyingTests.java +++ b/spring-aspects/src/test/java/org/springframework/beans/factory/aspectj/SpringConfiguredWithAutoProxyingTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-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,10 @@ import org.springframework.context.support.ClassPathXmlApplicationContext; +/** + * @author Ramnivas Laddad + * @author Juergen Hoeller + */ public class SpringConfiguredWithAutoProxyingTests { @Test diff --git a/spring-aspects/src/test/resources/org/springframework/aop/aspectj/autoproxy/ajcAutoproxyTests.xml b/spring-aspects/src/test/resources/org/springframework/aop/aspectj/autoproxy/ajcAutoproxyTests.xml index 6be707bf51dd..e6c494c4f966 100644 --- a/spring-aspects/src/test/resources/org/springframework/aop/aspectj/autoproxy/ajcAutoproxyTests.xml +++ b/spring-aspects/src/test/resources/org/springframework/aop/aspectj/autoproxy/ajcAutoproxyTests.xml @@ -2,16 +2,29 @@ + http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop-2.0.xsd + http://www.springframework.org/schema/cache https://www.springframework.org/schema/cache/spring-cache-3.1.xsd + http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context-2.5.xsd"> - + + + + + + + - + + + + + diff --git a/spring-aspects/src/test/resources/org/springframework/cache/config/annotation-jcache-aspectj.xml b/spring-aspects/src/test/resources/org/springframework/cache/config/annotation-jcache-aspectj.xml index 54ddbfd44a7b..9366abc646ee 100644 --- a/spring-aspects/src/test/resources/org/springframework/cache/config/annotation-jcache-aspectj.xml +++ b/spring-aspects/src/test/resources/org/springframework/cache/config/annotation-jcache-aspectj.xml @@ -24,8 +24,7 @@ - + diff --git a/spring-aspects/src/test/resources/org/springframework/scheduling/aspectj/annotationDrivenContext.xml b/spring-aspects/src/test/resources/org/springframework/scheduling/aspectj/annotationDrivenContext.xml index 61d1d3a8e9bf..2bc3dc1d113d 100644 --- a/spring-aspects/src/test/resources/org/springframework/scheduling/aspectj/annotationDrivenContext.xml +++ b/spring-aspects/src/test/resources/org/springframework/scheduling/aspectj/annotationDrivenContext.xml @@ -7,12 +7,10 @@ http://www.springframework.org/schema/task https://www.springframework.org/schema/task/spring-task.xsd"> - + - + diff --git a/spring-beans/src/main/java/org/springframework/beans/AbstractNestablePropertyAccessor.java b/spring-beans/src/main/java/org/springframework/beans/AbstractNestablePropertyAccessor.java index b56ec6b9533b..7638a8af8b68 100644 --- a/spring-beans/src/main/java/org/springframework/beans/AbstractNestablePropertyAccessor.java +++ b/spring-beans/src/main/java/org/springframework/beans/AbstractNestablePropertyAccessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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. @@ -1004,18 +1004,20 @@ public String toString() { */ protected abstract static class PropertyHandler { + @Nullable private final Class propertyType; private final boolean readable; private final boolean writable; - public PropertyHandler(Class propertyType, boolean readable, boolean writable) { + public PropertyHandler(@Nullable Class propertyType, boolean readable, boolean writable) { this.propertyType = propertyType; this.readable = readable; this.writable = writable; } + @Nullable public Class getPropertyType() { return this.propertyType; } diff --git a/spring-beans/src/main/java/org/springframework/beans/BeanUtils.java b/spring-beans/src/main/java/org/springframework/beans/BeanUtils.java index 61b9541a12db..81283ac27ce6 100644 --- a/spring-beans/src/main/java/org/springframework/beans/BeanUtils.java +++ b/spring-beans/src/main/java/org/springframework/beans/BeanUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; +import java.lang.reflect.Type; import java.net.URI; import java.net.URL; import java.time.temporal.Temporal; @@ -30,6 +31,7 @@ import java.util.Collections; import java.util.Date; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; @@ -62,7 +64,7 @@ *

    Mainly for internal use within the framework, but to some degree also * useful for application classes. Consider * Apache Commons BeanUtils, - * BULL - Bean Utils Light Library, + * BULL - Bean Utils Light Library, * or similar third-party frameworks for more comprehensive bean utilities. * * @author Rod Johnson @@ -247,7 +249,8 @@ public static Constructor getResolvableConstructor(Class clazz) { // A single public constructor return (Constructor) ctors[0]; } - else if (ctors.length == 0){ + else if (ctors.length == 0) { + // No public constructors -> check non-public ctors = clazz.getDeclaredConstructors(); if (ctors.length == 1) { // A single non-public constructor, e.g. from a non-public record type @@ -786,38 +789,28 @@ private static void copyProperties(Object source, Object target, @Nullable Class if (editable != null) { if (!editable.isInstance(target)) { throw new IllegalArgumentException("Target class [" + target.getClass().getName() + - "] not assignable to Editable class [" + editable.getName() + "]"); + "] not assignable to editable class [" + editable.getName() + "]"); } actualEditable = editable; } PropertyDescriptor[] targetPds = getPropertyDescriptors(actualEditable); - List ignoreList = (ignoreProperties != null ? Arrays.asList(ignoreProperties) : null); + Set ignoredProps = (ignoreProperties != null ? new HashSet<>(Arrays.asList(ignoreProperties)) : null); + CachedIntrospectionResults sourceResults = (actualEditable != source.getClass() ? + CachedIntrospectionResults.forClass(source.getClass()) : null); for (PropertyDescriptor targetPd : targetPds) { Method writeMethod = targetPd.getWriteMethod(); - if (writeMethod != null && (ignoreList == null || !ignoreList.contains(targetPd.getName()))) { - PropertyDescriptor sourcePd = getPropertyDescriptor(source.getClass(), targetPd.getName()); + if (writeMethod != null && (ignoredProps == null || !ignoredProps.contains(targetPd.getName()))) { + PropertyDescriptor sourcePd = (sourceResults != null ? + sourceResults.getPropertyDescriptor(targetPd.getName()) : targetPd); if (sourcePd != null) { Method readMethod = sourcePd.getReadMethod(); if (readMethod != null) { - ResolvableType sourceResolvableType = ResolvableType.forMethodReturnType(readMethod); - ResolvableType targetResolvableType = ResolvableType.forMethodParameter(writeMethod, 0); - - // Ignore generic types in assignable check if either ResolvableType has unresolvable generics. - boolean isAssignable = - (sourceResolvableType.hasUnresolvableGenerics() || targetResolvableType.hasUnresolvableGenerics() ? - ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], readMethod.getReturnType()) : - targetResolvableType.isAssignableFrom(sourceResolvableType)); - - if (isAssignable) { + if (isAssignable(writeMethod, readMethod)) { try { - if (!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers())) { - readMethod.setAccessible(true); - } + ReflectionUtils.makeAccessible(readMethod); Object value = readMethod.invoke(source); - if (!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())) { - writeMethod.setAccessible(true); - } + ReflectionUtils.makeAccessible(writeMethod); writeMethod.invoke(target, value); } catch (Throwable ex) { @@ -831,6 +824,24 @@ private static void copyProperties(Object source, Object target, @Nullable Class } } + private static boolean isAssignable(Method writeMethod, Method readMethod) { + Type paramType = writeMethod.getGenericParameterTypes()[0]; + if (paramType instanceof Class) { + return ClassUtils.isAssignable((Class) paramType, readMethod.getReturnType()); + } + else if (paramType.equals(readMethod.getGenericReturnType())) { + return true; + } + else { + ResolvableType sourceType = ResolvableType.forMethodReturnType(readMethod); + ResolvableType targetType = ResolvableType.forMethodParameter(writeMethod, 0); + // Ignore generic types in assignable check if either ResolvableType has unresolvable generics. + return (sourceType.hasUnresolvableGenerics() || targetType.hasUnresolvableGenerics() ? + ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], readMethod.getReturnType()) : + targetType.isAssignableFrom(sourceType)); + } + } + /** * Inner class to avoid a hard dependency on Kotlin at runtime. @@ -891,7 +902,6 @@ public static T instantiateClass(Constructor ctor, Object... args) } return kotlinConstructor.callBy(argParameters); } - } } diff --git a/spring-beans/src/main/java/org/springframework/beans/CachedIntrospectionResults.java b/spring-beans/src/main/java/org/springframework/beans/CachedIntrospectionResults.java index 42e9acca27f1..1af0bde135c6 100644 --- a/spring-beans/src/main/java/org/springframework/beans/CachedIntrospectionResults.java +++ b/spring-beans/src/main/java/org/springframework/beans/CachedIntrospectionResults.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -248,9 +248,22 @@ private static BeanInfo getBeanInfo(Class beanClass) throws IntrospectionExce return beanInfo; } } - return (shouldIntrospectorIgnoreBeaninfoClasses ? + + BeanInfo beanInfo = (shouldIntrospectorIgnoreBeaninfoClasses ? Introspector.getBeanInfo(beanClass, Introspector.IGNORE_ALL_BEANINFO) : Introspector.getBeanInfo(beanClass)); + + // Immediately remove class from Introspector cache to allow for proper garbage + // collection on class loader shutdown; we cache it in CachedIntrospectionResults + // in a GC-friendly manner. This is necessary (again) for the JDK ClassInfo cache. + Class classToFlush = beanClass; + do { + Introspector.flushFromCaches(classToFlush); + classToFlush = classToFlush.getSuperclass(); + } + while (classToFlush != null && classToFlush != Object.class); + + return beanInfo; } diff --git a/spring-beans/src/main/java/org/springframework/beans/DirectFieldAccessor.java b/spring-beans/src/main/java/org/springframework/beans/DirectFieldAccessor.java index a98c6eb41b03..0a9b39e2e477 100644 --- a/spring-beans/src/main/java/org/springframework/beans/DirectFieldAccessor.java +++ b/spring-beans/src/main/java/org/springframework/beans/DirectFieldAccessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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. @@ -129,7 +129,6 @@ public Object getValue() throws Exception { ReflectionUtils.makeAccessible(this.field); return this.field.get(getWrappedInstance()); } - catch (IllegalAccessException ex) { throw new InvalidPropertyException(getWrappedClass(), this.field.getName(), "Field is not accessible", ex); diff --git a/spring-beans/src/main/java/org/springframework/beans/SimpleBeanInfoFactory.java b/spring-beans/src/main/java/org/springframework/beans/SimpleBeanInfoFactory.java index 1313867007e6..d9e811f79bca 100644 --- a/spring-beans/src/main/java/org/springframework/beans/SimpleBeanInfoFactory.java +++ b/spring-beans/src/main/java/org/springframework/beans/SimpleBeanInfoFactory.java @@ -16,6 +16,7 @@ package org.springframework.beans; +import java.beans.BeanDescriptor; import java.beans.BeanInfo; import java.beans.IntrospectionException; import java.beans.PropertyDescriptor; @@ -51,6 +52,10 @@ public BeanInfo getBeanInfo(Class beanClass) throws IntrospectionException { PropertyDescriptorUtils.determineBasicProperties(beanClass); return new SimpleBeanInfo() { + @Override + public BeanDescriptor getBeanDescriptor() { + return new BeanDescriptor(beanClass); + } @Override public PropertyDescriptor[] getPropertyDescriptors() { return pds.toArray(PropertyDescriptorUtils.EMPTY_PROPERTY_DESCRIPTOR_ARRAY); diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/BeanFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/BeanFactory.java index 93c909e047b6..e61bd06599d6 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/BeanFactory.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/BeanFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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. @@ -136,7 +136,10 @@ public interface BeanFactory { *

    Translates aliases back to the corresponding canonical bean name. *

    Will ask the parent factory if the bean cannot be found in this factory instance. * @param name the name of the bean to retrieve - * @return an instance of the bean + * @return an instance of the bean. + * Note that the return value will never be {@code null} but possibly a stub for + * {@code null} returned from a factory method, to be checked via {@code equals(null)}. + * Consider using {@link #getBeanProvider(Class)} for resolving optional dependencies. * @throws NoSuchBeanDefinitionException if there is no bean with the specified name * @throws BeansException if the bean could not be obtained */ @@ -152,7 +155,11 @@ public interface BeanFactory { *

    Will ask the parent factory if the bean cannot be found in this factory instance. * @param name the name of the bean to retrieve * @param requiredType type the bean must match; can be an interface or superclass - * @return an instance of the bean + * @return an instance of the bean. + * Note that the return value will never be {@code null}. In case of a stub for + * {@code null} from a factory method having been resolved for the requested bean, a + * {@code BeanNotOfRequiredTypeException} against the NullBean stub will be raised. + * Consider using {@link #getBeanProvider(Class)} for resolving optional dependencies. * @throws NoSuchBeanDefinitionException if there is no such bean definition * @throws BeanNotOfRequiredTypeException if the bean is not of the required type * @throws BeansException if the bean could not be created @@ -232,7 +239,7 @@ public interface BeanFactory { * specific type, specify the actual bean type as an argument here and subsequently * use {@link ObjectProvider#orderedStream()} or its lazy streaming/iteration options. *

    Also, generics matching is strict here, as per the Java assignment rules. - * For lenient fallback matching with unchecked semantics (similar to the ´unchecked´ + * For lenient fallback matching with unchecked semantics (similar to the 'unchecked' * Java compiler warning), consider calling {@link #getBeanProvider(Class)} with the * raw type as a second step if no full generic match is * {@link ObjectProvider#getIfAvailable() available} with this variant. diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessor.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessor.java index 77c3d66597fe..95fa55766032 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessor.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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.beans.factory.annotation; import java.beans.PropertyDescriptor; +import java.io.IOException; import java.lang.annotation.Annotation; import java.lang.reflect.AccessibleObject; import java.lang.reflect.Constructor; @@ -62,6 +63,10 @@ import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.annotation.MergedAnnotation; import org.springframework.core.annotation.MergedAnnotations; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.core.type.MethodMetadata; +import org.springframework.core.type.classreading.MetadataReaderFactory; +import org.springframework.core.type.classreading.SimpleMetadataReaderFactory; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -144,6 +149,9 @@ public class AutowiredAnnotationBeanPostProcessor implements SmartInstantiationA @Nullable private ConfigurableListableBeanFactory beanFactory; + @Nullable + private MetadataReaderFactory metadataReaderFactory; + private final Set lookupMethodsChecked = Collections.newSetFromMap(new ConcurrentHashMap<>(256)); private final Map, Constructor[]> candidateConstructorsCache = new ConcurrentHashMap<>(256); @@ -238,6 +246,7 @@ public void setBeanFactory(BeanFactory beanFactory) { "AutowiredAnnotationBeanPostProcessor requires a ConfigurableListableBeanFactory: " + beanFactory); } this.beanFactory = (ConfigurableListableBeanFactory) beanFactory; + this.metadataReaderFactory = new SimpleMetadataReaderFactory(this.beanFactory.getBeanClassLoader()); } @@ -463,12 +472,11 @@ private InjectionMetadata buildAutowiringMetadata(Class clazz) { return InjectionMetadata.EMPTY; } - List elements = new ArrayList<>(); + final List elements = new ArrayList<>(); Class targetClass = clazz; do { - final List currElements = new ArrayList<>(); - + final List fieldElements = new ArrayList<>(); ReflectionUtils.doWithLocalFields(targetClass, field -> { MergedAnnotation ann = findAutowiredAnnotation(field); if (ann != null) { @@ -479,10 +487,11 @@ private InjectionMetadata buildAutowiringMetadata(Class clazz) { return; } boolean required = determineRequiredStatus(ann); - currElements.add(new AutowiredFieldElement(field, required)); + fieldElements.add(new AutowiredFieldElement(field, required)); } }); + final List methodElements = new ArrayList<>(); ReflectionUtils.doWithLocalMethods(targetClass, method -> { Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(method); if (!BridgeMethodResolver.isVisibilityBridgeMethodPair(method, bridgedMethod)) { @@ -504,11 +513,12 @@ private InjectionMetadata buildAutowiringMetadata(Class clazz) { } boolean required = determineRequiredStatus(ann); PropertyDescriptor pd = BeanUtils.findPropertyForMethod(bridgedMethod, clazz); - currElements.add(new AutowiredMethodElement(method, required, pd)); + methodElements.add(new AutowiredMethodElement(method, required, pd)); } }); - elements.addAll(0, currElements); + elements.addAll(0, sortMethodElements(methodElements, targetClass)); + elements.addAll(0, fieldElements); targetClass = targetClass.getSuperclass(); } while (targetClass != null && targetClass != Object.class); @@ -573,6 +583,47 @@ protected Map findAutowireCandidates(Class type) throws BeansE return BeanFactoryUtils.beansOfTypeIncludingAncestors(this.beanFactory, type); } + /** + * Sort the method elements via ASM for deterministic declaration order if possible. + */ + private List sortMethodElements( + List methodElements, Class targetClass) { + + if (this.metadataReaderFactory != null && methodElements.size() > 1) { + // Try reading the class file via ASM for deterministic declaration order... + // Unfortunately, the JVM's standard reflection returns methods in arbitrary + // order, even between different runs of the same application on the same JVM. + try { + AnnotationMetadata asm = + this.metadataReaderFactory.getMetadataReader(targetClass.getName()).getAnnotationMetadata(); + Set asmMethods = asm.getAnnotatedMethods(Autowired.class.getName()); + if (asmMethods.size() >= methodElements.size()) { + List candidateMethods = new ArrayList<>(methodElements); + List selectedMethods = new ArrayList<>(asmMethods.size()); + for (MethodMetadata asmMethod : asmMethods) { + for (Iterator it = candidateMethods.iterator(); it.hasNext();) { + InjectionMetadata.InjectedElement element = it.next(); + if (element.getMember().getName().equals(asmMethod.getMethodName())) { + selectedMethods.add(element); + it.remove(); + break; + } + } + } + if (selectedMethods.size() == methodElements.size()) { + // All reflection-detected methods found in ASM method set -> proceed + return selectedMethods; + } + } + } + catch (IOException ex) { + logger.debug("Failed to read class file via ASM for determining @Autowired method order", ex); + // No worries, let's continue with the reflection metadata we started with... + } + } + return methodElements; + } + /** * Register the specified bean as dependent on the autowired beans. */ @@ -594,7 +645,7 @@ private void registerDependentBeans(@Nullable String beanName, Set autow * Resolve the specified cached method argument or field value. */ @Nullable - private Object resolvedCachedArgument(@Nullable String beanName, @Nullable Object cachedArgument) { + private Object resolveCachedArgument(@Nullable String beanName, @Nullable Object cachedArgument) { if (cachedArgument instanceof DependencyDescriptor) { DependencyDescriptor descriptor = (DependencyDescriptor) cachedArgument; Assert.state(this.beanFactory != null, "No BeanFactory available"); @@ -629,10 +680,12 @@ protected void inject(Object bean, @Nullable String beanName, @Nullable Property Object value; if (this.cached) { try { - value = resolvedCachedArgument(beanName, this.cachedFieldValue); + value = resolveCachedArgument(beanName, this.cachedFieldValue); } - catch (NoSuchBeanDefinitionException ex) { - // Unexpected removal of target bean for cached argument -> re-resolve + catch (BeansException ex) { + // Unexpected target bean mismatch for cached argument -> re-resolve + this.cached = false; + logger.debug("Failed to resolve cached argument", ex); value = resolveFieldValue(field, bean, beanName); } } @@ -649,7 +702,7 @@ protected void inject(Object bean, @Nullable String beanName, @Nullable Property private Object resolveFieldValue(Field field, Object bean, @Nullable String beanName) { DependencyDescriptor desc = new DependencyDescriptor(field, this.required); desc.setContainingClass(bean.getClass()); - Set autowiredBeanNames = new LinkedHashSet<>(1); + Set autowiredBeanNames = new LinkedHashSet<>(2); Assert.state(beanFactory != null, "No BeanFactory available"); TypeConverter typeConverter = beanFactory.getTypeConverter(); Object value; @@ -661,21 +714,23 @@ private Object resolveFieldValue(Field field, Object bean, @Nullable String bean } synchronized (this) { if (!this.cached) { - Object cachedFieldValue = null; if (value != null || this.required) { - cachedFieldValue = desc; + Object cachedFieldValue = desc; registerDependentBeans(beanName, autowiredBeanNames); - if (autowiredBeanNames.size() == 1) { + if (value != null && autowiredBeanNames.size() == 1) { String autowiredBeanName = autowiredBeanNames.iterator().next(); if (beanFactory.containsBean(autowiredBeanName) && beanFactory.isTypeMatch(autowiredBeanName, field.getType())) { - cachedFieldValue = new ShortcutDependencyDescriptor( - desc, autowiredBeanName, field.getType()); + cachedFieldValue = new ShortcutDependencyDescriptor(desc, autowiredBeanName); } } + this.cachedFieldValue = cachedFieldValue; + this.cached = true; + } + else { + this.cachedFieldValue = null; + // cached flag remains false } - this.cachedFieldValue = cachedFieldValue; - this.cached = true; } } return value; @@ -709,10 +764,12 @@ protected void inject(Object bean, @Nullable String beanName, @Nullable Property Object[] arguments; if (this.cached) { try { - arguments = resolveCachedArguments(beanName); + arguments = resolveCachedArguments(beanName, this.cachedMethodArguments); } - catch (NoSuchBeanDefinitionException ex) { - // Unexpected removal of target bean for cached argument -> re-resolve + catch (BeansException ex) { + // Unexpected target bean mismatch for cached argument -> re-resolve + this.cached = false; + logger.debug("Failed to resolve cached argument", ex); arguments = resolveMethodArguments(method, bean, beanName); } } @@ -731,14 +788,13 @@ protected void inject(Object bean, @Nullable String beanName, @Nullable Property } @Nullable - private Object[] resolveCachedArguments(@Nullable String beanName) { - Object[] cachedMethodArguments = this.cachedMethodArguments; + private Object[] resolveCachedArguments(@Nullable String beanName, @Nullable Object[] cachedMethodArguments) { if (cachedMethodArguments == null) { return null; } Object[] arguments = new Object[cachedMethodArguments.length]; for (int i = 0; i < arguments.length; i++) { - arguments[i] = resolvedCachedArgument(beanName, cachedMethodArguments[i]); + arguments[i] = resolveCachedArgument(beanName, cachedMethodArguments[i]); } return arguments; } @@ -748,7 +804,7 @@ private Object[] resolveMethodArguments(Method method, Object bean, @Nullable St int argumentCount = method.getParameterCount(); Object[] arguments = new Object[argumentCount]; DependencyDescriptor[] descriptors = new DependencyDescriptor[argumentCount]; - Set autowiredBeans = new LinkedHashSet<>(argumentCount); + Set autowiredBeanNames = new LinkedHashSet<>(argumentCount * 2); Assert.state(beanFactory != null, "No BeanFactory available"); TypeConverter typeConverter = beanFactory.getTypeConverter(); for (int i = 0; i < arguments.length; i++) { @@ -757,7 +813,7 @@ private Object[] resolveMethodArguments(Method method, Object bean, @Nullable St currDesc.setContainingClass(bean.getClass()); descriptors[i] = currDesc; try { - Object arg = beanFactory.resolveDependency(currDesc, beanName, autowiredBeans, typeConverter); + Object arg = beanFactory.resolveDependency(currDesc, beanName, autowiredBeanNames, typeConverter); if (arg == null && !this.required) { arguments = null; break; @@ -771,26 +827,27 @@ private Object[] resolveMethodArguments(Method method, Object bean, @Nullable St synchronized (this) { if (!this.cached) { if (arguments != null) { - DependencyDescriptor[] cachedMethodArguments = Arrays.copyOf(descriptors, arguments.length); - registerDependentBeans(beanName, autowiredBeans); - if (autowiredBeans.size() == argumentCount) { - Iterator it = autowiredBeans.iterator(); + DependencyDescriptor[] cachedMethodArguments = Arrays.copyOf(descriptors, argumentCount); + registerDependentBeans(beanName, autowiredBeanNames); + if (autowiredBeanNames.size() == argumentCount) { + Iterator it = autowiredBeanNames.iterator(); Class[] paramTypes = method.getParameterTypes(); for (int i = 0; i < paramTypes.length; i++) { String autowiredBeanName = it.next(); - if (beanFactory.containsBean(autowiredBeanName) && + if (arguments[i] != null && beanFactory.containsBean(autowiredBeanName) && beanFactory.isTypeMatch(autowiredBeanName, paramTypes[i])) { cachedMethodArguments[i] = new ShortcutDependencyDescriptor( - descriptors[i], autowiredBeanName, paramTypes[i]); + descriptors[i], autowiredBeanName); } } } this.cachedMethodArguments = cachedMethodArguments; + this.cached = true; } else { this.cachedMethodArguments = null; + // cached flag remains false } - this.cached = true; } } return arguments; @@ -806,17 +863,14 @@ private static class ShortcutDependencyDescriptor extends DependencyDescriptor { private final String shortcut; - private final Class requiredType; - - public ShortcutDependencyDescriptor(DependencyDescriptor original, String shortcut, Class requiredType) { + public ShortcutDependencyDescriptor(DependencyDescriptor original, String shortcut) { super(original); this.shortcut = shortcut; - this.requiredType = requiredType; } @Override public Object resolveShortcut(BeanFactory beanFactory) { - return beanFactory.getBean(this.shortcut, this.requiredType); + return beanFactory.getBean(this.shortcut, getDependencyType()); } } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/QualifierAnnotationAutowireCandidateResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/QualifierAnnotationAutowireCandidateResolver.java index d7e7e45574f1..4dacbbb47750 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/QualifierAnnotationAutowireCandidateResolver.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/QualifierAnnotationAutowireCandidateResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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 org.springframework.beans.SimpleTypeConverter; import org.springframework.beans.TypeConverter; +import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.config.BeanDefinitionHolder; import org.springframework.beans.factory.config.DependencyDescriptor; @@ -240,10 +241,11 @@ protected boolean checkQualifier( } } if (targetAnnotation == null) { + BeanFactory beanFactory = getBeanFactory(); // Look for matching annotation on the target class - if (getBeanFactory() != null) { + if (beanFactory != null) { try { - Class beanType = getBeanFactory().getType(bdHolder.getBeanName()); + Class beanType = beanFactory.getType(bdHolder.getBeanName()); if (beanType != null) { targetAnnotation = AnnotationUtils.getAnnotation(ClassUtils.getUserClass(beanType), type); } @@ -290,7 +292,7 @@ protected boolean checkQualifier( if (actualValue != null) { actualValue = typeConverter.convertIfNecessary(actualValue, expectedValue.getClass()); } - if (!expectedValue.equals(actualValue)) { + if (!ObjectUtils.nullSafeEquals(expectedValue, actualValue)) { return false; } } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/YamlProcessor.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/YamlProcessor.java index cd53b6082c6f..b6ac3e0133ca 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/YamlProcessor.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/YamlProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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. @@ -184,8 +184,9 @@ protected void process(MatchCallback callback) { protected Yaml createYaml() { LoaderOptions loaderOptions = new LoaderOptions(); loaderOptions.setAllowDuplicateKeys(false); - return new Yaml(new FilteringConstructor(loaderOptions), new Representer(), - new DumperOptions(), loaderOptions); + DumperOptions dumperOptions = new DumperOptions(); + return new Yaml(new FilteringConstructor(loaderOptions), new Representer(dumperOptions), + dumperOptions, loaderOptions); } private boolean process(MatchCallback callback, Yaml yaml, Resource resource) { diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java index fcb2ef67e36a..32ac333e0b0d 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-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. @@ -97,10 +97,10 @@ * Supports autowiring constructors, properties by name, and properties by type. * *

    The main template method to be implemented by subclasses is - * {@link #resolveDependency(DependencyDescriptor, String, Set, TypeConverter)}, - * used for autowiring by type. In case of a factory which is capable of searching - * its bean definitions, matching beans will typically be implemented through such - * a search. For other factory styles, simplified matching algorithms can be implemented. + * {@link #resolveDependency(DependencyDescriptor, String, Set, TypeConverter)}, used for + * autowiring. In case of a {@link org.springframework.beans.factory.ListableBeanFactory} + * which is capable of searching its bean definitions, matching beans will typically be + * implemented through such a search. Otherwise, simplified matching can be implemented. * *

    Note that this class does not assume or implement bean definition * registry capabilities. See {@link DefaultListableBeanFactory} for an implementation @@ -675,7 +675,7 @@ protected Class predictBeanType(String beanName, RootBeanDefinition mbd, Clas // Apply SmartInstantiationAwareBeanPostProcessors to predict the // eventual type after a before-instantiation shortcut. if (targetType != null && !mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) { - boolean matchingOnlyFactoryBean = typesToMatch.length == 1 && typesToMatch[0] == FactoryBean.class; + boolean matchingOnlyFactoryBean = (typesToMatch.length == 1 && typesToMatch[0] == FactoryBean.class); for (SmartInstantiationAwareBeanPostProcessor bp : getBeanPostProcessorCache().smartInstantiationAware) { Class predicted = bp.predictBeanType(targetType, beanName); if (predicted != null && @@ -835,11 +835,11 @@ protected Class getTypeForFactoryMethod(String beanName, RootBeanDefinition m /** * This implementation attempts to query the FactoryBean's generic parameter metadata * if present to determine the object type. If not present, i.e. the FactoryBean is - * declared as a raw type, checks the FactoryBean's {@code getObjectType} method + * declared as a raw type, it checks the FactoryBean's {@code getObjectType} method * on a plain instance of the FactoryBean, without bean properties applied yet. - * If this doesn't return a type yet, and {@code allowInit} is {@code true} a - * full creation of the FactoryBean is used as fallback (through delegation to the - * superclass's implementation). + * If this doesn't return a type yet and {@code allowInit} is {@code true}, full + * creation of the FactoryBean is attempted as fallback (through delegation to the + * superclass implementation). *

    The shortcut check for a FactoryBean is only applied in case of a singleton * FactoryBean. If the FactoryBean instance itself is not kept as singleton, * it will be fully created to check the type of its exposed object. @@ -1504,8 +1504,8 @@ protected void autowireByType( converter = bw; } - Set autowiredBeanNames = new LinkedHashSet<>(4); String[] propertyNames = unsatisfiedNonSimpleProperties(mbd, bw); + Set autowiredBeanNames = new LinkedHashSet<>(propertyNames.length * 2); for (String propertyName : propertyNames) { try { PropertyDescriptor pd = bw.getPropertyDescriptor(propertyName); diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanFactory.java index 66d33cb6c020..9c3a4081b92c 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanFactory.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-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. @@ -166,6 +166,9 @@ public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport imp /** Map from scope identifier String to corresponding Scope. */ private final Map scopes = new LinkedHashMap<>(8); + /** Application startup metrics. **/ + private ApplicationStartup applicationStartup = ApplicationStartup.DEFAULT; + /** Security context used when running with a SecurityManager. */ @Nullable private SecurityContextProvider securityContextProvider; @@ -180,8 +183,6 @@ public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport imp private final ThreadLocal prototypesCurrentlyInCreation = new NamedThreadLocal<>("Prototype beans currently in creation"); - /** Application startup metrics. **/ - private ApplicationStartup applicationStartup = ApplicationStartup.DEFAULT; /** * Create a new AbstractBeanFactory. @@ -749,7 +750,7 @@ public String[] getAliases(String name) { aliases.add(fullBeanName); } String[] retrievedAliases = super.getAliases(beanName); - String prefix = factoryPrefix ? FACTORY_BEAN_PREFIX : ""; + String prefix = (factoryPrefix ? FACTORY_BEAN_PREFIX : ""); for (String retrievedAlias : retrievedAliases) { String alias = prefix + retrievedAlias; if (!alias.equals(name)) { @@ -1079,7 +1080,7 @@ public void setSecurityContextProvider(SecurityContextProvider securityProvider) @Override public void setApplicationStartup(ApplicationStartup applicationStartup) { - Assert.notNull(applicationStartup, "applicationStartup should not be null"); + Assert.notNull(applicationStartup, "ApplicationStartup must not be null"); this.applicationStartup = applicationStartup; } @@ -1694,7 +1695,7 @@ protected boolean isFactoryBean(String beanName, RootBeanDefinition mbd) { * already. The implementation is allowed to instantiate the target factory bean if * {@code allowInit} is {@code true} and the type cannot be determined another way; * otherwise it is restricted to introspecting signatures and related metadata. - *

    If no {@link FactoryBean#OBJECT_TYPE_ATTRIBUTE} if set on the bean definition + *

    If no {@link FactoryBean#OBJECT_TYPE_ATTRIBUTE} is set on the bean definition * and {@code allowInit} is {@code true}, the default implementation will create * the FactoryBean via {@code getBean} to call its {@code getObjectType} method. * Subclasses are encouraged to optimize this, typically by inspecting the generic diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionValueResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionValueResolver.java index be9667b19a38..a15ebb5a1ab4 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionValueResolver.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionValueResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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. @@ -134,7 +134,7 @@ else if (value instanceof BeanDefinition) { return resolveInnerBean(argName, innerBeanName, bd); } else if (value instanceof DependencyDescriptor) { - Set autowiredBeanNames = new LinkedHashSet<>(4); + Set autowiredBeanNames = new LinkedHashSet<>(2); Object result = this.beanFactory.resolveDependency( (DependencyDescriptor) value, this.beanName, autowiredBeanNames, this.typeConverter); for (String autowiredBeanName : autowiredBeanNames) { diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/ConstructorResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/ConstructorResolver.java index dd268fc517fe..9ad0ab826ed4 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/ConstructorResolver.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/ConstructorResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-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. @@ -45,6 +45,7 @@ import org.springframework.beans.TypeMismatchException; import org.springframework.beans.factory.BeanCreationException; import org.springframework.beans.factory.BeanDefinitionStoreException; +import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.InjectionPoint; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.NoUniqueBeanDefinitionException; @@ -85,12 +86,6 @@ class ConstructorResolver { private static final Object[] EMPTY_ARGS = new Object[0]; - /** - * Marker for autowired arguments in a cached argument array, to be replaced - * by a {@linkplain #resolveAutowiredArgument resolved autowired argument}. - */ - private static final Object autowiredArgumentMarker = new Object(); - private static final NamedThreadLocal currentInjectionPoint = new NamedThreadLocal<>("Current injection point"); @@ -112,11 +107,7 @@ public ConstructorResolver(AbstractAutowireCapableBeanFactory beanFactory) { /** * "autowire constructor" (with constructor arguments by type) behavior. - * Also applied if explicit constructor argument values are specified, - * matching all remaining arguments with beans from the bean factory. - *

    This corresponds to constructor injection: In this mode, a Spring - * bean factory is able to host components that expect constructor-based - * dependency resolution. + * Also applied if explicit constructor argument values are specified. * @param beanName the name of the bean * @param mbd the merged bean definition for the bean * @param chosenCtors chosen candidate constructors (or {@code null} if none) @@ -609,13 +600,10 @@ else if (resolvedValues != null) { String argDesc = StringUtils.collectionToCommaDelimitedString(argTypes); throw new BeanCreationException(mbd.getResourceDescription(), beanName, "No matching factory method found on class [" + factoryClass.getName() + "]: " + - (mbd.getFactoryBeanName() != null ? - "factory bean '" + mbd.getFactoryBeanName() + "'; " : "") + + (mbd.getFactoryBeanName() != null ? "factory bean '" + mbd.getFactoryBeanName() + "'; " : "") + "factory method '" + mbd.getFactoryMethodName() + "(" + argDesc + ")'. " + - "Check that a method with the specified name " + - (minNrOfArgs > 0 ? "and arguments " : "") + - "exists and that it is " + - (isStatic ? "static" : "non-static") + "."); + "Check that a method with the specified name " + (minNrOfArgs > 0 ? "and arguments " : "") + + "exists and that it is " + (isStatic ? "static" : "non-static") + "."); } else if (void.class == factoryMethodToUse.getReturnType()) { throw new BeanCreationException(mbd.getResourceDescription(), beanName, @@ -729,7 +717,7 @@ private ArgumentsHolder createArgumentArray( ArgumentsHolder args = new ArgumentsHolder(paramTypes.length); Set usedValueHolders = new HashSet<>(paramTypes.length); - Set autowiredBeanNames = new LinkedHashSet<>(4); + Set allAutowiredBeanNames = new LinkedHashSet<>(paramTypes.length * 2); for (int paramIndex = 0; paramIndex < paramTypes.length; paramIndex++) { Class paramType = paramTypes[paramIndex]; @@ -764,8 +752,8 @@ private ArgumentsHolder createArgumentArray( throw new UnsatisfiedDependencyException( mbd.getResourceDescription(), beanName, new InjectionPoint(methodParam), "Could not convert argument value of type [" + - ObjectUtils.nullSafeClassName(valueHolder.getValue()) + - "] to required type [" + paramType.getName() + "]: " + ex.getMessage()); + ObjectUtils.nullSafeClassName(valueHolder.getValue()) + + "] to required type [" + paramType.getName() + "]: " + ex.getMessage()); } Object sourceHolder = valueHolder.getSource(); if (sourceHolder instanceof ConstructorArgumentValues.ValueHolder) { @@ -788,11 +776,17 @@ private ArgumentsHolder createArgumentArray( "] - did you specify the correct bean references as arguments?"); } try { - Object autowiredArgument = resolveAutowiredArgument( - methodParam, beanName, autowiredBeanNames, converter, fallback); - args.rawArguments[paramIndex] = autowiredArgument; - args.arguments[paramIndex] = autowiredArgument; - args.preparedArguments[paramIndex] = autowiredArgumentMarker; + ConstructorDependencyDescriptor desc = new ConstructorDependencyDescriptor(methodParam, true); + Set autowiredBeanNames = new LinkedHashSet<>(2); + Object arg = resolveAutowiredArgument( + desc, paramType, beanName, autowiredBeanNames, converter, fallback); + if (arg != null) { + setShortcutIfPossible(desc, paramType, autowiredBeanNames); + } + allAutowiredBeanNames.addAll(autowiredBeanNames); + args.rawArguments[paramIndex] = arg; + args.arguments[paramIndex] = arg; + args.preparedArguments[paramIndex] = desc; args.resolveNecessary = true; } catch (BeansException ex) { @@ -802,14 +796,7 @@ private ArgumentsHolder createArgumentArray( } } - for (String autowiredBeanName : autowiredBeanNames) { - this.beanFactory.registerDependentBean(autowiredBeanName, beanName); - if (logger.isDebugEnabled()) { - logger.debug("Autowiring by type from bean name '" + beanName + - "' via " + (executable instanceof Constructor ? "constructor" : "factory method") + - " to bean named '" + autowiredBeanName + "'"); - } - } + registerDependentBeans(executable, beanName, allAutowiredBeanNames); return args; } @@ -829,31 +816,61 @@ private Object[] resolvePreparedArguments(String beanName, RootBeanDefinition mb Object[] resolvedArgs = new Object[argsToResolve.length]; for (int argIndex = 0; argIndex < argsToResolve.length; argIndex++) { Object argValue = argsToResolve[argIndex]; - MethodParameter methodParam = MethodParameter.forExecutable(executable, argIndex); - if (argValue == autowiredArgumentMarker) { - argValue = resolveAutowiredArgument(methodParam, beanName, null, converter, true); + Class paramType = paramTypes[argIndex]; + boolean convertNecessary = false; + if (argValue instanceof ConstructorDependencyDescriptor) { + ConstructorDependencyDescriptor descriptor = (ConstructorDependencyDescriptor) argValue; + try { + argValue = resolveAutowiredArgument(descriptor, paramType, beanName, + null, converter, true); + } + catch (BeansException ex) { + // Unexpected target bean mismatch for cached argument -> re-resolve + Set autowiredBeanNames = null; + if (descriptor.hasShortcut()) { + // Reset shortcut and try to re-resolve it in this thread... + descriptor.setShortcut(null); + autowiredBeanNames = new LinkedHashSet<>(2); + } + logger.debug("Failed to resolve cached argument", ex); + argValue = resolveAutowiredArgument(descriptor, paramType, beanName, + autowiredBeanNames, converter, true); + if (autowiredBeanNames != null && !descriptor.hasShortcut()) { + // We encountered as stale shortcut before, and the shortcut has + // not been re-resolved by another thread in the meantime... + if (argValue != null) { + setShortcutIfPossible(descriptor, paramType, autowiredBeanNames); + } + registerDependentBeans(executable, beanName, autowiredBeanNames); + } + } } else if (argValue instanceof BeanMetadataElement) { argValue = valueResolver.resolveValueIfNecessary("constructor argument", argValue); + convertNecessary = true; } else if (argValue instanceof String) { argValue = this.beanFactory.evaluateBeanDefinitionString((String) argValue, mbd); + convertNecessary = true; } - Class paramType = paramTypes[argIndex]; - try { - resolvedArgs[argIndex] = converter.convertIfNecessary(argValue, paramType, methodParam); - } - catch (TypeMismatchException ex) { - throw new UnsatisfiedDependencyException( - mbd.getResourceDescription(), beanName, new InjectionPoint(methodParam), - "Could not convert argument value of type [" + ObjectUtils.nullSafeClassName(argValue) + - "] to required type [" + paramType.getName() + "]: " + ex.getMessage()); + if (convertNecessary) { + MethodParameter methodParam = MethodParameter.forExecutable(executable, argIndex); + try { + argValue = converter.convertIfNecessary(argValue, paramType, methodParam); + } + catch (TypeMismatchException ex) { + throw new UnsatisfiedDependencyException( + mbd.getResourceDescription(), beanName, new InjectionPoint(methodParam), + "Could not convert argument value of type [" + ObjectUtils.nullSafeClassName(argValue) + + "] to required type [" + paramType.getName() + "]: " + ex.getMessage()); + } } + resolvedArgs[argIndex] = argValue; } return resolvedArgs; } - protected Constructor getUserDeclaredConstructor(Constructor constructor) { + private Constructor getUserDeclaredConstructor(Constructor constructor) { Class declaringClass = constructor.getDeclaringClass(); Class userClass = ClassUtils.getUserClass(declaringClass); if (userClass != declaringClass) { @@ -869,23 +886,22 @@ protected Constructor getUserDeclaredConstructor(Constructor constructor) } /** - * Template method for resolving the specified argument which is supposed to be autowired. + * Resolve the specified argument which is supposed to be autowired. */ @Nullable - protected Object resolveAutowiredArgument(MethodParameter param, String beanName, + Object resolveAutowiredArgument(DependencyDescriptor descriptor, Class paramType, String beanName, @Nullable Set autowiredBeanNames, TypeConverter typeConverter, boolean fallback) { - Class paramType = param.getParameterType(); if (InjectionPoint.class.isAssignableFrom(paramType)) { InjectionPoint injectionPoint = currentInjectionPoint.get(); if (injectionPoint == null) { - throw new IllegalStateException("No current InjectionPoint available for " + param); + throw new IllegalStateException("No current InjectionPoint available for " + descriptor); } return injectionPoint; } + try { - return this.beanFactory.resolveDependency( - new DependencyDescriptor(param, true), beanName, autowiredBeanNames, typeConverter); + return this.beanFactory.resolveDependency(descriptor, beanName, autowiredBeanNames, typeConverter); } catch (NoUniqueBeanDefinitionException ex) { throw ex; @@ -908,6 +924,31 @@ else if (CollectionFactory.isApproximableMapType(paramType)) { } } + private void setShortcutIfPossible( + ConstructorDependencyDescriptor descriptor, Class paramType, Set autowiredBeanNames) { + + if (autowiredBeanNames.size() == 1) { + String autowiredBeanName = autowiredBeanNames.iterator().next(); + if (this.beanFactory.containsBean(autowiredBeanName) && + this.beanFactory.isTypeMatch(autowiredBeanName, paramType)) { + descriptor.setShortcut(autowiredBeanName); + } + } + } + + private void registerDependentBeans( + Executable executable, String beanName, Set autowiredBeanNames) { + + for (String autowiredBeanName : autowiredBeanNames) { + this.beanFactory.registerDependentBean(autowiredBeanName, beanName); + if (logger.isDebugEnabled()) { + logger.debug("Autowiring by type from bean name '" + beanName + "' via " + + (executable instanceof Constructor ? "constructor" : "factory method") + + " to bean named '" + autowiredBeanName + "'"); + } + } + } + static InjectionPoint setCurrentInjectionPoint(@Nullable InjectionPoint injectionPoint) { InjectionPoint old = currentInjectionPoint.get(); if (injectionPoint != null) { @@ -1006,4 +1047,35 @@ public static String[] evaluate(Constructor candidate, int paramCount) { } } + + /** + * DependencyDescriptor marker for constructor arguments, + * for differentiating between a provided DependencyDescriptor instance + * and an internally built DependencyDescriptor for autowiring purposes. + */ + @SuppressWarnings("serial") + private static class ConstructorDependencyDescriptor extends DependencyDescriptor { + + @Nullable + private volatile String shortcut; + + public ConstructorDependencyDescriptor(MethodParameter methodParameter, boolean required) { + super(methodParameter, required); + } + + public void setShortcut(@Nullable String shortcut) { + this.shortcut = shortcut; + } + + public boolean hasShortcut() { + return (this.shortcut != null); + } + + @Override + public Object resolveShortcut(BeanFactory beanFactory) { + String shortcut = this.shortcut; + return (shortcut != null ? beanFactory.getBean(shortcut, getDependencyType()) : null); + } + } + } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java index 1d65384d4aff..91db94e3f157 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -961,7 +961,7 @@ public void preInstantiateSingletons() throws BeansException { for (String beanName : beanNames) { Object singletonInstance = getSingleton(beanName); if (singletonInstance instanceof SmartInitializingSingleton) { - StartupStep smartInitialize = this.getApplicationStartup().start("spring.beans.smart-initialize") + StartupStep smartInitialize = getApplicationStartup().start("spring.beans.smart-initialize") .tag("beanName", beanName); SmartInitializingSingleton smartSingleton = (SmartInitializingSingleton) singletonInstance; if (System.getSecurityManager() != null) { diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java index 0a81aef4e9fe..81e442404973 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -102,7 +102,7 @@ public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements private boolean singletonsCurrentlyInDestruction = false; /** Disposable bean instances: bean name to disposable instance. */ - private final Map disposableBeans = new LinkedHashMap<>(); + private final Map disposableBeans = new LinkedHashMap<>(); /** Map between containing bean names: bean name to Set of bean names that the bean contains. */ private final Map> containedBeanMap = new ConcurrentHashMap<>(16); @@ -340,7 +340,7 @@ protected boolean isActuallyInCreation(String beanName) { * (within the entire factory). * @param beanName the name of the bean */ - public boolean isSingletonCurrentlyInCreation(String beanName) { + public boolean isSingletonCurrentlyInCreation(@Nullable String beanName) { return this.singletonsCurrentlyInCreation.contains(beanName); } @@ -447,17 +447,17 @@ private boolean isDependent(String beanName, String dependentBeanName, @Nullable } String canonicalName = canonicalName(beanName); Set dependentBeans = this.dependentBeanMap.get(canonicalName); - if (dependentBeans == null) { + if (dependentBeans == null || dependentBeans.isEmpty()) { return false; } if (dependentBeans.contains(dependentBeanName)) { return true; } + if (alreadySeen == null) { + alreadySeen = new HashSet<>(); + } + alreadySeen.add(beanName); for (String transitiveDependency : dependentBeans) { - if (alreadySeen == null) { - alreadySeen = new HashSet<>(); - } - alreadySeen.add(beanName); if (isDependent(transitiveDependency, dependentBeanName, alreadySeen)) { return true; } @@ -554,7 +554,7 @@ public void destroySingleton(String beanName) { // Destroy the corresponding DisposableBean instance. DisposableBean disposableBean; synchronized (this.disposableBeans) { - disposableBean = (DisposableBean) this.disposableBeans.remove(beanName); + disposableBean = this.disposableBeans.remove(beanName); } destroyBean(beanName, disposableBean); } @@ -567,16 +567,16 @@ public void destroySingleton(String beanName) { */ protected void destroyBean(String beanName, @Nullable DisposableBean bean) { // Trigger destruction of dependent beans first... - Set dependencies; + Set dependentBeanNames; synchronized (this.dependentBeanMap) { // Within full synchronization in order to guarantee a disconnected Set - dependencies = this.dependentBeanMap.remove(beanName); + dependentBeanNames = this.dependentBeanMap.remove(beanName); } - if (dependencies != null) { + if (dependentBeanNames != null) { if (logger.isTraceEnabled()) { - logger.trace("Retrieved dependent beans for bean '" + beanName + "': " + dependencies); + logger.trace("Retrieved dependent beans for bean '" + beanName + "': " + dependentBeanNames); } - for (String dependentBeanName : dependencies) { + for (String dependentBeanName : dependentBeanNames) { destroySingleton(dependentBeanName); } } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/DisposableBeanAdapter.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/DisposableBeanAdapter.java index 4317ae901c9b..62a277e0cfc5 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/DisposableBeanAdapter.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/DisposableBeanAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -68,8 +68,8 @@ class DisposableBeanAdapter implements DisposableBean, Runnable, Serializable { private static final String SHUTDOWN_METHOD_NAME = "shutdown"; - private static final Log logger = LogFactory.getLog(DisposableBeanAdapter.class); + private static final Log logger = LogFactory.getLog(DisposableBeanAdapter.class); private final Object bean; @@ -214,12 +214,15 @@ public void destroy() { } } catch (Throwable ex) { - String msg = "Invocation of destroy method failed on bean with name '" + this.beanName + "'"; - if (logger.isDebugEnabled()) { - logger.warn(msg, ex); - } - else { - logger.warn(msg + ": " + ex); + if (logger.isWarnEnabled()) { + String msg = "Invocation of destroy method failed on bean with name '" + this.beanName + "'"; + if (logger.isDebugEnabled()) { + // Log at warn level like below but add the exception stacktrace only with debug level + logger.warn(msg, ex); + } + else { + logger.warn(msg + ": " + ex); + } } } } @@ -240,12 +243,15 @@ public void destroy() { } } catch (Throwable ex) { - String msg = "Invocation of close method failed on bean with name '" + this.beanName + "'"; - if (logger.isDebugEnabled()) { - logger.warn(msg, ex); - } - else { - logger.warn(msg + ": " + ex); + if (logger.isWarnEnabled()) { + String msg = "Invocation of close method failed on bean with name '" + this.beanName + "'"; + if (logger.isDebugEnabled()) { + // Log at warn level like below but add the exception stacktrace only with debug level + logger.warn(msg, ex); + } + else { + logger.warn(msg + ": " + ex); + } } } } @@ -320,18 +326,23 @@ private void invokeCustomDestroyMethod(final Method destroyMethod) { } } catch (InvocationTargetException ex) { - String msg = "Custom destroy method '" + this.destroyMethodName + "' on bean with name '" + - this.beanName + "' threw an exception"; - if (logger.isDebugEnabled()) { - logger.warn(msg, ex.getTargetException()); - } - else { - logger.warn(msg + ": " + ex.getTargetException()); + if (logger.isWarnEnabled()) { + String msg = "Custom destroy method '" + this.destroyMethodName + "' on bean with name '" + + this.beanName + "' threw an exception"; + if (logger.isDebugEnabled()) { + // Log at warn level like below but add the exception stacktrace only with debug level + logger.warn(msg, ex.getTargetException()); + } + else { + logger.warn(msg + ": " + ex.getTargetException()); + } } } catch (Throwable ex) { - logger.warn("Failed to invoke custom destroy method '" + this.destroyMethodName + - "' on bean with name '" + this.beanName + "'", ex); + if (logger.isWarnEnabled()) { + logger.warn("Failed to invoke custom destroy method '" + this.destroyMethodName + + "' on bean with name '" + this.beanName + "'", ex); + } } } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/LookupOverride.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/LookupOverride.java index c441bf7e9cc3..f24001330206 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/LookupOverride.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/LookupOverride.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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. @@ -112,7 +112,7 @@ public boolean equals(@Nullable Object other) { @Override public int hashCode() { - return (29 * super.hashCode() + ObjectUtils.nullSafeHashCode(this.beanName)); + return super.hashCode() * 29 + ObjectUtils.nullSafeHashCode(this.beanName); } @Override diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/RootBeanDefinition.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/RootBeanDefinition.java index 25e894e8ceaf..f96ad9e2f380 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/RootBeanDefinition.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/RootBeanDefinition.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -506,23 +506,12 @@ boolean hasAnyExternallyManagedInitMethod(String initMethod) { if (isExternallyManagedInitMethod(initMethod)) { return true; } - if (this.externallyManagedInitMethods != null) { - for (String candidate : this.externallyManagedInitMethods) { - int indexOfDot = candidate.lastIndexOf('.'); - if (indexOfDot >= 0) { - String methodName = candidate.substring(indexOfDot + 1); - if (methodName.equals(initMethod)) { - return true; - } - } - } - } - return false; + return hasAnyExternallyManagedMethod(this.externallyManagedInitMethods, initMethod); } } /** - * Return all externally managed initialization methods (as an immutable Set). + * Get all externally managed initialization methods (as an immutable Set). *

    See {@link #registerExternallyManagedInitMethod} for details * regarding the format for the initialization methods in the returned set. * @since 5.3.11 @@ -583,19 +572,23 @@ boolean hasAnyExternallyManagedDestroyMethod(String destroyMethod) { if (isExternallyManagedDestroyMethod(destroyMethod)) { return true; } - if (this.externallyManagedDestroyMethods != null) { - for (String candidate : this.externallyManagedDestroyMethods) { - int indexOfDot = candidate.lastIndexOf('.'); - if (indexOfDot >= 0) { - String methodName = candidate.substring(indexOfDot + 1); - if (methodName.equals(destroyMethod)) { - return true; - } + return hasAnyExternallyManagedMethod(this.externallyManagedDestroyMethods, destroyMethod); + } + } + + private static boolean hasAnyExternallyManagedMethod(Set candidates, String methodName) { + if (candidates != null) { + for (String candidate : candidates) { + int indexOfDot = candidate.lastIndexOf('.'); + if (indexOfDot > 0) { + String candidateMethodName = candidate.substring(indexOfDot + 1); + if (candidateMethodName.equals(methodName)) { + return true; } } } - return false; } + return false; } /** diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/xml/XmlBeanDefinitionReader.java b/spring-beans/src/main/java/org/springframework/beans/factory/xml/XmlBeanDefinitionReader.java index 4c930c3820d8..65a71d5f232b 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/xml/XmlBeanDefinitionReader.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/xml/XmlBeanDefinitionReader.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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. @@ -547,7 +547,8 @@ public NamespaceHandlerResolver getNamespaceHandlerResolver() { * @see DefaultNamespaceHandlerResolver#DefaultNamespaceHandlerResolver(ClassLoader) */ protected NamespaceHandlerResolver createDefaultNamespaceHandlerResolver() { - ClassLoader cl = (getResourceLoader() != null ? getResourceLoader().getClassLoader() : getBeanClassLoader()); + ResourceLoader resourceLoader = getResourceLoader(); + ClassLoader cl = (resourceLoader != null ? resourceLoader.getClassLoader() : getBeanClassLoader()); return new DefaultNamespaceHandlerResolver(cl); } diff --git a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/PathEditor.java b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/PathEditor.java index 0ee0f5e80f60..4323263ab400 100644 --- a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/PathEditor.java +++ b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/PathEditor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-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. @@ -78,8 +78,10 @@ public void setAsText(String text) throws IllegalArgumentException { if (nioPathCandidate && !text.startsWith("/")) { try { URI uri = new URI(text); - if (uri.getScheme() != null) { - nioPathCandidate = false; + String scheme = uri.getScheme(); + if (scheme != null) { + // No NIO candidate except for "C:" style drive letters + nioPathCandidate = (scheme.length() == 1); // Let's try NIO file system providers via Paths.get(URI) setValue(Paths.get(uri).normalize()); return; @@ -90,9 +92,9 @@ public void setAsText(String text) throws IllegalArgumentException { // a file prefix (let's try as Spring resource location) nioPathCandidate = !text.startsWith(ResourceUtils.FILE_URL_PREFIX); } - catch (FileSystemNotFoundException ex) { - // URI scheme not registered for NIO (let's try URL - // protocol handlers via Spring's resource mechanism). + catch (FileSystemNotFoundException | IllegalArgumentException ex) { + // URI scheme not registered for NIO or not meeting Paths requirements: + // let's try URL protocol handlers via Spring's resource mechanism. } } @@ -109,7 +111,13 @@ else if (nioPathCandidate && !resource.exists()) { setValue(resource.getFile().toPath()); } catch (IOException ex) { - throw new IllegalArgumentException("Failed to retrieve file for " + resource, ex); + String msg = "Could not resolve \"" + text + "\" to 'java.nio.file.Path' for " + resource + ": " + + ex.getMessage(); + if (nioPathCandidate) { + msg += " - In case of ambiguity, consider adding the 'file:' prefix for an explicit reference " + + "to a file system resource of the same name: \"file:" + text + "\""; + } + throw new IllegalArgumentException(msg); } } } diff --git a/spring-beans/src/test/java/org/springframework/beans/BeanUtilsTests.java b/spring-beans/src/test/java/org/springframework/beans/BeanUtilsTests.java index 7510c8bc2765..1a03af2bce9d 100644 --- a/spring-beans/src/test/java/org/springframework/beans/BeanUtilsTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/BeanUtilsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-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,6 +42,8 @@ import org.springframework.beans.testfixture.beans.DerivedTestBean; import org.springframework.beans.testfixture.beans.ITestBean; import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.cglib.proxy.Enhancer; +import org.springframework.cglib.proxy.MethodInterceptor; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceEditor; import org.springframework.lang.Nullable; @@ -52,7 +54,7 @@ import static org.assertj.core.api.SoftAssertions.assertSoftly; /** - * Unit tests for {@link BeanUtils}. + * Tests for {@link BeanUtils}. * * @author Juergen Hoeller * @author Rob Harrop @@ -136,7 +138,7 @@ void getPropertyDescriptors() throws Exception { PropertyDescriptor[] actual = Introspector.getBeanInfo(TestBean.class).getPropertyDescriptors(); PropertyDescriptor[] descriptors = BeanUtils.getPropertyDescriptors(TestBean.class); assertThat(descriptors).as("Descriptors should not be null").isNotNull(); - assertThat(descriptors.length).as("Invalid number of descriptors returned").isEqualTo(actual.length); + assertThat(descriptors).as("Invalid number of descriptors returned").hasSameSizeAs(actual); } @Test @@ -162,13 +164,13 @@ void copyProperties() throws Exception { tb.setAge(32); tb.setTouchy("touchy"); TestBean tb2 = new TestBean(); - assertThat(tb2.getName() == null).as("Name empty").isTrue(); - assertThat(tb2.getAge() == 0).as("Age empty").isTrue(); - assertThat(tb2.getTouchy() == null).as("Touchy empty").isTrue(); + assertThat(tb2.getName()).as("Name empty").isNull(); + assertThat(tb2.getAge()).as("Age empty").isEqualTo(0); + assertThat(tb2.getTouchy()).as("Touchy empty").isNull(); BeanUtils.copyProperties(tb, tb2); - assertThat(tb2.getName().equals(tb.getName())).as("Name copied").isTrue(); - assertThat(tb2.getAge() == tb.getAge()).as("Age copied").isTrue(); - assertThat(tb2.getTouchy().equals(tb.getTouchy())).as("Touchy copied").isTrue(); + assertThat(tb2.getName()).as("Name copied").isEqualTo(tb.getName()); + assertThat(tb2.getAge()).as("Age copied").isEqualTo(tb.getAge()); + assertThat(tb2.getTouchy()).as("Touchy copied").isEqualTo(tb.getTouchy()); } @Test @@ -178,13 +180,13 @@ void copyPropertiesWithDifferentTypes1() throws Exception { tb.setAge(32); tb.setTouchy("touchy"); TestBean tb2 = new TestBean(); - assertThat(tb2.getName() == null).as("Name empty").isTrue(); - assertThat(tb2.getAge() == 0).as("Age empty").isTrue(); - assertThat(tb2.getTouchy() == null).as("Touchy empty").isTrue(); + assertThat(tb2.getName()).as("Name empty").isNull(); + assertThat(tb2.getAge()).as("Age empty").isEqualTo(0); + assertThat(tb2.getTouchy()).as("Touchy empty").isNull(); BeanUtils.copyProperties(tb, tb2); - assertThat(tb2.getName().equals(tb.getName())).as("Name copied").isTrue(); - assertThat(tb2.getAge() == tb.getAge()).as("Age copied").isTrue(); - assertThat(tb2.getTouchy().equals(tb.getTouchy())).as("Touchy copied").isTrue(); + assertThat(tb2.getName()).as("Name copied").isEqualTo(tb.getName()); + assertThat(tb2.getAge()).as("Age copied").isEqualTo(tb.getAge()); + assertThat(tb2.getTouchy()).as("Touchy copied").isEqualTo(tb.getTouchy()); } @Test @@ -194,13 +196,13 @@ void copyPropertiesWithDifferentTypes2() throws Exception { tb.setAge(32); tb.setTouchy("touchy"); DerivedTestBean tb2 = new DerivedTestBean(); - assertThat(tb2.getName() == null).as("Name empty").isTrue(); - assertThat(tb2.getAge() == 0).as("Age empty").isTrue(); - assertThat(tb2.getTouchy() == null).as("Touchy empty").isTrue(); + assertThat(tb2.getName()).as("Name empty").isNull(); + assertThat(tb2.getAge()).as("Age empty").isEqualTo(0); + assertThat(tb2.getTouchy()).as("Touchy empty").isNull(); BeanUtils.copyProperties(tb, tb2); - assertThat(tb2.getName().equals(tb.getName())).as("Name copied").isTrue(); - assertThat(tb2.getAge() == tb.getAge()).as("Age copied").isTrue(); - assertThat(tb2.getTouchy().equals(tb.getTouchy())).as("Touchy copied").isTrue(); + assertThat(tb2.getName()).as("Name copied").isEqualTo(tb.getName()); + assertThat(tb2.getAge()).as("Age copied").isEqualTo(tb.getAge()); + assertThat(tb2.getTouchy()).as("Touchy copied").isEqualTo(tb.getTouchy()); } /** @@ -227,8 +229,8 @@ void copyPropertiesHonorsGenericTypeMatchesFromIntegerToInteger() { IntegerListHolder2 integerListHolder2 = new IntegerListHolder2(); BeanUtils.copyProperties(integerListHolder1, integerListHolder2); - assertThat(integerListHolder1.getList()).containsOnly(42); - assertThat(integerListHolder2.getList()).containsOnly(42); + assertThat(integerListHolder1.getList()).containsExactly(42); + assertThat(integerListHolder2.getList()).containsExactly(42); } /** @@ -257,7 +259,7 @@ void copyPropertiesHonorsGenericTypeMatchesFromIntegerToWildcard() { WildcardListHolder2 wildcardListHolder2 = new WildcardListHolder2(); BeanUtils.copyProperties(integerListHolder1, wildcardListHolder2); - assertThat(integerListHolder1.getList()).containsOnly(42); + assertThat(integerListHolder1.getList()).containsExactly(42); assertThat(wildcardListHolder2.getList()).isEqualTo(Arrays.asList(42)); } @@ -271,9 +273,8 @@ void copyPropertiesHonorsGenericTypeMatchesForUpperBoundedWildcard() { NumberUpperBoundedWildcardListHolder numberListHolder = new NumberUpperBoundedWildcardListHolder(); BeanUtils.copyProperties(integerListHolder1, numberListHolder); - assertThat(integerListHolder1.getList()).containsOnly(42); - assertThat(numberListHolder.getList()).hasSize(1); - assertThat(numberListHolder.getList().contains(Integer.valueOf(42))).isTrue(); + assertThat(integerListHolder1.getList()).containsExactly(42); + assertThat(numberListHolder.getList()).isEqualTo(Arrays.asList(42)); } /** @@ -282,7 +283,7 @@ void copyPropertiesHonorsGenericTypeMatchesForUpperBoundedWildcard() { @Test void copyPropertiesDoesNotCopyFromSuperTypeToSubType() { NumberHolder numberHolder = new NumberHolder(); - numberHolder.setNumber(Integer.valueOf(42)); + numberHolder.setNumber(42); IntegerHolder integerHolder = new IntegerHolder(); BeanUtils.copyProperties(numberHolder, integerHolder); @@ -300,7 +301,7 @@ void copyPropertiesDoesNotHonorGenericTypeMismatches() { LongListHolder longListHolder = new LongListHolder(); BeanUtils.copyProperties(integerListHolder, longListHolder); - assertThat(integerListHolder.getList()).containsOnly(42); + assertThat(integerListHolder.getList()).containsExactly(42); assertThat(longListHolder.getList()).isEmpty(); } @@ -314,7 +315,7 @@ void copyPropertiesDoesNotHonorGenericTypeMismatchesFromSubTypeToSuperType() { NumberListHolder numberListHolder = new NumberListHolder(); BeanUtils.copyProperties(integerListHolder, numberListHolder); - assertThat(integerListHolder.getList()).containsOnly(42); + assertThat(integerListHolder.getList()).containsExactly(42); assertThat(numberListHolder.getList()).isEmpty(); } @@ -323,12 +324,13 @@ void copyPropertiesIgnoresGenericsIfSourceOrTargetHasUnresolvableGenerics() thro Order original = new Order("test", Arrays.asList("foo", "bar")); // Create a Proxy that loses the generic type information for the getLineItems() method. - OrderSummary proxy = proxyOrder(original); + OrderSummary proxy = (OrderSummary) Proxy.newProxyInstance(getClass().getClassLoader(), + new Class[] {OrderSummary.class}, new OrderInvocationHandler(original)); assertThat(OrderSummary.class.getDeclaredMethod("getLineItems").toGenericString()) - .contains("java.util.List"); + .contains("java.util.List"); assertThat(proxy.getClass().getDeclaredMethod("getLineItems").toGenericString()) - .contains("java.util.List") - .doesNotContain(""); + .contains("java.util.List") + .doesNotContain(""); // Ensure that our custom Proxy works as expected. assertThat(proxy.getId()).isEqualTo("test"); @@ -341,40 +343,57 @@ void copyPropertiesIgnoresGenericsIfSourceOrTargetHasUnresolvableGenerics() thro assertThat(target.getLineItems()).containsExactly("foo", "bar"); } + @Test // gh-32888 + public void copyPropertiesWithGenericCglibClass() { + Enhancer enhancer = new Enhancer(); + enhancer.setSuperclass(User.class); + enhancer.setCallback((MethodInterceptor) (obj, method, args, proxy) -> proxy.invokeSuper(obj, args)); + User user = (User) enhancer.create(); + user.setId(1); + user.setName("proxy"); + user.setAddress("addr"); + + User target = new User(); + BeanUtils.copyProperties(user, target); + assertThat(target.getId()).isEqualTo(user.getId()); + assertThat(target.getName()).isEqualTo(user.getName()); + assertThat(target.getAddress()).isEqualTo(user.getAddress()); + } + @Test void copyPropertiesWithEditable() throws Exception { TestBean tb = new TestBean(); - assertThat(tb.getName() == null).as("Name empty").isTrue(); + assertThat(tb.getName()).as("Name empty").isNull(); tb.setAge(32); tb.setTouchy("bla"); TestBean tb2 = new TestBean(); tb2.setName("rod"); - assertThat(tb2.getAge() == 0).as("Age empty").isTrue(); - assertThat(tb2.getTouchy() == null).as("Touchy empty").isTrue(); + assertThat(tb2.getAge()).as("Age empty").isEqualTo(0); + assertThat(tb2.getTouchy()).as("Touchy empty").isNull(); // "touchy" should not be copied: it's not defined in ITestBean BeanUtils.copyProperties(tb, tb2, ITestBean.class); - assertThat(tb2.getName() == null).as("Name copied").isTrue(); - assertThat(tb2.getAge() == 32).as("Age copied").isTrue(); - assertThat(tb2.getTouchy() == null).as("Touchy still empty").isTrue(); + assertThat(tb2.getName()).as("Name copied").isNull(); + assertThat(tb2.getAge()).as("Age copied").isEqualTo(32); + assertThat(tb2.getTouchy()).as("Touchy still empty").isNull(); } @Test void copyPropertiesWithIgnore() throws Exception { TestBean tb = new TestBean(); - assertThat(tb.getName() == null).as("Name empty").isTrue(); + assertThat(tb.getName()).as("Name empty").isNull(); tb.setAge(32); tb.setTouchy("bla"); TestBean tb2 = new TestBean(); tb2.setName("rod"); - assertThat(tb2.getAge() == 0).as("Age empty").isTrue(); - assertThat(tb2.getTouchy() == null).as("Touchy empty").isTrue(); + assertThat(tb2.getAge()).as("Age empty").isEqualTo(0); + assertThat(tb2.getTouchy()).as("Touchy empty").isNull(); // "spouse", "touchy", "age" should not be copied BeanUtils.copyProperties(tb, tb2, "spouse", "touchy", "age"); - assertThat(tb2.getName() == null).as("Name copied").isTrue(); - assertThat(tb2.getAge() == 0).as("Age still empty").isTrue(); - assertThat(tb2.getTouchy() == null).as("Touchy still empty").isTrue(); + assertThat(tb2.getName()).as("Name copied").isNull(); + assertThat(tb2.getAge()).as("Age still empty").isEqualTo(0); + assertThat(tb2.getTouchy()).as("Touchy still empty").isNull(); } @Test @@ -383,7 +402,7 @@ void copyPropertiesWithIgnoredNonExistingProperty() { source.setName("name"); TestBean target = new TestBean(); BeanUtils.copyProperties(source, target, "specialProperty"); - assertThat("name").isEqualTo(target.getName()); + assertThat(target.getName()).isEqualTo("name"); } @Test @@ -520,6 +539,7 @@ public void setNumber(Number number) { } } + @SuppressWarnings("unused") private static class IntegerHolder { @@ -534,6 +554,7 @@ public void setNumber(Integer number) { } } + @SuppressWarnings("unused") private static class WildcardListHolder1 { @@ -548,6 +569,7 @@ public void setList(List list) { } } + @SuppressWarnings("unused") private static class WildcardListHolder2 { @@ -562,6 +584,7 @@ public void setList(List list) { } } + @SuppressWarnings("unused") private static class NumberUpperBoundedWildcardListHolder { @@ -576,6 +599,7 @@ public void setList(List list) { } } + @SuppressWarnings("unused") private static class NumberListHolder { @@ -590,6 +614,7 @@ public void setList(List list) { } } + @SuppressWarnings("unused") private static class IntegerListHolder1 { @@ -604,6 +629,7 @@ public void setList(List list) { } } + @SuppressWarnings("unused") private static class IntegerListHolder2 { @@ -618,6 +644,7 @@ public void setList(List list) { } } + @SuppressWarnings("unused") private static class LongListHolder { @@ -798,6 +825,7 @@ public void setValue(String aValue) { } } + private static class BeanWithNullableTypes { private Integer counter; @@ -828,6 +856,7 @@ public String getValue() { } } + private static class BeanWithPrimitiveTypes { private boolean flag; @@ -840,7 +869,6 @@ private static class BeanWithPrimitiveTypes { private char character; private String text; - @SuppressWarnings("unused") public BeanWithPrimitiveTypes(boolean flag, byte byteCount, short shortCount, int intCount, long longCount, float floatCount, double doubleCount, char character, String text) { @@ -891,21 +919,22 @@ public char getCharacter() { public String getText() { return text; } - } + private static class PrivateBeanWithPrivateConstructor { private PrivateBeanWithPrivateConstructor() { } } + @SuppressWarnings("unused") private static class Order { private String id; - private List lineItems; + private List lineItems; Order() { } @@ -937,6 +966,7 @@ public String toString() { } } + private interface OrderSummary { String getId(); @@ -945,17 +975,10 @@ private interface OrderSummary { } - private OrderSummary proxyOrder(Order order) { - return (OrderSummary) Proxy.newProxyInstance(getClass().getClassLoader(), - new Class[] { OrderSummary.class }, new OrderInvocationHandler(order)); - } - - private static class OrderInvocationHandler implements InvocationHandler { private final Order order; - OrderInvocationHandler(Order order) { this.order = order; } @@ -973,4 +996,46 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl } } + + private static class GenericBaseModel { + + private T id; + + private String name; + + public T getId() { + return id; + } + + public void setId(T id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } + + + private static class User extends GenericBaseModel { + + private String address; + + public User() { + super(); + } + + public String getAddress() { + return address; + } + + public void setAddress(String address) { + this.address = address; + } + } + } diff --git a/spring-beans/src/test/java/org/springframework/beans/BeanWrapperTests.java b/spring-beans/src/test/java/org/springframework/beans/BeanWrapperTests.java index 17a775a7f123..896389e836eb 100644 --- a/spring-beans/src/test/java/org/springframework/beans/BeanWrapperTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/BeanWrapperTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -77,6 +77,21 @@ void aliasedSetterThroughDefaultMethod() { assertThat(accessor.getPropertyValue("aliasedName")).isEqualTo("tom"); } + @Test + void replaceWrappedInstance() { + GetterBean target = new GetterBean(); + BeanWrapperImpl accessor = createAccessor(target); + accessor.setPropertyValue("name", "tom"); + assertThat(target.getAliasedName()).isEqualTo("tom"); + assertThat(accessor.getPropertyValue("aliasedName")).isEqualTo("tom"); + + target = new GetterBean(); + accessor.setWrappedInstance(target); + accessor.setPropertyValue("name", "tom"); + assertThat(target.getAliasedName()).isEqualTo("tom"); + assertThat(accessor.getPropertyValue("aliasedName")).isEqualTo("tom"); + } + @Test void setValidAndInvalidPropertyValuesShouldContainExceptionDetails() { TestBean target = new TestBean(); @@ -380,6 +395,7 @@ public String getObject() { } + @SuppressWarnings("try") public static class ActiveResource implements AutoCloseable { public ActiveResource getResource() { diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/DefaultListableBeanFactoryTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/DefaultListableBeanFactoryTests.java index bd01ba2df11e..0c227674fd75 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/DefaultListableBeanFactoryTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/DefaultListableBeanFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -436,7 +436,6 @@ void nonStaticPrototypeFactoryMethodFoundByNonEagerTypeMatching() { @Test void empty() { - ListableBeanFactory lbf = new DefaultListableBeanFactory(); assertThat(lbf.getBeanDefinitionNames() != null).as("No beans defined --> array != null").isTrue(); assertThat(lbf.getBeanDefinitionNames().length == 0).as("No beans defined after no arg constructor").isTrue(); assertThat(lbf.getBeanDefinitionCount() == 0).as("No beans defined after no arg constructor").isTrue(); @@ -778,21 +777,6 @@ void getTypeWorksAfterParentChildMerging() { assertThat(factory.getType("child")).isEqualTo(DerivedTestBean.class); } - @Test - void nameAlreadyBound() { - Properties p = new Properties(); - p.setProperty("kerry.(class)", TestBean.class.getName()); - p.setProperty("kerry.age", "35"); - registerBeanDefinitions(p); - try { - registerBeanDefinitions(p); - } - catch (BeanDefinitionStoreException ex) { - assertThat(ex.getBeanName()).isEqualTo("kerry"); - // expected - } - } - private void singleTestBean(ListableBeanFactory lbf) { assertThat(lbf.getBeanDefinitionCount() == 1).as("1 beans defined").isTrue(); String[] names = lbf.getBeanDefinitionNames(); @@ -1287,10 +1271,11 @@ void autowireWithTwoMatchesForConstructorDependency() { lbf.registerBeanDefinition("rod", bd); RootBeanDefinition bd2 = new RootBeanDefinition(TestBean.class); lbf.registerBeanDefinition("rod2", bd2); - assertThatExceptionOfType(UnsatisfiedDependencyException.class).isThrownBy(() -> - lbf.autowire(ConstructorDependency.class, AutowireCapableBeanFactory.AUTOWIRE_CONSTRUCTOR, false)) - .withMessageContaining("rod") - .withMessageContaining("rod2"); + + assertThatExceptionOfType(UnsatisfiedDependencyException.class) + .isThrownBy(() -> lbf.autowire(ConstructorDependency.class, AutowireCapableBeanFactory.AUTOWIRE_CONSTRUCTOR, false)) + .withMessageContaining("rod") + .withMessageContaining("rod2"); } @Test @@ -1352,11 +1337,12 @@ void dependsOnCycle() { RootBeanDefinition bd2 = new RootBeanDefinition(TestBean.class); bd2.setDependsOn("tb1"); lbf.registerBeanDefinition("tb2", bd2); - assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() -> - lbf.preInstantiateSingletons()) - .withMessageContaining("Circular") - .withMessageContaining("'tb2'") - .withMessageContaining("'tb1'"); + + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(() -> lbf.preInstantiateSingletons()) + .withMessageContaining("Circular") + .withMessageContaining("'tb2'") + .withMessageContaining("'tb1'"); } @Test @@ -1370,11 +1356,12 @@ void implicitDependsOnCycle() { RootBeanDefinition bd3 = new RootBeanDefinition(TestBean.class); bd3.setDependsOn("tb1"); lbf.registerBeanDefinition("tb3", bd3); - assertThatExceptionOfType(BeanCreationException.class).isThrownBy( - lbf::preInstantiateSingletons) - .withMessageContaining("Circular") - .withMessageContaining("'tb3'") - .withMessageContaining("'tb1'"); + + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(lbf::preInstantiateSingletons) + .withMessageContaining("Circular") + .withMessageContaining("'tb3'") + .withMessageContaining("'tb1'"); } @Test @@ -1509,10 +1496,11 @@ void getBeanByTypeWithMultiplePriority() { RootBeanDefinition bd2 = new RootBeanDefinition(HighPriorityTestBean.class); lbf.registerBeanDefinition("bd1", bd1); lbf.registerBeanDefinition("bd2", bd2); - assertThatExceptionOfType(NoUniqueBeanDefinitionException.class).isThrownBy(() -> - lbf.getBean(TestBean.class)) - .withMessageContaining("Multiple beans found with the same priority") - .withMessageContaining("5"); // conflicting priority + + assertThatExceptionOfType(NoUniqueBeanDefinitionException.class) + .isThrownBy(() -> lbf.getBean(TestBean.class)) + .withMessageContaining("Multiple beans found with the same priority") + .withMessageContaining("5"); // conflicting priority } @Test @@ -1714,9 +1702,9 @@ void getBeanByTypeInstanceWithMultiplePrimary() { lbf.registerBeanDefinition("bd1", bd1); lbf.registerBeanDefinition("bd2", bd2); - assertThatExceptionOfType(NoUniqueBeanDefinitionException.class).isThrownBy(() -> - lbf.getBean(ConstructorDependency.class, 42)) - .withMessageContaining("more than one 'primary'"); + assertThatExceptionOfType(NoUniqueBeanDefinitionException.class) + .isThrownBy(() -> lbf.getBean(ConstructorDependency.class, 42)) + .withMessageContaining("more than one 'primary'"); } @Test @@ -1897,10 +1885,11 @@ void autowireBeanByTypeWithTwoMatches() { RootBeanDefinition bd2 = new RootBeanDefinition(TestBean.class); lbf.registerBeanDefinition("test", bd); lbf.registerBeanDefinition("spouse", bd2); - assertThatExceptionOfType(UnsatisfiedDependencyException.class).isThrownBy(() -> - lbf.autowire(DependenciesBean.class, AutowireCapableBeanFactory.AUTOWIRE_BY_TYPE, true)) - .withMessageContaining("test") - .withMessageContaining("spouse"); + + assertThatExceptionOfType(UnsatisfiedDependencyException.class) + .isThrownBy(() -> lbf.autowire(DependenciesBean.class, AutowireCapableBeanFactory.AUTOWIRE_BY_TYPE, true)) + .withMessageContaining("test") + .withMessageContaining("spouse"); } @Test @@ -1962,10 +1951,11 @@ void autowireBeanByTypeWithIdenticalPriorityCandidates() { RootBeanDefinition bd2 = new RootBeanDefinition(HighPriorityTestBean.class); lbf.registerBeanDefinition("test", bd); lbf.registerBeanDefinition("spouse", bd2); - assertThatExceptionOfType(UnsatisfiedDependencyException.class).isThrownBy(() -> - lbf.autowire(DependenciesBean.class, AutowireCapableBeanFactory.AUTOWIRE_BY_TYPE, true)) - .withCauseExactlyInstanceOf(NoUniqueBeanDefinitionException.class) - .withMessageContaining("5"); + + assertThatExceptionOfType(UnsatisfiedDependencyException.class) + .isThrownBy(() -> lbf.autowire(DependenciesBean.class, AutowireCapableBeanFactory.AUTOWIRE_BY_TYPE, true)) + .withCauseExactlyInstanceOf(NoUniqueBeanDefinitionException.class) + .withMessageContaining("5"); } @Test @@ -2201,19 +2191,21 @@ void constructorDependencyWithUnresolvableClass() { @Test void beanDefinitionWithInterface() { lbf.registerBeanDefinition("test", new RootBeanDefinition(ITestBean.class)); - assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() -> - lbf.getBean("test")) - .withMessageContaining("interface") - .satisfies(ex -> assertThat(ex.getBeanName()).isEqualTo("test")); + + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(() -> lbf.getBean("test")) + .withMessageContaining("interface") + .satisfies(ex -> assertThat(ex.getBeanName()).isEqualTo("test")); } @Test void beanDefinitionWithAbstractClass() { lbf.registerBeanDefinition("test", new RootBeanDefinition(AbstractBeanFactory.class)); - assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() -> - lbf.getBean("test")) - .withMessageContaining("abstract") - .satisfies(ex -> assertThat(ex.getBeanName()).isEqualTo("test")); + + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(() -> lbf.getBean("test")) + .withMessageContaining("abstract") + .satisfies(ex -> assertThat(ex.getBeanName()).isEqualTo("test")); } @Test diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessorTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessorTests.java index b2ad93854e2a..45f59fb1f689 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessorTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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. @@ -72,6 +72,9 @@ import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.core.annotation.Order; import org.springframework.core.testfixture.io.SerializationTestUtils; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; import org.springframework.util.ReflectionUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -124,13 +127,136 @@ public void testResourceInjection() { TestBean tb = new TestBean(); bf.registerSingleton("testBean", tb); - ResourceInjectionBean bean = (ResourceInjectionBean) bf.getBean("annotatedBean"); + ResourceInjectionBean bean = bf.getBean("annotatedBean", ResourceInjectionBean.class); assertThat(bean.getTestBean()).isSameAs(tb); assertThat(bean.getTestBean2()).isSameAs(tb); - bean = (ResourceInjectionBean) bf.getBean("annotatedBean"); + bean = bf.getBean("annotatedBean", ResourceInjectionBean.class); assertThat(bean.getTestBean()).isSameAs(tb); assertThat(bean.getTestBean2()).isSameAs(tb); + + assertThat(bf.getDependenciesForBean("annotatedBean")).isEqualTo(new String[] {"testBean"}); + } + + @Test + public void testResourceInjectionWithNullBean() { + RootBeanDefinition bd = new RootBeanDefinition(NonPublicResourceInjectionBean.class); + bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + bf.registerBeanDefinition("annotatedBean", bd); + RootBeanDefinition tb = new RootBeanDefinition(NullFactoryMethods.class); + tb.setFactoryMethodName("createTestBean"); + bf.registerBeanDefinition("testBean", tb); + + @SuppressWarnings("rawtypes") + NonPublicResourceInjectionBean bean = bf.getBean("annotatedBean", NonPublicResourceInjectionBean.class); + assertThat(bean.getTestBean()).isNull(); + assertThat(bean.getTestBean2()).isNull(); + assertThat(bean.getTestBean3()).isNull(); + + bean = bf.getBean("annotatedBean", NonPublicResourceInjectionBean.class); + assertThat(bean.getTestBean()).isNull(); + assertThat(bean.getTestBean2()).isNull(); + assertThat(bean.getTestBean3()).isNull(); + + assertThat(bf.getDependenciesForBean("annotatedBean")).isEqualTo(new String[] {"testBean"}); + } + + @Test + void resourceInjectionWithSometimesNullBeanEarly() { + RootBeanDefinition bd = new RootBeanDefinition(OptionalResourceInjectionBean.class); + bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + bf.registerBeanDefinition("annotatedBean", bd); + RootBeanDefinition tb = new RootBeanDefinition(SometimesNullFactoryMethods.class); + tb.setFactoryMethodName("createTestBean"); + tb.setScope(BeanDefinition.SCOPE_PROTOTYPE); + bf.registerBeanDefinition("testBean", tb); + + SometimesNullFactoryMethods.active = false; + OptionalResourceInjectionBean bean = (OptionalResourceInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean()).isNull(); + assertThat(bean.getTestBean2()).isNull(); + assertThat(bean.getTestBean3()).isNull(); + + SometimesNullFactoryMethods.active = false; + bean = (OptionalResourceInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean()).isNull(); + assertThat(bean.getTestBean2()).isNull(); + assertThat(bean.getTestBean3()).isNull(); + + SometimesNullFactoryMethods.active = true; + bean = (OptionalResourceInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean()).isNotNull(); + assertThat(bean.getTestBean2()).isNotNull(); + assertThat(bean.getTestBean3()).isNotNull(); + + SometimesNullFactoryMethods.active = true; + bean = (OptionalResourceInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean()).isNotNull(); + assertThat(bean.getTestBean2()).isNotNull(); + assertThat(bean.getTestBean3()).isNotNull(); + + SometimesNullFactoryMethods.active = false; + bean = (OptionalResourceInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean()).isNull(); + assertThat(bean.getTestBean2()).isNull(); + assertThat(bean.getTestBean3()).isNull(); + + SometimesNullFactoryMethods.active = false; + bean = (OptionalResourceInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean()).isNull(); + assertThat(bean.getTestBean2()).isNull(); + assertThat(bean.getTestBean3()).isNull(); + + assertThat(bf.getDependenciesForBean("annotatedBean")).isEqualTo(new String[] {"testBean"}); + } + + @Test + void resourceInjectionWithSometimesNullBeanLate() { + RootBeanDefinition bd = new RootBeanDefinition(OptionalResourceInjectionBean.class); + bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + bf.registerBeanDefinition("annotatedBean", bd); + RootBeanDefinition tb = new RootBeanDefinition(SometimesNullFactoryMethods.class); + tb.setFactoryMethodName("createTestBean"); + tb.setScope(BeanDefinition.SCOPE_PROTOTYPE); + bf.registerBeanDefinition("testBean", tb); + + SometimesNullFactoryMethods.active = true; + OptionalResourceInjectionBean bean = (OptionalResourceInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean()).isNotNull(); + assertThat(bean.getTestBean2()).isNotNull(); + assertThat(bean.getTestBean3()).isNotNull(); + + SometimesNullFactoryMethods.active = true; + bean = (OptionalResourceInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean()).isNotNull(); + assertThat(bean.getTestBean2()).isNotNull(); + assertThat(bean.getTestBean3()).isNotNull(); + + SometimesNullFactoryMethods.active = false; + bean = (OptionalResourceInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean()).isNull(); + assertThat(bean.getTestBean2()).isNull(); + assertThat(bean.getTestBean3()).isNull(); + + SometimesNullFactoryMethods.active = false; + bean = (OptionalResourceInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean()).isNull(); + assertThat(bean.getTestBean2()).isNull(); + assertThat(bean.getTestBean3()).isNull(); + + SometimesNullFactoryMethods.active = true; + bean = (OptionalResourceInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean()).isNotNull(); + assertThat(bean.getTestBean2()).isNotNull(); + assertThat(bean.getTestBean3()).isNotNull(); + + SometimesNullFactoryMethods.active = false; + bean = (OptionalResourceInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean()).isNull(); + assertThat(bean.getTestBean2()).isNull(); + assertThat(bean.getTestBean3()).isNull(); + + assertThat(bf.getDependenciesForBean("annotatedBean")).isEqualTo(new String[] {"testBean"}); } @Test @@ -143,7 +269,7 @@ public void testExtendedResourceInjection() { NestedTestBean ntb = new NestedTestBean(); bf.registerSingleton("nestedTestBean", ntb); - TypedExtendedResourceInjectionBean bean = (TypedExtendedResourceInjectionBean) bf.getBean("annotatedBean"); + TypedExtendedResourceInjectionBean bean = bf.getBean("annotatedBean", TypedExtendedResourceInjectionBean.class); assertThat(bean.getTestBean()).isSameAs(tb); assertThat(bean.getTestBean2()).isSameAs(tb); assertThat(bean.getTestBean3()).isSameAs(tb); @@ -151,7 +277,7 @@ public void testExtendedResourceInjection() { assertThat(bean.getNestedTestBean()).isSameAs(ntb); assertThat(bean.getBeanFactory()).isSameAs(bf); - bean = (TypedExtendedResourceInjectionBean) bf.getBean("annotatedBean"); + bean = bf.getBean("annotatedBean", TypedExtendedResourceInjectionBean.class); assertThat(bean.getTestBean()).isSameAs(tb); assertThat(bean.getTestBean2()).isSameAs(tb); assertThat(bean.getTestBean3()).isSameAs(tb); @@ -159,10 +285,7 @@ public void testExtendedResourceInjection() { assertThat(bean.getNestedTestBean()).isSameAs(ntb); assertThat(bean.getBeanFactory()).isSameAs(bf); - String[] depBeans = bf.getDependenciesForBean("annotatedBean"); - assertThat(depBeans.length).isEqualTo(2); - assertThat(depBeans[0]).isEqualTo("testBean"); - assertThat(depBeans[1]).isEqualTo("nestedTestBean"); + assertThat(bf.getDependenciesForBean("annotatedBean")).isEqualTo(new String[] {"testBean", "nestedTestBean"}); } @Test @@ -173,7 +296,7 @@ public void testExtendedResourceInjectionWithDestruction() { bf.registerSingleton("nestedTestBean", ntb); TestBean tb = bf.getBean("testBean", TestBean.class); - TypedExtendedResourceInjectionBean bean = (TypedExtendedResourceInjectionBean) bf.getBean("annotatedBean"); + TypedExtendedResourceInjectionBean bean = bf.getBean("annotatedBean", TypedExtendedResourceInjectionBean.class); assertThat(bean.getTestBean()).isSameAs(tb); assertThat(bean.getTestBean2()).isSameAs(tb); assertThat(bean.getTestBean3()).isSameAs(tb); @@ -200,7 +323,7 @@ public void testExtendedResourceInjectionWithOverriding() { NestedTestBean ntb = new NestedTestBean(); bf.registerSingleton("nestedTestBean", ntb); - TypedExtendedResourceInjectionBean bean = (TypedExtendedResourceInjectionBean) bf.getBean("annotatedBean"); + TypedExtendedResourceInjectionBean bean = bf.getBean("annotatedBean", TypedExtendedResourceInjectionBean.class); assertThat(bean.getTestBean()).isSameAs(tb); assertThat(bean.getTestBean2()).isSameAs(tb2); assertThat(bean.getTestBean3()).isSameAs(tb); @@ -218,7 +341,7 @@ public void testExtendedResourceInjectionWithSkippedOverriddenMethods() { NestedTestBean ntb = new NestedTestBean(); bf.registerSingleton("nestedTestBean", ntb); - OverriddenExtendedResourceInjectionBean bean = (OverriddenExtendedResourceInjectionBean) bf.getBean("annotatedBean"); + OverriddenExtendedResourceInjectionBean bean = bf.getBean("annotatedBean", OverriddenExtendedResourceInjectionBean.class); assertThat(bean.getTestBean()).isSameAs(tb); assertThat(bean.getTestBean2()).isNull(); assertThat(bean.getTestBean3()).isSameAs(tb); @@ -238,7 +361,7 @@ public void testExtendedResourceInjectionWithDefaultMethod() { NestedTestBean ntb = new NestedTestBean(); bf.registerSingleton("nestedTestBean", ntb); - DefaultMethodResourceInjectionBean bean = (DefaultMethodResourceInjectionBean) bf.getBean("annotatedBean"); + DefaultMethodResourceInjectionBean bean = bf.getBean("annotatedBean", DefaultMethodResourceInjectionBean.class); assertThat(bean.getTestBean()).isSameAs(tb); assertThat(bean.getTestBean2()).isNull(); assertThat(bean.getTestBean3()).isSameAs(tb); @@ -261,7 +384,7 @@ public void testExtendedResourceInjectionWithAtRequired() { NestedTestBean ntb = new NestedTestBean(); bf.registerSingleton("nestedTestBean", ntb); - TypedExtendedResourceInjectionBean bean = (TypedExtendedResourceInjectionBean) bf.getBean("annotatedBean"); + TypedExtendedResourceInjectionBean bean = bf.getBean("annotatedBean", TypedExtendedResourceInjectionBean.class); assertThat(bean.getTestBean()).isSameAs(tb); assertThat(bean.getTestBean2()).isSameAs(tb); assertThat(bean.getTestBean3()).isSameAs(tb); @@ -282,7 +405,7 @@ public void testOptionalResourceInjection() { NestedTestBean ntb2 = new NestedTestBean(); bf.registerSingleton("nestedTestBean2", ntb2); - OptionalResourceInjectionBean bean = (OptionalResourceInjectionBean) bf.getBean("annotatedBean"); + OptionalResourceInjectionBean bean = bf.getBean("annotatedBean", OptionalResourceInjectionBean.class); assertThat(bean.getTestBean()).isSameAs(tb); assertThat(bean.getTestBean2()).isSameAs(tb); assertThat(bean.getTestBean3()).isSameAs(tb); @@ -310,7 +433,7 @@ public void testOptionalResourceInjectionWithSingletonRemoval() { NestedTestBean ntb2 = new NestedTestBean(); bf.registerSingleton("nestedTestBean2", ntb2); - OptionalResourceInjectionBean bean = (OptionalResourceInjectionBean) bf.getBean("annotatedBean"); + OptionalResourceInjectionBean bean = bf.getBean("annotatedBean", OptionalResourceInjectionBean.class); assertThat(bean.getTestBean()).isSameAs(tb); assertThat(bean.getTestBean2()).isSameAs(tb); assertThat(bean.getTestBean3()).isSameAs(tb); @@ -324,8 +447,24 @@ public void testOptionalResourceInjectionWithSingletonRemoval() { assertThat(bean.nestedTestBeansField[1]).isSameAs(ntb2); bf.destroySingleton("testBean"); + bf.registerSingleton("testBeanX", tb); - bean = (OptionalResourceInjectionBean) bf.getBean("annotatedBean"); + bean = bf.getBean("annotatedBean", OptionalResourceInjectionBean.class); + assertThat(bean.getTestBean()).isSameAs(tb); + assertThat(bean.getTestBean2()).isSameAs(tb); + assertThat(bean.getTestBean3()).isSameAs(tb); + assertThat(bean.getTestBean4()).isSameAs(tb); + assertThat(bean.getIndexedTestBean()).isSameAs(itb); + assertThat(bean.getNestedTestBeans()).hasSize(2); + assertThat(bean.getNestedTestBeans()[0]).isSameAs(ntb1); + assertThat(bean.getNestedTestBeans()[1]).isSameAs(ntb2); + assertThat(bean.nestedTestBeansField).hasSize(2); + assertThat(bean.nestedTestBeansField[0]).isSameAs(ntb1); + assertThat(bean.nestedTestBeansField[1]).isSameAs(ntb2); + + bf.destroySingleton("testBeanX"); + + bean = bf.getBean("annotatedBean", OptionalResourceInjectionBean.class); assertThat(bean.getTestBean()).isNull(); assertThat(bean.getTestBean2()).isNull(); assertThat(bean.getTestBean3()).isNull(); @@ -340,7 +479,7 @@ public void testOptionalResourceInjectionWithSingletonRemoval() { bf.registerSingleton("testBean", tb); - bean = (OptionalResourceInjectionBean) bf.getBean("annotatedBean"); + bean = bf.getBean("annotatedBean", OptionalResourceInjectionBean.class); assertThat(bean.getTestBean()).isSameAs(tb); assertThat(bean.getTestBean2()).isSameAs(tb); assertThat(bean.getTestBean3()).isSameAs(tb); @@ -367,7 +506,7 @@ public void testOptionalResourceInjectionWithBeanDefinitionRemoval() { NestedTestBean ntb2 = new NestedTestBean(); bf.registerSingleton("nestedTestBean2", ntb2); - OptionalResourceInjectionBean bean = (OptionalResourceInjectionBean) bf.getBean("annotatedBean"); + OptionalResourceInjectionBean bean = bf.getBean("annotatedBean", OptionalResourceInjectionBean.class); assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean")); assertThat(bean.getTestBean2()).isSameAs(bf.getBean("testBean")); assertThat(bean.getTestBean3()).isSameAs(bf.getBean("testBean")); @@ -381,8 +520,24 @@ public void testOptionalResourceInjectionWithBeanDefinitionRemoval() { assertThat(bean.nestedTestBeansField[1]).isSameAs(ntb2); bf.removeBeanDefinition("testBean"); + bf.registerBeanDefinition("testBeanX", new RootBeanDefinition(TestBean.class)); - bean = (OptionalResourceInjectionBean) bf.getBean("annotatedBean"); + bean = bf.getBean("annotatedBean", OptionalResourceInjectionBean.class); + assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBeanX")); + assertThat(bean.getTestBean2()).isSameAs(bf.getBean("testBeanX")); + assertThat(bean.getTestBean3()).isSameAs(bf.getBean("testBeanX")); + assertThat(bean.getTestBean4()).isSameAs(bf.getBean("testBeanX")); + assertThat(bean.getIndexedTestBean()).isSameAs(itb); + assertThat(bean.getNestedTestBeans()).hasSize(2); + assertThat(bean.getNestedTestBeans()[0]).isSameAs(ntb1); + assertThat(bean.getNestedTestBeans()[1]).isSameAs(ntb2); + assertThat(bean.nestedTestBeansField).hasSize(2); + assertThat(bean.nestedTestBeansField[0]).isSameAs(ntb1); + assertThat(bean.nestedTestBeansField[1]).isSameAs(ntb2); + + bf.removeBeanDefinition("testBeanX"); + + bean = bf.getBean("annotatedBean", OptionalResourceInjectionBean.class); assertThat(bean.getTestBean()).isNull(); assertThat(bean.getTestBean2()).isNull(); assertThat(bean.getTestBean3()).isNull(); @@ -397,7 +552,7 @@ public void testOptionalResourceInjectionWithBeanDefinitionRemoval() { bf.registerBeanDefinition("testBean", new RootBeanDefinition(TestBean.class)); - bean = (OptionalResourceInjectionBean) bf.getBean("annotatedBean"); + bean = bf.getBean("annotatedBean", OptionalResourceInjectionBean.class); assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean")); assertThat(bean.getTestBean2()).isSameAs(bf.getBean("testBean")); assertThat(bean.getTestBean3()).isSameAs(bf.getBean("testBean")); @@ -426,8 +581,8 @@ public void testOptionalCollectionResourceInjection() { bf.registerSingleton("nestedTestBean2", ntb2); // Two calls to verify that caching doesn't break re-creation. - OptionalCollectionResourceInjectionBean bean = (OptionalCollectionResourceInjectionBean) bf.getBean("annotatedBean"); - bean = (OptionalCollectionResourceInjectionBean) bf.getBean("annotatedBean"); + OptionalCollectionResourceInjectionBean bean = bf.getBean("annotatedBean", OptionalCollectionResourceInjectionBean.class); + bean = bf.getBean("annotatedBean", OptionalCollectionResourceInjectionBean.class); assertThat(bean.getTestBean()).isSameAs(tb); assertThat(bean.getTestBean2()).isSameAs(tb); assertThat(bean.getTestBean3()).isSameAs(tb); @@ -457,8 +612,8 @@ public void testOptionalCollectionResourceInjectionWithSingleElement() { bf.registerSingleton("nestedTestBean1", ntb1); // Two calls to verify that caching doesn't break re-creation. - OptionalCollectionResourceInjectionBean bean = (OptionalCollectionResourceInjectionBean) bf.getBean("annotatedBean"); - bean = (OptionalCollectionResourceInjectionBean) bf.getBean("annotatedBean"); + OptionalCollectionResourceInjectionBean bean = bf.getBean("annotatedBean", OptionalCollectionResourceInjectionBean.class); + bean = bf.getBean("annotatedBean", OptionalCollectionResourceInjectionBean.class); assertThat(bean.getTestBean()).isSameAs(tb); assertThat(bean.getTestBean2()).isSameAs(tb); assertThat(bean.getTestBean3()).isSameAs(tb); @@ -478,7 +633,7 @@ public void testOptionalResourceInjectionWithIncompleteDependencies() { TestBean tb = new TestBean(); bf.registerSingleton("testBean", tb); - OptionalResourceInjectionBean bean = (OptionalResourceInjectionBean) bf.getBean("annotatedBean"); + OptionalResourceInjectionBean bean = bf.getBean("annotatedBean", OptionalResourceInjectionBean.class); assertThat(bean.getTestBean()).isSameAs(tb); assertThat(bean.getTestBean2()).isSameAs(tb); assertThat(bean.getTestBean3()).isSameAs(tb); @@ -490,7 +645,7 @@ public void testOptionalResourceInjectionWithIncompleteDependencies() { public void testOptionalResourceInjectionWithNoDependencies() { bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(OptionalResourceInjectionBean.class)); - OptionalResourceInjectionBean bean = (OptionalResourceInjectionBean) bf.getBean("annotatedBean"); + OptionalResourceInjectionBean bean = bf.getBean("annotatedBean", OptionalResourceInjectionBean.class); assertThat(bean.getTestBean()).isNull(); assertThat(bean.getTestBean2()).isNull(); assertThat(bean.getTestBean3()).isNull(); @@ -512,7 +667,7 @@ public void testOrderedResourceInjection() { ntb2.setOrder(1); bf.registerSingleton("nestedTestBean2", ntb2); - OptionalResourceInjectionBean bean = (OptionalResourceInjectionBean) bf.getBean("annotatedBean"); + OptionalResourceInjectionBean bean = bf.getBean("annotatedBean", OptionalResourceInjectionBean.class); assertThat(bean.getTestBean()).isSameAs(tb); assertThat(bean.getTestBean2()).isSameAs(tb); assertThat(bean.getTestBean3()).isSameAs(tb); @@ -538,7 +693,7 @@ public void testAnnotationOrderedResourceInjection() { FixedOrder1NestedTestBean ntb2 = new FixedOrder1NestedTestBean(); bf.registerSingleton("nestedTestBean2", ntb2); - OptionalResourceInjectionBean bean = (OptionalResourceInjectionBean) bf.getBean("annotatedBean"); + OptionalResourceInjectionBean bean = bf.getBean("annotatedBean", OptionalResourceInjectionBean.class); assertThat(bean.getTestBean()).isSameAs(tb); assertThat(bean.getTestBean2()).isSameAs(tb); assertThat(bean.getTestBean3()).isSameAs(tb); @@ -569,8 +724,8 @@ public void testOrderedCollectionResourceInjection() { bf.registerSingleton("nestedTestBean2", ntb2); // Two calls to verify that caching doesn't break re-creation. - OptionalCollectionResourceInjectionBean bean = (OptionalCollectionResourceInjectionBean) bf.getBean("annotatedBean"); - bean = (OptionalCollectionResourceInjectionBean) bf.getBean("annotatedBean"); + OptionalCollectionResourceInjectionBean bean = bf.getBean("annotatedBean", OptionalCollectionResourceInjectionBean.class); + bean = bf.getBean("annotatedBean", OptionalCollectionResourceInjectionBean.class); assertThat(bean.getTestBean()).isSameAs(tb); assertThat(bean.getTestBean2()).isSameAs(tb); assertThat(bean.getTestBean3()).isSameAs(tb); @@ -602,8 +757,8 @@ public void testAnnotationOrderedCollectionResourceInjection() { bf.registerSingleton("nestedTestBean2", ntb2); // Two calls to verify that caching doesn't break re-creation. - OptionalCollectionResourceInjectionBean bean = (OptionalCollectionResourceInjectionBean) bf.getBean("annotatedBean"); - bean = (OptionalCollectionResourceInjectionBean) bf.getBean("annotatedBean"); + OptionalCollectionResourceInjectionBean bean = bf.getBean("annotatedBean", OptionalCollectionResourceInjectionBean.class); + bean = bf.getBean("annotatedBean", OptionalCollectionResourceInjectionBean.class); assertThat(bean.getTestBean()).isSameAs(tb); assertThat(bean.getTestBean2()).isSameAs(tb); assertThat(bean.getTestBean3()).isSameAs(tb); @@ -630,7 +785,7 @@ public void testConstructorResourceInjection() { NestedTestBean ntb = new NestedTestBean(); bf.registerSingleton("nestedTestBean", ntb); - ConstructorResourceInjectionBean bean = (ConstructorResourceInjectionBean) bf.getBean("annotatedBean"); + ConstructorResourceInjectionBean bean = bf.getBean("annotatedBean", ConstructorResourceInjectionBean.class); assertThat(bean.getTestBean()).isSameAs(tb); assertThat(bean.getTestBean2()).isSameAs(tb); assertThat(bean.getTestBean3()).isSameAs(tb); @@ -638,13 +793,16 @@ public void testConstructorResourceInjection() { assertThat(bean.getNestedTestBean()).isSameAs(ntb); assertThat(bean.getBeanFactory()).isSameAs(bf); - bean = (ConstructorResourceInjectionBean) bf.getBean("annotatedBean"); + bean = bf.getBean("annotatedBean", ConstructorResourceInjectionBean.class); assertThat(bean.getTestBean()).isSameAs(tb); assertThat(bean.getTestBean2()).isSameAs(tb); assertThat(bean.getTestBean3()).isSameAs(tb); assertThat(bean.getTestBean4()).isSameAs(tb); assertThat(bean.getNestedTestBean()).isSameAs(ntb); assertThat(bean.getBeanFactory()).isSameAs(bf); + + assertThat(bf.getDependenciesForBean("annotatedBean")).isEqualTo( + new String[] {"testBean", "nestedTestBean", ObjectUtils.identityToString(bf)}); } @Test @@ -657,7 +815,7 @@ public void testConstructorResourceInjectionWithSingletonRemoval() { NestedTestBean ntb = new NestedTestBean(); bf.registerSingleton("nestedTestBean", ntb); - ConstructorResourceInjectionBean bean = (ConstructorResourceInjectionBean) bf.getBean("annotatedBean"); + ConstructorResourceInjectionBean bean = bf.getBean("annotatedBean", ConstructorResourceInjectionBean.class); assertThat(bean.getTestBean()).isSameAs(tb); assertThat(bean.getTestBean2()).isSameAs(tb); assertThat(bean.getTestBean3()).isSameAs(tb); @@ -666,8 +824,19 @@ public void testConstructorResourceInjectionWithSingletonRemoval() { assertThat(bean.getBeanFactory()).isSameAs(bf); bf.destroySingleton("nestedTestBean"); + bf.registerSingleton("nestedTestBeanX", ntb); + + bean = bf.getBean("annotatedBean", ConstructorResourceInjectionBean.class); + assertThat(bean.getTestBean()).isSameAs(tb); + assertThat(bean.getTestBean2()).isSameAs(tb); + assertThat(bean.getTestBean3()).isSameAs(tb); + assertThat(bean.getTestBean4()).isSameAs(tb); + assertThat(bean.getNestedTestBean()).isSameAs(ntb); + assertThat(bean.getBeanFactory()).isSameAs(bf); + + bf.destroySingleton("nestedTestBeanX"); - bean = (ConstructorResourceInjectionBean) bf.getBean("annotatedBean"); + bean = bf.getBean("annotatedBean", ConstructorResourceInjectionBean.class); assertThat(bean.getTestBean()).isSameAs(tb); assertThat(bean.getTestBean2()).isSameAs(tb); assertThat(bean.getTestBean3()).isSameAs(tb); @@ -677,7 +846,7 @@ public void testConstructorResourceInjectionWithSingletonRemoval() { bf.registerSingleton("nestedTestBean", ntb); - bean = (ConstructorResourceInjectionBean) bf.getBean("annotatedBean"); + bean = bf.getBean("annotatedBean", ConstructorResourceInjectionBean.class); assertThat(bean.getTestBean()).isSameAs(tb); assertThat(bean.getTestBean2()).isSameAs(tb); assertThat(bean.getTestBean3()).isSameAs(tb); @@ -695,7 +864,7 @@ public void testConstructorResourceInjectionWithBeanDefinitionRemoval() { bf.registerSingleton("testBean", tb); bf.registerBeanDefinition("nestedTestBean", new RootBeanDefinition(NestedTestBean.class)); - ConstructorResourceInjectionBean bean = (ConstructorResourceInjectionBean) bf.getBean("annotatedBean"); + ConstructorResourceInjectionBean bean = bf.getBean("annotatedBean", ConstructorResourceInjectionBean.class); assertThat(bean.getTestBean()).isSameAs(tb); assertThat(bean.getTestBean2()).isSameAs(tb); assertThat(bean.getTestBean3()).isSameAs(tb); @@ -704,8 +873,19 @@ public void testConstructorResourceInjectionWithBeanDefinitionRemoval() { assertThat(bean.getBeanFactory()).isSameAs(bf); bf.removeBeanDefinition("nestedTestBean"); + bf.registerBeanDefinition("nestedTestBeanX", new RootBeanDefinition(NestedTestBean.class)); + + bean = bf.getBean("annotatedBean", ConstructorResourceInjectionBean.class); + assertThat(bean.getTestBean()).isSameAs(tb); + assertThat(bean.getTestBean2()).isSameAs(tb); + assertThat(bean.getTestBean3()).isSameAs(tb); + assertThat(bean.getTestBean4()).isSameAs(tb); + assertThat(bean.getNestedTestBean()).isSameAs(bf.getBean("nestedTestBeanX")); + assertThat(bean.getBeanFactory()).isSameAs(bf); + + bf.removeBeanDefinition("nestedTestBeanX"); - bean = (ConstructorResourceInjectionBean) bf.getBean("annotatedBean"); + bean = bf.getBean("annotatedBean", ConstructorResourceInjectionBean.class); assertThat(bean.getTestBean()).isSameAs(tb); assertThat(bean.getTestBean2()).isSameAs(tb); assertThat(bean.getTestBean3()).isSameAs(tb); @@ -715,7 +895,7 @@ public void testConstructorResourceInjectionWithBeanDefinitionRemoval() { bf.registerBeanDefinition("nestedTestBean", new RootBeanDefinition(NestedTestBean.class)); - bean = (ConstructorResourceInjectionBean) bf.getBean("annotatedBean"); + bean = bf.getBean("annotatedBean", ConstructorResourceInjectionBean.class); assertThat(bean.getTestBean()).isSameAs(tb); assertThat(bean.getTestBean2()).isSameAs(tb); assertThat(bean.getTestBean3()).isSameAs(tb); @@ -734,7 +914,7 @@ public void testConstructorResourceInjectionWithNullFromFactoryBean() { bf.registerBeanDefinition("nestedTestBean", new RootBeanDefinition(NullNestedTestBeanFactoryBean.class)); bf.registerSingleton("nestedTestBean2", new NestedTestBean()); - ConstructorResourceInjectionBean bean = (ConstructorResourceInjectionBean) bf.getBean("annotatedBean"); + ConstructorResourceInjectionBean bean = bf.getBean("annotatedBean", ConstructorResourceInjectionBean.class); assertThat(bean.getTestBean()).isSameAs(tb); assertThat(bean.getTestBean2()).isSameAs(tb); assertThat(bean.getTestBean3()).isSameAs(tb); @@ -742,7 +922,7 @@ public void testConstructorResourceInjectionWithNullFromFactoryBean() { assertThat(bean.getNestedTestBean()).isNull(); assertThat(bean.getBeanFactory()).isSameAs(bf); - bean = (ConstructorResourceInjectionBean) bf.getBean("annotatedBean"); + bean = bf.getBean("annotatedBean", ConstructorResourceInjectionBean.class); assertThat(bean.getTestBean()).isSameAs(tb); assertThat(bean.getTestBean2()).isSameAs(tb); assertThat(bean.getTestBean3()).isSameAs(tb); @@ -764,7 +944,7 @@ public void testConstructorResourceInjectionWithNullFromFactoryMethod() { bf.registerBeanDefinition("nestedTestBean", ntb); bf.registerSingleton("nestedTestBean2", new NestedTestBean()); - ConstructorResourceInjectionBean bean = (ConstructorResourceInjectionBean) bf.getBean("annotatedBean"); + ConstructorResourceInjectionBean bean = bf.getBean("annotatedBean", ConstructorResourceInjectionBean.class); assertThat(bean.getTestBean()).isNull(); assertThat(bean.getTestBean2()).isNull(); assertThat(bean.getTestBean3()).isNull(); @@ -772,7 +952,7 @@ public void testConstructorResourceInjectionWithNullFromFactoryMethod() { assertThat(bean.getNestedTestBean()).isNull(); assertThat(bean.getBeanFactory()).isSameAs(bf); - bean = (ConstructorResourceInjectionBean) bf.getBean("annotatedBean"); + bean = bf.getBean("annotatedBean", ConstructorResourceInjectionBean.class); assertThat(bean.getTestBean()).isNull(); assertThat(bean.getTestBean2()).isNull(); assertThat(bean.getTestBean3()).isNull(); @@ -791,7 +971,7 @@ public void testConstructorResourceInjectionWithMultipleCandidates() { NestedTestBean ntb2 = new NestedTestBean(); bf.registerSingleton("nestedTestBean2", ntb2); - ConstructorsResourceInjectionBean bean = (ConstructorsResourceInjectionBean) bf.getBean("annotatedBean"); + ConstructorsResourceInjectionBean bean = bf.getBean("annotatedBean", ConstructorsResourceInjectionBean.class); assertThat(bean.getTestBean3()).isNull(); assertThat(bean.getTestBean4()).isSameAs(tb); assertThat(bean.getNestedTestBeans().length).isEqualTo(2); @@ -802,11 +982,85 @@ public void testConstructorResourceInjectionWithMultipleCandidates() { @Test public void testConstructorResourceInjectionWithNoCandidatesAndNoFallback() { bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(ConstructorWithoutFallbackBean.class)); - assertThatExceptionOfType(UnsatisfiedDependencyException.class).isThrownBy(() -> - bf.getBean("annotatedBean")) + assertThatExceptionOfType(UnsatisfiedDependencyException.class) + .isThrownBy(() -> bf.getBean("annotatedBean")) .satisfies(methodParameterDeclaredOn(ConstructorWithoutFallbackBean.class)); } + @Test + void constructorResourceInjectionWithSometimesNullBeanEarly() { + RootBeanDefinition bd = new RootBeanDefinition(ConstructorWithNullableArgument.class); + bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + bf.registerBeanDefinition("annotatedBean", bd); + RootBeanDefinition tb = new RootBeanDefinition(SometimesNullFactoryMethods.class); + tb.setFactoryMethodName("createTestBean"); + tb.setScope(BeanDefinition.SCOPE_PROTOTYPE); + bf.registerBeanDefinition("testBean", tb); + + SometimesNullFactoryMethods.active = false; + ConstructorWithNullableArgument bean = (ConstructorWithNullableArgument) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean3()).isNull(); + + SometimesNullFactoryMethods.active = false; + bean = (ConstructorWithNullableArgument) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean3()).isNull(); + + SometimesNullFactoryMethods.active = true; + bean = (ConstructorWithNullableArgument) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean3()).isNotNull(); + + SometimesNullFactoryMethods.active = true; + bean = (ConstructorWithNullableArgument) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean3()).isNotNull(); + + SometimesNullFactoryMethods.active = false; + bean = (ConstructorWithNullableArgument) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean3()).isNull(); + + SometimesNullFactoryMethods.active = false; + bean = (ConstructorWithNullableArgument) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean3()).isNull(); + + assertThat(bf.getDependenciesForBean("annotatedBean")).isEqualTo(new String[] {"testBean"}); + } + + @Test + void constructorResourceInjectionWithSometimesNullBeanLate() { + RootBeanDefinition bd = new RootBeanDefinition(ConstructorWithNullableArgument.class); + bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + bf.registerBeanDefinition("annotatedBean", bd); + RootBeanDefinition tb = new RootBeanDefinition(SometimesNullFactoryMethods.class); + tb.setFactoryMethodName("createTestBean"); + tb.setScope(BeanDefinition.SCOPE_PROTOTYPE); + bf.registerBeanDefinition("testBean", tb); + + SometimesNullFactoryMethods.active = true; + ConstructorWithNullableArgument bean = (ConstructorWithNullableArgument) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean3()).isNotNull(); + + SometimesNullFactoryMethods.active = true; + bean = (ConstructorWithNullableArgument) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean3()).isNotNull(); + + SometimesNullFactoryMethods.active = false; + bean = (ConstructorWithNullableArgument) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean3()).isNull(); + + SometimesNullFactoryMethods.active = false; + bean = (ConstructorWithNullableArgument) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean3()).isNull(); + + SometimesNullFactoryMethods.active = true; + bean = (ConstructorWithNullableArgument) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean3()).isNotNull(); + + SometimesNullFactoryMethods.active = false; + bean = (ConstructorWithNullableArgument) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean3()).isNull(); + + assertThat(bf.getDependenciesForBean("annotatedBean")).isEqualTo(new String[] {"testBean"}); + } + @Test public void testConstructorResourceInjectionWithCollectionAndNullFromFactoryBean() { bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition( @@ -817,7 +1071,7 @@ public void testConstructorResourceInjectionWithCollectionAndNullFromFactoryBean NestedTestBean ntb2 = new NestedTestBean(); bf.registerSingleton("nestedTestBean2", ntb2); - ConstructorsCollectionResourceInjectionBean bean = (ConstructorsCollectionResourceInjectionBean) bf.getBean("annotatedBean"); + ConstructorsCollectionResourceInjectionBean bean = bf.getBean("annotatedBean", ConstructorsCollectionResourceInjectionBean.class); assertThat(bean.getTestBean3()).isNull(); assertThat(bean.getTestBean4()).isSameAs(tb); assertThat(bean.getNestedTestBeans().size()).isEqualTo(1); @@ -839,7 +1093,7 @@ public void testConstructorResourceInjectionWithMultipleCandidatesAsCollection() NestedTestBean ntb2 = new NestedTestBean(); bf.registerSingleton("nestedTestBean2", ntb2); - ConstructorsCollectionResourceInjectionBean bean = (ConstructorsCollectionResourceInjectionBean) bf.getBean("annotatedBean"); + ConstructorsCollectionResourceInjectionBean bean = bf.getBean("annotatedBean", ConstructorsCollectionResourceInjectionBean.class); assertThat(bean.getTestBean3()).isNull(); assertThat(bean.getTestBean4()).isSameAs(tb); assertThat(bean.getNestedTestBeans().size()).isEqualTo(2); @@ -857,7 +1111,7 @@ public void testConstructorResourceInjectionWithMultipleOrderedCandidates() { FixedOrder1NestedTestBean ntb2 = new FixedOrder1NestedTestBean(); bf.registerSingleton("nestedTestBean2", ntb2); - ConstructorsResourceInjectionBean bean = (ConstructorsResourceInjectionBean) bf.getBean("annotatedBean"); + ConstructorsResourceInjectionBean bean = bf.getBean("annotatedBean", ConstructorsResourceInjectionBean.class); assertThat(bean.getTestBean3()).isNull(); assertThat(bean.getTestBean4()).isSameAs(tb); assertThat(bean.getNestedTestBeans().length).isEqualTo(2); @@ -875,7 +1129,7 @@ public void testConstructorResourceInjectionWithMultipleCandidatesAsOrderedColle FixedOrder1NestedTestBean ntb2 = new FixedOrder1NestedTestBean(); bf.registerSingleton("nestedTestBean2", ntb2); - ConstructorsCollectionResourceInjectionBean bean = (ConstructorsCollectionResourceInjectionBean) bf.getBean("annotatedBean"); + ConstructorsCollectionResourceInjectionBean bean = bf.getBean("annotatedBean", ConstructorsCollectionResourceInjectionBean.class); assertThat(bean.getTestBean3()).isNull(); assertThat(bean.getTestBean4()).isSameAs(tb); assertThat(bean.getNestedTestBeans().size()).isEqualTo(2); @@ -893,7 +1147,7 @@ public void testSingleConstructorInjectionWithMultipleCandidatesAsRequiredVararg FixedOrder1NestedTestBean ntb2 = new FixedOrder1NestedTestBean(); bf.registerSingleton("nestedTestBean2", ntb2); - SingleConstructorVarargBean bean = (SingleConstructorVarargBean) bf.getBean("annotatedBean"); + SingleConstructorVarargBean bean = bf.getBean("annotatedBean", SingleConstructorVarargBean.class); assertThat(bean.getTestBean()).isSameAs(tb); assertThat(bean.getNestedTestBeans().size()).isEqualTo(2); assertThat(bean.getNestedTestBeans().get(0)).isSameAs(ntb2); @@ -906,7 +1160,7 @@ public void testSingleConstructorInjectionWithEmptyVararg() { TestBean tb = new TestBean(); bf.registerSingleton("testBean", tb); - SingleConstructorVarargBean bean = (SingleConstructorVarargBean) bf.getBean("annotatedBean"); + SingleConstructorVarargBean bean = bf.getBean("annotatedBean", SingleConstructorVarargBean.class); assertThat(bean.getTestBean()).isSameAs(tb); assertThat(bean.getNestedTestBeans()).isNotNull(); assertThat(bean.getNestedTestBeans().isEmpty()).isTrue(); @@ -922,7 +1176,7 @@ public void testSingleConstructorInjectionWithMultipleCandidatesAsRequiredCollec FixedOrder1NestedTestBean ntb2 = new FixedOrder1NestedTestBean(); bf.registerSingleton("nestedTestBean2", ntb2); - SingleConstructorRequiredCollectionBean bean = (SingleConstructorRequiredCollectionBean) bf.getBean("annotatedBean"); + SingleConstructorRequiredCollectionBean bean = bf.getBean("annotatedBean", SingleConstructorRequiredCollectionBean.class); assertThat(bean.getTestBean()).isSameAs(tb); assertThat(bean.getNestedTestBeans().size()).isEqualTo(2); assertThat(bean.getNestedTestBeans().get(0)).isSameAs(ntb2); @@ -935,7 +1189,7 @@ public void testSingleConstructorInjectionWithEmptyCollection() { TestBean tb = new TestBean(); bf.registerSingleton("testBean", tb); - SingleConstructorRequiredCollectionBean bean = (SingleConstructorRequiredCollectionBean) bf.getBean("annotatedBean"); + SingleConstructorRequiredCollectionBean bean = bf.getBean("annotatedBean", SingleConstructorRequiredCollectionBean.class); assertThat(bean.getTestBean()).isSameAs(tb); assertThat(bean.getNestedTestBeans()).isNotNull(); assertThat(bean.getNestedTestBeans().isEmpty()).isTrue(); @@ -951,7 +1205,7 @@ public void testSingleConstructorInjectionWithMultipleCandidatesAsOrderedCollect FixedOrder1NestedTestBean ntb2 = new FixedOrder1NestedTestBean(); bf.registerSingleton("nestedTestBean2", ntb2); - SingleConstructorOptionalCollectionBean bean = (SingleConstructorOptionalCollectionBean) bf.getBean("annotatedBean"); + SingleConstructorOptionalCollectionBean bean = bf.getBean("annotatedBean", SingleConstructorOptionalCollectionBean.class); assertThat(bean.getTestBean()).isSameAs(tb); assertThat(bean.getNestedTestBeans().size()).isEqualTo(2); assertThat(bean.getNestedTestBeans().get(0)).isSameAs(ntb2); @@ -964,7 +1218,7 @@ public void testSingleConstructorInjectionWithEmptyCollectionAsNull() { TestBean tb = new TestBean(); bf.registerSingleton("testBean", tb); - SingleConstructorOptionalCollectionBean bean = (SingleConstructorOptionalCollectionBean) bf.getBean("annotatedBean"); + SingleConstructorOptionalCollectionBean bean = bf.getBean("annotatedBean", SingleConstructorOptionalCollectionBean.class); assertThat(bean.getTestBean()).isSameAs(tb); assertThat(bean.getNestedTestBeans()).isNull(); } @@ -972,8 +1226,8 @@ public void testSingleConstructorInjectionWithEmptyCollectionAsNull() { @Test public void testSingleConstructorInjectionWithMissingDependency() { bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(SingleConstructorOptionalCollectionBean.class)); - assertThatExceptionOfType(UnsatisfiedDependencyException.class).isThrownBy(() -> - bf.getBean("annotatedBean")); + assertThatExceptionOfType(UnsatisfiedDependencyException.class) + .isThrownBy(() -> bf.getBean("annotatedBean")); } @Test @@ -982,8 +1236,8 @@ public void testSingleConstructorInjectionWithNullDependency() { RootBeanDefinition tb = new RootBeanDefinition(NullFactoryMethods.class); tb.setFactoryMethodName("createTestBean"); bf.registerBeanDefinition("testBean", tb); - assertThatExceptionOfType(UnsatisfiedDependencyException.class).isThrownBy(() -> - bf.getBean("annotatedBean")); + assertThatExceptionOfType(UnsatisfiedDependencyException.class) + .isThrownBy(() -> bf.getBean("annotatedBean")); } @Test @@ -992,7 +1246,7 @@ public void testConstructorResourceInjectionWithMultipleCandidatesAndFallback() TestBean tb = new TestBean(); bf.registerSingleton("testBean", tb); - ConstructorsResourceInjectionBean bean = (ConstructorsResourceInjectionBean) bf.getBean("annotatedBean"); + ConstructorsResourceInjectionBean bean = bf.getBean("annotatedBean", ConstructorsResourceInjectionBean.class); assertThat(bean.getTestBean3()).isSameAs(tb); assertThat(bean.getTestBean4()).isNull(); } @@ -1001,7 +1255,7 @@ public void testConstructorResourceInjectionWithMultipleCandidatesAndFallback() public void testConstructorResourceInjectionWithMultipleCandidatesAndDefaultFallback() { bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(ConstructorsResourceInjectionBean.class)); - ConstructorsResourceInjectionBean bean = (ConstructorsResourceInjectionBean) bf.getBean("annotatedBean"); + ConstructorsResourceInjectionBean bean = bf.getBean("annotatedBean", ConstructorsResourceInjectionBean.class); assertThat(bean.getTestBean3()).isNull(); assertThat(bean.getTestBean4()).isNull(); } @@ -1017,12 +1271,12 @@ public void testConstructorInjectionWithMap() { tb2.setFactoryMethodName("createTestBean"); bf.registerBeanDefinition("testBean2", tb2); - MapConstructorInjectionBean bean = (MapConstructorInjectionBean) bf.getBean("annotatedBean"); + MapConstructorInjectionBean bean = bf.getBean("annotatedBean", MapConstructorInjectionBean.class); assertThat(bean.getTestBeanMap().size()).isEqualTo(1); assertThat(bean.getTestBeanMap().get("testBean1")).isSameAs(tb1); assertThat(bean.getTestBeanMap().get("testBean2")).isNull(); - bean = (MapConstructorInjectionBean) bf.getBean("annotatedBean"); + bean = bf.getBean("annotatedBean", MapConstructorInjectionBean.class); assertThat(bean.getTestBeanMap().size()).isEqualTo(1); assertThat(bean.getTestBeanMap().get("testBean1")).isSameAs(tb1); assertThat(bean.getTestBeanMap().get("testBean2")).isNull(); @@ -1038,14 +1292,14 @@ public void testFieldInjectionWithMap() { bf.registerSingleton("testBean1", tb1); bf.registerSingleton("testBean2", tb2); - MapFieldInjectionBean bean = (MapFieldInjectionBean) bf.getBean("annotatedBean"); + MapFieldInjectionBean bean = bf.getBean("annotatedBean", MapFieldInjectionBean.class); assertThat(bean.getTestBeanMap().size()).isEqualTo(2); assertThat(bean.getTestBeanMap().keySet().contains("testBean1")).isTrue(); assertThat(bean.getTestBeanMap().keySet().contains("testBean2")).isTrue(); assertThat(bean.getTestBeanMap().values().contains(tb1)).isTrue(); assertThat(bean.getTestBeanMap().values().contains(tb2)).isTrue(); - bean = (MapFieldInjectionBean) bf.getBean("annotatedBean"); + bean = bf.getBean("annotatedBean", MapFieldInjectionBean.class); assertThat(bean.getTestBeanMap().size()).isEqualTo(2); assertThat(bean.getTestBeanMap().keySet().contains("testBean1")).isTrue(); assertThat(bean.getTestBeanMap().keySet().contains("testBean2")).isTrue(); @@ -1061,13 +1315,13 @@ public void testMethodInjectionWithMap() { TestBean tb = new TestBean(); bf.registerSingleton("testBean", tb); - MapMethodInjectionBean bean = (MapMethodInjectionBean) bf.getBean("annotatedBean"); + MapMethodInjectionBean bean = bf.getBean("annotatedBean", MapMethodInjectionBean.class); assertThat(bean.getTestBeanMap().size()).isEqualTo(1); assertThat(bean.getTestBeanMap().keySet().contains("testBean")).isTrue(); assertThat(bean.getTestBeanMap().values().contains(tb)).isTrue(); assertThat(bean.getTestBean()).isSameAs(tb); - bean = (MapMethodInjectionBean) bf.getBean("annotatedBean"); + bean = bf.getBean("annotatedBean", MapMethodInjectionBean.class); assertThat(bean.getTestBeanMap().size()).isEqualTo(1); assertThat(bean.getTestBeanMap().keySet().contains("testBean")).isTrue(); assertThat(bean.getTestBeanMap().values().contains(tb)).isTrue(); @@ -1079,9 +1333,9 @@ public void testMethodInjectionWithMapAndMultipleMatches() { bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(MapMethodInjectionBean.class)); bf.registerBeanDefinition("testBean1", new RootBeanDefinition(TestBean.class)); bf.registerBeanDefinition("testBean2", new RootBeanDefinition(TestBean.class)); - assertThatExceptionOfType(UnsatisfiedDependencyException.class).as("should have failed, more than one bean of type").isThrownBy(() -> - bf.getBean("annotatedBean")) - .satisfies(methodParameterDeclaredOn(MapMethodInjectionBean.class)); + assertThatExceptionOfType(UnsatisfiedDependencyException.class).as("should have failed, more than one bean of type") + .isThrownBy(() -> bf.getBean("annotatedBean")) + .satisfies(methodParameterDeclaredOn(MapMethodInjectionBean.class)); } @Test @@ -1092,8 +1346,8 @@ public void testMethodInjectionWithMapAndMultipleMatchesButOnlyOneAutowireCandid rbd2.setAutowireCandidate(false); bf.registerBeanDefinition("testBean2", rbd2); - MapMethodInjectionBean bean = (MapMethodInjectionBean) bf.getBean("annotatedBean"); - TestBean tb = (TestBean) bf.getBean("testBean1"); + MapMethodInjectionBean bean = bf.getBean("annotatedBean", MapMethodInjectionBean.class); + TestBean tb = bf.getBean("testBean1", TestBean.class); assertThat(bean.getTestBeanMap().size()).isEqualTo(1); assertThat(bean.getTestBeanMap().keySet().contains("testBean1")).isTrue(); assertThat(bean.getTestBeanMap().values().contains(tb)).isTrue(); @@ -1104,7 +1358,7 @@ public void testMethodInjectionWithMapAndMultipleMatchesButOnlyOneAutowireCandid public void testMethodInjectionWithMapAndNoMatches() { bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(MapMethodInjectionBean.class)); - MapMethodInjectionBean bean = (MapMethodInjectionBean) bf.getBean("annotatedBean"); + MapMethodInjectionBean bean = bf.getBean("annotatedBean", MapMethodInjectionBean.class); assertThat(bean.getTestBeanMap()).isNull(); assertThat(bean.getTestBean()).isNull(); } @@ -1120,9 +1374,9 @@ public void testConstructorInjectionWithTypedMapAsBean() { bf.registerSingleton("testBeans", tbm); bf.registerSingleton("otherMap", new Properties()); - MapConstructorInjectionBean bean = (MapConstructorInjectionBean) bf.getBean("annotatedBean"); + MapConstructorInjectionBean bean = bf.getBean("annotatedBean", MapConstructorInjectionBean.class); assertThat(bean.getTestBeanMap()).isSameAs(tbm); - bean = (MapConstructorInjectionBean) bf.getBean("annotatedBean"); + bean = bf.getBean("annotatedBean", MapConstructorInjectionBean.class); assertThat(bean.getTestBeanMap()).isSameAs(tbm); } @@ -1136,9 +1390,9 @@ public void testConstructorInjectionWithPlainMapAsBean() { bf.registerBeanDefinition("myTestBeanMap", tbm); bf.registerSingleton("otherMap", new HashMap<>()); - MapConstructorInjectionBean bean = (MapConstructorInjectionBean) bf.getBean("annotatedBean"); + MapConstructorInjectionBean bean = bf.getBean("annotatedBean", MapConstructorInjectionBean.class); assertThat(bean.getTestBeanMap()).isSameAs(bf.getBean("myTestBeanMap")); - bean = (MapConstructorInjectionBean) bf.getBean("annotatedBean"); + bean = bf.getBean("annotatedBean", MapConstructorInjectionBean.class); assertThat(bean.getTestBeanMap()).isSameAs(bf.getBean("myTestBeanMap")); } @@ -1153,9 +1407,9 @@ public void testConstructorInjectionWithCustomMapAsBean() { bf.registerSingleton("testBean1", new TestBean()); bf.registerSingleton("testBean2", new TestBean()); - CustomMapConstructorInjectionBean bean = (CustomMapConstructorInjectionBean) bf.getBean("annotatedBean"); + CustomMapConstructorInjectionBean bean = bf.getBean("annotatedBean", CustomMapConstructorInjectionBean.class); assertThat(bean.getTestBeanMap()).isSameAs(bf.getBean("myTestBeanMap")); - bean = (CustomMapConstructorInjectionBean) bf.getBean("annotatedBean"); + bean = bf.getBean("annotatedBean", CustomMapConstructorInjectionBean.class); assertThat(bean.getTestBeanMap()).isSameAs(bf.getBean("myTestBeanMap")); } @@ -1166,9 +1420,9 @@ public void testConstructorInjectionWithPlainHashMapAsBean() { bf.registerBeanDefinition("annotatedBean", bd); bf.registerBeanDefinition("myTestBeanMap", new RootBeanDefinition(HashMap.class)); - QualifiedMapConstructorInjectionBean bean = (QualifiedMapConstructorInjectionBean) bf.getBean("annotatedBean"); + QualifiedMapConstructorInjectionBean bean = bf.getBean("annotatedBean", QualifiedMapConstructorInjectionBean.class); assertThat(bean.getTestBeanMap()).isSameAs(bf.getBean("myTestBeanMap")); - bean = (QualifiedMapConstructorInjectionBean) bf.getBean("annotatedBean"); + bean = bf.getBean("annotatedBean", QualifiedMapConstructorInjectionBean.class); assertThat(bean.getTestBeanMap()).isSameAs(bf.getBean("myTestBeanMap")); } @@ -1183,9 +1437,9 @@ public void testConstructorInjectionWithTypedSetAsBean() { bf.registerSingleton("testBeans", tbs); bf.registerSingleton("otherSet", new HashSet<>()); - SetConstructorInjectionBean bean = (SetConstructorInjectionBean) bf.getBean("annotatedBean"); + SetConstructorInjectionBean bean = bf.getBean("annotatedBean", SetConstructorInjectionBean.class); assertThat(bean.getTestBeanSet()).isSameAs(tbs); - bean = (SetConstructorInjectionBean) bf.getBean("annotatedBean"); + bean = bf.getBean("annotatedBean", SetConstructorInjectionBean.class); assertThat(bean.getTestBeanSet()).isSameAs(tbs); } @@ -1199,9 +1453,9 @@ public void testConstructorInjectionWithPlainSetAsBean() { bf.registerBeanDefinition("myTestBeanSet", tbs); bf.registerSingleton("otherSet", new HashSet<>()); - SetConstructorInjectionBean bean = (SetConstructorInjectionBean) bf.getBean("annotatedBean"); + SetConstructorInjectionBean bean = bf.getBean("annotatedBean", SetConstructorInjectionBean.class); assertThat(bean.getTestBeanSet()).isSameAs(bf.getBean("myTestBeanSet")); - bean = (SetConstructorInjectionBean) bf.getBean("annotatedBean"); + bean = bf.getBean("annotatedBean", SetConstructorInjectionBean.class); assertThat(bean.getTestBeanSet()).isSameAs(bf.getBean("myTestBeanSet")); } @@ -1214,9 +1468,9 @@ public void testConstructorInjectionWithCustomSetAsBean() { tbs.setUniqueFactoryMethodName("testBeanSet"); bf.registerBeanDefinition("myTestBeanSet", tbs); - CustomSetConstructorInjectionBean bean = (CustomSetConstructorInjectionBean) bf.getBean("annotatedBean"); + CustomSetConstructorInjectionBean bean = bf.getBean("annotatedBean", CustomSetConstructorInjectionBean.class); assertThat(bean.getTestBeanSet()).isSameAs(bf.getBean("myTestBeanSet")); - bean = (CustomSetConstructorInjectionBean) bf.getBean("annotatedBean"); + bean = bf.getBean("annotatedBean", CustomSetConstructorInjectionBean.class); assertThat(bean.getTestBeanSet()).isSameAs(bf.getBean("myTestBeanSet")); } @@ -1224,7 +1478,7 @@ public void testConstructorInjectionWithCustomSetAsBean() { public void testSelfReference() { bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(SelfInjectionBean.class)); - SelfInjectionBean bean = (SelfInjectionBean) bf.getBean("annotatedBean"); + SelfInjectionBean bean = bf.getBean("annotatedBean", SelfInjectionBean.class); assertThat(bean.reference).isSameAs(bean); assertThat(bean.referenceCollection).isNull(); } @@ -1234,8 +1488,8 @@ public void testSelfReferenceWithOther() { bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(SelfInjectionBean.class)); bf.registerBeanDefinition("annotatedBean2", new RootBeanDefinition(SelfInjectionBean.class)); - SelfInjectionBean bean = (SelfInjectionBean) bf.getBean("annotatedBean"); - SelfInjectionBean bean2 = (SelfInjectionBean) bf.getBean("annotatedBean2"); + SelfInjectionBean bean = bf.getBean("annotatedBean", SelfInjectionBean.class); + SelfInjectionBean bean2 = bf.getBean("annotatedBean2", SelfInjectionBean.class); assertThat(bean.reference).isSameAs(bean2); assertThat(bean.referenceCollection.size()).isEqualTo(1); assertThat(bean.referenceCollection.get(0)).isSameAs(bean2); @@ -1245,7 +1499,7 @@ public void testSelfReferenceWithOther() { public void testSelfReferenceCollection() { bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(SelfInjectionCollectionBean.class)); - SelfInjectionCollectionBean bean = (SelfInjectionCollectionBean) bf.getBean("annotatedBean"); + SelfInjectionCollectionBean bean = bf.getBean("annotatedBean", SelfInjectionCollectionBean.class); assertThat(bean.reference).isSameAs(bean); assertThat(bean.referenceCollection).isNull(); } @@ -1255,8 +1509,8 @@ public void testSelfReferenceCollectionWithOther() { bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(SelfInjectionCollectionBean.class)); bf.registerBeanDefinition("annotatedBean2", new RootBeanDefinition(SelfInjectionCollectionBean.class)); - SelfInjectionCollectionBean bean = (SelfInjectionCollectionBean) bf.getBean("annotatedBean"); - SelfInjectionCollectionBean bean2 = (SelfInjectionCollectionBean) bf.getBean("annotatedBean2"); + SelfInjectionCollectionBean bean = bf.getBean("annotatedBean", SelfInjectionCollectionBean.class); + SelfInjectionCollectionBean bean2 = bf.getBean("annotatedBean2", SelfInjectionCollectionBean.class); assertThat(bean.reference).isSameAs(bean2); assertThat(bean2.referenceCollection.size()).isSameAs(1); assertThat(bean.referenceCollection.get(0)).isSameAs(bean2); @@ -1267,7 +1521,7 @@ public void testObjectFactoryFieldInjection() { bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(ObjectFactoryFieldInjectionBean.class)); bf.registerBeanDefinition("testBean", new RootBeanDefinition(TestBean.class)); - ObjectFactoryFieldInjectionBean bean = (ObjectFactoryFieldInjectionBean) bf.getBean("annotatedBean"); + ObjectFactoryFieldInjectionBean bean = bf.getBean("annotatedBean", ObjectFactoryFieldInjectionBean.class); assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean")); } @@ -1276,7 +1530,7 @@ public void testObjectFactoryConstructorInjection() { bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(ObjectFactoryConstructorInjectionBean.class)); bf.registerBeanDefinition("testBean", new RootBeanDefinition(TestBean.class)); - ObjectFactoryConstructorInjectionBean bean = (ObjectFactoryConstructorInjectionBean) bf.getBean("annotatedBean"); + ObjectFactoryConstructorInjectionBean bean = bf.getBean("annotatedBean", ObjectFactoryConstructorInjectionBean.class); assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean")); } @@ -1287,9 +1541,9 @@ public void testObjectFactoryInjectionIntoPrototypeBean() { bf.registerBeanDefinition("annotatedBean", annotatedBeanDefinition); bf.registerBeanDefinition("testBean", new RootBeanDefinition(TestBean.class)); - ObjectFactoryFieldInjectionBean bean = (ObjectFactoryFieldInjectionBean) bf.getBean("annotatedBean"); + ObjectFactoryFieldInjectionBean bean = bf.getBean("annotatedBean", ObjectFactoryFieldInjectionBean.class); assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean")); - ObjectFactoryFieldInjectionBean anotherBean = (ObjectFactoryFieldInjectionBean) bf.getBean("annotatedBean"); + ObjectFactoryFieldInjectionBean anotherBean = bf.getBean("annotatedBean", ObjectFactoryFieldInjectionBean.class); assertThat(bean).isNotSameAs(anotherBean); assertThat(anotherBean.getTestBean()).isSameAs(bf.getBean("testBean")); } @@ -1302,7 +1556,7 @@ public void testObjectFactoryQualifierInjection() { bf.registerBeanDefinition("dependencyBean", bd); bf.registerBeanDefinition("dependencyBean2", new RootBeanDefinition(TestBean.class)); - ObjectFactoryQualifierInjectionBean bean = (ObjectFactoryQualifierInjectionBean) bf.getBean("annotatedBean"); + ObjectFactoryQualifierInjectionBean bean = bf.getBean("annotatedBean", ObjectFactoryQualifierInjectionBean.class); assertThat(bean.getTestBean()).isSameAs(bf.getBean("dependencyBean")); } @@ -1314,7 +1568,7 @@ public void testObjectFactoryQualifierProviderInjection() { bf.registerBeanDefinition("dependencyBean", bd); bf.registerBeanDefinition("dependencyBean2", new RootBeanDefinition(TestBean.class)); - ObjectFactoryQualifierInjectionBean bean = (ObjectFactoryQualifierInjectionBean) bf.getBean("annotatedBean"); + ObjectFactoryQualifierInjectionBean bean = bf.getBean("annotatedBean", ObjectFactoryQualifierInjectionBean.class); assertThat(bean.getTestBean()).isSameAs(bf.getBean("dependencyBean")); } @@ -1324,7 +1578,7 @@ public void testObjectFactorySerialization() throws Exception { bf.registerBeanDefinition("testBean", new RootBeanDefinition(TestBean.class)); bf.setSerializationId("test"); - ObjectFactoryFieldInjectionBean bean = (ObjectFactoryFieldInjectionBean) bf.getBean("annotatedBean"); + ObjectFactoryFieldInjectionBean bean = bf.getBean("annotatedBean", ObjectFactoryFieldInjectionBean.class); assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean")); bean = SerializationTestUtils.serializeAndDeserialize(bean); assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean")); @@ -1337,7 +1591,7 @@ public void testObjectProviderInjectionWithPrototype() { tbd.setScope(BeanDefinition.SCOPE_PROTOTYPE); bf.registerBeanDefinition("testBean", tbd); - ObjectProviderInjectionBean bean = (ObjectProviderInjectionBean) bf.getBean("annotatedBean"); + ObjectProviderInjectionBean bean = bf.getBean("annotatedBean", ObjectProviderInjectionBean.class); assertThat(bean.getTestBean()).isEqualTo(bf.getBean("testBean")); assertThat(bean.getTestBean("myName")).isEqualTo(bf.getBean("testBean", "myName")); assertThat(bean.getOptionalTestBean()).isEqualTo(bf.getBean("testBean")); @@ -1366,7 +1620,7 @@ public void testObjectProviderInjectionWithSingletonTarget() { bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(ObjectProviderInjectionBean.class)); bf.registerBeanDefinition("testBean", new RootBeanDefinition(TestBean.class)); - ObjectProviderInjectionBean bean = (ObjectProviderInjectionBean) bf.getBean("annotatedBean"); + ObjectProviderInjectionBean bean = bf.getBean("annotatedBean", ObjectProviderInjectionBean.class); assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean")); assertThat(bean.getOptionalTestBean()).isSameAs(bf.getBean("testBean")); assertThat(bean.getOptionalTestBeanWithDefault()).isSameAs(bf.getBean("testBean")); @@ -1393,7 +1647,7 @@ public void testObjectProviderInjectionWithSingletonTarget() { public void testObjectProviderInjectionWithTargetNotAvailable() { bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(ObjectProviderInjectionBean.class)); - ObjectProviderInjectionBean bean = (ObjectProviderInjectionBean) bf.getBean("annotatedBean"); + ObjectProviderInjectionBean bean = bf.getBean("annotatedBean", ObjectProviderInjectionBean.class); assertThatExceptionOfType(NoSuchBeanDefinitionException.class).isThrownBy( bean::getTestBean); assertThat(bean.getOptionalTestBean()).isNull(); @@ -1419,7 +1673,7 @@ public void testObjectProviderInjectionWithTargetNotUnique() { bf.registerBeanDefinition("testBean1", new RootBeanDefinition(TestBean.class)); bf.registerBeanDefinition("testBean2", new RootBeanDefinition(TestBean.class)); - ObjectProviderInjectionBean bean = (ObjectProviderInjectionBean) bf.getBean("annotatedBean"); + ObjectProviderInjectionBean bean = bf.getBean("annotatedBean", ObjectProviderInjectionBean.class); assertThatExceptionOfType(NoUniqueBeanDefinitionException.class).isThrownBy(bean::getTestBean); assertThatExceptionOfType(NoUniqueBeanDefinitionException.class).isThrownBy(bean::getOptionalTestBean); assertThatExceptionOfType(NoUniqueBeanDefinitionException.class).isThrownBy(bean::consumeOptionalTestBean); @@ -1456,7 +1710,7 @@ public void testObjectProviderInjectionWithTargetPrimary() { tb2.setLazyInit(true); bf.registerBeanDefinition("testBean2", tb2); - ObjectProviderInjectionBean bean = (ObjectProviderInjectionBean) bf.getBean("annotatedBean"); + ObjectProviderInjectionBean bean = bf.getBean("annotatedBean", ObjectProviderInjectionBean.class); assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean1")); assertThat(bean.getOptionalTestBean()).isSameAs(bf.getBean("testBean1")); assertThat(bean.consumeOptionalTestBean()).isSameAs(bf.getBean("testBean1")); @@ -1494,7 +1748,7 @@ public void testObjectProviderInjectionWithUnresolvedOrderedStream() { tb2.setLazyInit(true); bf.registerBeanDefinition("testBean2", tb2); - ObjectProviderInjectionBean bean = (ObjectProviderInjectionBean) bf.getBean("annotatedBean"); + ObjectProviderInjectionBean bean = bf.getBean("annotatedBean", ObjectProviderInjectionBean.class); List testBeans = bean.sortedTestBeans(); assertThat(testBeans.size()).isEqualTo(2); assertThat(testBeans.get(0)).isSameAs(bf.getBean("testBean2")); @@ -1717,7 +1971,7 @@ public void testGenericsBasedFieldInjection() { IntegerRepository ir = new IntegerRepository(); bf.registerSingleton("integerRepo", ir); - RepositoryFieldInjectionBean bean = (RepositoryFieldInjectionBean) bf.getBean("annotatedBean"); + RepositoryFieldInjectionBean bean = bf.getBean("annotatedBean", RepositoryFieldInjectionBean.class); assertThat(bean.string).isSameAs(sv); assertThat(bean.integer).isSameAs(iv); assertThat(bean.stringArray.length).isSameAs(1); @@ -1762,7 +2016,8 @@ public void testGenericsBasedFieldInjectionWithSubstitutedVariables() { IntegerRepository ir = new IntegerRepository(); bf.registerSingleton("integerRepo", ir); - RepositoryFieldInjectionBeanWithSubstitutedVariables bean = (RepositoryFieldInjectionBeanWithSubstitutedVariables) bf.getBean("annotatedBean"); + RepositoryFieldInjectionBeanWithSubstitutedVariables bean = + bf.getBean("annotatedBean", RepositoryFieldInjectionBeanWithSubstitutedVariables.class); assertThat(bean.string).isSameAs(sv); assertThat(bean.integer).isSameAs(iv); assertThat(bean.stringArray.length).isSameAs(1); @@ -1803,7 +2058,8 @@ public void testGenericsBasedFieldInjectionWithQualifiers() { IntegerRepository ir = new IntegerRepository(); bf.registerSingleton("integerRepo", ir); - RepositoryFieldInjectionBeanWithQualifiers bean = (RepositoryFieldInjectionBeanWithQualifiers) bf.getBean("annotatedBean"); + RepositoryFieldInjectionBeanWithQualifiers bean = + bf.getBean("annotatedBean", RepositoryFieldInjectionBeanWithQualifiers.class); assertThat(bean.stringRepository).isSameAs(sr); assertThat(bean.integerRepository).isSameAs(ir); assertThat(bean.stringRepositoryArray.length).isSameAs(1); @@ -1840,7 +2096,8 @@ public void testGenericsBasedFieldInjectionWithMocks() { rbd.setQualifiedElement(ReflectionUtils.findField(getClass(), "integerRepositoryQualifierProvider")); bf.registerBeanDefinition("integerRepository", rbd); // Bean name not matching qualifier - RepositoryFieldInjectionBeanWithQualifiers bean = (RepositoryFieldInjectionBeanWithQualifiers) bf.getBean("annotatedBean"); + RepositoryFieldInjectionBeanWithQualifiers bean = + bf.getBean("annotatedBean", RepositoryFieldInjectionBeanWithQualifiers.class); Repository sr = bf.getBean("stringRepo", Repository.class); Repository ir = bf.getBean("integerRepository", Repository.class); assertThat(bean.stringRepository).isSameAs(sr); @@ -1867,7 +2124,8 @@ public void testGenericsBasedFieldInjectionWithSimpleMatch() { bf.registerSingleton("repo", new StringRepository()); - RepositoryFieldInjectionBeanWithSimpleMatch bean = (RepositoryFieldInjectionBeanWithSimpleMatch) bf.getBean("annotatedBean"); + RepositoryFieldInjectionBeanWithSimpleMatch bean = + bf.getBean("annotatedBean", RepositoryFieldInjectionBeanWithSimpleMatch.class); Repository repo = bf.getBean("repo", Repository.class); assertThat(bean.repository).isSameAs(repo); assertThat(bean.stringRepository).isSameAs(repo); @@ -1894,7 +2152,7 @@ public void testGenericsBasedFactoryBeanInjectionWithBeanDefinition() { bf.registerBeanDefinition("annotatedBean", bd); bf.registerBeanDefinition("repoFactoryBean", new RootBeanDefinition(RepositoryFactoryBean.class)); - RepositoryFactoryBeanInjectionBean bean = (RepositoryFactoryBeanInjectionBean) bf.getBean("annotatedBean"); + RepositoryFactoryBeanInjectionBean bean = bf.getBean("annotatedBean", RepositoryFactoryBeanInjectionBean.class); RepositoryFactoryBean repoFactoryBean = bf.getBean("&repoFactoryBean", RepositoryFactoryBean.class); assertThat(bean.repositoryFactoryBean).isSameAs(repoFactoryBean); } @@ -1906,7 +2164,7 @@ public void testGenericsBasedFactoryBeanInjectionWithSingletonBean() { bf.registerBeanDefinition("annotatedBean", bd); bf.registerSingleton("repoFactoryBean", new RepositoryFactoryBean<>()); - RepositoryFactoryBeanInjectionBean bean = (RepositoryFactoryBeanInjectionBean) bf.getBean("annotatedBean"); + RepositoryFactoryBeanInjectionBean bean = bf.getBean("annotatedBean", RepositoryFactoryBeanInjectionBean.class); RepositoryFactoryBean repoFactoryBean = bf.getBean("&repoFactoryBean", RepositoryFactoryBean.class); assertThat(bean.repositoryFactoryBean).isSameAs(repoFactoryBean); } @@ -1925,7 +2183,7 @@ public void testGenericsBasedFieldInjectionWithSimpleMatchAndMock() { rbd.getConstructorArgumentValues().addGenericArgumentValue(Repository.class); bf.registerBeanDefinition("repo", rbd); - RepositoryFieldInjectionBeanWithSimpleMatch bean = (RepositoryFieldInjectionBeanWithSimpleMatch) bf.getBean("annotatedBean"); + RepositoryFieldInjectionBeanWithSimpleMatch bean = bf.getBean("annotatedBean", RepositoryFieldInjectionBeanWithSimpleMatch.class); Repository repo = bf.getBean("repo", Repository.class); assertThat(bean.repository).isSameAs(repo); assertThat(bean.stringRepository).isSameAs(repo); @@ -1956,7 +2214,7 @@ public void testGenericsBasedFieldInjectionWithSimpleMatchAndMockito() { rbd.getConstructorArgumentValues().addGenericArgumentValue(new TypedStringValue(Repository.class.getName())); bf.registerBeanDefinition("repo", rbd); - RepositoryFieldInjectionBeanWithSimpleMatch bean = (RepositoryFieldInjectionBeanWithSimpleMatch) bf.getBean("annotatedBean"); + RepositoryFieldInjectionBeanWithSimpleMatch bean = bf.getBean("annotatedBean", RepositoryFieldInjectionBeanWithSimpleMatch.class); Repository repo = bf.getBean("repo", Repository.class); assertThat(bean.repository).isSameAs(repo); assertThat(bean.stringRepository).isSameAs(repo); @@ -1988,7 +2246,7 @@ public void testGenericsBasedMethodInjection() { IntegerRepository ir = new IntegerRepository(); bf.registerSingleton("integerRepo", ir); - RepositoryMethodInjectionBean bean = (RepositoryMethodInjectionBean) bf.getBean("annotatedBean"); + RepositoryMethodInjectionBean bean = bf.getBean("annotatedBean", RepositoryMethodInjectionBean.class); assertThat(bean.string).isSameAs(sv); assertThat(bean.integer).isSameAs(iv); assertThat(bean.stringArray.length).isSameAs(1); @@ -2033,7 +2291,8 @@ public void testGenericsBasedMethodInjectionWithSubstitutedVariables() { IntegerRepository ir = new IntegerRepository(); bf.registerSingleton("integerRepo", ir); - RepositoryMethodInjectionBeanWithSubstitutedVariables bean = (RepositoryMethodInjectionBeanWithSubstitutedVariables) bf.getBean("annotatedBean"); + RepositoryMethodInjectionBeanWithSubstitutedVariables bean = + bf.getBean("annotatedBean", RepositoryMethodInjectionBeanWithSubstitutedVariables.class); assertThat(bean.string).isSameAs(sv); assertThat(bean.integer).isSameAs(iv); assertThat(bean.stringArray.length).isSameAs(1); @@ -2074,7 +2333,7 @@ public void testGenericsBasedConstructorInjection() { IntegerRepository ir = new IntegerRepository(); bf.registerSingleton("integerRepo", ir); - RepositoryConstructorInjectionBean bean = (RepositoryConstructorInjectionBean) bf.getBean("annotatedBean"); + RepositoryConstructorInjectionBean bean = bf.getBean("annotatedBean", RepositoryConstructorInjectionBean.class); assertThat(bean.stringRepository).isSameAs(sr); assertThat(bean.integerRepository).isSameAs(ir); assertThat(bean.stringRepositoryArray.length).isSameAs(1); @@ -2100,7 +2359,7 @@ public void testGenericsBasedConstructorInjectionWithNonTypedTarget() { GenericRepository gr = new GenericRepository(); bf.registerSingleton("genericRepo", gr); - RepositoryConstructorInjectionBean bean = (RepositoryConstructorInjectionBean) bf.getBean("annotatedBean"); + RepositoryConstructorInjectionBean bean = bf.getBean("annotatedBean", RepositoryConstructorInjectionBean.class); assertThat(bean.stringRepository).isSameAs(gr); assertThat(bean.integerRepository).isSameAs(gr); assertThat(bean.stringRepositoryArray.length).isSameAs(1); @@ -2125,7 +2384,7 @@ public void testGenericsBasedConstructorInjectionWithNonGenericTarget() { SimpleRepository ngr = new SimpleRepository(); bf.registerSingleton("simpleRepo", ngr); - RepositoryConstructorInjectionBean bean = (RepositoryConstructorInjectionBean) bf.getBean("annotatedBean"); + RepositoryConstructorInjectionBean bean = bf.getBean("annotatedBean", RepositoryConstructorInjectionBean.class); assertThat(bean.stringRepository).isSameAs(ngr); assertThat(bean.integerRepository).isSameAs(ngr); assertThat(bean.stringRepositoryArray.length).isSameAs(1); @@ -2153,7 +2412,7 @@ public void testGenericsBasedConstructorInjectionWithMixedTargets() { GenericRepository gr = new GenericRepositorySubclass(); bf.registerSingleton("genericRepo", gr); - RepositoryConstructorInjectionBean bean = (RepositoryConstructorInjectionBean) bf.getBean("annotatedBean"); + RepositoryConstructorInjectionBean bean = bf.getBean("annotatedBean", RepositoryConstructorInjectionBean.class); assertThat(bean.stringRepository).isSameAs(sr); assertThat(bean.integerRepository).isSameAs(gr); assertThat(bean.stringRepositoryArray.length).isSameAs(1); @@ -2180,7 +2439,7 @@ public void testGenericsBasedConstructorInjectionWithMixedTargetsIncludingNonGen SimpleRepository ngr = new SimpleRepositorySubclass(); bf.registerSingleton("simpleRepo", ngr); - RepositoryConstructorInjectionBean bean = (RepositoryConstructorInjectionBean) bf.getBean("annotatedBean"); + RepositoryConstructorInjectionBean bean = bf.getBean("annotatedBean", RepositoryConstructorInjectionBean.class); assertThat(bean.stringRepository).isSameAs(sr); assertThat(bean.integerRepository).isSameAs(ngr); assertThat(bean.stringRepositoryArray.length).isSameAs(1); @@ -2362,13 +2621,12 @@ public static class ResourceInjectionBean { @Autowired(required = false) private TestBean testBean; - private TestBean testBean2; + TestBean testBean2; @Autowired public void setTestBean2(TestBean testBean2) { - if (this.testBean2 != null) { - throw new IllegalStateException("Already called"); - } + Assert.state(this.testBean != null, "Wrong initialization order"); + Assert.state(this.testBean2 == null, "Already called"); this.testBean2 = testBean2; } @@ -2403,7 +2661,7 @@ public NonPublicResourceInjectionBean() { @Required @SuppressWarnings("deprecation") public void setTestBean2(TestBean testBean2) { - super.setTestBean2(testBean2); + this.testBean2 = testBean2; } @Autowired @@ -2419,6 +2677,7 @@ private void inject(ITestBean testBean4) { @Autowired protected void initBeanFactory(BeanFactory beanFactory) { + Assert.state(this.baseInjected, "Wrong initialization order"); this.beanFactory = beanFactory; } @@ -2725,6 +2984,21 @@ public ITestBean getTestBean3() { } + public static class ConstructorWithNullableArgument { + + protected ITestBean testBean3; + + @Autowired(required = false) + public ConstructorWithNullableArgument(@Nullable ITestBean testBean3) { + this.testBean3 = testBean3; + } + + public ITestBean getTestBean3() { + return this.testBean3; + } + } + + public static class ConstructorsCollectionResourceInjectionBean { protected ITestBean testBean3; @@ -3827,9 +4101,7 @@ public static abstract class Foo> { private RT obj; protected void setObj(RT obj) { - if (this.obj != null) { - throw new IllegalStateException("Already called"); - } + Assert.state(this.obj == null, "Already called"); this.obj = obj; } } @@ -3876,6 +4148,20 @@ public static NestedTestBean createNestedTestBean() { } + public static class SometimesNullFactoryMethods { + + public static boolean active = false; + + public static TestBean createTestBean() { + return (active ? new TestBean() : null); + } + + public static NestedTestBean createNestedTestBean() { + return (active ? new NestedTestBean() : null); + } + } + + public static class ProvidedArgumentBean { public ProvidedArgumentBean(String[] args) { diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistryTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistryTests.java index b7bc696c68d5..c051f749b512 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistryTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 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. @@ -28,36 +28,32 @@ * @author Chris Beams * @since 04.07.2006 */ -public class DefaultSingletonBeanRegistryTests { +class DefaultSingletonBeanRegistryTests { + + private final DefaultSingletonBeanRegistry beanRegistry = new DefaultSingletonBeanRegistry(); - @Test - public void testSingletons() { - DefaultSingletonBeanRegistry beanRegistry = new DefaultSingletonBeanRegistry(); + @Test + void singletons() { TestBean tb = new TestBean(); beanRegistry.registerSingleton("tb", tb); assertThat(beanRegistry.getSingleton("tb")).isSameAs(tb); - TestBean tb2 = (TestBean) beanRegistry.getSingleton("tb2", () -> new TestBean()); + TestBean tb2 = (TestBean) beanRegistry.getSingleton("tb2", TestBean::new); assertThat(beanRegistry.getSingleton("tb2")).isSameAs(tb2); assertThat(beanRegistry.getSingleton("tb")).isSameAs(tb); assertThat(beanRegistry.getSingleton("tb2")).isSameAs(tb2); assertThat(beanRegistry.getSingletonCount()).isEqualTo(2); - String[] names = beanRegistry.getSingletonNames(); - assertThat(names.length).isEqualTo(2); - assertThat(names[0]).isEqualTo("tb"); - assertThat(names[1]).isEqualTo("tb2"); + assertThat(beanRegistry.getSingletonNames()).containsExactly("tb", "tb2"); beanRegistry.destroySingletons(); - assertThat(beanRegistry.getSingletonCount()).isEqualTo(0); - assertThat(beanRegistry.getSingletonNames().length).isEqualTo(0); + assertThat(beanRegistry.getSingletonCount()).isZero(); + assertThat(beanRegistry.getSingletonNames()).isEmpty(); } @Test - public void testDisposableBean() { - DefaultSingletonBeanRegistry beanRegistry = new DefaultSingletonBeanRegistry(); - + void disposableBean() { DerivedTestBean tb = new DerivedTestBean(); beanRegistry.registerSingleton("tb", tb); beanRegistry.registerDisposableBean("tb", tb); @@ -65,21 +61,16 @@ public void testDisposableBean() { assertThat(beanRegistry.getSingleton("tb")).isSameAs(tb); assertThat(beanRegistry.getSingletonCount()).isEqualTo(1); - String[] names = beanRegistry.getSingletonNames(); - assertThat(names.length).isEqualTo(1); - assertThat(names[0]).isEqualTo("tb"); + assertThat(beanRegistry.getSingletonNames()).containsExactly("tb"); assertThat(tb.wasDestroyed()).isFalse(); beanRegistry.destroySingletons(); - assertThat(beanRegistry.getSingletonCount()).isEqualTo(0); - assertThat(beanRegistry.getSingletonNames().length).isEqualTo(0); - assertThat(tb.wasDestroyed()).isTrue(); + assertThat(beanRegistry.getSingletonCount()).isZero(); + assertThat(beanRegistry.getSingletonNames()).isEmpty(); } @Test - public void testDependentRegistration() { - DefaultSingletonBeanRegistry beanRegistry = new DefaultSingletonBeanRegistry(); - + void dependentRegistration() { beanRegistry.registerDependentBean("a", "b"); beanRegistry.registerDependentBean("b", "c"); beanRegistry.registerDependentBean("c", "b"); diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/support/QualifierAnnotationAutowireBeanFactoryTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/support/QualifierAnnotationAutowireBeanFactoryTests.java index f660a8af020d..fe190c8b9f5c 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/support/QualifierAnnotationAutowireBeanFactoryTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/support/QualifierAnnotationAutowireBeanFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 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,7 +27,7 @@ import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.config.ConstructorArgumentValues; import org.springframework.beans.factory.config.DependencyDescriptor; -import org.springframework.core.LocalVariableTableParameterNameDiscoverer; +import org.springframework.core.DefaultParameterNameDiscoverer; import org.springframework.core.MethodParameter; import org.springframework.util.ClassUtils; @@ -148,7 +148,7 @@ public void testAutowireCandidateWithConstructorDescriptor() throws Exception { lbf.registerBeanDefinition(MARK, person2); MethodParameter param = new MethodParameter(QualifiedTestBean.class.getDeclaredConstructor(Person.class), 0); DependencyDescriptor qualifiedDescriptor = new DependencyDescriptor(param, false); - param.initParameterNameDiscovery(new LocalVariableTableParameterNameDiscoverer()); + param.initParameterNameDiscovery(new DefaultParameterNameDiscoverer()); assertThat(param.getParameterName()).isEqualTo("tpb"); assertThat(lbf.isAutowireCandidate(JUERGEN, null)).isTrue(); assertThat(lbf.isAutowireCandidate(JUERGEN, qualifiedDescriptor)).isTrue(); @@ -174,9 +174,9 @@ public void testAutowireCandidateWithMethodDescriptor() throws Exception { new MethodParameter(QualifiedTestBean.class.getDeclaredMethod("autowireNonqualified", Person.class), 0); DependencyDescriptor qualifiedDescriptor = new DependencyDescriptor(qualifiedParam, false); DependencyDescriptor nonqualifiedDescriptor = new DependencyDescriptor(nonqualifiedParam, false); - qualifiedParam.initParameterNameDiscovery(new LocalVariableTableParameterNameDiscoverer()); + qualifiedParam.initParameterNameDiscovery(new DefaultParameterNameDiscoverer()); assertThat(qualifiedParam.getParameterName()).isEqualTo("tpb"); - nonqualifiedParam.initParameterNameDiscovery(new LocalVariableTableParameterNameDiscoverer()); + nonqualifiedParam.initParameterNameDiscovery(new DefaultParameterNameDiscoverer()); assertThat(nonqualifiedParam.getParameterName()).isEqualTo("tpb"); assertThat(lbf.isAutowireCandidate(JUERGEN, null)).isTrue(); assertThat(lbf.isAutowireCandidate(JUERGEN, nonqualifiedDescriptor)).isTrue(); diff --git a/spring-beans/src/test/java/org/springframework/beans/propertyeditors/CustomEditorTests.java b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/CustomEditorTests.java index 696e3088583a..02d6eacfdf83 100644 --- a/spring-beans/src/test/java/org/springframework/beans/propertyeditors/CustomEditorTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/CustomEditorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 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. @@ -502,7 +502,7 @@ void testCharacterEditor() { CharBean cb = new CharBean(); BeanWrapper bw = new BeanWrapperImpl(cb); - bw.setPropertyValue("myChar", new Character('c')); + bw.setPropertyValue("myChar", 'c'); assertThat(cb.getMyChar()).isEqualTo('c'); bw.setPropertyValue("myChar", "c"); @@ -526,16 +526,16 @@ void testCharacterEditorWithAllowEmpty() { bw.registerCustomEditor(Character.class, new CharacterEditor(true)); bw.setPropertyValue("myCharacter", 'c'); - assertThat(cb.getMyCharacter()).isEqualTo(Character.valueOf('c')); + assertThat(cb.getMyCharacter()).isEqualTo('c'); bw.setPropertyValue("myCharacter", "c"); - assertThat(cb.getMyCharacter()).isEqualTo(Character.valueOf('c')); + assertThat(cb.getMyCharacter()).isEqualTo('c'); bw.setPropertyValue("myCharacter", "\u0041"); - assertThat(cb.getMyCharacter()).isEqualTo(Character.valueOf('A')); + assertThat(cb.getMyCharacter()).isEqualTo('A'); bw.setPropertyValue("myCharacter", " "); - assertThat(cb.getMyCharacter()).isEqualTo(Character.valueOf(' ')); + assertThat(cb.getMyCharacter()).isEqualTo(' '); bw.setPropertyValue("myCharacter", ""); assertThat(cb.getMyCharacter()).isNull(); diff --git a/spring-beans/src/test/java/org/springframework/beans/propertyeditors/FileEditorTests.java b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/FileEditorTests.java index ec5cc0f469d8..84d6eff38126 100644 --- a/spring-beans/src/test/java/org/springframework/beans/propertyeditors/FileEditorTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/FileEditorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -31,79 +31,82 @@ * @author Chris Beams * @author Juergen Hoeller */ -public class FileEditorTests { +class FileEditorTests { @Test - public void testClasspathFileName() throws Exception { + void testClasspathFileName() { PropertyEditor fileEditor = new FileEditor(); fileEditor.setAsText("classpath:" + ClassUtils.classPackageAsResourcePath(getClass()) + "/" + ClassUtils.getShortName(getClass()) + ".class"); Object value = fileEditor.getValue(); - boolean condition = value instanceof File; - assertThat(condition).isTrue(); + assertThat(value).isInstanceOf(File.class); File file = (File) value; - assertThat(file.exists()).isTrue(); + assertThat(file).exists(); } @Test - public void testWithNonExistentResource() throws Exception { - PropertyEditor propertyEditor = new FileEditor(); + void testWithNonExistentResource() { + PropertyEditor fileEditor = new FileEditor(); assertThatIllegalArgumentException().isThrownBy(() -> - propertyEditor.setAsText("classpath:no_way_this_file_is_found.doc")); + fileEditor.setAsText("classpath:no_way_this_file_is_found.doc")); } @Test - public void testWithNonExistentFile() throws Exception { + void testWithNonExistentFile() { PropertyEditor fileEditor = new FileEditor(); fileEditor.setAsText("file:no_way_this_file_is_found.doc"); Object value = fileEditor.getValue(); - boolean condition1 = value instanceof File; - assertThat(condition1).isTrue(); + assertThat(value).isInstanceOf(File.class); File file = (File) value; - boolean condition = !file.exists(); - assertThat(condition).isTrue(); + assertThat(file).doesNotExist(); } @Test - public void testAbsoluteFileName() throws Exception { + void testAbsoluteFileName() { PropertyEditor fileEditor = new FileEditor(); fileEditor.setAsText("/no_way_this_file_is_found.doc"); Object value = fileEditor.getValue(); - boolean condition1 = value instanceof File; - assertThat(condition1).isTrue(); + assertThat(value).isInstanceOf(File.class); + File file = (File) value; + assertThat(file).doesNotExist(); + } + + @Test + void testCurrentDirectory() { + PropertyEditor fileEditor = new FileEditor(); + fileEditor.setAsText("file:."); + Object value = fileEditor.getValue(); + assertThat(value).isInstanceOf(File.class); File file = (File) value; - boolean condition = !file.exists(); - assertThat(condition).isTrue(); + assertThat(file).isEqualTo(new File(".")); } @Test - public void testUnqualifiedFileNameFound() throws Exception { + void testUnqualifiedFileNameFound() { PropertyEditor fileEditor = new FileEditor(); String fileName = ClassUtils.classPackageAsResourcePath(getClass()) + "/" + ClassUtils.getShortName(getClass()) + ".class"; fileEditor.setAsText(fileName); Object value = fileEditor.getValue(); - boolean condition = value instanceof File; - assertThat(condition).isTrue(); + assertThat(value).isInstanceOf(File.class); File file = (File) value; - assertThat(file.exists()).isTrue(); + assertThat(file).exists(); String absolutePath = file.getAbsolutePath().replace('\\', '/'); - assertThat(absolutePath.endsWith(fileName)).isTrue(); + assertThat(absolutePath).endsWith(fileName); } @Test - public void testUnqualifiedFileNameNotFound() throws Exception { + void testUnqualifiedFileNameNotFound() { PropertyEditor fileEditor = new FileEditor(); String fileName = ClassUtils.classPackageAsResourcePath(getClass()) + "/" + ClassUtils.getShortName(getClass()) + ".clazz"; fileEditor.setAsText(fileName); Object value = fileEditor.getValue(); - boolean condition = value instanceof File; - assertThat(condition).isTrue(); + assertThat(value).isInstanceOf(File.class); File file = (File) value; - assertThat(file.exists()).isFalse(); + assertThat(file).doesNotExist(); String absolutePath = file.getAbsolutePath().replace('\\', '/'); - assertThat(absolutePath.endsWith(fileName)).isTrue(); + assertThat(absolutePath).endsWith(fileName); } } diff --git a/spring-beans/src/test/java/org/springframework/beans/propertyeditors/PathEditorTests.java b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/PathEditorTests.java index f0c659bcbdb7..ed4058b4fe53 100644 --- a/spring-beans/src/test/java/org/springframework/beans/propertyeditors/PathEditorTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/PathEditorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-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.beans.PropertyEditor; import java.io.File; import java.nio.file.Path; +import java.nio.file.Paths; import org.junit.jupiter.api.Test; @@ -31,65 +32,65 @@ * @author Juergen Hoeller * @since 4.3.2 */ -public class PathEditorTests { +class PathEditorTests { @Test - public void testClasspathPathName() { + void testClasspathPathName() { PropertyEditor pathEditor = new PathEditor(); pathEditor.setAsText("classpath:" + ClassUtils.classPackageAsResourcePath(getClass()) + "/" + ClassUtils.getShortName(getClass()) + ".class"); Object value = pathEditor.getValue(); - assertThat(value instanceof Path).isTrue(); + assertThat(value).isInstanceOf(Path.class); Path path = (Path) value; - assertThat(path.toFile().exists()).isTrue(); + assertThat(path.toFile()).exists(); } @Test - public void testWithNonExistentResource() { - PropertyEditor propertyEditor = new PathEditor(); + void testWithNonExistentResource() { + PropertyEditor pathEditor = new PathEditor(); assertThatIllegalArgumentException().isThrownBy(() -> - propertyEditor.setAsText("classpath:/no_way_this_file_is_found.doc")); + pathEditor.setAsText("classpath:/no_way_this_file_is_found.doc")); } @Test - public void testWithNonExistentPath() { + void testWithNonExistentPath() { PropertyEditor pathEditor = new PathEditor(); pathEditor.setAsText("file:/no_way_this_file_is_found.doc"); Object value = pathEditor.getValue(); - assertThat(value instanceof Path).isTrue(); + assertThat(value).isInstanceOf(Path.class); Path path = (Path) value; - assertThat(!path.toFile().exists()).isTrue(); + assertThat(path.toFile()).doesNotExist(); } @Test - public void testAbsolutePath() { + void testAbsolutePath() { PropertyEditor pathEditor = new PathEditor(); pathEditor.setAsText("/no_way_this_file_is_found.doc"); Object value = pathEditor.getValue(); - assertThat(value instanceof Path).isTrue(); + assertThat(value).isInstanceOf(Path.class); Path path = (Path) value; - assertThat(!path.toFile().exists()).isTrue(); + assertThat(path.toFile()).doesNotExist(); } @Test - public void testWindowsAbsolutePath() { + void testWindowsAbsolutePath() { PropertyEditor pathEditor = new PathEditor(); pathEditor.setAsText("C:\\no_way_this_file_is_found.doc"); Object value = pathEditor.getValue(); - assertThat(value instanceof Path).isTrue(); + assertThat(value).isInstanceOf(Path.class); Path path = (Path) value; - assertThat(!path.toFile().exists()).isTrue(); + assertThat(path.toFile()).doesNotExist(); } @Test - public void testWindowsAbsoluteFilePath() { + void testWindowsAbsoluteFilePath() { PropertyEditor pathEditor = new PathEditor(); try { pathEditor.setAsText("file://C:\\no_way_this_file_is_found.doc"); Object value = pathEditor.getValue(); - assertThat(value instanceof Path).isTrue(); + assertThat(value).isInstanceOf(Path.class); Path path = (Path) value; - assertThat(!path.toFile().exists()).isTrue(); + assertThat(path.toFile()).doesNotExist(); } catch (IllegalArgumentException ex) { if (File.separatorChar == '\\') { // on Windows, otherwise silently ignore @@ -99,39 +100,49 @@ public void testWindowsAbsoluteFilePath() { } @Test - public void testUnqualifiedPathNameFound() { + void testCurrentDirectory() { + PropertyEditor pathEditor = new PathEditor(); + pathEditor.setAsText("file:."); + Object value = pathEditor.getValue(); + assertThat(value).isInstanceOf(Path.class); + Path path = (Path) value; + assertThat(path).isEqualTo(Paths.get(".")); + } + + @Test + void testUnqualifiedPathNameFound() { PropertyEditor pathEditor = new PathEditor(); String fileName = ClassUtils.classPackageAsResourcePath(getClass()) + "/" + ClassUtils.getShortName(getClass()) + ".class"; pathEditor.setAsText(fileName); Object value = pathEditor.getValue(); - assertThat(value instanceof Path).isTrue(); + assertThat(value).isInstanceOf(Path.class); Path path = (Path) value; File file = path.toFile(); - assertThat(file.exists()).isTrue(); + assertThat(file).exists(); String absolutePath = file.getAbsolutePath(); if (File.separatorChar == '\\') { absolutePath = absolutePath.replace('\\', '/'); } - assertThat(absolutePath.endsWith(fileName)).isTrue(); + assertThat(absolutePath).endsWith(fileName); } @Test - public void testUnqualifiedPathNameNotFound() { + void testUnqualifiedPathNameNotFound() { PropertyEditor pathEditor = new PathEditor(); String fileName = ClassUtils.classPackageAsResourcePath(getClass()) + "/" + ClassUtils.getShortName(getClass()) + ".clazz"; pathEditor.setAsText(fileName); Object value = pathEditor.getValue(); - assertThat(value instanceof Path).isTrue(); + assertThat(value).isInstanceOf(Path.class); Path path = (Path) value; File file = path.toFile(); - assertThat(file.exists()).isFalse(); + assertThat(file).doesNotExist(); String absolutePath = file.getAbsolutePath(); if (File.separatorChar == '\\') { absolutePath = absolutePath.replace('\\', '/'); } - assertThat(absolutePath.endsWith(fileName)).isTrue(); + assertThat(absolutePath).endsWith(fileName); } } diff --git a/spring-beans/src/test/resources/org/springframework/beans/factory/FactoryBeanTests-withAutowiring.xml b/spring-beans/src/test/resources/org/springframework/beans/factory/FactoryBeanTests-withAutowiring.xml index 90ce2158a93a..1d7d5cdedcf5 100644 --- a/spring-beans/src/test/resources/org/springframework/beans/factory/FactoryBeanTests-withAutowiring.xml +++ b/spring-beans/src/test/resources/org/springframework/beans/factory/FactoryBeanTests-withAutowiring.xml @@ -4,25 +4,27 @@ - + - - - + + + - + - - - - yourName - - - + + + + yourName + betaFactory + getGamma + + + diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/ITestBean.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/ITestBean.java index 43ce2dd40ed8..1fa63057745d 100644 --- a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/ITestBean.java +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/ITestBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-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. @@ -33,6 +33,10 @@ public interface ITestBean extends AgeHolder { void setName(String name); + default void applyName(Object name) { + setName(String.valueOf(name)); + } + ITestBean getSpouse(); void setSpouse(ITestBean spouse); diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/TestBean.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/TestBean.java index ed54d0d05f4b..ce870f57846e 100644 --- a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/TestBean.java +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/TestBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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. @@ -477,7 +477,7 @@ public boolean equals(Object other) { @Override public int hashCode() { - return this.age; + return TestBean.class.hashCode(); } @Override diff --git a/spring-context-indexer/src/main/java/org/springframework/context/index/processor/TypeHelper.java b/spring-context-indexer/src/main/java/org/springframework/context/index/processor/TypeHelper.java index 706225aae527..4098d172ea8d 100644 --- a/spring-context-indexer/src/main/java/org/springframework/context/index/processor/TypeHelper.java +++ b/spring-context-indexer/src/main/java/org/springframework/context/index/processor/TypeHelper.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2022 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-context-support/src/main/java/org/springframework/cache/caffeine/CaffeineCache.java b/spring-context-support/src/main/java/org/springframework/cache/caffeine/CaffeineCache.java index ef8c3b03e5bd..5192ba1677a5 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/caffeine/CaffeineCache.java +++ b/spring-context-support/src/main/java/org/springframework/cache/caffeine/CaffeineCache.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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. @@ -59,8 +59,8 @@ public CaffeineCache(String name, com.github.benmanes.caffeine.cache.Cache cache, boolean allowNullValues) { @@ -86,7 +86,7 @@ public final com.github.benmanes.caffeine.cache.Cache getNativeC @SuppressWarnings("unchecked") @Override @Nullable - public T get(Object key, final Callable valueLoader) { + public T get(Object key, Callable valueLoader) { return (T) fromStoreValue(this.cache.get(key, new LoadFunction(valueLoader))); } @@ -106,7 +106,7 @@ public void put(Object key, @Nullable Object value) { @Override @Nullable - public ValueWrapper putIfAbsent(Object key, @Nullable final Object value) { + public ValueWrapper putIfAbsent(Object key, @Nullable Object value) { PutIfAbsentFunction callable = new PutIfAbsentFunction(value); Object result = this.cache.get(key, callable); return (callable.called ? null : toValueWrapper(result)); @@ -140,7 +140,7 @@ private class PutIfAbsentFunction implements Function { @Nullable private final Object value; - private boolean called; + boolean called; public PutIfAbsentFunction(@Nullable Object value) { this.value = value; @@ -159,16 +159,17 @@ private class LoadFunction implements Function { private final Callable valueLoader; public LoadFunction(Callable valueLoader) { + Assert.notNull(valueLoader, "Callable must not be null"); this.valueLoader = valueLoader; } @Override - public Object apply(Object o) { + public Object apply(Object key) { try { return toStoreValue(this.valueLoader.call()); } catch (Exception ex) { - throw new ValueRetrievalException(o, this.valueLoader, ex); + throw new ValueRetrievalException(key, this.valueLoader, ex); } } } diff --git a/spring-context-support/src/main/java/org/springframework/cache/caffeine/CaffeineCacheManager.java b/spring-context-support/src/main/java/org/springframework/cache/caffeine/CaffeineCacheManager.java index d5b77cbc1bb9..e03d19c31412 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/caffeine/CaffeineCacheManager.java +++ b/spring-context-support/src/main/java/org/springframework/cache/caffeine/CaffeineCacheManager.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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. @@ -51,6 +51,7 @@ * @author Juergen Hoeller * @author Stephane Nicoll * @author Sam Brannen + * @author Brian Clozel * @since 4.3 * @see CaffeineCache */ @@ -109,7 +110,7 @@ public void setCacheNames(@Nullable Collection cacheNames) { * Set the Caffeine to use for building each individual * {@link CaffeineCache} instance. * @see #createNativeCaffeineCache - * @see com.github.benmanes.caffeine.cache.Caffeine#build() + * @see Caffeine#build() */ public void setCaffeine(Caffeine caffeine) { Assert.notNull(caffeine, "Caffeine must not be null"); @@ -120,7 +121,7 @@ public void setCaffeine(Caffeine caffeine) { * Set the {@link CaffeineSpec} to use for building each individual * {@link CaffeineCache} instance. * @see #createNativeCaffeineCache - * @see com.github.benmanes.caffeine.cache.Caffeine#from(CaffeineSpec) + * @see Caffeine#from(CaffeineSpec) */ public void setCaffeineSpec(CaffeineSpec caffeineSpec) { doSetCaffeine(Caffeine.from(caffeineSpec)); @@ -131,7 +132,7 @@ public void setCaffeineSpec(CaffeineSpec caffeineSpec) { * individual {@link CaffeineCache} instance. The given value needs to * comply with Caffeine's {@link CaffeineSpec} (see its javadoc). * @see #createNativeCaffeineCache - * @see com.github.benmanes.caffeine.cache.Caffeine#from(String) + * @see Caffeine#from(String) */ public void setCacheSpecification(String cacheSpecification) { doSetCaffeine(Caffeine.from(cacheSpecification)); @@ -148,7 +149,7 @@ private void doSetCaffeine(Caffeine cacheBuilder) { * Set the Caffeine CacheLoader to use for building each individual * {@link CaffeineCache} instance, turning it into a LoadingCache. * @see #createNativeCaffeineCache - * @see com.github.benmanes.caffeine.cache.Caffeine#build(CacheLoader) + * @see Caffeine#build(CacheLoader) * @see com.github.benmanes.caffeine.cache.LoadingCache */ public void setCacheLoader(CacheLoader cacheLoader) { @@ -188,8 +189,11 @@ public Collection getCacheNames() { @Override @Nullable public Cache getCache(String name) { - return this.cacheMap.computeIfAbsent(name, cacheName -> - this.dynamic ? createCaffeineCache(cacheName) : null); + Cache cache = this.cacheMap.get(name); + if (cache == null && this.dynamic) { + cache = this.cacheMap.computeIfAbsent(name, this::createCaffeineCache); + } + return cache; } @@ -232,7 +236,7 @@ protected Cache adaptCaffeineCache(String name, com.github.benmanes.caffeine.cac * Build a common {@link CaffeineCache} instance for the specified cache name, * using the common Caffeine configuration specified on this cache manager. *

    Delegates to {@link #adaptCaffeineCache} as the adaptation method to - * Spring's cache abstraction (allowing for centralized decoration etc), + * Spring's cache abstraction (allowing for centralized decoration etc.), * passing in a freshly built native Caffeine Cache instance. * @param name the name of the cache * @return the Spring CaffeineCache adapter (or a decorator thereof) diff --git a/spring-context-support/src/main/java/org/springframework/mail/SimpleMailMessage.java b/spring-context-support/src/main/java/org/springframework/mail/SimpleMailMessage.java index a13f188ad7a6..2941630b94ce 100644 --- a/spring-context-support/src/main/java/org/springframework/mail/SimpleMailMessage.java +++ b/spring-context-support/src/main/java/org/springframework/mail/SimpleMailMessage.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2022 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-context-support/src/main/java/org/springframework/mail/javamail/JavaMailSenderImpl.java b/spring-context-support/src/main/java/org/springframework/mail/javamail/JavaMailSenderImpl.java index 06d0e126082f..cd2ea46e58e3 100644 --- a/spring-context-support/src/main/java/org/springframework/mail/javamail/JavaMailSenderImpl.java +++ b/spring-context-support/src/main/java/org/springframework/mail/javamail/JavaMailSenderImpl.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2023 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. @@ -132,10 +132,10 @@ public void setJavaMailProperties(Properties javaMailProperties) { } /** - * Allow {code Map} access to the JavaMail properties of this sender, + * Allow {@code Map} access to the JavaMail properties of this sender, * with the option to add or override specific entries. *

    Useful for specifying entries directly, for example via - * {code javaMailProperties[mail.smtp.auth]}. + * {@code javaMailProperties[mail.smtp.auth]}. */ public Properties getJavaMailProperties() { return this.javaMailProperties; diff --git a/spring-context-support/src/test/java/org/springframework/cache/caffeine/CaffeineCacheManagerTests.java b/spring-context-support/src/test/java/org/springframework/cache/caffeine/CaffeineCacheManagerTests.java index f8c0de21f2d7..4c1589e97d15 100644 --- a/spring-context-support/src/test/java/org/springframework/cache/caffeine/CaffeineCacheManagerTests.java +++ b/spring-context-support/src/test/java/org/springframework/cache/caffeine/CaffeineCacheManagerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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. @@ -38,19 +38,17 @@ public class CaffeineCacheManagerTests { @Test public void testDynamicMode() { CacheManager cm = new CaffeineCacheManager(); + Cache cache1 = cm.getCache("c1"); - boolean condition2 = cache1 instanceof CaffeineCache; - assertThat(condition2).isTrue(); + assertThat(cache1).isInstanceOf(CaffeineCache.class); Cache cache1again = cm.getCache("c1"); assertThat(cache1).isSameAs(cache1again); Cache cache2 = cm.getCache("c2"); - boolean condition1 = cache2 instanceof CaffeineCache; - assertThat(condition1).isTrue(); + assertThat(cache2).isInstanceOf(CaffeineCache.class); Cache cache2again = cm.getCache("c2"); assertThat(cache2).isSameAs(cache2again); Cache cache3 = cm.getCache("c3"); - boolean condition = cache3 instanceof CaffeineCache; - assertThat(condition).isTrue(); + assertThat(cache3).isInstanceOf(CaffeineCache.class); Cache cache3again = cm.getCache("c3"); assertThat(cache3).isSameAs(cache3again); @@ -62,19 +60,23 @@ public void testDynamicMode() { assertThat(cache1.get("key3").get()).isNull(); cache1.evict("key3"); assertThat(cache1.get("key3")).isNull(); + assertThat(cache1.get("key3", () -> "value3")).isEqualTo("value3"); + assertThat(cache1.get("key3", () -> "value3")).isEqualTo("value3"); + cache1.evict("key3"); + assertThat(cache1.get("key3", () -> (String) null)).isNull(); + assertThat(cache1.get("key3", () -> (String) null)).isNull(); } @Test public void testStaticMode() { CaffeineCacheManager cm = new CaffeineCacheManager("c1", "c2"); + Cache cache1 = cm.getCache("c1"); - boolean condition3 = cache1 instanceof CaffeineCache; - assertThat(condition3).isTrue(); + assertThat(cache1).isInstanceOf(CaffeineCache.class); Cache cache1again = cm.getCache("c1"); assertThat(cache1).isSameAs(cache1again); Cache cache2 = cm.getCache("c2"); - boolean condition2 = cache2 instanceof CaffeineCache; - assertThat(condition2).isTrue(); + assertThat(cache2).isInstanceOf(CaffeineCache.class); Cache cache2again = cm.getCache("c2"); assertThat(cache2).isSameAs(cache2again); Cache cache3 = cm.getCache("c3"); @@ -91,13 +93,11 @@ public void testStaticMode() { cm.setAllowNullValues(false); Cache cache1x = cm.getCache("c1"); - boolean condition1 = cache1x instanceof CaffeineCache; - assertThat(condition1).isTrue(); - assertThat(cache1x != cache1).isTrue(); + assertThat(cache1x).isInstanceOf(CaffeineCache.class); + assertThat(cache1x).isNotSameAs(cache1); Cache cache2x = cm.getCache("c2"); - boolean condition = cache2x instanceof CaffeineCache; - assertThat(condition).isTrue(); - assertThat(cache2x != cache2).isTrue(); + assertThat(cache2x).isInstanceOf(CaffeineCache.class); + assertThat(cache2x).isNotSameAs(cache2); Cache cache3x = cm.getCache("c3"); assertThat(cache3x).isNull(); @@ -190,7 +190,7 @@ public void cacheLoaderUseLoadingCache() { assertThat(value.get()).isEqualTo("pong"); assertThatIllegalArgumentException().isThrownBy(() -> assertThat(cache1.get("foo")).isNull()) - .withMessageContaining("I only know ping"); + .withMessageContaining("I only know ping"); } @Test diff --git a/spring-context-support/src/test/java/org/springframework/scheduling/quartz/QuartzSupportTests.java b/spring-context-support/src/test/java/org/springframework/scheduling/quartz/QuartzSupportTests.java index 9d461d2e400f..597633475112 100644 --- a/spring-context-support/src/test/java/org/springframework/scheduling/quartz/QuartzSupportTests.java +++ b/spring-context-support/src/test/java/org/springframework/scheduling/quartz/QuartzSupportTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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. @@ -114,7 +114,7 @@ void schedulerWithTaskExecutor() throws Exception { trigger.setName("myTrigger"); trigger.setJobDetail(jobDetail); trigger.setStartDelay(1); - trigger.setRepeatInterval(500); + trigger.setRepeatInterval(100); trigger.setRepeatCount(1); trigger.afterPropertiesSet(); @@ -126,14 +126,14 @@ void schedulerWithTaskExecutor() throws Exception { bean.start(); Thread.sleep(500); - assertThat(DummyJob.count > 0).as("DummyJob should have been executed at least once.").isTrue(); + assertThat(DummyJob.count).as("DummyJob should have been executed at least once.").isGreaterThan(0); assertThat(taskExecutor.count).isEqualTo(DummyJob.count); bean.destroy(); } @Test - @SuppressWarnings({ "unchecked", "rawtypes" }) + @SuppressWarnings({"unchecked", "rawtypes"}) void jobDetailWithRunnableInsteadOfJob() { JobDetailImpl jobDetail = new JobDetailImpl(); assertThatIllegalArgumentException().isThrownBy(() -> @@ -156,7 +156,7 @@ void schedulerWithQuartzJobBean() throws Exception { trigger.setName("myTrigger"); trigger.setJobDetail(jobDetail); trigger.setStartDelay(1); - trigger.setRepeatInterval(500); + trigger.setRepeatInterval(100); trigger.setRepeatCount(1); trigger.afterPropertiesSet(); @@ -168,7 +168,7 @@ void schedulerWithQuartzJobBean() throws Exception { Thread.sleep(500); assertThat(DummyJobBean.param).isEqualTo(10); - assertThat(DummyJobBean.count > 0).isTrue(); + assertThat(DummyJobBean.count).isGreaterThan(0); bean.destroy(); } @@ -190,7 +190,7 @@ void schedulerWithSpringBeanJobFactory() throws Exception { trigger.setName("myTrigger"); trigger.setJobDetail(jobDetail); trigger.setStartDelay(1); - trigger.setRepeatInterval(500); + trigger.setRepeatInterval(100); trigger.setRepeatCount(1); trigger.afterPropertiesSet(); @@ -203,7 +203,7 @@ void schedulerWithSpringBeanJobFactory() throws Exception { Thread.sleep(500); assertThat(DummyJob.param).isEqualTo(10); - assertThat(DummyJob.count > 0).as("DummyJob should have been executed at least once.").isTrue(); + assertThat(DummyJob.count).as("DummyJob should have been executed at least once.").isGreaterThan(0); bean.destroy(); } @@ -225,7 +225,7 @@ void schedulerWithSpringBeanJobFactoryAndParamMismatchNotIgnored() throws Except trigger.setName("myTrigger"); trigger.setJobDetail(jobDetail); trigger.setStartDelay(1); - trigger.setRepeatInterval(500); + trigger.setRepeatInterval(100); trigger.setRepeatCount(1); trigger.afterPropertiesSet(); @@ -239,7 +239,7 @@ void schedulerWithSpringBeanJobFactoryAndParamMismatchNotIgnored() throws Except Thread.sleep(500); assertThat(DummyJob.param).isEqualTo(0); - assertThat(DummyJob.count == 0).isTrue(); + assertThat(DummyJob.count).isEqualTo(0); bean.destroy(); } @@ -260,7 +260,7 @@ void schedulerWithSpringBeanJobFactoryAndQuartzJobBean() throws Exception { trigger.setName("myTrigger"); trigger.setJobDetail(jobDetail); trigger.setStartDelay(1); - trigger.setRepeatInterval(500); + trigger.setRepeatInterval(100); trigger.setRepeatCount(1); trigger.afterPropertiesSet(); @@ -273,7 +273,7 @@ void schedulerWithSpringBeanJobFactoryAndQuartzJobBean() throws Exception { Thread.sleep(500); assertThat(DummyJobBean.param).isEqualTo(10); - assertThat(DummyJobBean.count > 0).isTrue(); + assertThat(DummyJobBean.count).isGreaterThan(0); bean.destroy(); } @@ -292,7 +292,7 @@ void schedulerWithSpringBeanJobFactoryAndJobSchedulingData() throws Exception { Thread.sleep(500); assertThat(DummyJob.param).isEqualTo(10); - assertThat(DummyJob.count > 0).as("DummyJob should have been executed at least once.").isTrue(); + assertThat(DummyJob.count).as("DummyJob should have been executed at least once.").isGreaterThan(0); bean.destroy(); } diff --git a/spring-context/src/main/java/org/springframework/cache/Cache.java b/spring-context/src/main/java/org/springframework/cache/Cache.java index 8a3b904f4904..648ff88e3957 100644 --- a/spring-context/src/main/java/org/springframework/cache/Cache.java +++ b/spring-context/src/main/java/org/springframework/cache/Cache.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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,14 +23,20 @@ /** * Interface that defines common cache operations. * - * Note: Due to the generic use of caching, it is recommended that - * implementations allow storage of {@code null} values (for example to - * cache methods that return {@code null}). + *

    Serves as an SPI for Spring's annotation-based caching model + * ({@link org.springframework.cache.annotation.Cacheable} and co) + * as well as an API for direct usage in applications. + * + *

    Note: Due to the generic use of caching, it is recommended + * that implementations allow storage of {@code null} values + * (for example to cache methods that return {@code null}). * * @author Costin Leau * @author Juergen Hoeller * @author Stephane Nicoll * @since 3.1 + * @see CacheManager + * @see org.springframework.cache.annotation.Cacheable */ public interface Cache { diff --git a/spring-context/src/main/java/org/springframework/cache/annotation/CacheConfig.java b/spring-context/src/main/java/org/springframework/cache/annotation/CacheConfig.java index 234f353b142d..78da3a22e5ab 100644 --- a/spring-context/src/main/java/org/springframework/cache/annotation/CacheConfig.java +++ b/spring-context/src/main/java/org/springframework/cache/annotation/CacheConfig.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2023 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. @@ -32,6 +32,7 @@ * @author Stephane Nicoll * @author Sam Brannen * @since 4.1 + * @see Cacheable */ @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @@ -42,8 +43,10 @@ * Names of the default caches to consider for caching operations defined * in the annotated class. *

    If none is set at the operation level, these are used instead of the default. - *

    May be used to determine the target cache (or caches), matching the - * qualifier value or the bean names of a specific bean definition. + *

    Names may be used to determine the target cache(s), to be resolved via the + * configured {@link #cacheResolver()} which typically delegates to + * {@link org.springframework.cache.CacheManager#getCache}. + * For further details see {@link Cacheable#cacheNames()}. */ String[] cacheNames() default {}; diff --git a/spring-context/src/main/java/org/springframework/cache/annotation/Cacheable.java b/spring-context/src/main/java/org/springframework/cache/annotation/Cacheable.java index 6d626e783230..d91249da5efc 100644 --- a/spring-context/src/main/java/org/springframework/cache/annotation/Cacheable.java +++ b/spring-context/src/main/java/org/springframework/cache/annotation/Cacheable.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -68,8 +68,12 @@ /** * Names of the caches in which method invocation results are stored. - *

    Names may be used to determine the target cache (or caches), matching - * the qualifier value or bean name of a specific bean definition. + *

    Names may be used to determine the target cache(s), to be resolved via the + * configured {@link #cacheResolver()} which typically delegates to + * {@link org.springframework.cache.CacheManager#getCache}. + *

    This will usually be a single cache name. If multiple names are specified, + * they will be consulted for a cache hit in the order of definition, and they + * will all receive a put/evict request for the same newly cached value. * @since 4.2 * @see #value * @see CacheConfig#cacheNames @@ -175,9 +179,9 @@ *

  7. Only one cache may be specified
  8. *
  9. No other cache-related operation can be combined
  10. * - * This is effectively a hint and the actual cache provider that you are - * using may not support it in a synchronized fashion. Check your provider - * documentation for more details on the actual semantics. + * This is effectively a hint and the chosen cache provider might not actually + * support it in a synchronized fashion. Check your provider documentation for + * more details on the actual semantics. * @since 4.3 * @see org.springframework.cache.Cache#get(Object, Callable) */ diff --git a/spring-context/src/main/java/org/springframework/cache/concurrent/ConcurrentMapCacheManager.java b/spring-context/src/main/java/org/springframework/cache/concurrent/ConcurrentMapCacheManager.java index aea913288d8b..d40746639f4a 100644 --- a/spring-context/src/main/java/org/springframework/cache/concurrent/ConcurrentMapCacheManager.java +++ b/spring-context/src/main/java/org/springframework/cache/concurrent/ConcurrentMapCacheManager.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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. @@ -166,13 +166,7 @@ public Collection getCacheNames() { public Cache getCache(String name) { Cache cache = this.cacheMap.get(name); if (cache == null && this.dynamic) { - synchronized (this.cacheMap) { - cache = this.cacheMap.get(name); - if (cache == null) { - cache = createConcurrentMapCache(name); - this.cacheMap.put(name, cache); - } - } + cache = this.cacheMap.computeIfAbsent(name, this::createConcurrentMapCache); } return cache; } diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/AbstractCacheInvoker.java b/spring-context/src/main/java/org/springframework/cache/interceptor/AbstractCacheInvoker.java index 4710f8e1c30d..d5c71acd8a06 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/AbstractCacheInvoker.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/AbstractCacheInvoker.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 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. @@ -82,12 +82,12 @@ protected Cache.ValueWrapper doGet(Cache cache, Object key) { * Execute {@link Cache#put(Object, Object)} on the specified {@link Cache} * and invoke the error handler if an exception occurs. */ - protected void doPut(Cache cache, Object key, @Nullable Object result) { + protected void doPut(Cache cache, Object key, @Nullable Object value) { try { - cache.put(key, result); + cache.put(key, value); } catch (RuntimeException ex) { - getErrorHandler().handleCachePutError(ex, cache, key, result); + getErrorHandler().handleCachePutError(ex, cache, key, value); } } diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheAspectSupport.java b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheAspectSupport.java index c528a83206bd..ed444aad3d96 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheAspectSupport.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheAspectSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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. @@ -214,7 +214,7 @@ public void afterPropertiesSet() { @Override public void afterSingletonsInstantiated() { if (getCacheResolver() == null) { - // Lazily initialize cache resolver via default cache manager... + // Lazily initialize cache resolver via default cache manager Assert.state(this.beanFactory != null, "CacheResolver or BeanFactory must be set on cache aspect"); try { setCacheManager(this.beanFactory.getBean(CacheManager.class)); @@ -307,22 +307,22 @@ else if (StringUtils.hasText(operation.getCacheManager())) { } /** - * Return a bean with the specified name and type. Used to resolve services that - * are referenced by name in a {@link CacheOperation}. - * @param beanName the name of the bean, as defined by the operation - * @param expectedType type for the bean - * @return the bean matching that name + * Retrieve a bean with the specified name and type. + * Used to resolve services that are referenced by name in a {@link CacheOperation}. + * @param name the name of the bean, as defined by the cache operation + * @param serviceType the type expected by the operation's service reference + * @return the bean matching the expected type, qualified by the given name * @throws org.springframework.beans.factory.NoSuchBeanDefinitionException if such bean does not exist * @see CacheOperation#getKeyGenerator() * @see CacheOperation#getCacheManager() * @see CacheOperation#getCacheResolver() */ - protected T getBean(String beanName, Class expectedType) { + protected T getBean(String name, Class serviceType) { if (this.beanFactory == null) { throw new IllegalStateException( - "BeanFactory must be set on cache aspect for " + expectedType.getSimpleName() + " retrieval"); + "BeanFactory must be set on cache aspect for " + serviceType.getSimpleName() + " retrieval"); } - return BeanFactoryAnnotationUtils.qualifiedBeanOfType(this.beanFactory, expectedType, beanName); + return BeanFactoryAnnotationUtils.qualifiedBeanOfType(this.beanFactory, serviceType, name); } /** @@ -388,21 +388,20 @@ private Object execute(final CacheOperationInvoker invoker, Method method, Cache } } else { - // No caching required, only call the underlying method + // No caching required, just call the underlying method return invokeOperation(invoker); } } - // Process any early evictions processCacheEvicts(contexts.get(CacheEvictOperation.class), true, CacheOperationExpressionEvaluator.NO_RESULT); - // Check if we have a cached item matching the conditions + // Check if we have a cached value matching the conditions Cache.ValueWrapper cacheHit = findCachedItem(contexts.get(CacheableOperation.class)); - // Collect puts from any @Cacheable miss, if no cached item is found - List cachePutRequests = new ArrayList<>(); + // Collect puts from any @Cacheable miss, if no cached value is found + List cachePutRequests = new ArrayList<>(1); if (cacheHit == null) { collectPutRequests(contexts.get(CacheableOperation.class), CacheOperationExpressionEvaluator.NO_RESULT, cachePutRequests); @@ -469,7 +468,7 @@ private Object unwrapReturnValue(@Nullable Object returnValue) { private boolean hasCachePut(CacheOperationContexts contexts) { // Evaluate the conditions *without* the result object because we don't have it yet... Collection cachePutContexts = contexts.get(CachePutOperation.class); - Collection excluded = new ArrayList<>(); + Collection excluded = new ArrayList<>(1); for (CacheOperationContext context : cachePutContexts) { try { if (!context.isConditionPassing(CacheOperationExpressionEvaluator.RESULT_UNAVAILABLE)) { @@ -522,9 +521,9 @@ private void logInvalidating(CacheOperationContext context, CacheEvictOperation } /** - * Find a cached item only for {@link CacheableOperation} that passes the condition. + * Find a cached value only for {@link CacheableOperation} that passes the condition. * @param contexts the cacheable operations - * @return a {@link Cache.ValueWrapper} holding the cached item, + * @return a {@link Cache.ValueWrapper} holding the cached value, * or {@code null} if none is found */ @Nullable @@ -548,10 +547,10 @@ private Cache.ValueWrapper findCachedItem(Collection cont } /** - * Collect the {@link CachePutRequest} for all {@link CacheOperation} using - * the specified result item. + * Collect a {@link CachePutRequest} for every {@link CacheOperation} + * using the specified result value. * @param contexts the contexts to handle - * @param result the result item (never {@code null}) + * @param result the result value * @param putRequests the collection to update */ private void collectPutRequests(Collection contexts, @@ -641,21 +640,22 @@ private boolean determineSyncFlag(Method method) { if (syncEnabled) { if (this.contexts.size() > 1) { throw new IllegalStateException( - "@Cacheable(sync=true) cannot be combined with other cache operations on '" + method + "'"); + "A sync=true operation cannot be combined with other cache operations on '" + method + "'"); } if (cacheOperationContexts.size() > 1) { throw new IllegalStateException( - "Only one @Cacheable(sync=true) entry is allowed on '" + method + "'"); + "Only one sync=true operation is allowed on '" + method + "'"); } CacheOperationContext cacheOperationContext = cacheOperationContexts.iterator().next(); - CacheableOperation operation = (CacheableOperation) cacheOperationContext.getOperation(); + CacheOperation operation = cacheOperationContext.getOperation(); if (cacheOperationContext.getCaches().size() > 1) { throw new IllegalStateException( - "@Cacheable(sync=true) only allows a single cache on '" + operation + "'"); + "A sync=true operation is restricted to a single cache on '" + operation + "'"); } - if (StringUtils.hasText(operation.getUnless())) { + if (operation instanceof CacheableOperation && + StringUtils.hasText(((CacheableOperation) operation).getUnless())) { throw new IllegalStateException( - "@Cacheable(sync=true) does not support unless attribute on '" + operation + "'"); + "A sync=true operation does not support the unless attribute on '" + operation + "'"); } return true; } @@ -722,7 +722,7 @@ public CacheOperationContext(CacheOperationMetadata metadata, Object[] args, Obj this.args = extractArgs(metadata.method, args); this.target = target; this.caches = CacheAspectSupport.this.getCaches(this, metadata.cacheResolver); - this.cacheNames = createCacheNames(this.caches); + this.cacheNames = prepareCacheNames(this.caches); } @Override @@ -810,8 +810,8 @@ protected Collection getCacheNames() { return this.cacheNames; } - private Collection createCacheNames(Collection caches) { - Collection names = new ArrayList<>(); + private Collection prepareCacheNames(Collection caches) { + Collection names = new ArrayList<>(caches.size()); for (Cache cache : caches) { names.add(cache.getName()); } @@ -885,13 +885,13 @@ public int compareTo(CacheOperationCacheKey other) { } } + /** * Internal holder class for recording that a cache method was invoked. */ private static class InvocationAwareResult { boolean invoked; - } } diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationInvoker.java b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationInvoker.java index cfaab08137bd..e37736480e51 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationInvoker.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationInvoker.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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,7 +22,7 @@ * Abstract the invocation of a cache operation. * *

    Does not provide a way to transmit checked exceptions but - * provide a special exception that should be used to wrap any + * provides a special exception that should be used to wrap any * exception that was thrown by the underlying invocation. * Callers are expected to handle this issue type specifically. * diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/NameMatchCacheOperationSource.java b/spring-context/src/main/java/org/springframework/cache/interceptor/NameMatchCacheOperationSource.java index ac23ae7e9fd2..90b08b2fabec 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/NameMatchCacheOperationSource.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/NameMatchCacheOperationSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2023 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. @@ -47,7 +47,7 @@ public class NameMatchCacheOperationSource implements CacheOperationSource, Seri /** Keys are method names; values are TransactionAttributes. */ - private Map> nameMap = new LinkedHashMap<>(); + private final Map> nameMap = new LinkedHashMap<>(); /** @@ -117,8 +117,8 @@ public boolean equals(@Nullable Object other) { if (!(other instanceof NameMatchCacheOperationSource)) { return false; } - NameMatchCacheOperationSource otherTas = (NameMatchCacheOperationSource) other; - return ObjectUtils.nullSafeEquals(this.nameMap, otherTas.nameMap); + NameMatchCacheOperationSource otherCos = (NameMatchCacheOperationSource) other; + return ObjectUtils.nullSafeEquals(this.nameMap, otherCos.nameMap); } @Override @@ -130,4 +130,5 @@ public int hashCode() { public String toString() { return getClass().getName() + ": " + this.nameMap; } + } diff --git a/spring-context/src/main/java/org/springframework/cache/support/SimpleCacheManager.java b/spring-context/src/main/java/org/springframework/cache/support/SimpleCacheManager.java index c130f8f2698c..08500e04606a 100644 --- a/spring-context/src/main/java/org/springframework/cache/support/SimpleCacheManager.java +++ b/spring-context/src/main/java/org/springframework/cache/support/SimpleCacheManager.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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,14 +24,16 @@ /** * Simple cache manager working against a given collection of caches. * Useful for testing or simple caching declarations. - *

    - * When using this implementation directly, i.e. not via a regular + * + *

    When using this implementation directly, i.e. not via a regular * bean registration, {@link #initializeCaches()} should be invoked * to initialize its internal state once the * {@linkplain #setCaches(Collection) caches have been provided}. * * @author Costin Leau * @since 3.1 + * @see NoOpCache + * @see org.springframework.cache.concurrent.ConcurrentMapCache */ public class SimpleCacheManager extends AbstractCacheManager { diff --git a/spring-context/src/main/java/org/springframework/context/SmartLifecycle.java b/spring-context/src/main/java/org/springframework/context/SmartLifecycle.java index 06f98a4048ca..5ad500d1b211 100644 --- a/spring-context/src/main/java/org/springframework/context/SmartLifecycle.java +++ b/spring-context/src/main/java/org/springframework/context/SmartLifecycle.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -121,7 +121,7 @@ default void stop(Runnable callback) { /** * Return the phase that this lifecycle object is supposed to run in. *

    The default implementation returns {@link #DEFAULT_PHASE} in order to - * let {@code stop()} callbacks execute after regular {@code Lifecycle} + * let {@code stop()} callbacks execute before regular {@code Lifecycle} * implementations. * @see #isAutoStartup() * @see #start() diff --git a/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigApplicationContext.java b/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigApplicationContext.java index 86ea5feb7335..6ca78078cb18 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigApplicationContext.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigApplicationContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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. @@ -65,7 +65,7 @@ public class AnnotationConfigApplicationContext extends GenericApplicationContex * through {@link #register} calls and then manually {@linkplain #refresh refreshed}. */ public AnnotationConfigApplicationContext() { - StartupStep createAnnotatedBeanDefReader = this.getApplicationStartup().start("spring.context.annotated-bean-reader.create"); + StartupStep createAnnotatedBeanDefReader = getApplicationStartup().start("spring.context.annotated-bean-reader.create"); this.reader = new AnnotatedBeanDefinitionReader(this); createAnnotatedBeanDefReader.end(); this.scanner = new ClassPathBeanDefinitionScanner(this); @@ -163,7 +163,7 @@ public void setScopeMetadataResolver(ScopeMetadataResolver scopeMetadataResolver @Override public void register(Class... componentClasses) { Assert.notEmpty(componentClasses, "At least one component class must be specified"); - StartupStep registerComponentClass = this.getApplicationStartup().start("spring.context.component-classes.register") + StartupStep registerComponentClass = getApplicationStartup().start("spring.context.component-classes.register") .tag("classes", () -> Arrays.toString(componentClasses)); this.reader.register(componentClasses); registerComponentClass.end(); @@ -180,7 +180,7 @@ public void register(Class... componentClasses) { @Override public void scan(String... basePackages) { Assert.notEmpty(basePackages, "At least one base package must be specified"); - StartupStep scanPackages = this.getApplicationStartup().start("spring.context.base-packages.scan") + StartupStep scanPackages = getApplicationStartup().start("spring.context.base-packages.scan") .tag("packages", () -> Arrays.toString(basePackages)); this.scanner.scan(basePackages); scanPackages.end(); diff --git a/spring-context/src/main/java/org/springframework/context/annotation/BeanMethod.java b/spring-context/src/main/java/org/springframework/context/annotation/BeanMethod.java index 6d3459fb08c7..abe73dacc60f 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/BeanMethod.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/BeanMethod.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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. @@ -39,6 +39,7 @@ final class BeanMethod extends ConfigurationMethod { super(metadata, configurationClass); } + @Override public void validate(ProblemReporter problemReporter) { if (getMetadata().isStatic()) { @@ -55,9 +56,9 @@ public void validate(ProblemReporter problemReporter) { } @Override - public boolean equals(@Nullable Object obj) { - return ((this == obj) || ((obj instanceof BeanMethod) && - this.metadata.equals(((BeanMethod) obj).metadata))); + public boolean equals(@Nullable Object other) { + return (this == other || (other instanceof BeanMethod && + this.metadata.equals(((BeanMethod) other).metadata))); } @Override @@ -70,6 +71,7 @@ public String toString() { return "BeanMethod: " + this.metadata; } + private class NonOverridableMethodError extends Problem { NonOverridableMethodError() { diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ClassPathBeanDefinitionScanner.java b/spring-context/src/main/java/org/springframework/context/annotation/ClassPathBeanDefinitionScanner.java index 65cbb9bdb9f2..7f41cd5962ce 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ClassPathBeanDefinitionScanner.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ClassPathBeanDefinitionScanner.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 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. @@ -329,8 +329,8 @@ protected void registerBeanDefinition(BeanDefinitionHolder definitionHolder, Bea * @return {@code true} if the bean can be registered as-is; * {@code false} if it should be skipped because there is an * existing, compatible bean definition for the specified name - * @throws ConflictingBeanDefinitionException if an existing, incompatible - * bean definition has been found for the specified name + * @throws IllegalStateException if an existing, incompatible bean definition + * has been found for the specified name */ protected boolean checkCandidate(String beanName, BeanDefinition beanDefinition) throws IllegalStateException { if (!this.registry.containsBeanDefinition(beanName)) { @@ -354,16 +354,16 @@ protected boolean checkCandidate(String beanName, BeanDefinition beanDefinition) * the given existing bean definition. *

    The default implementation considers them as compatible when the existing * bean definition comes from the same source or from a non-scanning source. - * @param newDefinition the new bean definition, originated from scanning - * @param existingDefinition the existing bean definition, potentially an + * @param newDef the new bean definition, originated from scanning + * @param existingDef the existing bean definition, potentially an * explicitly defined one or a previously generated one from scanning * @return whether the definitions are considered as compatible, with the * new definition to be skipped in favor of the existing definition */ - protected boolean isCompatible(BeanDefinition newDefinition, BeanDefinition existingDefinition) { - return (!(existingDefinition instanceof ScannedGenericBeanDefinition) || // explicitly registered overriding bean - (newDefinition.getSource() != null && newDefinition.getSource().equals(existingDefinition.getSource())) || // scanned same file twice - newDefinition.equals(existingDefinition)); // scanned equivalent class twice + protected boolean isCompatible(BeanDefinition newDef, BeanDefinition existingDef) { + return (!(existingDef instanceof ScannedGenericBeanDefinition) || // explicitly registered overriding bean + (newDef.getSource() != null && newDef.getSource().equals(existingDef.getSource())) || // scanned same file twice + newDef.equals(existingDef)); // scanned equivalent class twice } diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ClassPathScanningCandidateComponentProvider.java b/spring-context/src/main/java/org/springframework/context/annotation/ClassPathScanningCandidateComponentProvider.java index 92d502a13f0b..4c67efc9c586 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ClassPathScanningCandidateComponentProvider.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ClassPathScanningCandidateComponentProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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. @@ -62,11 +62,13 @@ import org.springframework.util.ClassUtils; /** - * A component provider that provides candidate components from a base package. Can - * use {@link CandidateComponentsIndex the index} if it is available of scans the - * classpath otherwise. Candidate components are identified by applying exclude and - * include filters. {@link AnnotationTypeFilter}, {@link AssignableTypeFilter} include - * filters on an annotation/superclass that are annotated with {@link Indexed} are + * A component provider that scans for candidate components starting from a + * specified base package. Can use the {@linkplain CandidateComponentsIndex component + * index}, if it is available, and scans the classpath otherwise. + * + *

    Candidate components are identified by applying exclude and include filters. + * {@link AnnotationTypeFilter} and {@link AssignableTypeFilter} include filters + * for an annotation/target-type that is annotated with {@link Indexed} are * supported: if any other include filter is specified, the index is ignored and * classpath scanning is used instead. * @@ -304,7 +306,7 @@ public final MetadataReaderFactory getMetadataReaderFactory() { /** - * Scan the class path for candidate components. + * Scan the component index or class path for candidate components. * @param basePackage the package to check for annotated classes * @return a corresponding Set of autodetected bean definitions */ @@ -318,7 +320,7 @@ public Set findCandidateComponents(String basePackage) { } /** - * Determine if the index can be used by this instance. + * Determine if the component index can be used by this instance. * @return {@code true} if the index is available and the configuration of this * instance is supported by it, {@code false} otherwise * @since 5.0 @@ -454,8 +456,7 @@ private Set scanCandidateComponents(String basePackage) { } } catch (Throwable ex) { - throw new BeanDefinitionStoreException( - "Failed to read candidate component class: " + resource, ex); + throw new BeanDefinitionStoreException("Failed to read candidate component class: " + resource, ex); } } } diff --git a/spring-context/src/main/java/org/springframework/context/annotation/CommonAnnotationBeanPostProcessor.java b/spring-context/src/main/java/org/springframework/context/annotation/CommonAnnotationBeanPostProcessor.java index f632e8b49ee0..d22afa79314f 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/CommonAnnotationBeanPostProcessor.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/CommonAnnotationBeanPostProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-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. @@ -402,8 +402,8 @@ else if (field.isAnnotationPresent(Resource.class)) { if (!BridgeMethodResolver.isVisibilityBridgeMethodPair(method, bridgedMethod)) { return; } - if (method.equals(ClassUtils.getMostSpecificMethod(method, clazz))) { - if (webServiceRefClass != null && bridgedMethod.isAnnotationPresent(webServiceRefClass)) { + if (webServiceRefClass != null && bridgedMethod.isAnnotationPresent(webServiceRefClass)) { + if (method.equals(ClassUtils.getMostSpecificMethod(method, clazz))) { if (Modifier.isStatic(method.getModifiers())) { throw new IllegalStateException("@WebServiceRef annotation is not supported on static methods"); } @@ -413,7 +413,9 @@ else if (field.isAnnotationPresent(Resource.class)) { PropertyDescriptor pd = BeanUtils.findPropertyForMethod(bridgedMethod, clazz); currElements.add(new WebServiceRefElement(method, bridgedMethod, pd)); } - else if (ejbClass != null && bridgedMethod.isAnnotationPresent(ejbClass)) { + } + else if (ejbClass != null && bridgedMethod.isAnnotationPresent(ejbClass)) { + if (method.equals(ClassUtils.getMostSpecificMethod(method, clazz))) { if (Modifier.isStatic(method.getModifiers())) { throw new IllegalStateException("@EJB annotation is not supported on static methods"); } @@ -423,7 +425,9 @@ else if (ejbClass != null && bridgedMethod.isAnnotationPresent(ejbClass)) { PropertyDescriptor pd = BeanUtils.findPropertyForMethod(bridgedMethod, clazz); currElements.add(new EjbRefElement(method, bridgedMethod, pd)); } - else if (bridgedMethod.isAnnotationPresent(Resource.class)) { + } + else if (bridgedMethod.isAnnotationPresent(Resource.class)) { + if (method.equals(ClassUtils.getMostSpecificMethod(method, clazz))) { if (Modifier.isStatic(method.getModifiers())) { throw new IllegalStateException("@Resource annotation is not supported on static methods"); } diff --git a/spring-context/src/main/java/org/springframework/context/annotation/Configuration.java b/spring-context/src/main/java/org/springframework/context/annotation/Configuration.java index 39856654f907..df2a04b394b7 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/Configuration.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/Configuration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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. @@ -104,8 +104,8 @@ * * } * - *

    {@code @Configuration} classes may not only be bootstrapped using - * component scanning, but may also themselves configure component scanning using + *

    {@code @Configuration} classes may not only be bootstrapped using component + * scanning, but may also themselves configure component scanning using * the {@link ComponentScan @ComponentScan} annotation: * *

    diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClass.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClass.java
    index b6155f6d7bfa..494197a37b66 100644
    --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClass.java
    +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClass.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright 2002-2021 the original author or authors.
    + * Copyright 2002-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.
    @@ -72,7 +72,6 @@ final class ConfigurationClass {
     	 * Create a new {@link ConfigurationClass} with the given name.
     	 * @param metadataReader reader used to parse the underlying {@link Class}
     	 * @param beanName must not be {@code null}
    -	 * @see ConfigurationClass#ConfigurationClass(Class, ConfigurationClass)
     	 */
     	ConfigurationClass(MetadataReader metadataReader, String beanName) {
     		Assert.notNull(beanName, "Bean name must not be null");
    @@ -86,10 +85,10 @@ final class ConfigurationClass {
     	 * using the {@link Import} annotation or automatically processed as a nested
     	 * configuration class (if importedBy is not {@code null}).
     	 * @param metadataReader reader used to parse the underlying {@link Class}
    -	 * @param importedBy the configuration class importing this one or {@code null}
    +	 * @param importedBy the configuration class importing this one
     	 * @since 3.1.1
     	 */
    -	ConfigurationClass(MetadataReader metadataReader, @Nullable ConfigurationClass importedBy) {
    +	ConfigurationClass(MetadataReader metadataReader, ConfigurationClass importedBy) {
     		this.metadata = metadataReader.getAnnotationMetadata();
     		this.resource = metadataReader.getResource();
     		this.importedBy.add(importedBy);
    @@ -99,7 +98,6 @@ final class ConfigurationClass {
     	 * Create a new {@link ConfigurationClass} with the given name.
     	 * @param clazz the underlying {@link Class} to represent
     	 * @param beanName name of the {@code @Configuration} class bean
    -	 * @see ConfigurationClass#ConfigurationClass(Class, ConfigurationClass)
     	 */
     	ConfigurationClass(Class clazz, String beanName) {
     		Assert.notNull(beanName, "Bean name must not be null");
    @@ -113,10 +111,10 @@ final class ConfigurationClass {
     	 * using the {@link Import} annotation or automatically processed as a nested
     	 * configuration class (if imported is {@code true}).
     	 * @param clazz the underlying {@link Class} to represent
    -	 * @param importedBy the configuration class importing this one (or {@code null})
    +	 * @param importedBy the configuration class importing this one
     	 * @since 3.1.1
     	 */
    -	ConfigurationClass(Class clazz, @Nullable ConfigurationClass importedBy) {
    +	ConfigurationClass(Class clazz, ConfigurationClass importedBy) {
     		this.metadata = AnnotationMetadata.introspect(clazz);
     		this.resource = new DescriptiveResource(clazz.getName());
     		this.importedBy.add(importedBy);
    @@ -126,7 +124,6 @@ final class ConfigurationClass {
     	 * Create a new {@link ConfigurationClass} with the given name.
     	 * @param metadata the metadata for the underlying class to represent
     	 * @param beanName name of the {@code @Configuration} class bean
    -	 * @see ConfigurationClass#ConfigurationClass(Class, ConfigurationClass)
     	 */
     	ConfigurationClass(AnnotationMetadata metadata, String beanName) {
     		Assert.notNull(beanName, "Bean name must not be null");
    @@ -148,12 +145,12 @@ String getSimpleName() {
     		return ClassUtils.getShortName(getMetadata().getClassName());
     	}
     
    -	void setBeanName(String beanName) {
    +	void setBeanName(@Nullable String beanName) {
     		this.beanName = beanName;
     	}
     
     	@Nullable
    -	public String getBeanName() {
    +	String getBeanName() {
     		return this.beanName;
     	}
     
    @@ -163,7 +160,7 @@ public String getBeanName() {
     	 * @since 3.1.1
     	 * @see #getImportedBy()
     	 */
    -	public boolean isImported() {
    +	boolean isImported() {
     		return !this.importedBy.isEmpty();
     	}
     
    @@ -197,6 +194,10 @@ void addImportedResource(String importedResource, Class> getImportedResources() {
    +		return this.importedResources;
    +	}
    +
     	void addImportBeanDefinitionRegistrar(ImportBeanDefinitionRegistrar registrar, AnnotationMetadata importingClassMetadata) {
     		this.importBeanDefinitionRegistrars.put(registrar, importingClassMetadata);
     	}
    @@ -205,10 +206,6 @@ Map getImportBeanDefinitionRe
     		return this.importBeanDefinitionRegistrars;
     	}
     
    -	Map> getImportedResources() {
    -		return this.importedResources;
    -	}
    -
     	void validate(ProblemReporter problemReporter) {
     		// A configuration class may not be final (CGLIB limitation) unless it declares proxyBeanMethods=false
     		Map attributes = this.metadata.getAnnotationAttributes(Configuration.class.getName());
    diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassBeanDefinitionReader.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassBeanDefinitionReader.java
    index 427e2bec83b2..fe050329e727 100644
    --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassBeanDefinitionReader.java
    +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassBeanDefinitionReader.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright 2002-2020 the original author or authors.
    + * Copyright 2002-2023 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.
    @@ -424,6 +424,7 @@ public ConfigurationClassBeanDefinition(
     
     		public ConfigurationClassBeanDefinition(RootBeanDefinition original,
     				ConfigurationClass configClass, MethodMetadata beanMethodMetadata, String derivedBeanName) {
    +
     			super(original);
     			this.annotationMetadata = configClass.getMetadata();
     			this.factoryMethodMetadata = beanMethodMetadata;
    diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java
    index fe58c8f2d648..633780ec4a70 100644
    --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java
    +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright 2002-2022 the original author or authors.
    + * Copyright 2002-2023 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.
    @@ -409,11 +409,14 @@ private Set retrieveBeanMethodMetadata(SourceClass sourceClass)
     						this.metadataReaderFactory.getMetadataReader(original.getClassName()).getAnnotationMetadata();
     				Set asmMethods = asm.getAnnotatedMethods(Bean.class.getName());
     				if (asmMethods.size() >= beanMethods.size()) {
    +					Set candidateMethods = new LinkedHashSet<>(beanMethods);
     					Set selectedMethods = new LinkedHashSet<>(asmMethods.size());
     					for (MethodMetadata asmMethod : asmMethods) {
    -						for (MethodMetadata beanMethod : beanMethods) {
    +						for (Iterator it = candidateMethods.iterator(); it.hasNext();) {
    +							MethodMetadata beanMethod = it.next();
     							if (beanMethod.getMethodName().equals(asmMethod.getMethodName())) {
     								selectedMethods.add(beanMethod);
    +								it.remove();
     								break;
     							}
     						}
    diff --git a/spring-context/src/main/java/org/springframework/context/annotation/LoadTimeWeavingConfigurer.java b/spring-context/src/main/java/org/springframework/context/annotation/LoadTimeWeavingConfigurer.java
    index ca0a9d8f6b69..747465bbb565 100644
    --- a/spring-context/src/main/java/org/springframework/context/annotation/LoadTimeWeavingConfigurer.java
    +++ b/spring-context/src/main/java/org/springframework/context/annotation/LoadTimeWeavingConfigurer.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright 2002-2012 the original author or authors.
    + * Copyright 2002-2023 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,8 +21,8 @@
     /**
      * Interface to be implemented by
      * {@link org.springframework.context.annotation.Configuration @Configuration}
    - * classes annotated with {@link EnableLoadTimeWeaving @EnableLoadTimeWeaving} that wish to
    - * customize the {@link LoadTimeWeaver} instance to be used.
    + * classes annotated with {@link EnableLoadTimeWeaving @EnableLoadTimeWeaving}
    + * that wish to customize the {@link LoadTimeWeaver} instance to be used.
      *
      * 

    See {@link org.springframework.scheduling.annotation.EnableAsync @EnableAsync} * for usage examples and information on how a default {@code LoadTimeWeaver} @@ -36,9 +36,9 @@ public interface LoadTimeWeavingConfigurer { /** - * Create, configure and return the {@code LoadTimeWeaver} instance to be used. Note - * that it is unnecessary to annotate this method with {@code @Bean}, because the - * object returned will automatically be registered as a bean by + * Create, configure and return the {@code LoadTimeWeaver} instance to be used. + * Note that it is unnecessary to annotate this method with {@code @Bean} + * because the object returned will automatically be registered as a bean by * {@link LoadTimeWeavingConfiguration#loadTimeWeaver()} */ LoadTimeWeaver getLoadTimeWeaver(); diff --git a/spring-context/src/main/java/org/springframework/context/event/ApplicationContextEvent.java b/spring-context/src/main/java/org/springframework/context/event/ApplicationContextEvent.java index fab9067b20d6..0823d051c340 100644 --- a/spring-context/src/main/java/org/springframework/context/event/ApplicationContextEvent.java +++ b/spring-context/src/main/java/org/springframework/context/event/ApplicationContextEvent.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-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,7 @@ public abstract class ApplicationContextEvent extends ApplicationEvent { /** - * Create a new ContextStartedEvent. + * Create a new {@code ApplicationContextEvent}. * @param source the {@code ApplicationContext} that the event is raised for * (must not be {@code null}) */ diff --git a/spring-context/src/main/java/org/springframework/context/event/ApplicationListenerMethodAdapter.java b/spring-context/src/main/java/org/springframework/context/event/ApplicationListenerMethodAdapter.java index bc7e1f939899..68aeb85aef70 100644 --- a/spring-context/src/main/java/org/springframework/context/event/ApplicationListenerMethodAdapter.java +++ b/spring-context/src/main/java/org/springframework/context/event/ApplicationListenerMethodAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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. @@ -274,7 +274,7 @@ else if (result instanceof CompletionStage) { handleAsyncError(ex); } else if (event != null) { - publishEvent(event); + publishEvents(event); } }); } @@ -367,7 +367,7 @@ protected Object doInvoke(Object... args) { * Return the target bean instance to use. */ protected Object getTargetBean() { - Assert.notNull(this.applicationContext, "ApplicationContext must no be null"); + Assert.notNull(this.applicationContext, "ApplicationContext must not be null"); return this.applicationContext.getBean(this.beanName); } @@ -468,6 +468,9 @@ public String toString() { } + /** + * Inner class to avoid a hard dependency on the Reactive Streams API at runtime. + */ private class ReactiveResultHandler { public boolean subscribeToPublisher(Object result) { @@ -481,6 +484,9 @@ public boolean subscribeToPublisher(Object result) { } + /** + * Reactive Streams Subscriber for publishing follow-up events. + */ private class EventPublicationSubscriber implements Subscriber { @Override diff --git a/spring-context/src/main/java/org/springframework/context/event/ContextClosedEvent.java b/spring-context/src/main/java/org/springframework/context/event/ContextClosedEvent.java index 900bf30e49ca..8d0e2e56541c 100644 --- a/spring-context/src/main/java/org/springframework/context/event/ContextClosedEvent.java +++ b/spring-context/src/main/java/org/springframework/context/event/ContextClosedEvent.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-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,7 @@ public class ContextClosedEvent extends ApplicationContextEvent { /** - * Creates a new ContextClosedEvent. + * Create a new {@code ContextClosedEvent}. * @param source the {@code ApplicationContext} that has been closed * (must not be {@code null}) */ diff --git a/spring-context/src/main/java/org/springframework/context/event/ContextRefreshedEvent.java b/spring-context/src/main/java/org/springframework/context/event/ContextRefreshedEvent.java index 27c657a948e6..ba55c6a56c27 100644 --- a/spring-context/src/main/java/org/springframework/context/event/ContextRefreshedEvent.java +++ b/spring-context/src/main/java/org/springframework/context/event/ContextRefreshedEvent.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-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,7 @@ public class ContextRefreshedEvent extends ApplicationContextEvent { /** - * Create a new ContextRefreshedEvent. + * Create a new {@code ContextRefreshedEvent}. * @param source the {@code ApplicationContext} that has been initialized * or refreshed (must not be {@code null}) */ diff --git a/spring-context/src/main/java/org/springframework/context/event/ContextStartedEvent.java b/spring-context/src/main/java/org/springframework/context/event/ContextStartedEvent.java index bfd615d5c120..f0cf6d6bb0d4 100644 --- a/spring-context/src/main/java/org/springframework/context/event/ContextStartedEvent.java +++ b/spring-context/src/main/java/org/springframework/context/event/ContextStartedEvent.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-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,7 +30,7 @@ public class ContextStartedEvent extends ApplicationContextEvent { /** - * Create a new ContextStartedEvent. + * Create a new {@code ContextStartedEvent}. * @param source the {@code ApplicationContext} that has been started * (must not be {@code null}) */ diff --git a/spring-context/src/main/java/org/springframework/context/event/ContextStoppedEvent.java b/spring-context/src/main/java/org/springframework/context/event/ContextStoppedEvent.java index 4a156b207b8c..791e08c282c2 100644 --- a/spring-context/src/main/java/org/springframework/context/event/ContextStoppedEvent.java +++ b/spring-context/src/main/java/org/springframework/context/event/ContextStoppedEvent.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-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,7 +30,7 @@ public class ContextStoppedEvent extends ApplicationContextEvent { /** - * Create a new ContextStoppedEvent. + * Create a new {@code ContextStoppedEvent}. * @param source the {@code ApplicationContext} that has been stopped * (must not be {@code null}) */ diff --git a/spring-context/src/main/java/org/springframework/context/event/SimpleApplicationEventMulticaster.java b/spring-context/src/main/java/org/springframework/context/event/SimpleApplicationEventMulticaster.java index d1f9b8ca330a..2de53ce29021 100644 --- a/spring-context/src/main/java/org/springframework/context/event/SimpleApplicationEventMulticaster.java +++ b/spring-context/src/main/java/org/springframework/context/event/SimpleApplicationEventMulticaster.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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. @@ -79,10 +79,11 @@ public SimpleApplicationEventMulticaster(BeanFactory beanFactory) { * to invoke each listener with. *

    Default is equivalent to {@link org.springframework.core.task.SyncTaskExecutor}, * executing all listeners synchronously in the calling thread. - *

    Consider specifying an asynchronous task executor here to not block the - * caller until all listeners have been executed. However, note that asynchronous - * execution will not participate in the caller's thread context (class loader, - * transaction association) unless the TaskExecutor explicitly supports this. + *

    Consider specifying an asynchronous task executor here to not block the caller + * until all listeners have been executed. However, note that asynchronous execution + * will not participate in the caller's thread context (class loader, transaction context) + * unless the TaskExecutor explicitly supports this. + * @since 2.0 * @see org.springframework.core.task.SyncTaskExecutor * @see org.springframework.core.task.SimpleAsyncTaskExecutor */ @@ -92,6 +93,7 @@ public void setTaskExecutor(@Nullable Executor taskExecutor) { /** * Return the current task executor for this multicaster. + * @since 2.0 */ @Nullable protected Executor getTaskExecutor() { diff --git a/spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java b/spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java index 38243b17944b..a10937c45ca6 100644 --- a/spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java +++ b/spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-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. @@ -137,31 +137,40 @@ public abstract class AbstractApplicationContext extends DefaultResourceLoader implements ConfigurableApplicationContext { /** - * Name of the MessageSource bean in the factory. + * The name of the {@link MessageSource} bean in the context. * If none is supplied, message resolution is delegated to the parent. - * @see MessageSource + * @see org.springframework.context.MessageSource + * @see org.springframework.context.support.ResourceBundleMessageSource + * @see org.springframework.context.support.ReloadableResourceBundleMessageSource + * @see #getMessage(MessageSourceResolvable, Locale) */ public static final String MESSAGE_SOURCE_BEAN_NAME = "messageSource"; /** - * Name of the LifecycleProcessor bean in the factory. - * If none is supplied, a DefaultLifecycleProcessor is used. + * The name of the {@link ApplicationEventMulticaster} bean in the context. + * If none is supplied, a {@link SimpleApplicationEventMulticaster} is used. + * @see org.springframework.context.event.ApplicationEventMulticaster + * @see org.springframework.context.event.SimpleApplicationEventMulticaster + * @see #publishEvent(ApplicationEvent) + * @see #addApplicationListener(ApplicationListener) + */ + public static final String APPLICATION_EVENT_MULTICASTER_BEAN_NAME = "applicationEventMulticaster"; + + /** + * The name of the {@link LifecycleProcessor} bean in the context. + * If none is supplied, a {@link DefaultLifecycleProcessor} is used. + * @since 3.0 * @see org.springframework.context.LifecycleProcessor * @see org.springframework.context.support.DefaultLifecycleProcessor + * @see #start() + * @see #stop() */ public static final String LIFECYCLE_PROCESSOR_BEAN_NAME = "lifecycleProcessor"; - /** - * Name of the ApplicationEventMulticaster bean in the factory. - * If none is supplied, a default SimpleApplicationEventMulticaster is used. - * @see org.springframework.context.event.ApplicationEventMulticaster - * @see org.springframework.context.event.SimpleApplicationEventMulticaster - */ - public static final String APPLICATION_EVENT_MULTICASTER_BEAN_NAME = "applicationEventMulticaster"; /** - * Boolean flag controlled by a {@code spring.spel.ignore} system property that instructs Spring to - * ignore SpEL, i.e. to not initialize the SpEL infrastructure. + * Boolean flag controlled by a {@code spring.spel.ignore} system property that + * instructs Spring to ignore SpEL, i.e. to not initialize the SpEL infrastructure. *

    The default is "false". */ private static final boolean shouldIgnoreSpel = SpringProperties.getFlag("spring.spel.ignore"); @@ -203,7 +212,7 @@ public abstract class AbstractApplicationContext extends DefaultResourceLoader /** Flag that indicates whether this context has been closed already. */ private final AtomicBoolean closed = new AtomicBoolean(); - /** Synchronization monitor for the "refresh" and "destroy". */ + /** Synchronization monitor for "refresh" and "close". */ private final Object startupShutdownMonitor = new Object(); /** Reference to the JVM shutdown hook, if registered. */ @@ -211,7 +220,7 @@ public abstract class AbstractApplicationContext extends DefaultResourceLoader private Thread shutdownHook; /** ResourcePatternResolver used by this context. */ - private ResourcePatternResolver resourcePatternResolver; + private final ResourcePatternResolver resourcePatternResolver; /** LifecycleProcessor for managing the lifecycle of beans within this context. */ @Nullable @@ -447,7 +456,7 @@ ApplicationEventMulticaster getApplicationEventMulticaster() throws IllegalState @Override public void setApplicationStartup(ApplicationStartup applicationStartup) { - Assert.notNull(applicationStartup, "applicationStartup should not be null"); + Assert.notNull(applicationStartup, "ApplicationStartup must not be null"); this.applicationStartup = applicationStartup; } @@ -562,7 +571,6 @@ public void refresh() throws BeansException, IllegalStateException { StartupStep beanPostProcess = this.applicationStartup.start("spring.context.beans.post-process"); // Invoke factory processors registered as beans in the context. invokeBeanFactoryPostProcessors(beanFactory); - // Register bean processors that intercept bean creation. registerBeanPostProcessors(beanFactory); beanPostProcess.end(); @@ -729,9 +737,11 @@ protected void prepareBeanFactory(ConfigurableListableBeanFactory beanFactory) { /** * Modify the application context's internal bean factory after its standard - * initialization. All bean definitions will have been loaded, but no beans - * will have been instantiated yet. This allows for registering special - * BeanPostProcessors etc in certain ApplicationContext implementations. + * initialization. The initial definition resources will have been loaded but no + * post-processors will have run and no derived bean definitions will have been + * registered, and most importantly, no beans will have been instantiated yet. + *

    This template method allows for registering special BeanPostProcessors + * etc in certain AbstractApplicationContext subclasses. * @param beanFactory the bean factory used by the application context */ protected void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) { @@ -747,7 +757,8 @@ protected void invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory b // Detect a LoadTimeWeaver and prepare for weaving, if found in the meantime // (e.g. through an @Bean method registered by ConfigurationClassPostProcessor) - if (!NativeDetector.inNativeImage() && beanFactory.getTempClassLoader() == null && beanFactory.containsBean(LOAD_TIME_WEAVER_BEAN_NAME)) { + if (!NativeDetector.inNativeImage() && beanFactory.getTempClassLoader() == null && + beanFactory.containsBean(LOAD_TIME_WEAVER_BEAN_NAME)) { beanFactory.addBeanPostProcessor(new LoadTimeWeaverAwareProcessor(beanFactory)); beanFactory.setTempClassLoader(new ContextTypeMatchClassLoader(beanFactory.getBeanClassLoader())); } @@ -763,8 +774,9 @@ protected void registerBeanPostProcessors(ConfigurableListableBeanFactory beanFa } /** - * Initialize the MessageSource. - * Use parent's if none defined in this context. + * Initialize the {@link MessageSource}. + *

    Uses parent's {@code MessageSource} if none defined in this context. + * @see #MESSAGE_SOURCE_BEAN_NAME */ protected void initMessageSource() { ConfigurableListableBeanFactory beanFactory = getBeanFactory(); @@ -796,8 +808,9 @@ protected void initMessageSource() { } /** - * Initialize the ApplicationEventMulticaster. - * Uses SimpleApplicationEventMulticaster if none defined in the context. + * Initialize the {@link ApplicationEventMulticaster}. + *

    Uses {@link SimpleApplicationEventMulticaster} if none defined in the context. + * @see #APPLICATION_EVENT_MULTICASTER_BEAN_NAME * @see org.springframework.context.event.SimpleApplicationEventMulticaster */ protected void initApplicationEventMulticaster() { @@ -820,15 +833,16 @@ protected void initApplicationEventMulticaster() { } /** - * Initialize the LifecycleProcessor. - * Uses DefaultLifecycleProcessor if none defined in the context. + * Initialize the {@link LifecycleProcessor}. + *

    Uses {@link DefaultLifecycleProcessor} if none defined in the context. + * @since 3.0 + * @see #LIFECYCLE_PROCESSOR_BEAN_NAME * @see org.springframework.context.support.DefaultLifecycleProcessor */ protected void initLifecycleProcessor() { ConfigurableListableBeanFactory beanFactory = getBeanFactory(); if (beanFactory.containsLocalBean(LIFECYCLE_PROCESSOR_BEAN_NAME)) { - this.lifecycleProcessor = - beanFactory.getBean(LIFECYCLE_PROCESSOR_BEAN_NAME, LifecycleProcessor.class); + this.lifecycleProcessor = beanFactory.getBean(LIFECYCLE_PROCESSOR_BEAN_NAME, LifecycleProcessor.class); if (logger.isTraceEnabled()) { logger.trace("Using LifecycleProcessor [" + this.lifecycleProcessor + "]"); } @@ -1080,6 +1094,9 @@ protected void doClose() { // Let subclasses do some final clean-up if they wish... onClose(); + // Reset common introspection caches to avoid class reference leaks. + resetCommonCaches(); + // Reset local application listeners to pre-refresh state. if (this.earlyApplicationListeners != null) { this.applicationListeners.clear(); diff --git a/spring-context/src/main/java/org/springframework/context/support/AbstractRefreshableApplicationContext.java b/spring-context/src/main/java/org/springframework/context/support/AbstractRefreshableApplicationContext.java index 9c87844e9d71..5b2e5288678e 100644 --- a/spring-context/src/main/java/org/springframework/context/support/AbstractRefreshableApplicationContext.java +++ b/spring-context/src/main/java/org/springframework/context/support/AbstractRefreshableApplicationContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -126,6 +126,7 @@ protected final void refreshBeanFactory() throws BeansException { try { DefaultListableBeanFactory beanFactory = createBeanFactory(); beanFactory.setSerializationId(getId()); + beanFactory.setApplicationStartup(getApplicationStartup()); customizeBeanFactory(beanFactory); loadBeanDefinitions(beanFactory); this.beanFactory = beanFactory; diff --git a/spring-context/src/main/java/org/springframework/context/support/AbstractXmlApplicationContext.java b/spring-context/src/main/java/org/springframework/context/support/AbstractXmlApplicationContext.java index 85cb2250b59e..c21002abd2cc 100644 --- a/spring-context/src/main/java/org/springframework/context/support/AbstractXmlApplicationContext.java +++ b/spring-context/src/main/java/org/springframework/context/support/AbstractXmlApplicationContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2023 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 org.springframework.beans.BeansException; import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.xml.BeanDefinitionDocumentReader; import org.springframework.beans.factory.xml.ResourceEntityResolver; import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; import org.springframework.context.ApplicationContext; @@ -84,7 +85,7 @@ protected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) throw // Configure the bean definition reader with this context's // resource loading environment. - beanDefinitionReader.setEnvironment(this.getEnvironment()); + beanDefinitionReader.setEnvironment(getEnvironment()); beanDefinitionReader.setResourceLoader(this); beanDefinitionReader.setEntityResolver(new ResourceEntityResolver(this)); @@ -95,12 +96,13 @@ protected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) throw } /** - * Initialize the bean definition reader used for loading the bean - * definitions of this context. Default implementation is empty. + * Initialize the bean definition reader used for loading the bean definitions + * of this context. The default implementation sets the validating flag. *

    Can be overridden in subclasses, e.g. for turning off XML validation - * or using a different XmlBeanDefinitionParser implementation. + * or using a different {@link BeanDefinitionDocumentReader} implementation. * @param reader the bean definition reader used by this context - * @see org.springframework.beans.factory.xml.XmlBeanDefinitionReader#setDocumentReaderClass + * @see XmlBeanDefinitionReader#setValidating + * @see XmlBeanDefinitionReader#setDocumentReaderClass */ protected void initBeanDefinitionReader(XmlBeanDefinitionReader reader) { reader.setValidating(this.validating); diff --git a/spring-context/src/main/java/org/springframework/context/support/ContextTypeMatchClassLoader.java b/spring-context/src/main/java/org/springframework/context/support/ContextTypeMatchClassLoader.java index 1f17f45719ba..998d061ca2cd 100644 --- a/spring-context/src/main/java/org/springframework/context/support/ContextTypeMatchClassLoader.java +++ b/spring-context/src/main/java/org/springframework/context/support/ContextTypeMatchClassLoader.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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,6 +21,8 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import org.apache.commons.logging.LogFactory; + import org.springframework.core.DecoratingClassLoader; import org.springframework.core.OverridingClassLoader; import org.springframework.core.SmartClassLoader; @@ -45,15 +47,27 @@ class ContextTypeMatchClassLoader extends DecoratingClassLoader implements Smart } - private static Method findLoadedClassMethod; + @Nullable + private static final Method findLoadedClassMethod; static { + // Try to enable findLoadedClass optimization which allows us to selectively + // override classes that have not been loaded yet. If not accessible, we will + // always override requested classes, even when the classes have been loaded + // by the parent ClassLoader already and cannot be transformed anymore anyway. + Method method; try { - findLoadedClassMethod = ClassLoader.class.getDeclaredMethod("findLoadedClass", String.class); + method = ClassLoader.class.getDeclaredMethod("findLoadedClass", String.class); + ReflectionUtils.makeAccessible(method); } - catch (NoSuchMethodException ex) { - throw new IllegalStateException("Invalid [java.lang.ClassLoader] class: no 'findLoadedClass' method defined!"); + catch (Throwable ex) { + // Typically a JDK 9+ InaccessibleObjectException... + // Avoid through JVM startup with --add-opens=java.base/java.lang=ALL-UNNAMED + method = null; + LogFactory.getLog(ContextTypeMatchClassLoader.class).debug( + "ClassLoader.findLoadedClass not accessible -> will always override requested class", ex); } + findLoadedClassMethod = method; } @@ -96,13 +110,14 @@ protected boolean isEligibleForOverriding(String className) { if (isExcluded(className) || ContextTypeMatchClassLoader.this.isExcluded(className)) { return false; } - ReflectionUtils.makeAccessible(findLoadedClassMethod); - ClassLoader parent = getParent(); - while (parent != null) { - if (ReflectionUtils.invokeMethod(findLoadedClassMethod, parent, className) != null) { - return false; + if (findLoadedClassMethod != null) { + ClassLoader parent = getParent(); + while (parent != null) { + if (ReflectionUtils.invokeMethod(findLoadedClassMethod, parent, className) != null) { + return false; + } + parent = parent.getParent(); } - parent = parent.getParent(); } return true; } diff --git a/spring-context/src/main/java/org/springframework/context/support/DefaultLifecycleProcessor.java b/spring-context/src/main/java/org/springframework/context/support/DefaultLifecycleProcessor.java index a269d5a81b15..927f203a3fec 100644 --- a/spring-context/src/main/java/org/springframework/context/support/DefaultLifecycleProcessor.java +++ b/spring-context/src/main/java/org/springframework/context/support/DefaultLifecycleProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -45,7 +45,11 @@ import org.springframework.util.Assert; /** - * Default implementation of the {@link LifecycleProcessor} strategy. + * Spring's default implementation of the {@link LifecycleProcessor} strategy. + * + *

    Provides interaction with {@link Lifecycle} and {@link SmartLifecycle} beans in + * groups for specific phases, on startup/shutdown as well as for explicit start/stop + * interactions on a {@link org.springframework.context.ConfigurableApplicationContext}. * * @author Mark Fisher * @author Juergen Hoeller @@ -64,9 +68,10 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor /** - * Specify the maximum time allotted in milliseconds for the shutdown of - * any phase (group of SmartLifecycle beans with the same 'phase' value). - *

    The default value is 30 seconds. + * Specify the maximum time allotted in milliseconds for the shutdown of any + * phase (group of {@link SmartLifecycle} beans with the same 'phase' value). + *

    The default value is 30000 milliseconds (30 seconds). + * @see SmartLifecycle#getPhase() */ public void setTimeoutPerShutdownPhase(long timeoutPerShutdownPhase) { this.timeoutPerShutdownPhase = timeoutPerShutdownPhase; @@ -144,13 +149,13 @@ private void startBeans(boolean autoStartupOnly) { lifecycleBeans.forEach((beanName, bean) -> { if (!autoStartupOnly || (bean instanceof SmartLifecycle && ((SmartLifecycle) bean).isAutoStartup())) { - int phase = getPhase(bean); - phases.computeIfAbsent( - phase, - p -> new LifecycleGroup(phase, this.timeoutPerShutdownPhase, lifecycleBeans, autoStartupOnly) + int startupPhase = getPhase(bean); + phases.computeIfAbsent(startupPhase, + phase -> new LifecycleGroup(phase, this.timeoutPerShutdownPhase, lifecycleBeans, autoStartupOnly) ).add(beanName, bean); } }); + if (!phases.isEmpty()) { phases.values().forEach(LifecycleGroup::start); } @@ -190,6 +195,7 @@ private void doStart(Map lifecycleBeans, String bea private void stopBeans() { Map lifecycleBeans = getLifecycleBeans(); Map phases = new HashMap<>(); + lifecycleBeans.forEach((beanName, bean) -> { int shutdownPhase = getPhase(bean); LifecycleGroup group = phases.get(shutdownPhase); @@ -199,6 +205,7 @@ private void stopBeans() { } group.add(beanName, bean); }); + if (!phases.isEmpty()) { List keys = new ArrayList<>(phases.keySet()); keys.sort(Collections.reverseOrder()); @@ -313,6 +320,8 @@ protected int getPhase(Lifecycle bean) { /** * Helper class for maintaining a group of Lifecycle beans that should be started * and stopped together based on their 'phase' value (or the default value of 0). + * The group is expected to be created in an ad-hoc fashion and group members are + * expected to always have the same 'phase' value. */ private class LifecycleGroup { diff --git a/spring-context/src/main/java/org/springframework/context/support/ReloadableResourceBundleMessageSource.java b/spring-context/src/main/java/org/springframework/context/support/ReloadableResourceBundleMessageSource.java index 329cb743cf3c..7830eecb7daf 100644 --- a/spring-context/src/main/java/org/springframework/context/support/ReloadableResourceBundleMessageSource.java +++ b/spring-context/src/main/java/org/springframework/context/support/ReloadableResourceBundleMessageSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-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.Properties; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import org.springframework.context.ResourceLoaderAware; @@ -143,7 +144,7 @@ public void setConcurrentRefresh(boolean concurrentRefresh) { /** * Set the PropertiesPersister to use for parsing properties files. - *

    The default is ResourcePropertiesPersister. + *

    The default is {@code ResourcePropertiesPersister}. * @see ResourcePropertiesPersister#INSTANCE */ public void setPropertiesPersister(@Nullable PropertiesPersister propertiesPersister) { @@ -401,7 +402,7 @@ protected PropertiesHolder getProperties(String filename) { /** * Refresh the PropertiesHolder for the given bundle filename. - * The holder can be {@code null} if not cached before, or a timed-out cache entry + *

    The holder can be {@code null} if not cached before, or a timed-out cache entry * (potentially getting re-validated against the current last-modified timestamp). * @param filename the bundle filename (basename + Locale) * @param propHolder the current PropertiesHolder for the bundle @@ -562,7 +563,7 @@ protected class PropertiesHolder { private volatile long refreshTimestamp = -2; - private final ReentrantLock refreshLock = new ReentrantLock(); + private final Lock refreshLock = new ReentrantLock(); /** Cache to hold already generated MessageFormats per message code. */ private final ConcurrentMap> cachedMessageFormats = diff --git a/spring-context/src/main/java/org/springframework/format/datetime/DateFormatter.java b/spring-context/src/main/java/org/springframework/format/datetime/DateFormatter.java index 5235ca74324c..e4185c3b0859 100644 --- a/spring-context/src/main/java/org/springframework/format/datetime/DateFormatter.java +++ b/spring-context/src/main/java/org/springframework/format/datetime/DateFormatter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-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,8 +22,10 @@ import java.util.Collections; import java.util.Date; import java.util.EnumMap; +import java.util.LinkedHashSet; import java.util.Locale; import java.util.Map; +import java.util.Set; import java.util.TimeZone; import org.springframework.format.Formatter; @@ -35,9 +37,14 @@ /** * A formatter for {@link java.util.Date} types. + * *

    Supports the configuration of an explicit date time pattern, timezone, * locale, and fallback date time patterns for lenient parsing. * + *

    Common ISO patterns for UTC instants are applied at millisecond precision. + * Note that {@link org.springframework.format.datetime.standard.InstantFormatter} + * is recommended for flexible UTC parsing into a {@link java.time.Instant} instead. + * * @author Keith Donald * @author Juergen Hoeller * @author Phillip Webb @@ -51,12 +58,21 @@ public class DateFormatter implements Formatter { private static final Map ISO_PATTERNS; + private static final Map ISO_FALLBACK_PATTERNS; + static { + // We use an EnumMap instead of Map.of(...) since the former provides better performance. Map formats = new EnumMap<>(ISO.class); formats.put(ISO.DATE, "yyyy-MM-dd"); formats.put(ISO.TIME, "HH:mm:ss.SSSXXX"); formats.put(ISO.DATE_TIME, "yyyy-MM-dd'T'HH:mm:ss.SSSXXX"); ISO_PATTERNS = Collections.unmodifiableMap(formats); + + // Fallback format for the time part without milliseconds. + Map fallbackFormats = new EnumMap<>(ISO.class); + fallbackFormats.put(ISO.TIME, "HH:mm:ssXXX"); + fallbackFormats.put(ISO.DATE_TIME, "yyyy-MM-dd'T'HH:mm:ssXXX"); + ISO_FALLBACK_PATTERNS = Collections.unmodifiableMap(fallbackFormats); } @@ -201,8 +217,16 @@ public Date parse(String text, Locale locale) throws ParseException { return getDateFormat(locale).parse(text); } catch (ParseException ex) { + Set fallbackPatterns = new LinkedHashSet<>(); + String isoPattern = ISO_FALLBACK_PATTERNS.get(this.iso); + if (isoPattern != null) { + fallbackPatterns.add(isoPattern); + } if (!ObjectUtils.isEmpty(this.fallbackPatterns)) { - for (String pattern : this.fallbackPatterns) { + Collections.addAll(fallbackPatterns, this.fallbackPatterns); + } + if (!fallbackPatterns.isEmpty()) { + for (String pattern : fallbackPatterns) { try { DateFormat dateFormat = configureDateFormat(new SimpleDateFormat(pattern, locale)); // Align timezone for parsing format with printing format if ISO is set. @@ -220,8 +244,8 @@ public Date parse(String text, Locale locale) throws ParseException { } if (this.source != null) { ParseException parseException = new ParseException( - String.format("Unable to parse date time value \"%s\" using configuration from %s", text, this.source), - ex.getErrorOffset()); + String.format("Unable to parse date time value \"%s\" using configuration from %s", text, this.source), + ex.getErrorOffset()); parseException.initCause(ex); throw parseException; } diff --git a/spring-context/src/main/java/org/springframework/format/datetime/standard/InstantFormatter.java b/spring-context/src/main/java/org/springframework/format/datetime/standard/InstantFormatter.java index 456c0ad09090..06ff8213de57 100644 --- a/spring-context/src/main/java/org/springframework/format/datetime/standard/InstantFormatter.java +++ b/spring-context/src/main/java/org/springframework/format/datetime/standard/InstantFormatter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -41,12 +41,12 @@ public class InstantFormatter implements Formatter { @Override public Instant parse(String text, Locale locale) throws ParseException { - if (text.length() > 0 && Character.isAlphabetic(text.charAt(0))) { + if (!text.isEmpty() && Character.isAlphabetic(text.charAt(0))) { // assuming RFC-1123 value a la "Tue, 3 Jun 2008 11:05:30 GMT" return Instant.from(DateTimeFormatter.RFC_1123_DATE_TIME.parse(text)); } else { - // assuming UTC instant a la "2007-12-03T10:15:30.00Z" + // assuming UTC instant a la "2007-12-03T10:15:30.000Z" return Instant.parse(text); } } diff --git a/spring-context/src/main/java/org/springframework/scheduling/TaskScheduler.java b/spring-context/src/main/java/org/springframework/scheduling/TaskScheduler.java index 326923d10d47..e8cd73e15f8c 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/TaskScheduler.java +++ b/spring-context/src/main/java/org/springframework/scheduling/TaskScheduler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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,7 @@ * {@link Runnable Runnables} based on different kinds of triggers. * *

    This interface is separate from {@link SchedulingTaskExecutor} since it - * usually represents for a different kind of backend, i.e. a thread pool with + * usually represents a different kind of backend, i.e. a thread pool with * different characteristics and capabilities. Implementations may implement * both interfaces if they can handle both kinds of execution characteristics. * diff --git a/spring-context/src/main/java/org/springframework/scheduling/annotation/Scheduled.java b/spring-context/src/main/java/org/springframework/scheduling/annotation/Scheduled.java index f3aa81296a23..c4206c635345 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/annotation/Scheduled.java +++ b/spring-context/src/main/java/org/springframework/scheduling/annotation/Scheduled.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-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. @@ -28,8 +28,8 @@ /** * Annotation that marks a method to be scheduled. Exactly one of the - * {@link #cron}, {@link #fixedDelay}, or {@link #fixedRate} attributes must be - * specified. + * {@link #cron}, {@link #fixedDelay}, or {@link #fixedRate} attributes + * must be specified. * *

    The annotated method must expect no arguments. It will typically have * a {@code void} return type; if not, the returned value will be ignored @@ -40,6 +40,12 @@ * done manually or, more conveniently, through the {@code } * XML element or {@link EnableScheduling @EnableScheduling} annotation. * + *

    This annotation can be used as a {@linkplain Repeatable repeatable} + * annotation. If several scheduled declarations are found on the same method, + * each of them will be processed independently, with a separate trigger firing + * for each of them. As a consequence, such co-located schedules may overlap + * and execute multiple times in parallel or in immediate succession. + * *

    This annotation may be used as a meta-annotation to create custom * composed annotations with attribute overrides. * @@ -94,9 +100,9 @@ /** * A time zone for which the cron expression will be resolved. By default, this - * attribute is the empty String (i.e. the server's local time zone will be used). + * attribute is the empty String (i.e. the scheduler's time zone will be used). * @return a zone id accepted by {@link java.util.TimeZone#getTimeZone(String)}, - * or an empty String to indicate the server's default time zone + * or an empty String to indicate the scheduler's default time zone * @since 4.0 * @see org.springframework.scheduling.support.CronTrigger#CronTrigger(String, java.util.TimeZone) * @see java.util.TimeZone @@ -117,9 +123,12 @@ * last invocation and the start of the next. *

    The time unit is milliseconds by default but can be overridden via * {@link #timeUnit}. + *

    This attribute variant supports Spring-style "${...}" placeholders + * as well as SpEL expressions. * @return the delay as a String value — for example, a placeholder * or a {@link java.time.Duration#parse java.time.Duration} compliant value * @since 3.2.2 + * @see #fixedDelay() */ String fixedDelayString() default ""; @@ -135,9 +144,12 @@ * Execute the annotated method with a fixed period between invocations. *

    The time unit is milliseconds by default but can be overridden via * {@link #timeUnit}. + *

    This attribute variant supports Spring-style "${...}" placeholders + * as well as SpEL expressions. * @return the period as a String value — for example, a placeholder * or a {@link java.time.Duration#parse java.time.Duration} compliant value * @since 3.2.2 + * @see #fixedRate() */ String fixedRateString() default ""; @@ -156,9 +168,12 @@ * {@link #fixedRate} or {@link #fixedDelay} task. *

    The time unit is milliseconds by default but can be overridden via * {@link #timeUnit}. + *

    This attribute variant supports Spring-style "${...}" placeholders + * as well as SpEL expressions. * @return the initial delay as a String value — for example, a placeholder * or a {@link java.time.Duration#parse java.time.Duration} compliant value * @since 3.2.2 + * @see #initialDelay() */ String initialDelayString() default ""; diff --git a/spring-context/src/main/java/org/springframework/scheduling/annotation/ScheduledAnnotationBeanPostProcessor.java b/spring-context/src/main/java/org/springframework/scheduling/annotation/ScheduledAnnotationBeanPostProcessor.java index f0aae203468a..28d1aea5f080 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/annotation/ScheduledAnnotationBeanPostProcessor.java +++ b/spring-context/src/main/java/org/springframework/scheduling/annotation/ScheduledAnnotationBeanPostProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-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,7 +27,6 @@ import java.util.List; import java.util.Map; import java.util.Set; -import java.util.TimeZone; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; @@ -84,7 +83,7 @@ * "fixedRate", "fixedDelay", or "cron" expression provided via the annotation. * *

    This post-processor is automatically registered by Spring's - * {@code } XML element, and also by the + * {@code } XML element and also by the * {@link EnableScheduling @EnableScheduling} annotation. * *

    Autodetects any {@link SchedulingConfigurer} instances in the container, @@ -160,7 +159,7 @@ public ScheduledAnnotationBeanPostProcessor() { * @since 5.1 */ public ScheduledAnnotationBeanPostProcessor(ScheduledTaskRegistrar registrar) { - Assert.notNull(registrar, "ScheduledTaskRegistrar is required"); + Assert.notNull(registrar, "ScheduledTaskRegistrar must not be null"); this.registrar = registrar; } @@ -434,14 +433,14 @@ protected void processScheduled(Scheduled scheduled, Method method, Object bean) Assert.isTrue(initialDelay == -1, "'initialDelay' not supported for cron triggers"); processedSchedule = true; if (!Scheduled.CRON_DISABLED.equals(cron)) { - TimeZone timeZone; + CronTrigger trigger; if (StringUtils.hasText(zone)) { - timeZone = StringUtils.parseTimeZoneString(zone); + trigger = new CronTrigger(cron, StringUtils.parseTimeZoneString(zone)); } else { - timeZone = TimeZone.getDefault(); + trigger = new CronTrigger(cron); } - tasks.add(this.registrar.scheduleCronTask(new CronTask(runnable, new CronTrigger(cron, timeZone)))); + tasks.add(this.registrar.scheduleCronTask(new CronTask(runnable, trigger))); } } } @@ -580,7 +579,7 @@ public void postProcessBeforeDestruction(Object bean, String beanName) { } if (tasks != null) { for (ScheduledTask task : tasks) { - task.cancel(); + task.cancel(false); } } } @@ -598,7 +597,7 @@ public void destroy() { Collection> allTasks = this.scheduledTasks.values(); for (Set tasks : allTasks) { for (ScheduledTask task : tasks) { - task.cancel(); + task.cancel(false); } } this.scheduledTasks.clear(); diff --git a/spring-context/src/main/java/org/springframework/scheduling/annotation/SchedulingConfigurer.java b/spring-context/src/main/java/org/springframework/scheduling/annotation/SchedulingConfigurer.java index 26f0076077f7..7ad039fb8dd7 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/annotation/SchedulingConfigurer.java +++ b/spring-context/src/main/java/org/springframework/scheduling/annotation/SchedulingConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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. @@ -40,10 +40,10 @@ public interface SchedulingConfigurer { /** - * Callback allowing a {@link org.springframework.scheduling.TaskScheduler - * TaskScheduler} and specific {@link org.springframework.scheduling.config.Task Task} - * instances to be registered against the given the {@link ScheduledTaskRegistrar}. - * @param taskRegistrar the registrar to be configured. + * Callback allowing a {@link org.springframework.scheduling.TaskScheduler} + * and specific {@link org.springframework.scheduling.config.Task} instances + * to be registered against the given the {@link ScheduledTaskRegistrar}. + * @param taskRegistrar the registrar to be configured */ void configureTasks(ScheduledTaskRegistrar taskRegistrar); diff --git a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ConcurrentTaskExecutor.java b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ConcurrentTaskExecutor.java index 9c19aad56603..66e86993c4e0 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ConcurrentTaskExecutor.java +++ b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ConcurrentTaskExecutor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-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,6 +83,9 @@ public class ConcurrentTaskExecutor implements AsyncListenableTaskExecutor, Sche private TaskExecutorAdapter adaptedExecutor; + @Nullable + private TaskDecorator taskDecorator; + /** * Create a new ConcurrentTaskExecutor, using a single thread executor as default. @@ -130,14 +133,10 @@ public final Executor getConcurrentExecutor() { * execution callback (which may be a wrapper around the user-supplied task). *

    The primary use case is to set some execution context around the task's * invocation, or to provide some monitoring/statistics for task execution. - *

    NOTE: Exception handling in {@code TaskDecorator} implementations - * is limited to plain {@code Runnable} execution via {@code execute} calls. - * In case of {@code #submit} calls, the exposed {@code Runnable} will be a - * {@code FutureTask} which does not propagate any exceptions; you might - * have to cast it and call {@code Future#get} to evaluate exceptions. * @since 4.3 */ public final void setTaskDecorator(TaskDecorator taskDecorator) { + this.taskDecorator = taskDecorator; this.adaptedExecutor.setTaskDecorator(taskDecorator); } @@ -174,11 +173,14 @@ public ListenableFuture submitListenable(Callable task) { } - private static TaskExecutorAdapter getAdaptedExecutor(Executor concurrentExecutor) { - if (managedExecutorServiceClass != null && managedExecutorServiceClass.isInstance(concurrentExecutor)) { - return new ManagedTaskExecutorAdapter(concurrentExecutor); + private TaskExecutorAdapter getAdaptedExecutor(Executor originalExecutor) { + TaskExecutorAdapter adapter = + (managedExecutorServiceClass != null && managedExecutorServiceClass.isInstance(originalExecutor) ? + new ManagedTaskExecutorAdapter(originalExecutor) : new TaskExecutorAdapter(originalExecutor)); + if (this.taskDecorator != null) { + adapter.setTaskDecorator(this.taskDecorator); } - return new TaskExecutorAdapter(concurrentExecutor); + return adapter; } diff --git a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ConcurrentTaskScheduler.java b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ConcurrentTaskScheduler.java index 887c3f596518..52e518183053 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ConcurrentTaskScheduler.java +++ b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ConcurrentTaskScheduler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -100,7 +100,7 @@ public class ConcurrentTaskScheduler extends ConcurrentTaskExecutor implements T */ public ConcurrentTaskScheduler() { super(); - this.scheduledExecutor = initScheduledExecutor(null); + initScheduledExecutor(null); } /** @@ -115,7 +115,7 @@ public ConcurrentTaskScheduler() { */ public ConcurrentTaskScheduler(ScheduledExecutorService scheduledExecutor) { super(scheduledExecutor); - this.scheduledExecutor = initScheduledExecutor(scheduledExecutor); + initScheduledExecutor(scheduledExecutor); } /** @@ -131,11 +131,11 @@ public ConcurrentTaskScheduler(ScheduledExecutorService scheduledExecutor) { */ public ConcurrentTaskScheduler(Executor concurrentExecutor, ScheduledExecutorService scheduledExecutor) { super(concurrentExecutor); - this.scheduledExecutor = initScheduledExecutor(scheduledExecutor); + initScheduledExecutor(scheduledExecutor); } - private ScheduledExecutorService initScheduledExecutor(@Nullable ScheduledExecutorService scheduledExecutor) { + private void initScheduledExecutor(@Nullable ScheduledExecutorService scheduledExecutor) { if (scheduledExecutor != null) { this.scheduledExecutor = scheduledExecutor; this.enterpriseConcurrentScheduler = (managedScheduledExecutorServiceClass != null && @@ -145,7 +145,6 @@ private ScheduledExecutorService initScheduledExecutor(@Nullable ScheduledExecut this.scheduledExecutor = Executors.newSingleThreadScheduledExecutor(); this.enterpriseConcurrentScheduler = false; } - return this.scheduledExecutor; } /** @@ -178,6 +177,7 @@ public void setErrorHandler(ErrorHandler errorHandler) { * @see Clock#systemDefaultZone() */ public void setClock(Clock clock) { + Assert.notNull(clock, "Clock must not be null"); this.clock = clock; } @@ -207,9 +207,9 @@ public ScheduledFuture schedule(Runnable task, Trigger trigger) { @Override public ScheduledFuture schedule(Runnable task, Date startTime) { - long initialDelay = startTime.getTime() - this.clock.millis(); + long delay = startTime.getTime() - this.clock.millis(); try { - return this.scheduledExecutor.schedule(decorateTask(task, false), initialDelay, TimeUnit.MILLISECONDS); + return this.scheduledExecutor.schedule(decorateTask(task, false), delay, TimeUnit.MILLISECONDS); } catch (RejectedExecutionException ex) { throw new TaskRejectedException("Executor [" + this.scheduledExecutor + "] did not accept task: " + task, ex); diff --git a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ExecutorConfigurationSupport.java b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ExecutorConfigurationSupport.java index 559890ef5096..5200edc054b9 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ExecutorConfigurationSupport.java +++ b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ExecutorConfigurationSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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. @@ -36,7 +36,10 @@ * Base class for setting up a {@link java.util.concurrent.ExecutorService} * (typically a {@link java.util.concurrent.ThreadPoolExecutor} or * {@link java.util.concurrent.ScheduledThreadPoolExecutor}). - * Defines common configuration settings and common lifecycle handling. + * + *

    Defines common configuration settings and common lifecycle handling, + * inheriting thread customization options (name, priority, etc) from + * {@link org.springframework.util.CustomizableThreadCreator}. * * @author Juergen Hoeller * @since 3.0 @@ -105,9 +108,9 @@ public void setRejectedExecutionHandler(@Nullable RejectedExecutionHandler rejec /** * Set whether to wait for scheduled tasks to complete on shutdown, * not interrupting running tasks and executing all tasks in the queue. - *

    Default is "false", shutting down immediately through interrupting - * ongoing tasks and clearing the queue. Switch this flag to "true" if you - * prefer fully completed tasks at the expense of a longer shutdown phase. + *

    Default is {@code false}, shutting down immediately through interrupting + * ongoing tasks and clearing the queue. Switch this flag to {@code true} if + * you prefer fully completed tasks at the expense of a longer shutdown phase. *

    Note that Spring's container shutdown continues while ongoing tasks * are being completed. If you want this executor to block and wait for the * termination of tasks before the rest of the container continues to shut @@ -199,8 +202,7 @@ protected abstract ExecutorService initializeExecutor( /** - * Calls {@code shutdown} when the BeanFactory destroys - * the task executor instance. + * Calls {@code shutdown} when the BeanFactory destroys the executor instance. * @see #shutdown() */ @Override @@ -209,9 +211,13 @@ public void destroy() { } /** - * Perform a shutdown on the underlying ExecutorService. + * Perform a full shutdown on the underlying ExecutorService, + * according to the corresponding configuration settings. + * @see #setWaitForTasksToCompleteOnShutdown + * @see #setAwaitTerminationMillis * @see java.util.concurrent.ExecutorService#shutdown() * @see java.util.concurrent.ExecutorService#shutdownNow() + * @see java.util.concurrent.ExecutorService#awaitTermination */ public void shutdown() { if (logger.isDebugEnabled()) { diff --git a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ReschedulingRunnable.java b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ReschedulingRunnable.java index ca79c80fc6db..3d2ce3f88aaa 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ReschedulingRunnable.java +++ b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ReschedulingRunnable.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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. @@ -78,8 +78,8 @@ public ScheduledFuture schedule() { if (this.scheduledExecutionTime == null) { return null; } - long initialDelay = this.scheduledExecutionTime.getTime() - this.triggerContext.getClock().millis(); - this.currentFuture = this.executor.schedule(this, initialDelay, TimeUnit.MILLISECONDS); + long delay = this.scheduledExecutionTime.getTime() - this.triggerContext.getClock().millis(); + this.currentFuture = this.executor.schedule(this, delay, TimeUnit.MILLISECONDS); return this; } } diff --git a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ThreadPoolTaskScheduler.java b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ThreadPoolTaskScheduler.java index 946f5bcc6f22..1094d0ddd48b 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ThreadPoolTaskScheduler.java +++ b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ThreadPoolTaskScheduler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-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. @@ -45,8 +45,9 @@ import org.springframework.util.concurrent.ListenableFutureTask; /** - * Implementation of Spring's {@link TaskScheduler} interface, wrapping - * a native {@link java.util.concurrent.ScheduledThreadPoolExecutor}. + * A standard implementation of Spring's {@link TaskScheduler} interface, wrapping + * a native {@link java.util.concurrent.ScheduledThreadPoolExecutor} and providing + * all applicable configuration options for it. * * @author Juergen Hoeller * @author Mark Fisher @@ -154,6 +155,7 @@ public void setErrorHandler(ErrorHandler errorHandler) { * @see Clock#systemDefaultZone() */ public void setClock(Clock clock) { + Assert.notNull(clock, "Clock must not be null"); this.clock = clock; } @@ -380,9 +382,9 @@ public ScheduledFuture schedule(Runnable task, Trigger trigger) { @Override public ScheduledFuture schedule(Runnable task, Date startTime) { ScheduledExecutorService executor = getScheduledExecutor(); - long initialDelay = startTime.getTime() - this.clock.millis(); + long delay = startTime.getTime() - this.clock.millis(); try { - return executor.schedule(errorHandlingTask(task, false), initialDelay, TimeUnit.MILLISECONDS); + return executor.schedule(errorHandlingTask(task, false), delay, TimeUnit.MILLISECONDS); } catch (RejectedExecutionException ex) { throw new TaskRejectedException("Executor [" + executor + "] did not accept task: " + task, ex); diff --git a/spring-context/src/main/java/org/springframework/scheduling/config/ScheduledTaskRegistrar.java b/spring-context/src/main/java/org/springframework/scheduling/config/ScheduledTaskRegistrar.java index 3b9cc27aa16c..9f141a33cc51 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/config/ScheduledTaskRegistrar.java +++ b/spring-context/src/main/java/org/springframework/scheduling/config/ScheduledTaskRegistrar.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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. @@ -41,8 +41,8 @@ * Helper bean for registering tasks with a {@link TaskScheduler}, typically using cron * expressions. * - *

    As of Spring 3.1, {@code ScheduledTaskRegistrar} has a more prominent user-facing - * role when used in conjunction with the {@link + *

    {@code ScheduledTaskRegistrar} has a more prominent user-facing role when used in + * conjunction with the {@link * org.springframework.scheduling.annotation.EnableAsync @EnableAsync} annotation and its * {@link org.springframework.scheduling.annotation.SchedulingConfigurer * SchedulingConfigurer} callback interface. @@ -552,7 +552,7 @@ public Set getScheduledTasks() { @Override public void destroy() { for (ScheduledTask task : this.scheduledTasks) { - task.cancel(); + task.cancel(false); } if (this.localExecutor != null) { this.localExecutor.shutdownNow(); diff --git a/spring-context/src/main/java/org/springframework/scheduling/support/BitsCronField.java b/spring-context/src/main/java/org/springframework/scheduling/support/BitsCronField.java index 673aaaf6a11c..f922a8b9ddaa 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/support/BitsCronField.java +++ b/spring-context/src/main/java/org/springframework/scheduling/support/BitsCronField.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-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,16 +29,14 @@ * Created using the {@code parse*} methods. * * @author Arjen Poutsma + * @author Juergen Hoeller * @since 5.3 */ final class BitsCronField extends CronField { - private static final long MASK = 0xFFFFFFFFFFFFFFFFL; - - - @Nullable - private static BitsCronField zeroNanos = null; + public static final BitsCronField ZERO_NANOS = forZeroNanos(); + private static final long MASK = 0xFFFFFFFFFFFFFFFFL; // we store at most 60 bits, for seconds and minutes, so a 64-bit long suffices private long bits; @@ -48,16 +46,14 @@ private BitsCronField(Type type) { super(type); } + /** * Return a {@code BitsCronField} enabled for 0 nanoseconds. */ - public static BitsCronField zeroNanos() { - if (zeroNanos == null) { - BitsCronField field = new BitsCronField(Type.NANO); - field.setBit(0); - zeroNanos = field; - } - return zeroNanos; + private static BitsCronField forZeroNanos() { + BitsCronField field = new BitsCronField(Type.NANO); + field.setBit(0); + return field; } /** @@ -108,7 +104,6 @@ public static BitsCronField parseDaysOfWeek(String value) { return result; } - private static BitsCronField parseDate(String value, BitsCronField.Type type) { if (value.equals("?")) { value = "*"; @@ -174,6 +169,7 @@ private static ValueRange parseRange(String value, Type type) { } } + @Nullable @Override public > T nextOrSame(T temporal) { @@ -217,7 +213,6 @@ private int nextSetBit(int fromIndex) { else { return -1; } - } private void setBits(ValueRange range) { @@ -247,24 +242,25 @@ private void setBit(int index) { } private void clearBit(int index) { - this.bits &= ~(1L << index); + this.bits &= ~(1L << index); } - @Override - public int hashCode() { - return Long.hashCode(this.bits); - } @Override - public boolean equals(Object o) { - if (this == o) { + public boolean equals(Object other) { + if (this == other) { return true; } - if (!(o instanceof BitsCronField)) { + if (!(other instanceof BitsCronField)) { return false; } - BitsCronField other = (BitsCronField) o; - return type() == other.type() && this.bits == other.bits; + BitsCronField otherField = (BitsCronField) other; + return (type() == otherField.type() && this.bits == otherField.bits); + } + + @Override + public int hashCode() { + return Long.hashCode(this.bits); } @Override diff --git a/spring-context/src/main/java/org/springframework/scheduling/support/CronExpression.java b/spring-context/src/main/java/org/springframework/scheduling/support/CronExpression.java index e2e6687055e3..7edabd8fa84b 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/support/CronExpression.java +++ b/spring-context/src/main/java/org/springframework/scheduling/support/CronExpression.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-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,9 +29,14 @@ * crontab expression * that can calculate the next time it matches. * - *

    {@code CronExpression} instances are created through - * {@link #parse(String)}; the next match is determined with - * {@link #next(Temporal)}. + *

    {@code CronExpression} instances are created through {@link #parse(String)}; + * the next match is determined with {@link #next(Temporal)}. + * + *

    Supports a Quartz day-of-month/week field with an L/# expression. Follows + * common cron conventions in every other respect, including 0-6 for SUN-SAT + * (plus 7 for SUN as well). Note that Quartz deviates from the day-of-week + * convention in cron through 1-7 for SUN-SAT whereas Spring strictly follows + * cron even in combination with the optional Quartz-specific L/# expressions. * * @author Arjen Poutsma * @since 5.3 @@ -57,18 +62,12 @@ public final class CronExpression { private final String expression; - private CronExpression( - CronField seconds, - CronField minutes, - CronField hours, - CronField daysOfMonth, - CronField months, - CronField daysOfWeek, - String expression) { + private CronExpression(CronField seconds, CronField minutes, CronField hours, + CronField daysOfMonth, CronField months, CronField daysOfWeek, String expression) { - // reverse order, to make big changes first - // to make sure we end up at 0 nanos, we add an extra field - this.fields = new CronField[]{daysOfWeek, months, daysOfMonth, hours, minutes, seconds, CronField.zeroNanos()}; + // Reverse order, to make big changes first. + // To make sure we end up at 0 nanos, we add an extra field. + this.fields = new CronField[] {daysOfWeek, months, daysOfMonth, hours, minutes, seconds, CronField.zeroNanos()}; this.expression = expression; } @@ -121,11 +120,8 @@ private CronExpression( * {@code LW}), it means "the last weekday of the month". * *

  11. - * In the "day of week" field, {@code L} stands for "the last day of the - * week". - * If prefixed by a number or three-letter name (i.e. {@code dL} or - * {@code DDDL}), it means "the last day of week {@code d} (or {@code DDD}) - * in the month". + * In the "day of week" field, {@code dL} or {@code DDDL} stands for + * "the last day of week {@code d} (or {@code DDD}) in the month". *
  12. * * @@ -177,7 +173,7 @@ private CronExpression( * the cron format */ public static CronExpression parse(String expression) { - Assert.hasLength(expression, "Expression string must not be empty"); + Assert.hasLength(expression, "Expression must not be empty"); expression = resolveMacros(expression); @@ -271,27 +267,18 @@ private > T nextOrSameInternal(T temp @Override - public int hashCode() { - return Arrays.hashCode(this.fields); + public boolean equals(@Nullable Object other) { + return (this == other || (other instanceof CronExpression && + Arrays.equals(this.fields, ((CronExpression) other).fields))); } @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o instanceof CronExpression) { - CronExpression other = (CronExpression) o; - return Arrays.equals(this.fields, other.fields); - } - else { - return false; - } + public int hashCode() { + return Arrays.hashCode(this.fields); } /** * Return the expression string used to create this {@code CronExpression}. - * @return the expression string */ @Override public String toString() { diff --git a/spring-context/src/main/java/org/springframework/scheduling/support/CronField.java b/spring-context/src/main/java/org/springframework/scheduling/support/CronField.java index 99d940613e5c..3124cc25b5e5 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/support/CronField.java +++ b/spring-context/src/main/java/org/springframework/scheduling/support/CronField.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-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,17 +29,24 @@ /** * Single field in a cron pattern. Created using the {@code parse*} methods, - * main and only entry point is {@link #nextOrSame(Temporal)}. + * the main and only entry point is {@link #nextOrSame(Temporal)}. + * + *

    Supports a Quartz day-of-month/week field with an L/# expression. Follows + * common cron conventions in every other respect, including 0-6 for SUN-SAT + * (plus 7 for SUN as well). Note that Quartz deviates from the day-of-week + * convention in cron through 1-7 for SUN-SAT whereas Spring strictly follows + * cron even in combination with the optional Quartz-specific L/# expressions. * * @author Arjen Poutsma * @since 5.3 */ abstract class CronField { - private static final String[] MONTHS = new String[]{"JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", - "OCT", "NOV", "DEC"}; + private static final String[] MONTHS = new String[] + {"JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"}; - private static final String[] DAYS = new String[]{"MON", "TUE", "WED", "THU", "FRI", "SAT", "SUN"}; + private static final String[] DAYS = new String[] + {"MON", "TUE", "WED", "THU", "FRI", "SAT", "SUN"}; private final Type type; @@ -48,11 +55,12 @@ protected CronField(Type type) { this.type = type; } + /** * Return a {@code CronField} enabled for 0 nanoseconds. */ public static CronField zeroNanos() { - return BitsCronField.zeroNanos(); + return BitsCronField.ZERO_NANOS; } /** @@ -169,6 +177,7 @@ protected static > T cast(Temporal te * day-of-month, month, day-of-week. */ protected enum Type { + NANO(ChronoField.NANO_OF_SECOND, ChronoUnit.SECONDS), SECOND(ChronoField.SECOND_OF_MINUTE, ChronoUnit.MINUTES, ChronoField.NANO_OF_SECOND), MINUTE(ChronoField.MINUTE_OF_HOUR, ChronoUnit.HOURS, ChronoField.SECOND_OF_MINUTE, ChronoField.NANO_OF_SECOND), @@ -177,21 +186,18 @@ protected enum Type { MONTH(ChronoField.MONTH_OF_YEAR, ChronoUnit.YEARS, ChronoField.DAY_OF_MONTH, ChronoField.HOUR_OF_DAY, ChronoField.MINUTE_OF_HOUR, ChronoField.SECOND_OF_MINUTE, ChronoField.NANO_OF_SECOND), DAY_OF_WEEK(ChronoField.DAY_OF_WEEK, ChronoUnit.WEEKS, ChronoField.HOUR_OF_DAY, ChronoField.MINUTE_OF_HOUR, ChronoField.SECOND_OF_MINUTE, ChronoField.NANO_OF_SECOND); - private final ChronoField field; private final ChronoUnit higherOrder; private final ChronoField[] lowerOrders; - Type(ChronoField field, ChronoUnit higherOrder, ChronoField... lowerOrders) { this.field = field; this.higherOrder = higherOrder; this.lowerOrders = lowerOrders; } - /** * Return the value of this type for the given temporal. * @return the value of this type diff --git a/spring-context/src/main/java/org/springframework/scheduling/support/CronTrigger.java b/spring-context/src/main/java/org/springframework/scheduling/support/CronTrigger.java index 6581cdd61fe0..7496d18f9d8e 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/support/CronTrigger.java +++ b/spring-context/src/main/java/org/springframework/scheduling/support/CronTrigger.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-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,8 +27,14 @@ import org.springframework.util.Assert; /** - * {@link Trigger} implementation for cron expressions. - * Wraps a {@link CronExpression}. + * {@link Trigger} implementation for cron expressions. Wraps a + * {@link CronExpression} which parses according to common crontab conventions. + * + *

    Supports a Quartz day-of-month/week field with an L/# expression. Follows + * common cron conventions in every other respect, including 0-6 for SUN-SAT + * (plus 7 for SUN as well). Note that Quartz deviates from the day-of-week + * convention in cron through 1-7 for SUN-SAT whereas Spring strictly follows + * cron even in combination with the optional Quartz-specific L/# expressions. * * @author Juergen Hoeller * @author Arjen Poutsma @@ -39,6 +45,7 @@ public class CronTrigger implements Trigger { private final CronExpression expression; + @Nullable private final ZoneId zoneId; @@ -48,7 +55,8 @@ public class CronTrigger implements Trigger { * expression conventions */ public CronTrigger(String expression) { - this(expression, ZoneId.systemDefault()); + this.expression = CronExpression.parse(expression); + this.zoneId = null; } /** @@ -58,7 +66,9 @@ public CronTrigger(String expression) { * @param timeZone a time zone in which the trigger times will be generated */ public CronTrigger(String expression, TimeZone timeZone) { - this(expression, timeZone.toZoneId()); + this.expression = CronExpression.parse(expression); + Assert.notNull(timeZone, "TimeZone must not be null"); + this.zoneId = timeZone.toZoneId(); } /** @@ -70,10 +80,8 @@ public CronTrigger(String expression, TimeZone timeZone) { * @see CronExpression#parse(String) */ public CronTrigger(String expression, ZoneId zoneId) { - Assert.hasLength(expression, "Expression must not be empty"); - Assert.notNull(zoneId, "ZoneId must not be null"); - this.expression = CronExpression.parse(expression); + Assert.notNull(zoneId, "ZoneId must not be null"); this.zoneId = zoneId; } @@ -94,22 +102,23 @@ public String getExpression() { */ @Override public Date nextExecutionTime(TriggerContext triggerContext) { - Date date = triggerContext.lastCompletionTime(); - if (date != null) { + Date timestamp = triggerContext.lastCompletionTime(); + if (timestamp != null) { Date scheduled = triggerContext.lastScheduledExecutionTime(); - if (scheduled != null && date.before(scheduled)) { + if (scheduled != null && timestamp.before(scheduled)) { // Previous task apparently executed too early... // Let's simply use the last calculated execution time then, // in order to prevent accidental re-fires in the same second. - date = scheduled; + timestamp = scheduled; } } else { - date = new Date(triggerContext.getClock().millis()); + timestamp = new Date(triggerContext.getClock().millis()); } - ZonedDateTime dateTime = ZonedDateTime.ofInstant(date.toInstant(), this.zoneId); - ZonedDateTime next = this.expression.next(dateTime); - return (next != null ? Date.from(next.toInstant()) : null); + ZoneId zone = (this.zoneId != null ? this.zoneId : triggerContext.getClock().getZone()); + ZonedDateTime zonedTimestamp = ZonedDateTime.ofInstant(timestamp.toInstant(), zone); + ZonedDateTime nextTimestamp = this.expression.next(zonedTimestamp); + return (nextTimestamp != null ? Date.from(nextTimestamp.toInstant()) : null); } diff --git a/spring-context/src/main/java/org/springframework/scheduling/support/QuartzCronField.java b/spring-context/src/main/java/org/springframework/scheduling/support/QuartzCronField.java index 36c70c088312..85700de710d0 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/support/QuartzCronField.java +++ b/spring-context/src/main/java/org/springframework/scheduling/support/QuartzCronField.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-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,10 +29,16 @@ /** * Extension of {@link CronField} for - * Quartz-specific fields. * Created using the {@code parse*} methods, uses a {@link TemporalAdjuster} * internally. * + *

    Supports a Quartz day-of-month/week field with an L/# expression. Follows + * common cron conventions in every other respect, including 0-6 for SUN-SAT + * (plus 7 for SUN as well). Note that Quartz deviates from the day-of-week + * convention in cron through 1-7 for SUN-SAT whereas Spring strictly follows + * cron even in combination with the optional Quartz-specific L/# expressions. + * * @author Arjen Poutsma * @since 5.3 */ @@ -60,16 +66,18 @@ private QuartzCronField(Type type, Type rollForwardType, TemporalAdjuster adjust this.rollForwardType = rollForwardType; } + /** - * Returns whether the given value is a Quartz day-of-month field. + * Determine whether the given value is a Quartz day-of-month field. */ public static boolean isQuartzDaysOfMonthField(String value) { return value.contains("L") || value.contains("W"); } /** - * Parse the given value into a days of months {@code QuartzCronField}, the fourth entry of a cron expression. - * Expects a "L" or "W" in the given value. + * Parse the given value into a days of months {@code QuartzCronField}, + * the fourth entry of a cron expression. + *

    Expects a "L" or "W" in the given value. */ public static QuartzCronField parseDaysOfMonth(String value) { int idx = value.lastIndexOf('L'); @@ -78,14 +86,14 @@ public static QuartzCronField parseDaysOfMonth(String value) { if (idx != 0) { throw new IllegalArgumentException("Unrecognized characters before 'L' in '" + value + "'"); } - else if (value.length() == 2 && value.charAt(1) == 'W') { // "LW" + else if (value.length() == 2 && value.charAt(1) == 'W') { // "LW" adjuster = lastWeekdayOfMonth(); } else { - if (value.length() == 1) { // "L" + if (value.length() == 1) { // "L" adjuster = lastDayOfMonth(); } - else { // "L-[0-9]+" + else { // "L-[0-9]+" int offset = Integer.parseInt(value.substring(idx + 1)); if (offset >= 0) { throw new IllegalArgumentException("Offset '" + offset + " should be < 0 '" + value + "'"); @@ -103,7 +111,7 @@ else if (value.length() == 2 && value.charAt(1) == 'W') { // "LW" else if (idx != value.length() - 1) { throw new IllegalArgumentException("Unrecognized characters after 'W' in '" + value + "'"); } - else { // "[0-9]+W" + else { // "[0-9]+W" int dayOfMonth = Integer.parseInt(value.substring(0, idx)); dayOfMonth = Type.DAY_OF_MONTH.checkValidValue(dayOfMonth); TemporalAdjuster adjuster = weekdayNearestTo(dayOfMonth); @@ -114,15 +122,16 @@ else if (idx != value.length() - 1) { } /** - * Returns whether the given value is a Quartz day-of-week field. + * Determine whether the given value is a Quartz day-of-week field. */ public static boolean isQuartzDaysOfWeekField(String value) { return value.contains("L") || value.contains("#"); } /** - * Parse the given value into a days of week {@code QuartzCronField}, the sixth entry of a cron expression. - * Expects a "L" or "#" in the given value. + * Parse the given value into a days of week {@code QuartzCronField}, + * the sixth entry of a cron expression. + *

    Expects a "L" or "#" in the given value. */ public static QuartzCronField parseDaysOfWeek(String value) { int idx = value.lastIndexOf('L'); @@ -135,7 +144,7 @@ public static QuartzCronField parseDaysOfWeek(String value) { if (idx == 0) { throw new IllegalArgumentException("No day-of-week before 'L' in '" + value + "'"); } - else { // "[0-7]L" + else { // "[0-7]L" DayOfWeek dayOfWeek = parseDayOfWeek(value.substring(0, idx)); adjuster = lastInMonth(dayOfWeek); } @@ -157,7 +166,6 @@ else if (idx == value.length() - 1) { throw new IllegalArgumentException("Ordinal '" + ordinal + "' in '" + value + "' must be positive number "); } - TemporalAdjuster adjuster = dayOfWeekInMonth(ordinal, dayOfWeek); return new QuartzCronField(Type.DAY_OF_WEEK, Type.DAY_OF_MONTH, adjuster, value); } @@ -167,14 +175,13 @@ else if (idx == value.length() - 1) { private static DayOfWeek parseDayOfWeek(String value) { int dayOfWeek = Integer.parseInt(value); if (dayOfWeek == 0) { - dayOfWeek = 7; // cron is 0 based; java.time 1 based + dayOfWeek = 7; // cron is 0 based; java.time 1 based } try { return DayOfWeek.of(dayOfWeek); } catch (DateTimeException ex) { - String msg = ex.getMessage() + " '" + value + "'"; - throw new IllegalArgumentException(msg, ex); + throw new IllegalArgumentException(ex.getMessage() + " '" + value + "'", ex); } } @@ -213,10 +220,10 @@ private static TemporalAdjuster lastWeekdayOfMonth() { Temporal lastDom = adjuster.adjustInto(temporal); Temporal result; int dow = lastDom.get(ChronoField.DAY_OF_WEEK); - if (dow == 6) { // Saturday + if (dow == 6) { // Saturday result = lastDom.minus(1, ChronoUnit.DAYS); } - else if (dow == 7) { // Sunday + else if (dow == 7) { // Sunday result = lastDom.minus(2, ChronoUnit.DAYS); } else { @@ -227,7 +234,7 @@ else if (dow == 7) { // Sunday } /** - * Return a temporal adjuster that finds the nth-to-last day of the month. + * Returns a temporal adjuster that finds the nth-to-last day of the month. * @param offset the negative offset, i.e. -3 means third-to-last * @return a nth-to-last day-of-month adjuster */ @@ -241,7 +248,7 @@ private static TemporalAdjuster lastDayWithOffset(int offset) { } /** - * Return a temporal adjuster that finds the weekday nearest to the given + * Returns a temporal adjuster that finds the weekday nearest to the given * day-of-month. If {@code dayOfMonth} falls on a Saturday, the date is * moved back to Friday; if it falls on a Sunday (or if {@code dayOfMonth} * is 1 and it falls on a Saturday), it is moved forward to Monday. @@ -253,10 +260,10 @@ private static TemporalAdjuster weekdayNearestTo(int dayOfMonth) { int current = Type.DAY_OF_MONTH.get(temporal); DayOfWeek dayOfWeek = DayOfWeek.from(temporal); - if ((current == dayOfMonth && isWeekday(dayOfWeek)) || // dayOfMonth is a weekday - (dayOfWeek == DayOfWeek.FRIDAY && current == dayOfMonth - 1) || // dayOfMonth is a Saturday, so Friday before - (dayOfWeek == DayOfWeek.MONDAY && current == dayOfMonth + 1) || // dayOfMonth is a Sunday, so Monday after - (dayOfWeek == DayOfWeek.MONDAY && dayOfMonth == 1 && current == 3)) { // dayOfMonth is Saturday 1st, so Monday 3rd + if ((current == dayOfMonth && isWeekday(dayOfWeek)) || // dayOfMonth is a weekday + (dayOfWeek == DayOfWeek.FRIDAY && current == dayOfMonth - 1) || // dayOfMonth is a Saturday, so Friday before + (dayOfWeek == DayOfWeek.MONDAY && current == dayOfMonth + 1) || // dayOfMonth is a Sunday, so Monday after + (dayOfWeek == DayOfWeek.MONDAY && dayOfMonth == 1 && current == 3)) { // dayOfMonth is Saturday 1st, so Monday 3rd return temporal; } int count = 0; @@ -292,7 +299,7 @@ private static boolean isWeekday(DayOfWeek dayOfWeek) { } /** - * Return a temporal adjuster that finds the last of the given doy-of-week + * Returns a temporal adjuster that finds the last of the given day-of-week * in a month. */ private static TemporalAdjuster lastInMonth(DayOfWeek dayOfWeek) { @@ -329,6 +336,7 @@ private static Temporal rollbackToMidnight(Temporal current, Temporal result) { } } + @Override public > T nextOrSame(T temporal) { T result = adjust(temporal); @@ -345,7 +353,6 @@ public > T nextOrSame(T temporal) { return result; } - @Nullable @SuppressWarnings("unchecked") private > T adjust(T temporal) { @@ -354,27 +361,25 @@ private > T adjust(T temporal) { @Override - public int hashCode() { - return this.value.hashCode(); - } - - @Override - public boolean equals(Object o) { - if (this == o) { + public boolean equals(@Nullable Object other) { + if (this == other) { return true; } - if (!(o instanceof QuartzCronField)) { + if (!(other instanceof QuartzCronField)) { return false; } - QuartzCronField other = (QuartzCronField) o; - return type() == other.type() && - this.value.equals(other.value); + QuartzCronField otherField = (QuartzCronField) other; + return (type() == otherField.type() && this.value.equals(otherField.value)); + } + + @Override + public int hashCode() { + return this.value.hashCode(); } @Override public String toString() { return type() + " '" + this.value + "'"; - } } diff --git a/spring-context/src/main/java/org/springframework/validation/AbstractBindingResult.java b/spring-context/src/main/java/org/springframework/validation/AbstractBindingResult.java index b8857b15b50b..18278e009d90 100644 --- a/spring-context/src/main/java/org/springframework/validation/AbstractBindingResult.java +++ b/spring-context/src/main/java/org/springframework/validation/AbstractBindingResult.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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. @@ -102,8 +102,8 @@ public void reject(String errorCode, @Nullable Object[] errorArgs, @Nullable Str } @Override - public void rejectValue(@Nullable String field, String errorCode, @Nullable Object[] errorArgs, - @Nullable String defaultMessage) { + public void rejectValue(@Nullable String field, String errorCode, + @Nullable Object[] errorArgs, @Nullable String defaultMessage) { if (!StringUtils.hasLength(getNestedPath()) && !StringUtils.hasLength(field)) { // We're at the top of the nested object hierarchy, diff --git a/spring-context/src/main/java/org/springframework/validation/AbstractErrors.java b/spring-context/src/main/java/org/springframework/validation/AbstractErrors.java index 9556dc3a286e..b9e5617a53c6 100644 --- a/spring-context/src/main/java/org/springframework/validation/AbstractErrors.java +++ b/spring-context/src/main/java/org/springframework/validation/AbstractErrors.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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. @@ -28,13 +28,14 @@ import org.springframework.util.StringUtils; /** - * Abstract implementation of the {@link Errors} interface. Provides common - * access to evaluated errors; however, does not define concrete management + * Abstract implementation of the {@link Errors} interface. + * Provides nested path handling but does not define concrete management * of {@link ObjectError ObjectErrors} and {@link FieldError FieldErrors}. * * @author Juergen Hoeller * @author Rossen Stoyanchev * @since 2.5.3 + * @see AbstractBindingResult */ @SuppressWarnings("serial") public abstract class AbstractErrors implements Errors, Serializable { @@ -81,8 +82,8 @@ protected void doSetNestedPath(@Nullable String nestedPath) { nestedPath = ""; } nestedPath = canonicalFieldName(nestedPath); - if (nestedPath.length() > 0 && !nestedPath.endsWith(Errors.NESTED_PATH_SEPARATOR)) { - nestedPath += Errors.NESTED_PATH_SEPARATOR; + if (nestedPath.length() > 0 && !nestedPath.endsWith(NESTED_PATH_SEPARATOR)) { + nestedPath += NESTED_PATH_SEPARATOR; } this.nestedPath = nestedPath; } @@ -97,7 +98,7 @@ protected String fixedField(@Nullable String field) { } else { String path = getNestedPath(); - return (path.endsWith(Errors.NESTED_PATH_SEPARATOR) ? + return (path.endsWith(NESTED_PATH_SEPARATOR) ? path.substring(0, path.length() - NESTED_PATH_SEPARATOR.length()) : path); } } @@ -201,9 +202,9 @@ public List getFieldErrors(String field) { List fieldErrors = getFieldErrors(); List result = new ArrayList<>(); String fixedField = fixedField(field); - for (FieldError error : fieldErrors) { - if (isMatchingFieldError(fixedField, error)) { - result.add(error); + for (FieldError fieldError : fieldErrors) { + if (isMatchingFieldError(fixedField, fieldError)) { + result.add(fieldError); } } return Collections.unmodifiableList(result); diff --git a/spring-context/src/main/java/org/springframework/validation/BeanPropertyBindingResult.java b/spring-context/src/main/java/org/springframework/validation/BeanPropertyBindingResult.java index 8558a2619c97..1cc6bb8e3654 100644 --- a/spring-context/src/main/java/org/springframework/validation/BeanPropertyBindingResult.java +++ b/spring-context/src/main/java/org/springframework/validation/BeanPropertyBindingResult.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2023 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. @@ -55,7 +55,7 @@ public class BeanPropertyBindingResult extends AbstractPropertyBindingResult imp /** - * Creates a new instance of the {@link BeanPropertyBindingResult} class. + * Create a new {@code BeanPropertyBindingResult} for the given target. * @param target the target bean to bind onto * @param objectName the name of the target object */ @@ -64,7 +64,7 @@ public BeanPropertyBindingResult(@Nullable Object target, String objectName) { } /** - * Creates a new instance of the {@link BeanPropertyBindingResult} class. + * Create a new {@code BeanPropertyBindingResult} for the given target. * @param target the target bean to bind onto * @param objectName the name of the target object * @param autoGrowNestedPaths whether to "auto-grow" a nested path that contains a null value diff --git a/spring-context/src/main/java/org/springframework/validation/BindException.java b/spring-context/src/main/java/org/springframework/validation/BindException.java index afc0c5c2ae71..b84c81081a87 100644 --- a/spring-context/src/main/java/org/springframework/validation/BindException.java +++ b/spring-context/src/main/java/org/springframework/validation/BindException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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. @@ -128,7 +128,9 @@ public void rejectValue(@Nullable String field, String errorCode, String default } @Override - public void rejectValue(@Nullable String field, String errorCode, @Nullable Object[] errorArgs, @Nullable String defaultMessage) { + public void rejectValue(@Nullable String field, String errorCode, + @Nullable Object[] errorArgs, @Nullable String defaultMessage) { + this.bindingResult.rejectValue(field, errorCode, errorArgs, defaultMessage); } diff --git a/spring-context/src/main/java/org/springframework/validation/DirectFieldBindingResult.java b/spring-context/src/main/java/org/springframework/validation/DirectFieldBindingResult.java index 5ad401de5bec..f01232ca1f6f 100644 --- a/spring-context/src/main/java/org/springframework/validation/DirectFieldBindingResult.java +++ b/spring-context/src/main/java/org/springframework/validation/DirectFieldBindingResult.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2023 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. @@ -46,7 +46,7 @@ public class DirectFieldBindingResult extends AbstractPropertyBindingResult { /** - * Create a new DirectFieldBindingResult instance. + * Create a new {@code DirectFieldBindingResult} for the given target. * @param target the target object to bind onto * @param objectName the name of the target object */ @@ -55,7 +55,7 @@ public DirectFieldBindingResult(@Nullable Object target, String objectName) { } /** - * Create a new DirectFieldBindingResult instance. + * Create a new {@code DirectFieldBindingResult} for the given target. * @param target the target object to bind onto * @param objectName the name of the target object * @param autoGrowNestedPaths whether to "auto-grow" a nested path that contains a null value diff --git a/spring-context/src/main/java/org/springframework/validation/Errors.java b/spring-context/src/main/java/org/springframework/validation/Errors.java index 45ae5d7b57af..18a7bc1910af 100644 --- a/spring-context/src/main/java/org/springframework/validation/Errors.java +++ b/spring-context/src/main/java/org/springframework/validation/Errors.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2023 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,24 +22,24 @@ import org.springframework.lang.Nullable; /** - * Stores and exposes information about data-binding and validation - * errors for a specific object. + * Stores and exposes information about data-binding and validation errors + * for a specific object. * - *

    Field names can be properties of the target object (e.g. "name" - * when binding to a customer object), or nested fields in case of - * subobjects (e.g. "address.street"). Supports subtree navigation - * via {@link #setNestedPath(String)}: for example, an - * {@code AddressValidator} validates "address", not being aware - * that this is a subobject of customer. + *

    Field names are typically properties of the target object (e.g. "name" + * when binding to a customer object). Implementations may also support nested + * fields in case of nested objects (e.g. "address.street"), in conjunction + * with subtree navigation via {@link #setNestedPath}: for example, an + * {@code AddressValidator} may validate "address", not being aware that this + * is a nested object of a top-level customer object. * *

    Note: {@code Errors} objects are single-threaded. * * @author Rod Johnson * @author Juergen Hoeller - * @see #setNestedPath - * @see BindException - * @see DataBinder + * @see Validator * @see ValidationUtils + * @see BindException + * @see BindingResult */ public interface Errors { @@ -66,6 +66,7 @@ public interface Errors { * @param nestedPath nested path within this object, * e.g. "address" (defaults to "", {@code null} is also acceptable). * Can end with a dot: both "address" and "address." are valid. + * @see #getNestedPath() */ void setNestedPath(String nestedPath); @@ -73,6 +74,7 @@ public interface Errors { * Return the current nested path of this {@link Errors} object. *

    Returns a nested path with a dot, i.e. "address.", for easy * building of concatenated paths. Default is an empty String. + * @see #setNestedPath(String) */ String getNestedPath(); @@ -86,14 +88,14 @@ public interface Errors { *

    For example: current path "spouse.", pushNestedPath("child") → * result path "spouse.child."; popNestedPath() → "spouse." again. * @param subPath the sub path to push onto the nested path stack - * @see #popNestedPath + * @see #popNestedPath() */ void pushNestedPath(String subPath); /** * Pop the former nested path from the nested path stack. * @throws IllegalStateException if there is no former nested path on the stack - * @see #pushNestedPath + * @see #pushNestedPath(String) */ void popNestedPath() throws IllegalStateException; @@ -101,6 +103,7 @@ public interface Errors { * Register a global error for the entire target object, * using the given error description. * @param errorCode error code, interpretable as a message key + * @see #reject(String, Object[], String) */ void reject(String errorCode); @@ -109,6 +112,7 @@ public interface Errors { * using the given error description. * @param errorCode error code, interpretable as a message key * @param defaultMessage fallback default message + * @see #reject(String, Object[], String) */ void reject(String errorCode, String defaultMessage); @@ -119,6 +123,7 @@ public interface Errors { * @param errorArgs error arguments, for argument binding via MessageFormat * (can be {@code null}) * @param defaultMessage fallback default message + * @see #rejectValue(String, String, Object[], String) */ void reject(String errorCode, @Nullable Object[] errorArgs, @Nullable String defaultMessage); @@ -132,7 +137,7 @@ public interface Errors { * global error if the current object is the top object. * @param field the field name (may be {@code null} or empty String) * @param errorCode error code, interpretable as a message key - * @see #getNestedPath() + * @see #rejectValue(String, String, Object[], String) */ void rejectValue(@Nullable String field, String errorCode); @@ -147,7 +152,7 @@ public interface Errors { * @param field the field name (may be {@code null} or empty String) * @param errorCode error code, interpretable as a message key * @param defaultMessage fallback default message - * @see #getNestedPath() + * @see #rejectValue(String, String, Object[], String) */ void rejectValue(@Nullable String field, String errorCode, String defaultMessage); @@ -164,7 +169,7 @@ public interface Errors { * @param errorArgs error arguments, for argument binding via MessageFormat * (can be {@code null}) * @param defaultMessage fallback default message - * @see #getNestedPath() + * @see #reject(String, Object[], String) */ void rejectValue(@Nullable String field, String errorCode, @Nullable Object[] errorArgs, @Nullable String defaultMessage); @@ -179,35 +184,40 @@ void rejectValue(@Nullable String field, String errorCode, * to refer to the same target object, or at least contain compatible errors * that apply to the target object of this {@code Errors} instance. * @param errors the {@code Errors} instance to merge in + * @see #getAllErrors() */ void addAllErrors(Errors errors); /** - * Return if there were any errors. + * Determine if there were any errors. + * @see #hasGlobalErrors() + * @see #hasFieldErrors() */ boolean hasErrors(); /** - * Return the total number of errors. + * Determine the total number of errors. + * @see #getGlobalErrorCount() + * @see #getFieldErrorCount() */ int getErrorCount(); /** * Get all errors, both global and field ones. - * @return a list of {@link ObjectError} instances + * @return a list of {@link ObjectError}/{@link FieldError} instances + * @see #getGlobalErrors() + * @see #getFieldErrors() */ List getAllErrors(); /** - * Are there any global errors? - * @return {@code true} if there are any global errors + * Determine if there were any global errors. * @see #hasFieldErrors() */ boolean hasGlobalErrors(); /** - * Return the number of global errors. - * @return the number of global errors + * Determine the number of global errors. * @see #getFieldErrorCount() */ int getGlobalErrorCount(); @@ -215,26 +225,26 @@ void rejectValue(@Nullable String field, String errorCode, /** * Get all global errors. * @return a list of {@link ObjectError} instances + * @see #getFieldErrors() */ List getGlobalErrors(); /** * Get the first global error, if any. * @return the global error, or {@code null} + * @see #getFieldError() */ @Nullable ObjectError getGlobalError(); /** - * Are there any field errors? - * @return {@code true} if there are any errors associated with a field + * Determine if there were any errors associated with a field. * @see #hasGlobalErrors() */ boolean hasFieldErrors(); /** - * Return the number of errors associated with a field. - * @return the number of errors associated with a field + * Determine the number of errors associated with a field. * @see #getGlobalErrorCount() */ int getFieldErrorCount(); @@ -242,36 +252,39 @@ void rejectValue(@Nullable String field, String errorCode, /** * Get all errors associated with a field. * @return a List of {@link FieldError} instances + * @see #getGlobalErrors() */ List getFieldErrors(); /** * Get the first error associated with a field, if any. * @return the field-specific error, or {@code null} + * @see #getGlobalError() */ @Nullable FieldError getFieldError(); /** - * Are there any errors associated with the given field? + * Determine if there were any errors associated with the given field. * @param field the field name - * @return {@code true} if there were any errors associated with the given field + * @see #hasFieldErrors() */ boolean hasFieldErrors(String field); /** - * Return the number of errors associated with the given field. + * Determine the number of errors associated with the given field. * @param field the field name - * @return the number of errors associated with the given field + * @see #getFieldErrorCount() */ int getFieldErrorCount(String field); /** * Get all errors associated with the given field. - *

    Implementations should support not only full field names like - * "name" but also pattern matches like "na*" or "address.*". + *

    Implementations may support not only full field names like + * "address.street" but also pattern matches like "address.*". * @param field the field name * @return a List of {@link FieldError} instances + * @see #getFieldErrors() */ List getFieldErrors(String field); @@ -279,6 +292,7 @@ void rejectValue(@Nullable String field, String errorCode, * Get the first error associated with the given field, if any. * @param field the field name * @return the field-specific error, or {@code null} + * @see #getFieldError() */ @Nullable FieldError getFieldError(String field); @@ -290,17 +304,19 @@ void rejectValue(@Nullable String field, String errorCode, * even if there were type mismatches. * @param field the field name * @return the current value of the given field + * @see #getFieldType(String) */ @Nullable Object getFieldValue(String field); /** - * Return the type of a given field. + * Determine the type of the given field, as far as possible. *

    Implementations should be able to determine the type even * when the field value is {@code null}, for example from some * associated descriptor. * @param field the field name * @return the type of the field, or {@code null} if not determinable + * @see #getFieldValue(String) */ @Nullable Class getFieldType(String field); diff --git a/spring-context/src/main/java/org/springframework/validation/FieldError.java b/spring-context/src/main/java/org/springframework/validation/FieldError.java index a6cd51aa41c5..e8b03717cf1d 100644 --- a/spring-context/src/main/java/org/springframework/validation/FieldError.java +++ b/spring-context/src/main/java/org/springframework/validation/FieldError.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2023 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. @@ -124,6 +124,8 @@ public int hashCode() { @Override public String toString() { + // We would preferably use ObjectUtils.nullSafeConciseToString(rejectedValue) here but + // keep including the full nullSafeToString representation for backwards compatibility. return "Field error in object '" + getObjectName() + "' on field '" + this.field + "': rejected value [" + ObjectUtils.nullSafeToString(this.rejectedValue) + "]; " + resolvableToString(); diff --git a/spring-context/src/main/java/org/springframework/validation/Validator.java b/spring-context/src/main/java/org/springframework/validation/Validator.java index b67b6d5d8b77..2aa282396358 100644 --- a/spring-context/src/main/java/org/springframework/validation/Validator.java +++ b/spring-context/src/main/java/org/springframework/validation/Validator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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. @@ -54,14 +54,14 @@ * } * } * - *

    See also the Spring reference manual for a fuller discussion of - * the {@code Validator} interface and its role in an enterprise - * application. + *

    See also the Spring reference manual for a fuller discussion of the + * {@code Validator} interface and its role in an enterprise application. * * @author Rod Johnson * @see SmartValidator * @see Errors * @see ValidationUtils + * @see DataBinder#setValidator */ public interface Validator { @@ -81,11 +81,14 @@ public interface Validator { boolean supports(Class clazz); /** - * Validate the supplied {@code target} object, which must be - * of a {@link Class} for which the {@link #supports(Class)} method - * typically has (or would) return {@code true}. + * Validate the given {@code target} object which must be of a + * {@link Class} for which the {@link #supports(Class)} method + * typically has returned (or would return) {@code true}. *

    The supplied {@link Errors errors} instance can be used to report - * any resulting validation errors. + * any resulting validation errors, typically as part of a larger + * binding process which this validator is meant to participate in. + * Binding errors have typically been pre-registered with the + * {@link Errors errors} instance before this invocation already. * @param target the object that is to be validated * @param errors contextual state about the validation process * @see ValidationUtils diff --git a/spring-context/src/main/java/org/springframework/validation/annotation/ValidationAnnotationUtils.java b/spring-context/src/main/java/org/springframework/validation/annotation/ValidationAnnotationUtils.java index 196f6fc6c74e..0842b812ba0e 100644 --- a/spring-context/src/main/java/org/springframework/validation/annotation/ValidationAnnotationUtils.java +++ b/spring-context/src/main/java/org/springframework/validation/annotation/ValidationAnnotationUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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,37 +26,46 @@ * Mainly for internal use within the framework. * * @author Christoph Dreis + * @author Juergen Hoeller * @since 5.3.7 */ public abstract class ValidationAnnotationUtils { private static final Object[] EMPTY_OBJECT_ARRAY = new Object[0]; + /** * Determine any validation hints by the given annotation. - *

    This implementation checks for {@code @javax.validation.Valid}, - * Spring's {@link org.springframework.validation.annotation.Validated}, - * and custom annotations whose name starts with "Valid". + *

    This implementation checks for Spring's + * {@link org.springframework.validation.annotation.Validated}, + * {@code @javax.validation.Valid}, and custom annotations whose + * name starts with "Valid" which may optionally declare validation + * hints through the "value" attribute. * @param ann the annotation (potentially a validation annotation) * @return the validation hints to apply (possibly an empty array), * or {@code null} if this annotation does not trigger any validation */ @Nullable public static Object[] determineValidationHints(Annotation ann) { + // Direct presence of @Validated ? + if (ann instanceof Validated) { + return ((Validated) ann).value(); + } + // Direct presence of @Valid ? Class annotationType = ann.annotationType(); - String annotationName = annotationType.getName(); - if ("javax.validation.Valid".equals(annotationName)) { + if ("javax.validation.Valid".equals(annotationType.getName())) { return EMPTY_OBJECT_ARRAY; } + // Meta presence of @Validated ? Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class); if (validatedAnn != null) { - Object hints = validatedAnn.value(); - return convertValidationHints(hints); + return validatedAnn.value(); } + // Custom validation annotation ? if (annotationType.getSimpleName().startsWith("Valid")) { - Object hints = AnnotationUtils.getValue(ann); - return convertValidationHints(hints); + return convertValidationHints(AnnotationUtils.getValue(ann)); } + // No validation triggered return null; } diff --git a/spring-context/src/main/kotlin/org/springframework/context/support/BeanDefinitionDsl.kt b/spring-context/src/main/kotlin/org/springframework/context/support/BeanDefinitionDsl.kt index f5dfa9186846..265861a273f1 100644 --- a/spring-context/src/main/kotlin/org/springframework/context/support/BeanDefinitionDsl.kt +++ b/spring-context/src/main/kotlin/org/springframework/context/support/BeanDefinitionDsl.kt @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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. @@ -108,7 +108,7 @@ open class BeanDefinitionDsl internal constructor (private val init: BeanDefinit SINGLETON, /** - * Scope constant for the standard singleton scope + * Scope constant for the standard prototype scope * @see org.springframework.beans.factory.config.BeanDefinition.SCOPE_PROTOTYPE */ PROTOTYPE diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/OverloadedAdviceTests.java b/spring-context/src/test/java/org/springframework/aop/aspectj/OverloadedAdviceTests.java index 91597c9ab85c..257b6bf37580 100644 --- a/spring-context/src/test/java/org/springframework/aop/aspectj/OverloadedAdviceTests.java +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/OverloadedAdviceTests.java @@ -28,17 +28,14 @@ * * @author Adrian Colyer * @author Chris Beams + * @author Juergen Hoeller */ class OverloadedAdviceTests { @Test @SuppressWarnings("resource") - void testExceptionOnConfigParsingWithMismatchedAdviceMethod() { - assertThatExceptionOfType(BeanCreationException.class) - .isThrownBy(() -> new ClassPathXmlApplicationContext(getClass().getSimpleName() + ".xml", getClass())) - .havingRootCause() - .isInstanceOf(IllegalArgumentException.class) - .as("invalidAbsoluteTypeName should be detected by AJ").withMessageContaining("invalidAbsoluteTypeName"); + void testConfigParsingWithMismatchedAdviceMethod() { + new ClassPathXmlApplicationContext(getClass().getSimpleName() + ".xml", getClass()); } @Test diff --git a/spring-context/src/test/java/org/springframework/aop/framework/CglibProxyTests.java b/spring-context/src/test/java/org/springframework/aop/framework/CglibProxyTests.java index b1d1cd59438b..1281410e49cd 100644 --- a/spring-context/src/test/java/org/springframework/aop/framework/CglibProxyTests.java +++ b/spring-context/src/test/java/org/springframework/aop/framework/CglibProxyTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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. @@ -28,6 +28,7 @@ import org.springframework.aop.Pointcut; import org.springframework.aop.support.AopUtils; import org.springframework.aop.support.DefaultPointcutAdvisor; +import org.springframework.aop.support.annotation.AnnotationMatchingPointcut; import org.springframework.aop.testfixture.advice.CountingBeforeAdvice; import org.springframework.aop.testfixture.interceptor.NopInterceptor; import org.springframework.beans.testfixture.beans.ITestBean; @@ -35,6 +36,8 @@ import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextException; import org.springframework.context.support.ClassPathXmlApplicationContext; +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; @@ -87,8 +90,7 @@ public void testNoTarget() { AdvisedSupport pc = new AdvisedSupport(ITestBean.class); pc.addAdvice(new NopInterceptor()); AopProxy aop = createAopProxy(pc); - assertThatExceptionOfType(AopConfigException.class).isThrownBy( - aop::getProxy); + assertThatExceptionOfType(AopConfigException.class).isThrownBy(aop::getProxy); } @Test @@ -136,8 +138,8 @@ public void testProxyCanBeClassNotInterface() { Object proxy = aop.getProxy(); assertThat(AopUtils.isCglibProxy(proxy)).isTrue(); - assertThat(proxy instanceof ITestBean).isTrue(); - assertThat(proxy instanceof TestBean).isTrue(); + assertThat(proxy).isInstanceOf(ITestBean.class); + assertThat(proxy).isInstanceOf(TestBean.class); TestBean tb = (TestBean) proxy; assertThat(tb.getAge()).isEqualTo(32); @@ -304,6 +306,8 @@ public void testProxyAProxyWithAdditionalInterface() { CglibAopProxy cglib = new CglibAopProxy(as); ITestBean proxy1 = (ITestBean) cglib.getProxy(); + ITestBean proxy1a = (ITestBean) cglib.getProxy(); + assertThat(proxy1a.getClass()).isSameAs(proxy1.getClass()); mockTargetSource.setTarget(proxy1); as = new AdvisedSupport(new Class[]{}); @@ -312,7 +316,40 @@ public void testProxyAProxyWithAdditionalInterface() { cglib = new CglibAopProxy(as); ITestBean proxy2 = (ITestBean) cglib.getProxy(); - assertThat(proxy2 instanceof Serializable).isTrue(); + assertThat(proxy2).isInstanceOf(Serializable.class); + assertThat(proxy2.getClass()).isNotSameAs(proxy1.getClass()); + + ITestBean proxy2a = (ITestBean) cglib.getProxy(); + assertThat(proxy2a).isInstanceOf(Serializable.class); + assertThat(proxy2a.getClass()).isSameAs(proxy2.getClass()); + + mockTargetSource.setTarget(proxy1); + as = new AdvisedSupport(new Class[]{}); + as.setTargetSource(mockTargetSource); + as.addAdvisor(new DefaultPointcutAdvisor(new AnnotationMatchingPointcut(Nullable.class), new NopInterceptor())); + cglib = new CglibAopProxy(as); + + ITestBean proxy3 = (ITestBean) cglib.getProxy(); + assertThat(proxy3).isInstanceOf(Serializable.class); + assertThat(proxy3.getClass()).isNotSameAs(proxy2.getClass()); + + ITestBean proxy3a = (ITestBean) cglib.getProxy(); + assertThat(proxy3a).isInstanceOf(Serializable.class); + assertThat(proxy3a.getClass()).isSameAs(proxy3.getClass()); + + mockTargetSource.setTarget(proxy1); + as = new AdvisedSupport(new Class[]{}); + as.setTargetSource(mockTargetSource); + as.addAdvisor(new DefaultPointcutAdvisor(new AnnotationMatchingPointcut(NonNull.class), new NopInterceptor())); + cglib = new CglibAopProxy(as); + + ITestBean proxy4 = (ITestBean) cglib.getProxy(); + assertThat(proxy4).isInstanceOf(Serializable.class); + assertThat(proxy4.getClass()).isNotSameAs(proxy3.getClass()); + + ITestBean proxy4a = (ITestBean) cglib.getProxy(); + assertThat(proxy4a).isInstanceOf(Serializable.class); + assertThat(proxy4a.getClass()).isSameAs(proxy4.getClass()); } @Test @@ -331,7 +368,7 @@ public void testExceptionHandling() { proxy.doTest(); } catch (Exception ex) { - assertThat(ex instanceof ApplicationContextException).as("Invalid exception class").isTrue(); + assertThat(ex).as("Invalid exception class").isInstanceOf(ApplicationContextException.class); } assertThat(proxy.isCatchInvoked()).as("Catch was not invoked").isTrue(); diff --git a/spring-context/src/test/java/org/springframework/aop/framework/ProxyFactoryBeanTests.java b/spring-context/src/test/java/org/springframework/aop/framework/ProxyFactoryBeanTests.java index d78ea22909d1..96f6c81da689 100644 --- a/spring-context/src/test/java/org/springframework/aop/framework/ProxyFactoryBeanTests.java +++ b/spring-context/src/test/java/org/springframework/aop/framework/ProxyFactoryBeanTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -92,7 +92,7 @@ public class ProxyFactoryBeanTests { @BeforeEach - public void setUp() throws Exception { + public void setup() throws Exception { DefaultListableBeanFactory parent = new DefaultListableBeanFactory(); parent.registerBeanDefinition("target2", new RootBeanDefinition(TestApplicationListener.class)); this.factory = new DefaultListableBeanFactory(parent); @@ -139,22 +139,24 @@ public void testDoubleTargetSourcesAreRejected() { private void testDoubleTargetSourceIsRejected(String name) { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions(new ClassPathResource(DBL_TARGETSOURCE_CONTEXT, CLASS)); + assertThatExceptionOfType(BeanCreationException.class).as("Should not allow TargetSource to be specified in interceptorNames as well as targetSource property") - .isThrownBy(() -> bf.getBean(name)) - .havingCause() - .isInstanceOf(AopConfigException.class) - .withMessageContaining("TargetSource"); + .isThrownBy(() -> bf.getBean(name)) + .havingCause() + .isInstanceOf(AopConfigException.class) + .withMessageContaining("TargetSource"); } @Test public void testTargetSourceNotAtEndOfInterceptorNamesIsRejected() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions(new ClassPathResource(NOTLAST_TARGETSOURCE_CONTEXT, CLASS)); + assertThatExceptionOfType(BeanCreationException.class).as("TargetSource or non-advised object must be last in interceptorNames") - .isThrownBy(() -> bf.getBean("targetSourceNotLast")) - .havingCause() - .isInstanceOf(AopConfigException.class) - .withMessageContaining("interceptorNames"); + .isThrownBy(() -> bf.getBean("targetSourceNotLast")) + .havingCause() + .isInstanceOf(AopConfigException.class) + .withMessageContaining("interceptorNames"); } @Test @@ -167,11 +169,11 @@ public void testGetObjectTypeWithDirectTarget() { assertThat(cba.getCalls()).isEqualTo(0); ITestBean tb = (ITestBean) bf.getBean("directTarget"); - assertThat(tb.getName().equals("Adam")).isTrue(); + assertThat(tb.getName()).isEqualTo("Adam"); assertThat(cba.getCalls()).isEqualTo(1); ProxyFactoryBean pfb = (ProxyFactoryBean) bf.getBean("&directTarget"); - assertThat(TestBean.class.isAssignableFrom(pfb.getObjectType())).as("Has correct object type").isTrue(); + assertThat(pfb.getObjectType()).isAssignableTo(TestBean.class); } @Test @@ -179,9 +181,9 @@ public void testGetObjectTypeWithTargetViaTargetSource() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions(new ClassPathResource(TARGETSOURCE_CONTEXT, CLASS)); ITestBean tb = (ITestBean) bf.getBean("viaTargetSource"); - assertThat(tb.getName().equals("Adam")).isTrue(); + assertThat(tb.getName()).isEqualTo("Adam"); ProxyFactoryBean pfb = (ProxyFactoryBean) bf.getBean("&viaTargetSource"); - assertThat(TestBean.class.isAssignableFrom(pfb.getObjectType())).as("Has correct object type").isTrue(); + assertThat(pfb.getObjectType()).isAssignableTo(TestBean.class); } @Test @@ -190,11 +192,15 @@ public void testGetObjectTypeWithNoTargetOrTargetSource() { new XmlBeanDefinitionReader(bf).loadBeanDefinitions(new ClassPathResource(TARGETSOURCE_CONTEXT, CLASS)); ITestBean tb = (ITestBean) bf.getBean("noTarget"); - assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> - tb.getName()) - .withMessage("getName"); + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(tb::getName).withMessage("getName"); FactoryBean pfb = (ProxyFactoryBean) bf.getBean("&noTarget"); - assertThat(ITestBean.class.isAssignableFrom(pfb.getObjectType())).as("Has correct object type").isTrue(); + assertThat(pfb.getObjectType()).isAssignableTo(ITestBean.class); + } + + @Test + public void testGetObjectTypeOnUninitializedFactoryBean() { + ProxyFactoryBean pfb = new ProxyFactoryBean(); + assertThat(pfb.getObjectType()).isNull(); } /** @@ -219,20 +225,20 @@ public void testSingletonInstancesAreEqual() { pc1.addAdvice(1, di); assertThat(pc2.getAdvisors()).isEqualTo(pc1.getAdvisors()); assertThat(pc2.getAdvisors().length).as("Now have one more advisor").isEqualTo((oldLength + 1)); - assertThat(0).isEqualTo(di.getCount()); + assertThat(di.getCount()).isEqualTo(0); test1.setAge(5); assertThat(test1.getAge()).isEqualTo(test1_1.getAge()); - assertThat(3).isEqualTo(di.getCount()); + assertThat(di.getCount()).isEqualTo(3); } @Test public void testPrototypeInstancesAreNotEqual() { - assertThat(ITestBean.class.isAssignableFrom(factory.getType("prototype"))).as("Has correct object type").isTrue(); + assertThat(factory.getType("prototype")).isAssignableTo(ITestBean.class); ITestBean test2 = (ITestBean) factory.getBean("prototype"); ITestBean test2_1 = (ITestBean) factory.getBean("prototype"); - assertThat(test2 != test2_1).as("Prototype instances !=").isTrue(); - assertThat(test2.equals(test2_1)).as("Prototype instances equal").isTrue(); - assertThat(ITestBean.class.isAssignableFrom(factory.getType("prototype"))).as("Has correct object type").isTrue(); + assertThat(test2).as("Prototype instances !=").isNotSameAs(test2_1); + assertThat(test2).as("Prototype instances equal").isEqualTo(test2_1); + assertThat(factory.getType("prototype")).isAssignableTo(ITestBean.class); } /** @@ -262,7 +268,7 @@ private Object testPrototypeInstancesAreIndependent(String beanName) { assertThat(prototype2FirstInstance.getCount()).isEqualTo(INITIAL_COUNT + 1); SideEffectBean prototype2SecondInstance = (SideEffectBean) bf.getBean(beanName); - assertThat(prototype2FirstInstance == prototype2SecondInstance).as("Prototypes are not ==").isFalse(); + assertThat(prototype2FirstInstance).as("Prototypes are not ==").isNotSameAs(prototype2SecondInstance); assertThat(prototype2SecondInstance.getCount()).isEqualTo(INITIAL_COUNT); assertThat(prototype2FirstInstance.getCount()).isEqualTo(INITIAL_COUNT + 1); @@ -285,19 +291,19 @@ public void testAutoInvoker() { TestBean target = (TestBean) factory.getBean("test"); target.setName(name); ITestBean autoInvoker = (ITestBean) factory.getBean("autoInvoker"); - assertThat(autoInvoker.getName().equals(name)).isTrue(); + assertThat(autoInvoker.getName()).isEqualTo(name); } @Test public void testCanGetFactoryReferenceAndManipulate() { ProxyFactoryBean config = (ProxyFactoryBean) factory.getBean("&test1"); - assertThat(ITestBean.class.isAssignableFrom(config.getObjectType())).as("Has correct object type").isTrue(); - assertThat(ITestBean.class.isAssignableFrom(factory.getType("test1"))).as("Has correct object type").isTrue(); + assertThat(config.getObjectType()).isAssignableTo(ITestBean.class); + assertThat(factory.getType("test1")).isAssignableTo(ITestBean.class); // Trigger lazy initialization. config.getObject(); assertThat(config.getAdvisors().length).as("Have one advisors").isEqualTo(1); - assertThat(ITestBean.class.isAssignableFrom(config.getObjectType())).as("Has correct object type").isTrue(); - assertThat(ITestBean.class.isAssignableFrom(factory.getType("test1"))).as("Has correct object type").isTrue(); + assertThat(config.getObjectType()).isAssignableTo(ITestBean.class); + assertThat(factory.getType("test1")).isAssignableTo(ITestBean.class); ITestBean tb = (ITestBean) factory.getBean("test1"); // no exception @@ -308,7 +314,7 @@ public void testCanGetFactoryReferenceAndManipulate() { config.addAdvice(0, (MethodInterceptor) invocation -> { throw ex; }); - assertThat(config.getAdvisors().length).as("Have correct advisor count").isEqualTo(2); + assertThat(config.getAdvisors()).as("Have correct advisor count").hasSize(2); ITestBean tb1 = (ITestBean) factory.getBean("test1"); assertThatException() @@ -348,31 +354,31 @@ public void testCanAddAndRemoveAspectInterfacesOnPrototype() { // Add to head of interceptor chain int oldCount = config.getAdvisors().length; config.addAdvisor(0, new DefaultIntroductionAdvisor(ti, TimeStamped.class)); - assertThat(config.getAdvisors().length == oldCount + 1).isTrue(); + assertThat(config.getAdvisors()).hasSize(oldCount + 1); TimeStamped ts = (TimeStamped) factory.getBean("test2"); assertThat(ts.getTimeStamp()).isEqualTo(time); // Can remove config.removeAdvice(ti); - assertThat(config.getAdvisors().length == oldCount).isTrue(); + assertThat(config.getAdvisors()).hasSize(oldCount); // Check no change on existing object reference - assertThat(ts.getTimeStamp() == time).isTrue(); + assertThat(ts.getTimeStamp()).isEqualTo(time); assertThat(factory.getBean("test2")).as("Should no longer implement TimeStamped") .isNotInstanceOf(TimeStamped.class); // Now check non-effect of removing interceptor that isn't there config.removeAdvice(new DebugInterceptor()); - assertThat(config.getAdvisors().length == oldCount).isTrue(); + assertThat(config.getAdvisors()).hasSize(oldCount); ITestBean it = (ITestBean) ts; DebugInterceptor debugInterceptor = new DebugInterceptor(); config.addAdvice(0, debugInterceptor); it.getSpouse(); // Won't affect existing reference - assertThat(debugInterceptor.getCount() == 0).isTrue(); + assertThat(debugInterceptor.getCount()).isEqualTo(0); it = (ITestBean) factory.getBean("test2"); it.getSpouse(); assertThat(debugInterceptor.getCount()).isEqualTo(1); @@ -412,16 +418,16 @@ public void testCanAddAndRemoveAdvicesOnSingleton() { public void testMethodPointcuts() { ITestBean tb = (ITestBean) factory.getBean("pointcuts"); PointcutForVoid.reset(); - assertThat(PointcutForVoid.methodNames.isEmpty()).as("No methods intercepted").isTrue(); + assertThat(PointcutForVoid.methodNames).as("No methods intercepted").isEmpty(); tb.getAge(); - assertThat(PointcutForVoid.methodNames.isEmpty()).as("Not void: shouldn't have intercepted").isTrue(); + assertThat(PointcutForVoid.methodNames).as("Not void: shouldn't have intercepted").isEmpty(); tb.setAge(1); tb.getAge(); tb.setName("Tristan"); tb.toString(); - assertThat(PointcutForVoid.methodNames.size()).as("Recorded wrong number of invocations").isEqualTo(2); - assertThat(PointcutForVoid.methodNames.get(0).equals("setAge")).isTrue(); - assertThat(PointcutForVoid.methodNames.get(1).equals("setName")).isTrue(); + assertThat(PointcutForVoid.methodNames).as("Recorded wrong number of invocations").hasSize(2); + assertThat(PointcutForVoid.methodNames.get(0)).isEqualTo("setAge"); + assertThat(PointcutForVoid.methodNames.get(1)).isEqualTo("setName"); } @Test @@ -498,17 +504,17 @@ public void testGlobalsWithoutTarget() { @Test public void testGlobalsCanAddAspectInterfaces() { AddedGlobalInterface agi = (AddedGlobalInterface) factory.getBean("autoInvoker"); - assertThat(agi.globalsAdded() == -1).isTrue(); + assertThat(agi.globalsAdded()).isEqualTo(-1); ProxyFactoryBean pfb = (ProxyFactoryBean) factory.getBean("&validGlobals"); // Trigger lazy initialization. pfb.getObject(); // 2 globals + 2 explicit - assertThat(pfb.getAdvisors().length).as("Have 2 globals and 2 explicit advisors").isEqualTo(3); + assertThat(pfb.getAdvisors()).as("Have 2 globals and 2 explicit advisors").hasSize(3); ApplicationListener l = (ApplicationListener) factory.getBean("validGlobals"); agi = (AddedGlobalInterface) l; - assertThat(agi.globalsAdded() == -1).isTrue(); + assertThat(agi.globalsAdded()).isEqualTo(-1); assertThat(factory.getBean("test1")).as("Aspect interface shouldn't be implemented without globals") .isNotInstanceOf(AddedGlobalInterface.class); @@ -709,6 +715,7 @@ public DependsOnITestBean(ITestBean tb) { } } + /** * Aspect interface */ diff --git a/spring-context/src/test/java/org/springframework/aop/scope/ScopedProxyTests.java b/spring-context/src/test/java/org/springframework/aop/scope/ScopedProxyTests.java index 1f215849aa09..9e2e1bb87184 100644 --- a/spring-context/src/test/java/org/springframework/aop/scope/ScopedProxyTests.java +++ b/spring-context/src/test/java/org/springframework/aop/scope/ScopedProxyTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -39,7 +39,7 @@ * @author Juergen Hoeller * @author Chris Beams */ -public class ScopedProxyTests { +class ScopedProxyTests { private static final Class CLASS = ScopedProxyTests.class; private static final String CLASSNAME = CLASS.getSimpleName(); @@ -51,27 +51,24 @@ public class ScopedProxyTests { @Test // SPR-2108 - public void testProxyAssignable() throws Exception { + void testProxyAssignable() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions(MAP_CONTEXT); Object baseMap = bf.getBean("singletonMap"); - boolean condition = baseMap instanceof Map; - assertThat(condition).isTrue(); + assertThat(baseMap instanceof Map).isTrue(); } @Test - public void testSimpleProxy() throws Exception { + void testSimpleProxy() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions(MAP_CONTEXT); Object simpleMap = bf.getBean("simpleMap"); - boolean condition1 = simpleMap instanceof Map; - assertThat(condition1).isTrue(); - boolean condition = simpleMap instanceof HashMap; - assertThat(condition).isTrue(); + assertThat(simpleMap instanceof Map).isTrue(); + assertThat(simpleMap instanceof HashMap).isTrue(); } @Test - public void testScopedOverride() throws Exception { + void testScopedOverride() { GenericApplicationContext ctx = new GenericApplicationContext(); new XmlBeanDefinitionReader(ctx).loadBeanDefinitions(OVERRIDE_CONTEXT); SimpleMapScope scope = new SimpleMapScope(); @@ -87,7 +84,7 @@ public void testScopedOverride() throws Exception { } @Test - public void testJdkScopedProxy() throws Exception { + void testJdkScopedProxy() throws Exception { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions(TESTBEAN_CONTEXT); bf.setSerializationId("X"); @@ -97,8 +94,7 @@ public void testJdkScopedProxy() throws Exception { ITestBean bean = (ITestBean) bf.getBean("testBean"); assertThat(bean).isNotNull(); assertThat(AopUtils.isJdkDynamicProxy(bean)).isTrue(); - boolean condition1 = bean instanceof ScopedObject; - assertThat(condition1).isTrue(); + assertThat(bean instanceof ScopedObject).isTrue(); ScopedObject scoped = (ScopedObject) bean; assertThat(scoped.getTargetObject().getClass()).isEqualTo(TestBean.class); bean.setAge(101); @@ -110,8 +106,7 @@ public void testJdkScopedProxy() throws Exception { assertThat(deserialized).isNotNull(); assertThat(AopUtils.isJdkDynamicProxy(deserialized)).isTrue(); assertThat(bean.getAge()).isEqualTo(101); - boolean condition = deserialized instanceof ScopedObject; - assertThat(condition).isTrue(); + assertThat(deserialized instanceof ScopedObject).isTrue(); ScopedObject scopedDeserialized = (ScopedObject) deserialized; assertThat(scopedDeserialized.getTargetObject().getClass()).isEqualTo(TestBean.class); @@ -119,7 +114,7 @@ public void testJdkScopedProxy() throws Exception { } @Test - public void testCglibScopedProxy() throws Exception { + void testCglibScopedProxy() throws Exception { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions(LIST_CONTEXT); bf.setSerializationId("Y"); @@ -128,8 +123,7 @@ public void testCglibScopedProxy() throws Exception { TestBean tb = (TestBean) bf.getBean("testBean"); assertThat(AopUtils.isCglibProxy(tb.getFriends())).isTrue(); - boolean condition1 = tb.getFriends() instanceof ScopedObject; - assertThat(condition1).isTrue(); + assertThat(tb.getFriends() instanceof ScopedObject).isTrue(); ScopedObject scoped = (ScopedObject) tb.getFriends(); assertThat(scoped.getTargetObject().getClass()).isEqualTo(ArrayList.class); tb.getFriends().add("myFriend"); @@ -137,12 +131,11 @@ public void testCglibScopedProxy() throws Exception { assertThat(scope.getMap().containsKey("scopedTarget.scopedList")).isTrue(); assertThat(scope.getMap().get("scopedTarget.scopedList").getClass()).isEqualTo(ArrayList.class); - ArrayList deserialized = (ArrayList) SerializationTestUtils.serializeAndDeserialize(tb.getFriends()); + ArrayList deserialized = (ArrayList) SerializationTestUtils.serializeAndDeserialize(tb.getFriends()); assertThat(deserialized).isNotNull(); assertThat(AopUtils.isCglibProxy(deserialized)).isTrue(); - assertThat(deserialized.contains("myFriend")).isTrue(); - boolean condition = deserialized instanceof ScopedObject; - assertThat(condition).isTrue(); + assertThat(deserialized).contains("myFriend"); + assertThat(deserialized instanceof ScopedObject).isTrue(); ScopedObject scopedDeserialized = (ScopedObject) deserialized; assertThat(scopedDeserialized.getTargetObject().getClass()).isEqualTo(ArrayList.class); diff --git a/spring-context/src/test/java/org/springframework/beans/factory/support/QualifierAnnotationAutowireContextTests.java b/spring-context/src/test/java/org/springframework/beans/factory/support/QualifierAnnotationAutowireContextTests.java index f126d20473f4..a1cceb0a798c 100644 --- a/spring-context/src/test/java/org/springframework/beans/factory/support/QualifierAnnotationAutowireContextTests.java +++ b/spring-context/src/test/java/org/springframework/beans/factory/support/QualifierAnnotationAutowireContextTests.java @@ -63,12 +63,12 @@ public void autowiredFieldWithSingleNonQualifiedCandidate() { new RootBeanDefinition(QualifiedFieldTestBean.class)); AnnotationConfigUtils.registerAnnotationConfigProcessors(context); - assertThatExceptionOfType(BeanCreationException.class).isThrownBy( - context::refresh) - .satisfies(ex -> { - assertThat(ex.getRootCause()).isInstanceOf(NoSuchBeanDefinitionException.class); - assertThat(ex.getBeanName()).isEqualTo("autowired"); - }); + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(context::refresh) + .satisfies(ex -> { + assertThat(ex.getRootCause()).isInstanceOf(NoSuchBeanDefinitionException.class); + assertThat(ex.getBeanName()).isEqualTo("autowired"); + }); } @Test @@ -81,12 +81,13 @@ public void autowiredMethodParameterWithSingleNonQualifiedCandidate() { context.registerBeanDefinition("autowired", new RootBeanDefinition(QualifiedMethodParameterTestBean.class)); AnnotationConfigUtils.registerAnnotationConfigProcessors(context); - assertThatExceptionOfType(BeanCreationException.class).isThrownBy( - context::refresh) - .satisfies(ex -> { - assertThat(ex.getRootCause()).isInstanceOf(NoSuchBeanDefinitionException.class); - assertThat(ex.getBeanName()).isEqualTo("autowired"); - }); + + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(context::refresh) + .satisfies(ex -> { + assertThat(ex.getRootCause()).isInstanceOf(NoSuchBeanDefinitionException.class); + assertThat(ex.getBeanName()).isEqualTo("autowired"); + }); } @@ -100,9 +101,10 @@ public void autowiredConstructorArgumentWithSingleNonQualifiedCandidate() { context.registerBeanDefinition("autowired", new RootBeanDefinition(QualifiedConstructorArgumentTestBean.class)); AnnotationConfigUtils.registerAnnotationConfigProcessors(context); - assertThatExceptionOfType(UnsatisfiedDependencyException.class).isThrownBy( - context::refresh) - .satisfies(ex -> assertThat(ex.getBeanName()).isEqualTo("autowired")); + + assertThatExceptionOfType(UnsatisfiedDependencyException.class) + .isThrownBy(context::refresh) + .satisfies(ex -> assertThat(ex.getBeanName()).isEqualTo("autowired")); } @Test @@ -205,12 +207,13 @@ public void autowiredFieldWithMultipleNonQualifiedCandidates() { context.registerBeanDefinition("autowired", new RootBeanDefinition(QualifiedFieldTestBean.class)); AnnotationConfigUtils.registerAnnotationConfigProcessors(context); - assertThatExceptionOfType(BeanCreationException.class).isThrownBy( - context::refresh) - .satisfies(ex -> { - assertThat(ex.getRootCause()).isInstanceOf(NoSuchBeanDefinitionException.class); - assertThat(ex.getBeanName()).isEqualTo("autowired"); - }); + + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(context::refresh) + .satisfies(ex -> { + assertThat(ex.getRootCause()).isInstanceOf(NoSuchBeanDefinitionException.class); + assertThat(ex.getBeanName()).isEqualTo("autowired"); + }); } @Test @@ -227,12 +230,13 @@ public void autowiredMethodParameterWithMultipleNonQualifiedCandidates() { context.registerBeanDefinition("autowired", new RootBeanDefinition(QualifiedMethodParameterTestBean.class)); AnnotationConfigUtils.registerAnnotationConfigProcessors(context); - assertThatExceptionOfType(BeanCreationException.class).isThrownBy( - context::refresh) - .satisfies(ex -> { - assertThat(ex.getRootCause()).isInstanceOf(NoSuchBeanDefinitionException.class); - assertThat(ex.getBeanName()).isEqualTo("autowired"); - }); + + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(context::refresh) + .satisfies(ex -> { + assertThat(ex.getRootCause()).isInstanceOf(NoSuchBeanDefinitionException.class); + assertThat(ex.getBeanName()).isEqualTo("autowired"); + }); } @Test @@ -249,9 +253,10 @@ public void autowiredConstructorArgumentWithMultipleNonQualifiedCandidates() { context.registerBeanDefinition("autowired", new RootBeanDefinition(QualifiedConstructorArgumentTestBean.class)); AnnotationConfigUtils.registerAnnotationConfigProcessors(context); - assertThatExceptionOfType(UnsatisfiedDependencyException.class).isThrownBy( - context::refresh) - .satisfies(ex -> assertThat(ex.getBeanName()).isEqualTo("autowired")); + + assertThatExceptionOfType(UnsatisfiedDependencyException.class) + .isThrownBy(context::refresh) + .satisfies(ex -> assertThat(ex.getBeanName()).isEqualTo("autowired")); } @Test @@ -374,12 +379,13 @@ public void autowiredFieldDoesNotResolveCandidateWithDefaultValueAndConflictingV context.registerBeanDefinition("autowired", new RootBeanDefinition(QualifiedFieldWithDefaultValueTestBean.class)); AnnotationConfigUtils.registerAnnotationConfigProcessors(context); - assertThatExceptionOfType(BeanCreationException.class).isThrownBy( - context::refresh) - .satisfies(ex -> { - assertThat(ex.getRootCause()).isInstanceOf(NoSuchBeanDefinitionException.class); - assertThat(ex.getBeanName()).isEqualTo("autowired"); - }); + + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(context::refresh) + .satisfies(ex -> { + assertThat(ex.getRootCause()).isInstanceOf(NoSuchBeanDefinitionException.class); + assertThat(ex.getBeanName()).isEqualTo("autowired"); + }); } @Test @@ -451,12 +457,13 @@ public void autowiredFieldDoesNotResolveWithMultipleQualifierValuesAndConflictin context.registerBeanDefinition("autowired", new RootBeanDefinition(QualifiedFieldWithMultipleAttributesTestBean.class)); AnnotationConfigUtils.registerAnnotationConfigProcessors(context); - assertThatExceptionOfType(BeanCreationException.class).isThrownBy( - context::refresh) - .satisfies(ex -> { - assertThat(ex.getRootCause()).isInstanceOf(NoSuchBeanDefinitionException.class); - assertThat(ex.getBeanName()).isEqualTo("autowired"); - }); + + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(context::refresh) + .satisfies(ex -> { + assertThat(ex.getRootCause()).isInstanceOf(NoSuchBeanDefinitionException.class); + assertThat(ex.getBeanName()).isEqualTo("autowired"); + }); } @Test @@ -507,12 +514,13 @@ public void autowiredFieldDoesNotResolveWithMultipleQualifierValuesAndMultipleMa context.registerBeanDefinition("autowired", new RootBeanDefinition(QualifiedFieldWithMultipleAttributesTestBean.class)); AnnotationConfigUtils.registerAnnotationConfigProcessors(context); - assertThatExceptionOfType(BeanCreationException.class).isThrownBy( - context::refresh) - .satisfies(ex -> { - assertThat(ex.getRootCause()).isInstanceOf(NoSuchBeanDefinitionException.class); - assertThat(ex.getBeanName()).isEqualTo("autowired"); - }); + + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(context::refresh) + .satisfies(ex -> { + assertThat(ex.getRootCause()).isInstanceOf(NoSuchBeanDefinitionException.class); + assertThat(ex.getBeanName()).isEqualTo("autowired"); + }); } @Test @@ -574,9 +582,10 @@ public void autowiredFieldDoesNotResolveWithBaseQualifierAndNonDefaultValueAndMu context.registerBeanDefinition("autowired", new RootBeanDefinition(QualifiedConstructorArgumentWithBaseQualifierNonDefaultValueTestBean.class)); AnnotationConfigUtils.registerAnnotationConfigProcessors(context); - assertThatExceptionOfType(UnsatisfiedDependencyException.class).isThrownBy( - context::refresh) - .satisfies(ex -> assertThat(ex.getBeanName()).isEqualTo("autowired")); + + assertThatExceptionOfType(UnsatisfiedDependencyException.class) + .isThrownBy(context::refresh) + .satisfies(ex -> assertThat(ex.getBeanName()).isEqualTo("autowired")); } @@ -752,7 +761,7 @@ public DefaultValueQualifiedPerson(String name) { @Qualifier @interface TestQualifierWithMultipleAttributes { - String value() default "default"; + String[] value() default "default"; int number(); } diff --git a/spring-context/src/test/java/org/springframework/beans/factory/xml/QualifierAnnotationTests.java b/spring-context/src/test/java/org/springframework/beans/factory/xml/QualifierAnnotationTests.java index 49236e0422a7..0e966b1742ac 100644 --- a/spring-context/src/test/java/org/springframework/beans/factory/xml/QualifierAnnotationTests.java +++ b/spring-context/src/test/java/org/springframework/beans/factory/xml/QualifierAnnotationTests.java @@ -58,9 +58,10 @@ public void testNonQualifiedFieldFails() { BeanDefinitionReader reader = new XmlBeanDefinitionReader(context); reader.loadBeanDefinitions(CONFIG_LOCATION); context.registerSingleton("testBean", NonQualifiedTestBean.class); - assertThatExceptionOfType(BeanCreationException.class).isThrownBy( - context::refresh) - .withMessageContaining("found 6"); + + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(context::refresh) + .withMessageContaining("found 6"); } @Test @@ -191,9 +192,10 @@ public void testQualifiedByAttributesFailsWithoutCustomQualifierRegistered() { BeanDefinitionReader reader = new XmlBeanDefinitionReader(context); reader.loadBeanDefinitions(CONFIG_LOCATION); context.registerSingleton("testBean", QualifiedByAttributesTestBean.class); - assertThatExceptionOfType(BeanCreationException.class).isThrownBy( - context::refresh) - .withMessageContaining("found 6"); + + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(context::refresh) + .withMessageContaining("found 6"); } @Test diff --git a/spring-context/src/test/java/org/springframework/cache/CacheReproTests.java b/spring-context/src/test/java/org/springframework/cache/CacheReproTests.java index 7b4911a8b49a..31e2d02a40d2 100644 --- a/spring-context/src/test/java/org/springframework/cache/CacheReproTests.java +++ b/spring-context/src/test/java/org/springframework/cache/CacheReproTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -118,6 +118,7 @@ void spr13081ConfigNoCacheNameIsRequired() { assertThat(cacheResolver.getCache("foo").get("foo")).isNull(); Object result = bean.getSimple("foo"); // cache name = id assertThat(cacheResolver.getCache("foo").get("foo").get()).isEqualTo(result); + context.close(); } @@ -127,7 +128,7 @@ void spr13081ConfigFailIfCacheResolverReturnsNullCacheName() { Spr13081Service bean = context.getBean(Spr13081Service.class); assertThatIllegalStateException().isThrownBy(() -> bean.getSimple(null)) - .withMessageContaining(MyCacheResolver.class.getName()); + .withMessageContaining(MyCacheResolver.class.getName()); context.close(); } @@ -146,6 +147,7 @@ void spr14230AdaptsToOptional() { TestBean tb2 = bean.findById("tb1").get(); assertThat(tb2).isNotSameAs(tb); assertThat(cache.get("tb1").get()).isSameAs(tb2); + context.close(); } @@ -177,6 +179,7 @@ void spr15271FindsOnInterfaceWithInterfaceProxy() { bean.insertItem(tb); assertThat(bean.findById("tb1").get()).isSameAs(tb); assertThat(cache.get("tb1").get()).isSameAs(tb); + context.close(); } @@ -190,6 +193,7 @@ void spr15271FindsOnInterfaceWithCglibProxy() { bean.insertItem(tb); assertThat(bean.findById("tb1").get()).isSameAs(tb); assertThat(cache.get("tb1").get()).isSameAs(tb); + context.close(); } diff --git a/spring-context/src/test/java/org/springframework/cache/interceptor/CacheSyncFailureTests.java b/spring-context/src/test/java/org/springframework/cache/interceptor/CacheSyncFailureTests.java index de4776adae2e..56b22970d1ac 100644 --- a/spring-context/src/test/java/org/springframework/cache/interceptor/CacheSyncFailureTests.java +++ b/spring-context/src/test/java/org/springframework/cache/interceptor/CacheSyncFailureTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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. @@ -48,8 +48,9 @@ public class CacheSyncFailureTests { private SimpleService simpleService; + @BeforeEach - public void setUp() { + public void setup() { this.context = new AnnotationConfigApplicationContext(Config.class); this.simpleService = this.context.getBean(SimpleService.class); } @@ -61,39 +62,40 @@ public void closeContext() { } } + @Test public void unlessSync() { - assertThatIllegalStateException().isThrownBy(() -> - this.simpleService.unlessSync("key")) - .withMessageContaining("@Cacheable(sync=true) does not support unless attribute"); + assertThatIllegalStateException() + .isThrownBy(() -> this.simpleService.unlessSync("key")) + .withMessageContaining("A sync=true operation does not support the unless attribute"); } @Test public void severalCachesSync() { - assertThatIllegalStateException().isThrownBy(() -> - this.simpleService.severalCachesSync("key")) - .withMessageContaining("@Cacheable(sync=true) only allows a single cache"); + assertThatIllegalStateException() + .isThrownBy(() -> this.simpleService.severalCachesSync("key")) + .withMessageContaining("A sync=true operation is restricted to a single cache"); } @Test public void severalCachesWithResolvedSync() { - assertThatIllegalStateException().isThrownBy(() -> - this.simpleService.severalCachesWithResolvedSync("key")) - .withMessageContaining("@Cacheable(sync=true) only allows a single cache"); + assertThatIllegalStateException() + .isThrownBy(() -> this.simpleService.severalCachesWithResolvedSync("key")) + .withMessageContaining("A sync=true operation is restricted to a single cache"); } @Test public void syncWithAnotherOperation() { - assertThatIllegalStateException().isThrownBy(() -> - this.simpleService.syncWithAnotherOperation("key")) - .withMessageContaining("@Cacheable(sync=true) cannot be combined with other cache operations"); + assertThatIllegalStateException() + .isThrownBy(() -> this.simpleService.syncWithAnotherOperation("key")) + .withMessageContaining("A sync=true operation cannot be combined with other cache operations"); } @Test public void syncWithTwoGetOperations() { - assertThatIllegalStateException().isThrownBy(() -> - this.simpleService.syncWithTwoGetOperations("key")) - .withMessageContaining("Only one @Cacheable(sync=true) entry is allowed"); + assertThatIllegalStateException() + .isThrownBy(() -> this.simpleService.syncWithTwoGetOperations("key")) + .withMessageContaining("Only one sync=true operation is allowed"); } @@ -131,6 +133,7 @@ public Object syncWithTwoGetOperations(Object arg1) { } } + @Configuration @EnableCaching static class Config implements CachingConfigurer { diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ClassPathBeanDefinitionScannerTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ClassPathBeanDefinitionScannerTests.java index cfb389e6a464..52ecc1e8389f 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/ClassPathBeanDefinitionScannerTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/ClassPathBeanDefinitionScannerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -197,6 +197,7 @@ public void testSimpleScanWithDefaultFiltersAndOverridingBean() { context.registerBeanDefinition("stubFooDao", new RootBeanDefinition(TestBean.class)); ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context); scanner.setIncludeAnnotationConfig(false); + // should not fail! scanner.scan(BASE_PACKAGE); } @@ -207,10 +208,10 @@ public void testSimpleScanWithDefaultFiltersAndDefaultBeanNameClash() { ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context); scanner.setIncludeAnnotationConfig(false); scanner.scan("org.springframework.context.annotation3"); - assertThatIllegalStateException().isThrownBy(() -> - scanner.scan(BASE_PACKAGE)) - .withMessageContaining("stubFooDao") - .withMessageContaining(StubFooDao.class.getName()); + + assertThatIllegalStateException().isThrownBy(() -> scanner.scan(BASE_PACKAGE)) + .withMessageContaining("stubFooDao") + .withMessageContaining(StubFooDao.class.getName()); } @Test @@ -267,11 +268,11 @@ public void testSimpleScanWithDefaultFiltersAndSpecifiedBeanNameClash() { ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context); scanner.setIncludeAnnotationConfig(false); scanner.scan("org.springframework.context.annotation2"); - assertThatIllegalStateException().isThrownBy(() -> - scanner.scan(BASE_PACKAGE)) - .withMessageContaining("myNamedDao") - .withMessageContaining(NamedStubDao.class.getName()) - .withMessageContaining(NamedStubDao2.class.getName()); + + assertThatIllegalStateException().isThrownBy(() -> scanner.scan(BASE_PACKAGE)) + .withMessageContaining("myNamedDao") + .withMessageContaining(NamedStubDao.class.getName()) + .withMessageContaining(NamedStubDao2.class.getName()); } @Test diff --git a/spring-context/src/test/java/org/springframework/context/annotation/CommonAnnotationBeanPostProcessorTests.java b/spring-context/src/test/java/org/springframework/context/annotation/CommonAnnotationBeanPostProcessorTests.java index e943d4d8f2fd..e999fe132fd7 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/CommonAnnotationBeanPostProcessorTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/CommonAnnotationBeanPostProcessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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. @@ -215,7 +215,7 @@ public void testResourceInjectionWithResolvableDependencyType() { bf.registerBeanDefinition("testBean4", tbd); bf.registerResolvableDependency(BeanFactory.class, bf); - bf.registerResolvableDependency(INestedTestBean.class, (ObjectFactory) () -> new NestedTestBean()); + bf.registerResolvableDependency(INestedTestBean.class, (ObjectFactory) NestedTestBean::new); @SuppressWarnings("deprecation") org.springframework.beans.factory.config.PropertyPlaceholderConfigurer ppc = new org.springframework.beans.factory.config.PropertyPlaceholderConfigurer(); @@ -233,7 +233,7 @@ public void testResourceInjectionWithResolvableDependencyType() { assertThat(tb).isNotSameAs(anotherBean.getTestBean6()); String[] depBeans = bf.getDependenciesForBean("annotatedBean"); - assertThat(depBeans.length).isEqualTo(1); + assertThat(depBeans).hasSize(1); assertThat(depBeans[0]).isEqualTo("testBean4"); } @@ -508,6 +508,25 @@ public void testLazyResolutionWithCglibProxy() { assertThat(tb.getName()).isEqualTo("notLazyAnymore"); } + @Test + public void testLazyResolutionWithFallbackTypeMatch() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + bf.setAutowireCandidateResolver(new ContextAnnotationAutowireCandidateResolver()); + CommonAnnotationBeanPostProcessor bpp = new CommonAnnotationBeanPostProcessor(); + bpp.setBeanFactory(bf); + bf.addBeanPostProcessor(bpp); + + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(LazyResourceCglibInjectionBean.class)); + bf.registerBeanDefinition("tb", new RootBeanDefinition(TestBean.class)); + + LazyResourceCglibInjectionBean bean = (LazyResourceCglibInjectionBean) bf.getBean("annotatedBean"); + assertThat(bf.containsSingleton("tb")).isFalse(); + bean.testBean.setName("notLazyAnymore"); + assertThat(bf.containsSingleton("tb")).isTrue(); + TestBean tb = (TestBean) bf.getBean("tb"); + assertThat(tb.getName()).isEqualTo("notLazyAnymore"); + } + public static class AnnotatedInitDestroyBean { diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationWithFactoryBeanBeanEarlyDeductionTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationWithFactoryBeanEarlyDeductionTests.java similarity index 70% rename from spring-context/src/test/java/org/springframework/context/annotation/ConfigurationWithFactoryBeanBeanEarlyDeductionTests.java rename to spring-context/src/test/java/org/springframework/context/annotation/ConfigurationWithFactoryBeanEarlyDeductionTests.java index 833e357f006e..1290c38a086b 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationWithFactoryBeanBeanEarlyDeductionTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationWithFactoryBeanEarlyDeductionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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 @@ import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.GenericBeanDefinition; +import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.context.support.GenericApplicationContext; +import org.springframework.core.ResolvableType; import org.springframework.core.type.AnnotationMetadata; import static org.assertj.core.api.Assertions.assertThat; @@ -39,51 +41,62 @@ * {@link FactoryBean FactoryBeans} defined in the configuration. * * @author Phillip Webb + * @author Juergen Hoeller */ -public class ConfigurationWithFactoryBeanBeanEarlyDeductionTests { +class ConfigurationWithFactoryBeanEarlyDeductionTests { @Test - public void preFreezeDirect() { + void preFreezeDirect() { assertPreFreeze(DirectConfiguration.class); } @Test - public void postFreezeDirect() { + void postFreezeDirect() { assertPostFreeze(DirectConfiguration.class); } @Test - public void preFreezeGenericMethod() { + void preFreezeGenericMethod() { assertPreFreeze(GenericMethodConfiguration.class); } @Test - public void postFreezeGenericMethod() { + void postFreezeGenericMethod() { assertPostFreeze(GenericMethodConfiguration.class); } @Test - public void preFreezeGenericClass() { + void preFreezeGenericClass() { assertPreFreeze(GenericClassConfiguration.class); } @Test - public void postFreezeGenericClass() { + void postFreezeGenericClass() { assertPostFreeze(GenericClassConfiguration.class); } @Test - public void preFreezeAttribute() { + void preFreezeAttribute() { assertPreFreeze(AttributeClassConfiguration.class); } @Test - public void postFreezeAttribute() { + void postFreezeAttribute() { assertPostFreeze(AttributeClassConfiguration.class); } @Test - public void preFreezeUnresolvedGenericFactoryBean() { + void preFreezeTargetType() { + assertPreFreeze(TargetTypeConfiguration.class); + } + + @Test + void postFreezeTargetType() { + assertPostFreeze(TargetTypeConfiguration.class); + } + + @Test + void preFreezeUnresolvedGenericFactoryBean() { // Covers the case where a @Configuration is picked up via component scanning // and its bean definition only has a String bean class. In such cases // beanDefinition.hasBeanClass() returns false so we need to actually @@ -108,14 +121,13 @@ public void preFreezeUnresolvedGenericFactoryBean() { } } + private void assertPostFreeze(Class configurationClass) { - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( - configurationClass); + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(configurationClass); assertContainsMyBeanName(context); } - private void assertPreFreeze(Class configurationClass, - BeanFactoryPostProcessor... postProcessors) { + private void assertPreFreeze(Class configurationClass, BeanFactoryPostProcessor... postProcessors) { NameCollectingBeanFactoryPostProcessor postProcessor = new NameCollectingBeanFactoryPostProcessor(); AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); try { @@ -138,41 +150,38 @@ private void assertContainsMyBeanName(String[] names) { assertThat(names).containsExactly("myBean"); } - private static class NameCollectingBeanFactoryPostProcessor - implements BeanFactoryPostProcessor { + + private static class NameCollectingBeanFactoryPostProcessor implements BeanFactoryPostProcessor { private String[] names; @Override - public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) - throws BeansException { - this.names = beanFactory.getBeanNamesForType(MyBean.class, true, false); + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { + ResolvableType typeToMatch = ResolvableType.forClassWithGenerics(MyBean.class, String.class); + this.names = beanFactory.getBeanNamesForType(typeToMatch, true, false); } public String[] getNames() { return this.names; } - } @Configuration static class DirectConfiguration { @Bean - MyBean myBean() { - return new MyBean(); + MyBean myBean() { + return new MyBean<>(); } - } @Configuration static class GenericMethodConfiguration { @Bean - FactoryBean myBean() { - return new TestFactoryBean<>(new MyBean()); + FactoryBean> myBean() { + return new TestFactoryBean<>(new MyBean<>()); } - } @Configuration @@ -182,13 +191,11 @@ static class GenericClassConfiguration { MyFactoryBean myBean() { return new MyFactoryBean(); } - } @Configuration @Import(AttributeClassRegistrar.class) static class AttributeClassConfiguration { - } static class AttributeClassRegistrar implements ImportBeanDefinitionRegistrar { @@ -197,16 +204,34 @@ static class AttributeClassRegistrar implements ImportBeanDefinitionRegistrar { public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { BeanDefinition definition = BeanDefinitionBuilder.genericBeanDefinition( RawWithAbstractObjectTypeFactoryBean.class).getBeanDefinition(); - definition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, MyBean.class); + definition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, + ResolvableType.forClassWithGenerics(MyBean.class, String.class)); registry.registerBeanDefinition("myBean", definition); } + } + @Configuration + @Import(TargetTypeRegistrar.class) + static class TargetTypeConfiguration { + } + + static class TargetTypeRegistrar implements ImportBeanDefinitionRegistrar { + + @Override + public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { + RootBeanDefinition definition = new RootBeanDefinition(RawWithAbstractObjectTypeFactoryBean.class); + definition.setTargetType(ResolvableType.forClassWithGenerics(FactoryBean.class, + ResolvableType.forClassWithGenerics(MyBean.class, String.class))); + definition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, + ResolvableType.forClassWithGenerics(MyBean.class, String.class)); + registry.registerBeanDefinition("myBean", definition); + } } abstract static class AbstractMyBean { } - static class MyBean extends AbstractMyBean { + static class MyBean extends AbstractMyBean { } static class TestFactoryBean implements FactoryBean { @@ -218,7 +243,7 @@ public TestFactoryBean(T instance) { } @Override - public T getObject() throws Exception { + public T getObject() { return this.instance; } @@ -226,31 +251,26 @@ public T getObject() throws Exception { public Class getObjectType() { return this.instance.getClass(); } - } - static class MyFactoryBean extends TestFactoryBean { + static class MyFactoryBean extends TestFactoryBean> { public MyFactoryBean() { - super(new MyBean()); + super(new MyBean<>()); } - } static class RawWithAbstractObjectTypeFactoryBean implements FactoryBean { - private final Object object = new MyBean(); - @Override public Object getObject() throws Exception { - return object; + throw new IllegalStateException(); } @Override public Class getObjectType() { return MyBean.class; } - } } diff --git a/spring-context/src/test/java/org/springframework/context/annotation/EnableLoadTimeWeavingTests.java b/spring-context/src/test/java/org/springframework/context/annotation/EnableLoadTimeWeavingTests.java index 34d394bc8ba1..95de2db864c6 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/EnableLoadTimeWeavingTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/EnableLoadTimeWeavingTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -79,7 +79,7 @@ void enableLTW_withAjWeavingEnabled() { @Configuration - @EnableLoadTimeWeaving(aspectjWeaving=AspectJWeaving.DISABLED) + @EnableLoadTimeWeaving(aspectjWeaving = AspectJWeaving.DISABLED) static class EnableLTWConfig_withAjWeavingDisabled implements LoadTimeWeavingConfigurer { @Override @@ -88,8 +88,9 @@ public LoadTimeWeaver getLoadTimeWeaver() { } } + @Configuration - @EnableLoadTimeWeaving(aspectjWeaving=AspectJWeaving.AUTODETECT) + @EnableLoadTimeWeaving(aspectjWeaving = AspectJWeaving.AUTODETECT) static class EnableLTWConfig_withAjWeavingAutodetect implements LoadTimeWeavingConfigurer { @Override @@ -98,8 +99,9 @@ public LoadTimeWeaver getLoadTimeWeaver() { } } + @Configuration - @EnableLoadTimeWeaving(aspectjWeaving=AspectJWeaving.ENABLED) + @EnableLoadTimeWeaving(aspectjWeaving = AspectJWeaving.ENABLED) static class EnableLTWConfig_withAjWeavingEnabled implements LoadTimeWeavingConfigurer { @Override diff --git a/spring-context/src/test/java/org/springframework/context/annotation/PropertySourceAnnotationTests.java b/spring-context/src/test/java/org/springframework/context/annotation/PropertySourceAnnotationTests.java index f2d96acf3c27..af477fc083d0 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/PropertySourceAnnotationTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/PropertySourceAnnotationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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. @@ -138,8 +138,8 @@ void withCustomFactoryAsMeta() { @Test void withUnresolvablePlaceholder() { assertThatExceptionOfType(BeanDefinitionStoreException.class) - .isThrownBy(() -> new AnnotationConfigApplicationContext(ConfigWithUnresolvablePlaceholder.class)) - .withCauseInstanceOf(IllegalArgumentException.class); + .isThrownBy(() -> new AnnotationConfigApplicationContext(ConfigWithUnresolvablePlaceholder.class)) + .withCauseInstanceOf(IllegalArgumentException.class); } @Test @@ -170,8 +170,8 @@ void withResolvablePlaceholderAndFactoryBean() { @Test void withEmptyResourceLocations() { assertThatExceptionOfType(BeanDefinitionStoreException.class) - .isThrownBy(() -> new AnnotationConfigApplicationContext(ConfigWithEmptyResourceLocations.class)) - .withCauseInstanceOf(IllegalArgumentException.class); + .isThrownBy(() -> new AnnotationConfigApplicationContext(ConfigWithEmptyResourceLocations.class)) + .withCauseInstanceOf(IllegalArgumentException.class); } @Test @@ -253,8 +253,8 @@ void withNamedPropertySources() { @Test void withMissingPropertySource() { assertThatExceptionOfType(BeanDefinitionStoreException.class) - .isThrownBy(() -> new AnnotationConfigApplicationContext(ConfigWithMissingPropertySource.class)) - .withCauseInstanceOf(FileNotFoundException.class); + .isThrownBy(() -> new AnnotationConfigApplicationContext(ConfigWithMissingPropertySource.class)) + .withCauseInstanceOf(FileNotFoundException.class); } @Test diff --git a/spring-context/src/test/java/org/springframework/context/annotation/configuration/AutowiredConfigurationTests.java b/spring-context/src/test/java/org/springframework/context/annotation/configuration/AutowiredConfigurationTests.java index b5b0b904ab26..a800895c1f1f 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/configuration/AutowiredConfigurationTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/configuration/AutowiredConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-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 org.junit.jupiter.api.Test; import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.ObjectFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; @@ -44,6 +45,7 @@ import org.springframework.core.annotation.AliasFor; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; +import org.springframework.util.Assert; import static org.assertj.core.api.Assertions.assertThat; @@ -92,8 +94,8 @@ void testAutowiredConfigurationMethodDependenciesWithOptionalAndNotAvailable() { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( OptionalAutowiredMethodConfig.class); - assertThat(context.getBeansOfType(Colour.class).isEmpty()).isTrue(); - assertThat(context.getBean(TestBean.class).getName()).isEqualTo(""); + assertThat(context.getBeansOfType(Colour.class)).isEmpty(); + assertThat(context.getBean(TestBean.class).getName()).isEmpty(); context.close(); } @@ -184,14 +186,22 @@ void testValueInjectionWithProviderMethodArguments() { context.close(); } + @Test + void testValueInjectionWithAccidentalAutowiredAnnotations() { + AnnotationConfigApplicationContext context = + new AnnotationConfigApplicationContext(ValueConfigWithAccidentalAutowiredAnnotations.class); + doTestValueInjection(context); + context.close(); + } + private void doTestValueInjection(BeanFactory context) { System.clearProperty("myProp"); TestBean testBean = context.getBean("testBean", TestBean.class); - assertThat((Object) testBean.getName()).isNull(); + assertThat(testBean.getName()).isNull(); testBean = context.getBean("testBean2", TestBean.class); - assertThat((Object) testBean.getName()).isNull(); + assertThat(testBean.getName()).isNull(); System.setProperty("myProp", "foo"); @@ -204,10 +214,10 @@ private void doTestValueInjection(BeanFactory context) { System.clearProperty("myProp"); testBean = context.getBean("testBean", TestBean.class); - assertThat((Object) testBean.getName()).isNull(); + assertThat(testBean.getName()).isNull(); testBean = context.getBean("testBean2", TestBean.class); - assertThat((Object) testBean.getName()).isNull(); + assertThat(testBean.getName()).isNull(); } @Test @@ -271,7 +281,7 @@ public TestBean testBean(Optional colour, Optional> colours return new TestBean(""); } else { - return new TestBean(colour.get().toString() + "-" + colours.get().get(0).toString()); + return new TestBean(colour.get() + "-" + colours.get().get(0).toString()); } } } @@ -484,6 +494,32 @@ public TestBean testBean2(@Value("#{systemProperties[myProp]}") Provider } + @Configuration + static class ValueConfigWithAccidentalAutowiredAnnotations implements InitializingBean { + + boolean invoked; + + @Override + public void afterPropertiesSet() { + Assert.state(!invoked, "Factory method must not get invoked on startup"); + } + + @Bean @Scope("prototype") + @Autowired + public TestBean testBean(@Value("#{systemProperties[myProp]}") Provider name) { + invoked = true; + return new TestBean(name.get()); + } + + @Bean @Scope("prototype") + @Autowired + public TestBean testBean2(@Value("#{systemProperties[myProp]}") Provider name2) { + invoked = true; + return new TestBean(name2.get()); + } + } + + @Configuration static class PropertiesConfig { diff --git a/spring-context/src/test/java/org/springframework/context/annotation/configuration/ImportTests.java b/spring-context/src/test/java/org/springframework/context/annotation/configuration/ImportTests.java index 0056376f5553..04638e1822e5 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/configuration/ImportTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/configuration/ImportTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -41,6 +41,7 @@ class ImportTests { private DefaultListableBeanFactory processConfigurationClasses(Class... classes) { DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + beanFactory.setAllowBeanDefinitionOverriding(false); for (Class clazz : classes) { beanFactory.registerBeanDefinition(clazz.getSimpleName(), new RootBeanDefinition(clazz)); } @@ -56,9 +57,10 @@ private void assertBeanDefinitionCount(int expectedCount, Class... classes) { for (Class clazz : classes) { beanFactory.getBean(clazz); } - } + // ------------------------------------------------------------------------ + @Test void testProcessImportsWithAsm() { int configClasses = 2; @@ -158,6 +160,13 @@ void testImportAnnotationWithThreeLevelRecursion() { assertBeanDefinitionCount(configClasses + beansInClasses, FirstLevel.class); } + @Test + void testImportAnnotationWithThreeLevelRecursionAndDoubleImport() { + int configClasses = 5; + int beansInClasses = 5; + assertBeanDefinitionCount(configClasses + beansInClasses, FirstLevel.class, FirstLevelPlus.class); + } + // ------------------------------------------------------------------------ @Test @@ -167,7 +176,6 @@ void testImportAnnotationWithMultipleArguments() { assertBeanDefinitionCount((configClasses + beansInClasses), WithMultipleArgumentsToImportAnnotation.class); } - @Test void testImportAnnotationWithMultipleArgumentsResultingInOverriddenBeanDefinition() { DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); @@ -245,6 +253,11 @@ TestBean m() { } } + @Configuration + @Import(ThirdLevel.class) + static class FirstLevelPlus { + } + @Configuration @Import({ThirdLevel.class, InitBean.class}) static class SecondLevel { diff --git a/spring-context/src/test/java/org/springframework/context/event/AbstractApplicationEventListenerTests.java b/spring-context/src/test/java/org/springframework/context/event/AbstractApplicationEventListenerTests.java index bf4360ef589b..22d33a109cb9 100644 --- a/spring-context/src/test/java/org/springframework/context/event/AbstractApplicationEventListenerTests.java +++ b/spring-context/src/test/java/org/springframework/context/event/AbstractApplicationEventListenerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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. @@ -36,6 +36,10 @@ protected ResolvableType getGenericApplicationEventType(String fieldName) { } } + protected GenericTestEvent createGenericTestEvent(T payload) { + return new GenericTestEvent<>(this, payload); + } + protected static class GenericTestEvent extends ApplicationEvent { @@ -51,6 +55,7 @@ public T getPayload() { } } + protected static class SmartGenericTestEvent extends GenericTestEvent implements ResolvableTypeProvider { private final ResolvableType resolvableType; @@ -67,6 +72,7 @@ public ResolvableType getResolvableType() { } } + protected static class StringEvent extends GenericTestEvent { public StringEvent(Object source, String payload) { @@ -74,6 +80,7 @@ public StringEvent(Object source, String payload) { } } + protected static class LongEvent extends GenericTestEvent { public LongEvent(Object source, Long payload) { @@ -81,31 +88,31 @@ public LongEvent(Object source, Long payload) { } } - protected GenericTestEvent createGenericTestEvent(T payload) { - return new GenericTestEvent<>(this, payload); - } - static class GenericEventListener implements ApplicationListener> { + @Override public void onApplicationEvent(GenericTestEvent event) { } } + static class ObjectEventListener implements ApplicationListener> { + @Override public void onApplicationEvent(GenericTestEvent event) { } } - static class UpperBoundEventListener - implements ApplicationListener> { + + static class UpperBoundEventListener implements ApplicationListener> { @Override public void onApplicationEvent(GenericTestEvent event) { } } + static class StringEventListener implements ApplicationListener> { @Override @@ -113,6 +120,7 @@ public void onApplicationEvent(GenericTestEvent event) { } } + @SuppressWarnings("rawtypes") static class RawApplicationListener implements ApplicationListener { @@ -121,10 +129,10 @@ public void onApplicationEvent(ApplicationEvent event) { } } + static class TestEvents { public GenericTestEvent wildcardEvent; - } } diff --git a/spring-context/src/test/java/org/springframework/context/event/AnnotationDrivenEventListenerTests.java b/spring-context/src/test/java/org/springframework/context/event/AnnotationDrivenEventListenerTests.java index c493f075b1ae..4374b4210d59 100644 --- a/spring-context/src/test/java/org/springframework/context/event/AnnotationDrivenEventListenerTests.java +++ b/spring-context/src/test/java/org/springframework/context/event/AnnotationDrivenEventListenerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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. @@ -164,12 +164,12 @@ void contextEventsAreReceived() { ContextEventListener listener = this.context.getBean(ContextEventListener.class); List events = this.eventCollector.getEvents(listener); - assertThat(events.size()).as("Wrong number of initial context events").isEqualTo(1); + assertThat(events).as("Wrong number of initial context events").hasSize(1); assertThat(events.get(0).getClass()).isEqualTo(ContextRefreshedEvent.class); this.context.stop(); List eventsAfterStop = this.eventCollector.getEvents(listener); - assertThat(eventsAfterStop.size()).as("Wrong number of context events on shutdown").isEqualTo(2); + assertThat(eventsAfterStop).as("Wrong number of context events on shutdown").hasSize(2); assertThat(eventsAfterStop.get(1).getClass()).isEqualTo(ContextStoppedEvent.class); this.eventCollector.assertTotalEventsCount(2); } @@ -334,7 +334,7 @@ void eventListenerWorksWithSimpleInterfaceProxy() { load(ScopedProxyTestBean.class); SimpleService proxy = this.context.getBean(SimpleService.class); - assertThat(proxy instanceof Advised).as("bean should be a proxy").isTrue(); + assertThat(proxy).as("bean should be a proxy").isInstanceOf(Advised.class); this.eventCollector.assertNoEventReceived(proxy.getId()); this.context.publishEvent(new ContextRefreshedEvent(this.context)); @@ -351,7 +351,7 @@ void eventListenerWorksWithAnnotatedInterfaceProxy() { load(AnnotatedProxyTestBean.class); AnnotatedSimpleService proxy = this.context.getBean(AnnotatedSimpleService.class); - assertThat(proxy instanceof Advised).as("bean should be a proxy").isTrue(); + assertThat(proxy).as("bean should be a proxy").isInstanceOf(Advised.class); this.eventCollector.assertNoEventReceived(proxy.getId()); this.context.publishEvent(new ContextRefreshedEvent(this.context)); @@ -517,7 +517,6 @@ void replyWithPayload() { ReplyEventListener replyEventListener = this.context.getBean(ReplyEventListener.class); TestEventListener listener = this.context.getBean(TestEventListener.class); - this.eventCollector.assertNoEventReceived(listener); this.eventCollector.assertNoEventReceived(replyEventListener); this.context.publishEvent(event); @@ -634,6 +633,17 @@ void orderedListeners() { assertThat(listener.order).contains("first", "second", "third"); } + @Test + void publicSubclassWithInheritedEventListener() { + load(PublicSubclassWithInheritedEventListener.class); + TestEventListener listener = this.context.getBean(PublicSubclassWithInheritedEventListener.class); + + this.eventCollector.assertNoEventReceived(listener); + this.context.publishEvent("test"); + this.eventCollector.assertEvent(listener, "test"); + this.eventCollector.assertTotalEventsCount(1); + } + @Test @Disabled // SPR-15122 void listenersReceiveEarlyEvents() { load(EventOnPostConstruct.class, OrderedTestListener.class); @@ -646,7 +656,7 @@ void listenersReceiveEarlyEvents() { void missingListenerBeanIgnored() { load(MissingEventListener.class); context.getBean(UseMissingEventListener.class); - context.getBean(ApplicationEventMulticaster.class).multicastEvent(new TestEvent(this)); + context.publishEvent(new TestEvent(this)); } @@ -753,7 +763,6 @@ static class ContextEventListener extends AbstractTestEventListener { public void handleContextEvent(ApplicationContextEvent event) { collectEvent(event); } - } @@ -980,7 +989,6 @@ public void handleString(GenericEventPojo value) { } - @EventListener @Retention(RetentionPolicy.RUNTIME) public @interface ConditionalEvent { @@ -1032,7 +1040,7 @@ public void handleRatio(Double ratio) { } - @Configuration + @Component static class OrderedTestListener extends TestEventListener { public final List order = new ArrayList<>(); @@ -1056,6 +1064,11 @@ public void handleSecond(String payload) { } + @Component + public static class PublicSubclassWithInheritedEventListener extends TestEventListener { + } + + static class EventOnPostConstruct { @Autowired diff --git a/spring-context/src/test/java/org/springframework/context/event/ApplicationListenerMethodAdapterTests.java b/spring-context/src/test/java/org/springframework/context/event/ApplicationListenerMethodAdapterTests.java index 83fea0f1e432..86fd0c367b8a 100644 --- a/spring-context/src/test/java/org/springframework/context/event/ApplicationListenerMethodAdapterTests.java +++ b/spring-context/src/test/java/org/springframework/context/event/ApplicationListenerMethodAdapterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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.IOException; import java.lang.reflect.Method; import java.lang.reflect.UndeclaredThrowableException; +import java.util.List; import org.junit.jupiter.api.Test; @@ -46,6 +47,8 @@ /** * @author Stephane Nicoll + * @author Juergen Hoeller + * @author Simon Baslé */ public class ApplicationListenerMethodAdapterTests extends AbstractApplicationEventListenerTests { @@ -80,16 +83,23 @@ public void genericListenerWrongParameterizedType() { supportsEventType(false, method, ResolvableType.forClassWithGenerics(GenericTestEvent.class, Long.class)); } + @Test + public void genericListenerWithUnresolvedGenerics() { + Method method = ReflectionUtils.findMethod( + SampleEvents.class, "handleGenericString", GenericTestEvent.class); + supportsEventType(true, method, ResolvableType.forClass(GenericTestEvent.class)); + } + @Test public void listenerWithPayloadAndGenericInformation() { Method method = ReflectionUtils.findMethod(SampleEvents.class, "handleString", String.class); - supportsEventType(true, method, createGenericEventType(String.class)); + supportsEventType(true, method, createPayloadEventType(String.class)); } @Test public void listenerWithInvalidPayloadAndGenericInformation() { Method method = ReflectionUtils.findMethod(SampleEvents.class, "handleString", String.class); - supportsEventType(false, method, createGenericEventType(Integer.class)); + supportsEventType(false, method, createPayloadEventType(Integer.class)); } @Test @@ -113,28 +123,28 @@ public void listenerWithSubTypeSeveralGenericsResolved() { @Test public void listenerWithAnnotationValue() { Method method = ReflectionUtils.findMethod(SampleEvents.class, "handleStringAnnotationValue"); - supportsEventType(true, method, createGenericEventType(String.class)); + supportsEventType(true, method, createPayloadEventType(String.class)); } @Test public void listenerWithAnnotationClasses() { Method method = ReflectionUtils.findMethod(SampleEvents.class, "handleStringAnnotationClasses"); - supportsEventType(true, method, createGenericEventType(String.class)); + supportsEventType(true, method, createPayloadEventType(String.class)); } @Test public void listenerWithAnnotationValueAndParameter() { Method method = ReflectionUtils.findMethod( SampleEvents.class, "handleStringAnnotationValueAndParameter", String.class); - supportsEventType(true, method, createGenericEventType(String.class)); + supportsEventType(true, method, createPayloadEventType(String.class)); } @Test public void listenerWithSeveralTypes() { Method method = ReflectionUtils.findMethod(SampleEvents.class, "handleStringOrInteger"); - supportsEventType(true, method, createGenericEventType(String.class)); - supportsEventType(true, method, createGenericEventType(Integer.class)); - supportsEventType(false, method, createGenericEventType(Double.class)); + supportsEventType(true, method, createPayloadEventType(String.class)); + supportsEventType(true, method, createPayloadEventType(Integer.class)); + supportsEventType(false, method, createPayloadEventType(Double.class)); } @Test @@ -325,6 +335,88 @@ public void beanInstanceRetrievedAtEveryInvocation() { verify(this.context, times(2)).getBean("testBean"); } + @Test // gh-30399 + void simplePayloadDoesNotSupportArbitraryGenericEventType() throws Exception { + Method method = SampleEvents.class.getDeclaredMethod("handleString", String.class); + ApplicationListenerMethodAdapter adapter = createTestInstance(method); + + assertThat(adapter.supportsEventType(createPayloadEventType(ResolvableType.forClassWithGenerics(EntityWrapper.class, Integer.class)))) + .as("handleString(String) with EntityWrapper").isFalse(); + assertThat(adapter.supportsEventType(createPayloadEventType(ResolvableType.forClass(EntityWrapper.class)))) + .as("handleString(String) with EntityWrapper").isFalse(); + assertThat(adapter.supportsEventType(createPayloadEventType(ResolvableType.forClass(String.class)))) + .as("handleString(String) with String").isTrue(); + } + + @Test // gh-30399 + void genericPayloadDoesNotSupportArbitraryGenericEventType() throws Exception { + Method method = SampleEvents.class.getDeclaredMethod("handleGenericStringPayload", EntityWrapper.class); + ApplicationListenerMethodAdapter adapter = createTestInstance(method); + + assertThat(adapter.supportsEventType(createPayloadEventType(ResolvableType.forClass(EntityWrapper.class)))) + .as("handleGenericStringPayload(EntityWrapper) with EntityWrapper").isFalse(); + assertThat(adapter.supportsEventType(createPayloadEventType(ResolvableType.forClassWithGenerics(EntityWrapper.class, Integer.class)))) + .as("handleGenericStringPayload(EntityWrapper) with EntityWrapper").isFalse(); + assertThat(adapter.supportsEventType(createPayloadEventType(ResolvableType.forClassWithGenerics(EntityWrapper.class, String.class)))) + .as("handleGenericStringPayload(EntityWrapper) with EntityWrapper").isTrue(); + } + + @Test // gh-30399 + void rawGenericPayloadDoesNotSupportArbitraryGenericEventType() throws Exception { + Method method = SampleEvents.class.getDeclaredMethod("handleGenericAnyPayload", EntityWrapper.class); + ApplicationListenerMethodAdapter adapter = createTestInstance(method); + + assertThat(adapter.supportsEventType(createPayloadEventType(ResolvableType.forClass(EntityWrapper.class)))) + .as("handleGenericAnyPayload(EntityWrapper) with EntityWrapper").isTrue(); + assertThat(adapter.supportsEventType(createPayloadEventType(ResolvableType.forClassWithGenerics(EntityWrapper.class, Integer.class)))) + .as("handleGenericAnyPayload(EntityWrapper) with EntityWrapper").isTrue(); + assertThat(adapter.supportsEventType(createPayloadEventType(ResolvableType.forClassWithGenerics(EntityWrapper.class, String.class)))) + .as("handleGenericAnyPayload(EntityWrapper) with EntityWrapper").isTrue(); + assertThat(adapter.supportsEventType(createPayloadEventType(ResolvableType.forClass(List.class)))) + .as("handleGenericAnyPayload(EntityWrapper) with List").isFalse(); + assertThat(adapter.supportsEventType(createPayloadEventType(ResolvableType.forClassWithGenerics(List.class, String.class)))) + .as("handleGenericAnyPayload(EntityWrapper) with List").isFalse(); + } + + @Test // gh-30399 + void genericApplicationEventSupportsSpecificType() throws Exception { + Method method = SampleEvents.class.getDeclaredMethod("handleGenericString", GenericTestEvent.class); + ApplicationListenerMethodAdapter adapter = createTestInstance(method); + + assertThat(adapter.supportsEventType(ResolvableType.forClass(GenericTestEvent.class))) + .as("handleGenericString(GenericTestEvent) with GenericTestEvent").isTrue(); + assertThat(adapter.supportsEventType(ResolvableType.forClassWithGenerics(GenericTestEvent.class, Integer.class))) + .as("handleGenericString(GenericTestEvent) with GenericTestEvent").isFalse(); + assertThat(adapter.supportsEventType(ResolvableType.forClassWithGenerics(GenericTestEvent.class, String.class))) + .as("handleGenericString(GenericTestEvent) with GenericTestEvent").isTrue(); + } + + @Test // gh-30399 + void genericRawApplicationEventSupportsRawTypeAndAnySpecificType() throws Exception { + Method method = SampleEvents.class.getDeclaredMethod("handleGenericRaw", GenericTestEvent.class); + ApplicationListenerMethodAdapter adapter = createTestInstance(method); + + assertThat(adapter.supportsEventType(ResolvableType.forClass(GenericTestEvent.class))) + .as("handleGenericRaw(GenericTestEvent) with GenericTestEvent").isTrue(); + assertThat(adapter.supportsEventType(ResolvableType.forClassWithGenerics(GenericTestEvent.class, String.class))) + .as("handleGenericRaw(GenericTestEvent) with GenericTestEvent").isTrue(); + assertThat(adapter.supportsEventType(ResolvableType.forClassWithGenerics(GenericTestEvent.class, Integer.class))) + .as("handleGenericRaw(GenericTestEvent) with GenericTestEvent").isTrue(); + } + + @Test // gh-30399 + void unrelatedApplicationEventDoesNotSupportRawTypeOrAnySpecificType() throws Exception { + Method method = SampleEvents.class.getDeclaredMethod("handleUnrelated", ContextRefreshedEvent.class); + ApplicationListenerMethodAdapter adapter = createTestInstance(method); + + assertThat(adapter.supportsEventType(ResolvableType.forClass(GenericTestEvent.class))) + .as("handleUnrelated(ContextRefreshedEvent) with GenericTestEvent").isTrue(); // known bug in 5.3.x + assertThat(adapter.supportsEventType(ResolvableType.forClassWithGenerics(GenericTestEvent.class, String.class))) + .as("handleUnrelated(ContextRefreshedEvent) with GenericTestEvent").isFalse(); + assertThat(adapter.supportsEventType(ResolvableType.forClassWithGenerics(GenericTestEvent.class, Integer.class))) + .as("handleUnrelated(ContextRefreshedEvent) with GenericTestEvent").isFalse(); + } + private void supportsEventType(boolean match, Method method, ResolvableType eventType) { ApplicationListenerMethodAdapter adapter = createTestInstance(method); @@ -341,7 +433,11 @@ private ApplicationListenerMethodAdapter createTestInstance(Method method) { return new StaticApplicationListenerMethodAdapter(method, this.sampleEvents); } - private ResolvableType createGenericEventType(Class payloadType) { + private ResolvableType createPayloadEventType(Class payloadType) { + return ResolvableType.forClassWithGenerics(PayloadApplicationEvent.class, payloadType); + } + + private ResolvableType createPayloadEventType(ResolvableType payloadType) { return ResolvableType.forClassWithGenerics(PayloadApplicationEvent.class, payloadType); } @@ -373,6 +469,14 @@ public void handleRaw(ApplicationEvent event) { public void handleGenericString(GenericTestEvent event) { } + @EventListener + public void handleGenericRaw(GenericTestEvent event) { + } + + @EventListener + public void handleUnrelated(ContextRefreshedEvent event) { + } + @EventListener public void handleString(String payload) { } diff --git a/spring-context/src/test/java/org/springframework/context/event/GenericApplicationListenerAdapterTests.java b/spring-context/src/test/java/org/springframework/context/event/GenericApplicationListenerAdapterTests.java index 7c0e299791e8..12682a5dcee3 100644 --- a/spring-context/src/test/java/org/springframework/context/event/GenericApplicationListenerAdapterTests.java +++ b/spring-context/src/test/java/org/springframework/context/event/GenericApplicationListenerAdapterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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. @@ -104,7 +104,8 @@ public void genericListenerStrictTypeNotMatchTypeErasure() { @Test public void genericListenerStrictTypeSubClass() { - supportsEventType(false, ObjectEventListener.class, ResolvableType.forClassWithGenerics(GenericTestEvent.class, Long.class)); + supportsEventType(false, ObjectEventListener.class, + ResolvableType.forClassWithGenerics(GenericTestEvent.class, Long.class)); } @Test diff --git a/spring-context/src/test/java/org/springframework/context/expression/ApplicationContextExpressionTests.java b/spring-context/src/test/java/org/springframework/context/expression/ApplicationContextExpressionTests.java index 7ba751b48e32..f8743c3da3f3 100644 --- a/spring-context/src/test/java/org/springframework/context/expression/ApplicationContextExpressionTests.java +++ b/spring-context/src/test/java/org/springframework/context/expression/ApplicationContextExpressionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 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-context/src/test/java/org/springframework/context/support/GenericApplicationContextTests.java b/spring-context/src/test/java/org/springframework/context/support/GenericApplicationContextTests.java index acc1894d3272..e8b8ac3ac090 100644 --- a/spring-context/src/test/java/org/springframework/context/support/GenericApplicationContextTests.java +++ b/spring-context/src/test/java/org/springframework/context/support/GenericApplicationContextTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -102,16 +102,16 @@ void accessAfterClosing() { assertThat(context.getBean(String.class)).isSameAs(context.getBean("testBean")); assertThat(context.getAutowireCapableBeanFactory().getBean(String.class)) - .isSameAs(context.getAutowireCapableBeanFactory().getBean("testBean")); + .isSameAs(context.getAutowireCapableBeanFactory().getBean("testBean")); context.close(); assertThatIllegalStateException() - .isThrownBy(() -> context.getBean(String.class)); + .isThrownBy(() -> context.getBean(String.class)); assertThatIllegalStateException() - .isThrownBy(() -> context.getAutowireCapableBeanFactory().getBean(String.class)); + .isThrownBy(() -> context.getAutowireCapableBeanFactory().getBean(String.class)); assertThatIllegalStateException() - .isThrownBy(() -> context.getAutowireCapableBeanFactory().getBean("testBean")); + .isThrownBy(() -> context.getAutowireCapableBeanFactory().getBean("testBean")); } @Test diff --git a/spring-context/src/test/java/org/springframework/format/datetime/DateFormatterTests.java b/spring-context/src/test/java/org/springframework/format/datetime/DateFormatterTests.java index 7737902082a9..f74f36edbd95 100644 --- a/spring-context/src/test/java/org/springframework/format/datetime/DateFormatterTests.java +++ b/spring-context/src/test/java/org/springframework/format/datetime/DateFormatterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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,9 +23,6 @@ import java.util.Locale; import java.util.TimeZone; -import org.joda.time.DateTimeZone; -import org.joda.time.format.DateTimeFormat; -import org.joda.time.format.DateTimeFormatter; import org.junit.jupiter.api.Test; import org.springframework.format.annotation.DateTimeFormat.ISO; @@ -33,83 +30,88 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; - - - /** * Tests for {@link DateFormatter}. * * @author Keith Donald * @author Phillip Webb + * @author Juergen Hoeller */ -public class DateFormatterTests { +class DateFormatterTests { private static final TimeZone UTC = TimeZone.getTimeZone("UTC"); @Test - public void shouldPrintAndParseDefault() throws Exception { + void shouldPrintAndParseDefault() throws Exception { DateFormatter formatter = new DateFormatter(); formatter.setTimeZone(UTC); + Date date = getDate(2009, Calendar.JUNE, 1); assertThat(formatter.print(date, Locale.US)).isEqualTo("Jun 1, 2009"); assertThat(formatter.parse("Jun 1, 2009", Locale.US)).isEqualTo(date); } @Test - public void shouldPrintAndParseFromPattern() throws ParseException { + void shouldPrintAndParseFromPattern() throws ParseException { DateFormatter formatter = new DateFormatter("yyyy-MM-dd"); formatter.setTimeZone(UTC); + Date date = getDate(2009, Calendar.JUNE, 1); assertThat(formatter.print(date, Locale.US)).isEqualTo("2009-06-01"); assertThat(formatter.parse("2009-06-01", Locale.US)).isEqualTo(date); } @Test - public void shouldPrintAndParseShort() throws Exception { + void shouldPrintAndParseShort() throws Exception { DateFormatter formatter = new DateFormatter(); formatter.setTimeZone(UTC); formatter.setStyle(DateFormat.SHORT); + Date date = getDate(2009, Calendar.JUNE, 1); assertThat(formatter.print(date, Locale.US)).isEqualTo("6/1/09"); assertThat(formatter.parse("6/1/09", Locale.US)).isEqualTo(date); } @Test - public void shouldPrintAndParseMedium() throws Exception { + void shouldPrintAndParseMedium() throws Exception { DateFormatter formatter = new DateFormatter(); formatter.setTimeZone(UTC); formatter.setStyle(DateFormat.MEDIUM); + Date date = getDate(2009, Calendar.JUNE, 1); assertThat(formatter.print(date, Locale.US)).isEqualTo("Jun 1, 2009"); assertThat(formatter.parse("Jun 1, 2009", Locale.US)).isEqualTo(date); } @Test - public void shouldPrintAndParseLong() throws Exception { + void shouldPrintAndParseLong() throws Exception { DateFormatter formatter = new DateFormatter(); formatter.setTimeZone(UTC); formatter.setStyle(DateFormat.LONG); + Date date = getDate(2009, Calendar.JUNE, 1); assertThat(formatter.print(date, Locale.US)).isEqualTo("June 1, 2009"); assertThat(formatter.parse("June 1, 2009", Locale.US)).isEqualTo(date); } @Test - public void shouldPrintAndParseFull() throws Exception { + void shouldPrintAndParseFull() throws Exception { DateFormatter formatter = new DateFormatter(); formatter.setTimeZone(UTC); formatter.setStyle(DateFormat.FULL); + Date date = getDate(2009, Calendar.JUNE, 1); assertThat(formatter.print(date, Locale.US)).isEqualTo("Monday, June 1, 2009"); assertThat(formatter.parse("Monday, June 1, 2009", Locale.US)).isEqualTo(date); } @Test - public void shouldPrintAndParseISODate() throws Exception { + void shouldPrintAndParseIsoDate() throws Exception { DateFormatter formatter = new DateFormatter(); formatter.setTimeZone(UTC); formatter.setIso(ISO.DATE); + Date date = getDate(2009, Calendar.JUNE, 1, 14, 23, 5, 3); assertThat(formatter.print(date, Locale.US)).isEqualTo("2009-06-01"); assertThat(formatter.parse("2009-6-01", Locale.US)) @@ -117,79 +119,56 @@ public void shouldPrintAndParseISODate() throws Exception { } @Test - public void shouldPrintAndParseISOTime() throws Exception { + void shouldPrintAndParseIsoTime() throws Exception { DateFormatter formatter = new DateFormatter(); formatter.setTimeZone(UTC); formatter.setIso(ISO.TIME); + Date date = getDate(2009, Calendar.JANUARY, 1, 14, 23, 5, 3); assertThat(formatter.print(date, Locale.US)).isEqualTo("14:23:05.003Z"); assertThat(formatter.parse("14:23:05.003Z", Locale.US)) .isEqualTo(getDate(1970, Calendar.JANUARY, 1, 14, 23, 5, 3)); + + date = getDate(2009, Calendar.JANUARY, 1, 14, 23, 5, 0); + assertThat(formatter.print(date, Locale.US)).isEqualTo("14:23:05.000Z"); + assertThat(formatter.parse("14:23:05Z", Locale.US)) + .isEqualTo(getDate(1970, Calendar.JANUARY, 1, 14, 23, 5, 0).toInstant()); } @Test - public void shouldPrintAndParseISODateTime() throws Exception { + void shouldPrintAndParseIsoDateTime() throws Exception { DateFormatter formatter = new DateFormatter(); formatter.setTimeZone(UTC); formatter.setIso(ISO.DATE_TIME); + Date date = getDate(2009, Calendar.JUNE, 1, 14, 23, 5, 3); assertThat(formatter.print(date, Locale.US)).isEqualTo("2009-06-01T14:23:05.003Z"); assertThat(formatter.parse("2009-06-01T14:23:05.003Z", Locale.US)).isEqualTo(date); - } - @Test - public void shouldSupportJodaStylePatterns() throws Exception { - String[] chars = { "S", "M", "-" }; - for (String d : chars) { - for (String t : chars) { - String style = d + t; - if (!style.equals("--")) { - Date date = getDate(2009, Calendar.JUNE, 10, 14, 23, 0, 0); - if (t.equals("-")) { - date = getDate(2009, Calendar.JUNE, 10); - } - else if (d.equals("-")) { - date = getDate(1970, Calendar.JANUARY, 1, 14, 23, 0, 0); - } - testJodaStylePatterns(style, Locale.US, date); - } - } - } - } - - private void testJodaStylePatterns(String style, Locale locale, Date date) throws Exception { - DateFormatter formatter = new DateFormatter(); - formatter.setTimeZone(UTC); - formatter.setStylePattern(style); - DateTimeFormatter jodaFormatter = DateTimeFormat.forStyle(style).withLocale(locale).withZone(DateTimeZone.UTC); - String jodaPrinted = jodaFormatter.print(date.getTime()); - assertThat(formatter.print(date, locale)) - .as("Unable to print style pattern " + style) - .isEqualTo(jodaPrinted); - assertThat(formatter.parse(jodaPrinted, locale)) - .as("Unable to parse style pattern " + style) - .isEqualTo(date); + date = getDate(2009, Calendar.JUNE, 1, 14, 23, 5, 0); + assertThat(formatter.print(date, Locale.US)).isEqualTo("2009-06-01T14:23:05.000Z"); + assertThat(formatter.parse("2009-06-01T14:23:05Z", Locale.US)).isEqualTo(date.toInstant()); } @Test - public void shouldThrowOnUnsupportedStylePattern() throws Exception { + void shouldThrowOnUnsupportedStylePattern() { DateFormatter formatter = new DateFormatter(); formatter.setStylePattern("OO"); - assertThatIllegalStateException().isThrownBy(() -> - formatter.parse("2009", Locale.US)) - .withMessageContaining("Unsupported style pattern 'OO'"); + + assertThatIllegalStateException().isThrownBy(() -> formatter.parse("2009", Locale.US)) + .withMessageContaining("Unsupported style pattern 'OO'"); } @Test - public void shouldUseCorrectOrder() throws Exception { + void shouldUseCorrectOrder() { DateFormatter formatter = new DateFormatter(); formatter.setTimeZone(UTC); formatter.setStyle(DateFormat.SHORT); formatter.setStylePattern("L-"); formatter.setIso(ISO.DATE_TIME); formatter.setPattern("yyyy"); - Date date = getDate(2009, Calendar.JUNE, 1, 14, 23, 5, 3); + Date date = getDate(2009, Calendar.JUNE, 1, 14, 23, 5, 3); assertThat(formatter.print(date, Locale.US)).as("uses pattern").isEqualTo("2009"); formatter.setPattern(""); diff --git a/spring-context/src/test/java/org/springframework/format/datetime/DateFormattingTests.java b/spring-context/src/test/java/org/springframework/format/datetime/DateFormattingTests.java index 948040c3b407..6f7751545e37 100644 --- a/spring-context/src/test/java/org/springframework/format/datetime/DateFormattingTests.java +++ b/spring-context/src/test/java/org/springframework/format/datetime/DateFormattingTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-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. @@ -52,7 +52,7 @@ * @author Juergen Hoeller * @author Sam Brannen */ -public class DateFormattingTests { +class DateFormattingTests { private final FormattingConversionService conversionService = new FormattingConversionService(); @@ -133,7 +133,7 @@ void styleDateWithInvalidFormat() { assertThat(exception) .hasMessageContaining("for property 'styleDate'") .hasCauseInstanceOf(ConversionFailedException.class).cause() - .hasMessageContaining("for value '99/01/01'") + .hasMessageContaining("for value [99/01/01]") .hasCauseInstanceOf(IllegalArgumentException.class).cause() .hasMessageContaining("Parse attempt failed for value [99/01/01]") .hasCauseInstanceOf(ParseException.class).cause() @@ -353,7 +353,7 @@ void patternDateWithUnsupportedPattern() { assertThat(fieldError.unwrap(TypeMismatchException.class)) .hasMessageContaining("for property 'patternDateWithFallbackPatterns'") .hasCauseInstanceOf(ConversionFailedException.class).cause() - .hasMessageContaining("for value '210302'") + .hasMessageContaining("for value [210302]") .hasCauseInstanceOf(IllegalArgumentException.class).cause() .hasMessageContaining("Parse attempt failed for value [210302]") .hasCauseInstanceOf(ParseException.class).cause() diff --git a/spring-context/src/test/java/org/springframework/format/datetime/standard/DateTimeFormattingTests.java b/spring-context/src/test/java/org/springframework/format/datetime/standard/DateTimeFormattingTests.java index 142bd6b28b76..0d5c243091d8 100644 --- a/spring-context/src/test/java/org/springframework/format/datetime/standard/DateTimeFormattingTests.java +++ b/spring-context/src/test/java/org/springframework/format/datetime/standard/DateTimeFormattingTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-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. @@ -105,7 +105,7 @@ void testBindLocalDate() { MutablePropertyValues propertyValues = new MutablePropertyValues(); propertyValues.add("localDate", "10/31/09"); binder.bind(propertyValues); - assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getErrorCount()).isZero(); assertThat(binder.getBindingResult().getFieldValue("localDate")).isEqualTo("10/31/09"); } @@ -117,7 +117,7 @@ void testBindLocalDateWithSpecificStyle() { MutablePropertyValues propertyValues = new MutablePropertyValues(); propertyValues.add("localDate", "October 31, 2009"); binder.bind(propertyValues); - assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getErrorCount()).isZero(); assertThat(binder.getBindingResult().getFieldValue("localDate")).isEqualTo("October 31, 2009"); } @@ -129,7 +129,7 @@ void testBindLocalDateWithSpecificFormatter() { MutablePropertyValues propertyValues = new MutablePropertyValues(); propertyValues.add("localDate", "20091031"); binder.bind(propertyValues); - assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getErrorCount()).isZero(); assertThat(binder.getBindingResult().getFieldValue("localDate")).isEqualTo("20091031"); } @@ -138,7 +138,7 @@ void testBindLocalDateArray() { MutablePropertyValues propertyValues = new MutablePropertyValues(); propertyValues.add("localDate", new String[] {"10/31/09"}); binder.bind(propertyValues); - assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getErrorCount()).isZero(); } @Test @@ -146,7 +146,7 @@ void testBindLocalDateAnnotated() { MutablePropertyValues propertyValues = new MutablePropertyValues(); propertyValues.add("styleLocalDate", "Oct 31, 2009"); binder.bind(propertyValues); - assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getErrorCount()).isZero(); assertThat(binder.getBindingResult().getFieldValue("styleLocalDate")).isEqualTo("Oct 31, 2009"); } @@ -164,7 +164,7 @@ void testBindNestedLocalDateAnnotated() { MutablePropertyValues propertyValues = new MutablePropertyValues(); propertyValues.add("children[0].styleLocalDate", "Oct 31, 2009"); binder.bind(propertyValues); - assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getErrorCount()).isZero(); assertThat(binder.getBindingResult().getFieldValue("children[0].styleLocalDate")).isEqualTo("Oct 31, 2009"); } @@ -174,7 +174,7 @@ void testBindLocalDateAnnotatedWithDirectFieldAccess() { MutablePropertyValues propertyValues = new MutablePropertyValues(); propertyValues.add("styleLocalDate", "Oct 31, 2009"); binder.bind(propertyValues); - assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getErrorCount()).isZero(); assertThat(binder.getBindingResult().getFieldValue("styleLocalDate")).isEqualTo("Oct 31, 2009"); } @@ -193,7 +193,7 @@ void testBindLocalDateFromJavaUtilCalendar() { MutablePropertyValues propertyValues = new MutablePropertyValues(); propertyValues.add("localDate", new GregorianCalendar(2009, 9, 31, 0, 0)); binder.bind(propertyValues); - assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getErrorCount()).isZero(); assertThat(binder.getBindingResult().getFieldValue("localDate")).isEqualTo("10/31/09"); } @@ -202,7 +202,7 @@ void testBindLocalTime() { MutablePropertyValues propertyValues = new MutablePropertyValues(); propertyValues.add("localTime", "12:00 PM"); binder.bind(propertyValues); - assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getErrorCount()).isZero(); assertThat(binder.getBindingResult().getFieldValue("localTime")).isEqualTo("12:00 PM"); } @@ -214,7 +214,7 @@ void testBindLocalTimeWithSpecificStyle() { MutablePropertyValues propertyValues = new MutablePropertyValues(); propertyValues.add("localTime", "12:00:00 PM"); binder.bind(propertyValues); - assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getErrorCount()).isZero(); assertThat(binder.getBindingResult().getFieldValue("localTime")).isEqualTo("12:00:00 PM"); } @@ -226,7 +226,7 @@ void testBindLocalTimeWithSpecificFormatter() { MutablePropertyValues propertyValues = new MutablePropertyValues(); propertyValues.add("localTime", "130000"); binder.bind(propertyValues); - assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getErrorCount()).isZero(); assertThat(binder.getBindingResult().getFieldValue("localTime")).isEqualTo("130000"); } @@ -235,7 +235,7 @@ void testBindLocalTimeAnnotated() { MutablePropertyValues propertyValues = new MutablePropertyValues(); propertyValues.add("styleLocalTime", "12:00:00 PM"); binder.bind(propertyValues); - assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getErrorCount()).isZero(); assertThat(binder.getBindingResult().getFieldValue("styleLocalTime")).isEqualTo("12:00:00 PM"); } @@ -244,7 +244,7 @@ void testBindLocalTimeFromJavaUtilCalendar() { MutablePropertyValues propertyValues = new MutablePropertyValues(); propertyValues.add("localTime", new GregorianCalendar(1970, 0, 0, 12, 0)); binder.bind(propertyValues); - assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getErrorCount()).isZero(); assertThat(binder.getBindingResult().getFieldValue("localTime")).isEqualTo("12:00 PM"); } @@ -253,10 +253,9 @@ void testBindLocalDateTime() { MutablePropertyValues propertyValues = new MutablePropertyValues(); propertyValues.add("localDateTime", LocalDateTime.of(2009, 10, 31, 12, 0)); binder.bind(propertyValues); - assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getErrorCount()).isZero(); String value = binder.getBindingResult().getFieldValue("localDateTime").toString(); - assertThat(value.startsWith("10/31/09")).isTrue(); - assertThat(value.endsWith("12:00 PM")).isTrue(); + assertThat(value).startsWith("10/31/09").endsWith("12:00 PM"); } @Test @@ -264,10 +263,9 @@ void testBindLocalDateTimeAnnotated() { MutablePropertyValues propertyValues = new MutablePropertyValues(); propertyValues.add("styleLocalDateTime", LocalDateTime.of(2009, 10, 31, 12, 0)); binder.bind(propertyValues); - assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getErrorCount()).isZero(); String value = binder.getBindingResult().getFieldValue("styleLocalDateTime").toString(); - assertThat(value.startsWith("Oct 31, 2009")).isTrue(); - assertThat(value.endsWith("12:00:00 PM")).isTrue(); + assertThat(value).startsWith("Oct 31, 2009").endsWith("12:00:00 PM"); } @Test @@ -275,10 +273,9 @@ void testBindLocalDateTimeFromJavaUtilCalendar() { MutablePropertyValues propertyValues = new MutablePropertyValues(); propertyValues.add("localDateTime", new GregorianCalendar(2009, 9, 31, 12, 0)); binder.bind(propertyValues); - assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getErrorCount()).isZero(); String value = binder.getBindingResult().getFieldValue("localDateTime").toString(); - assertThat(value.startsWith("10/31/09")).isTrue(); - assertThat(value.endsWith("12:00 PM")).isTrue(); + assertThat(value).startsWith("10/31/09").endsWith("12:00 PM"); } @Test @@ -289,10 +286,9 @@ void testBindDateTimeWithSpecificStyle() { MutablePropertyValues propertyValues = new MutablePropertyValues(); propertyValues.add("localDateTime", LocalDateTime.of(2009, 10, 31, 12, 0)); binder.bind(propertyValues); - assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getErrorCount()).isZero(); String value = binder.getBindingResult().getFieldValue("localDateTime").toString(); - assertThat(value.startsWith("Oct 31, 2009")).isTrue(); - assertThat(value.endsWith("12:00:00 PM")).isTrue(); + assertThat(value).startsWith("Oct 31, 2009").endsWith("12:00:00 PM"); } @Test @@ -300,7 +296,7 @@ void testBindPatternLocalDateTime() { MutablePropertyValues propertyValues = new MutablePropertyValues(); propertyValues.add("patternLocalDateTime", "10/31/09 12:00 PM"); binder.bind(propertyValues); - assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getErrorCount()).isZero(); assertThat(binder.getBindingResult().getFieldValue("patternLocalDateTime")).isEqualTo("10/31/09 12:00 PM"); } @@ -317,7 +313,7 @@ void testBindISODate() { MutablePropertyValues propertyValues = new MutablePropertyValues(); propertyValues.add("isoLocalDate", "2009-10-31"); binder.bind(propertyValues); - assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getErrorCount()).isZero(); assertThat(binder.getBindingResult().getFieldValue("isoLocalDate")).isEqualTo("2009-10-31"); } @@ -333,7 +329,7 @@ void isoLocalDateWithInvalidFormat() { assertThat(fieldError.unwrap(TypeMismatchException.class)) .hasMessageContaining("for property 'isoLocalDate'") .hasCauseInstanceOf(ConversionFailedException.class).cause() - .hasMessageContaining("for value '2009-31-10'") + .hasMessageContaining("for value [2009-31-10]") .hasCauseInstanceOf(IllegalArgumentException.class).cause() .hasMessageContaining("Parse attempt failed for value [2009-31-10]") .hasCauseInstanceOf(DateTimeParseException.class).cause() @@ -356,7 +352,7 @@ void testBindISOTime() { MutablePropertyValues propertyValues = new MutablePropertyValues(); propertyValues.add("isoLocalTime", "12:00:00"); binder.bind(propertyValues); - assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getErrorCount()).isZero(); assertThat(binder.getBindingResult().getFieldValue("isoLocalTime")).isEqualTo("12:00:00"); } @@ -365,7 +361,7 @@ void testBindISOTimeWithZone() { MutablePropertyValues propertyValues = new MutablePropertyValues(); propertyValues.add("isoLocalTime", "12:00:00.000-05:00"); binder.bind(propertyValues); - assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getErrorCount()).isZero(); assertThat(binder.getBindingResult().getFieldValue("isoLocalTime")).isEqualTo("12:00:00"); } @@ -374,7 +370,7 @@ void testBindISODateTime() { MutablePropertyValues propertyValues = new MutablePropertyValues(); propertyValues.add("isoLocalDateTime", "2009-10-31T12:00:00"); binder.bind(propertyValues); - assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getErrorCount()).isZero(); assertThat(binder.getBindingResult().getFieldValue("isoLocalDateTime")).isEqualTo("2009-10-31T12:00:00"); } @@ -383,7 +379,7 @@ void testBindISODateTimeWithZone() { MutablePropertyValues propertyValues = new MutablePropertyValues(); propertyValues.add("isoLocalDateTime", "2009-10-31T12:00:00.000Z"); binder.bind(propertyValues); - assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getErrorCount()).isZero(); assertThat(binder.getBindingResult().getFieldValue("isoLocalDateTime")).isEqualTo("2009-10-31T12:00:00"); } @@ -392,8 +388,8 @@ void testBindInstant() { MutablePropertyValues propertyValues = new MutablePropertyValues(); propertyValues.add("instant", "2009-10-31T12:00:00.000Z"); binder.bind(propertyValues); - assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); - assertThat(binder.getBindingResult().getFieldValue("instant").toString().startsWith("2009-10-31T12:00")).isTrue(); + assertThat(binder.getBindingResult().getErrorCount()).isZero(); + assertThat(binder.getBindingResult().getFieldValue("instant").toString()).startsWith("2009-10-31T12:00"); } @Test @@ -405,8 +401,8 @@ void testBindInstantFromJavaUtilDate() { MutablePropertyValues propertyValues = new MutablePropertyValues(); propertyValues.add("instant", new Date(109, 9, 31, 12, 0)); binder.bind(propertyValues); - assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); - assertThat(binder.getBindingResult().getFieldValue("instant").toString().startsWith("2009-10-31")).isTrue(); + assertThat(binder.getBindingResult().getErrorCount()).isZero(); + assertThat(binder.getBindingResult().getFieldValue("instant").toString()).startsWith("2009-10-31"); } finally { TimeZone.setDefault(defaultZone); @@ -418,8 +414,8 @@ void testBindPeriod() { MutablePropertyValues propertyValues = new MutablePropertyValues(); propertyValues.add("period", "P6Y3M1D"); binder.bind(propertyValues); - assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); - assertThat(binder.getBindingResult().getFieldValue("period").toString().equals("P6Y3M1D")).isTrue(); + assertThat(binder.getBindingResult().getErrorCount()).isZero(); + assertThat(binder.getBindingResult().getFieldValue("period").toString()).isEqualTo("P6Y3M1D"); } @Test @@ -427,8 +423,8 @@ void testBindDuration() { MutablePropertyValues propertyValues = new MutablePropertyValues(); propertyValues.add("duration", "PT8H6M12.345S"); binder.bind(propertyValues); - assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); - assertThat(binder.getBindingResult().getFieldValue("duration").toString().equals("PT8H6M12.345S")).isTrue(); + assertThat(binder.getBindingResult().getErrorCount()).isZero(); + assertThat(binder.getBindingResult().getFieldValue("duration").toString()).isEqualTo("PT8H6M12.345S"); } @Test @@ -436,8 +432,8 @@ void testBindYear() { MutablePropertyValues propertyValues = new MutablePropertyValues(); propertyValues.add("year", "2007"); binder.bind(propertyValues); - assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); - assertThat(binder.getBindingResult().getFieldValue("year").toString().equals("2007")).isTrue(); + assertThat(binder.getBindingResult().getErrorCount()).isZero(); + assertThat(binder.getBindingResult().getFieldValue("year").toString()).isEqualTo("2007"); } @Test @@ -445,8 +441,8 @@ void testBindMonth() { MutablePropertyValues propertyValues = new MutablePropertyValues(); propertyValues.add("month", "JULY"); binder.bind(propertyValues); - assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); - assertThat(binder.getBindingResult().getFieldValue("month").toString().equals("JULY")).isTrue(); + assertThat(binder.getBindingResult().getErrorCount()).isZero(); + assertThat(binder.getBindingResult().getFieldValue("month").toString()).isEqualTo("JULY"); } @Test @@ -454,8 +450,8 @@ void testBindMonthInAnyCase() { MutablePropertyValues propertyValues = new MutablePropertyValues(); propertyValues.add("month", "July"); binder.bind(propertyValues); - assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); - assertThat(binder.getBindingResult().getFieldValue("month").toString().equals("JULY")).isTrue(); + assertThat(binder.getBindingResult().getErrorCount()).isZero(); + assertThat(binder.getBindingResult().getFieldValue("month").toString()).isEqualTo("JULY"); } @Test @@ -463,8 +459,8 @@ void testBindYearMonth() { MutablePropertyValues propertyValues = new MutablePropertyValues(); propertyValues.add("yearMonth", "2007-12"); binder.bind(propertyValues); - assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); - assertThat(binder.getBindingResult().getFieldValue("yearMonth").toString().equals("2007-12")).isTrue(); + assertThat(binder.getBindingResult().getErrorCount()).isZero(); + assertThat(binder.getBindingResult().getFieldValue("yearMonth").toString()).isEqualTo("2007-12"); } @Test @@ -472,10 +468,11 @@ void testBindMonthDay() { MutablePropertyValues propertyValues = new MutablePropertyValues(); propertyValues.add("monthDay", "--12-03"); binder.bind(propertyValues); - assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); - assertThat(binder.getBindingResult().getFieldValue("monthDay").toString().equals("--12-03")).isTrue(); + assertThat(binder.getBindingResult().getErrorCount()).isZero(); + assertThat(binder.getBindingResult().getFieldValue("monthDay").toString()).isEqualTo("--12-03"); } + @Nested class FallbackPatternTests { @@ -487,7 +484,7 @@ void styleLocalDate(String propertyValue) { propertyValues.add(propertyName, propertyValue); binder.bind(propertyValues); BindingResult bindingResult = binder.getBindingResult(); - assertThat(bindingResult.getErrorCount()).isEqualTo(0); + assertThat(bindingResult.getErrorCount()).isZero(); assertThat(bindingResult.getFieldValue(propertyName)).isEqualTo("3/2/21"); } @@ -499,11 +496,12 @@ void patternLocalDate(String propertyValue) { propertyValues.add(propertyName, propertyValue); binder.bind(propertyValues); BindingResult bindingResult = binder.getBindingResult(); - assertThat(bindingResult.getErrorCount()).isEqualTo(0); + assertThat(bindingResult.getErrorCount()).isZero(); assertThat(bindingResult.getFieldValue(propertyName)).isEqualTo("2021-03-02"); } @ParameterizedTest(name = "input date: {0}") + // @ValueSource(strings = {"12:00:00\u202FPM", "12:00:00", "12:00"}) @ValueSource(strings = {"12:00:00 PM", "12:00:00", "12:00"}) void styleLocalTime(String propertyValue) { String propertyName = "styleLocalTimeWithFallbackPatterns"; @@ -511,7 +509,8 @@ void styleLocalTime(String propertyValue) { propertyValues.add(propertyName, propertyValue); binder.bind(propertyValues); BindingResult bindingResult = binder.getBindingResult(); - assertThat(bindingResult.getErrorCount()).isEqualTo(0); + assertThat(bindingResult.getErrorCount()).isZero(); + // assertThat(bindingResult.getFieldValue(propertyName)).asString().matches("12:00:00\\SPM"); assertThat(bindingResult.getFieldValue(propertyName)).isEqualTo("12:00:00 PM"); } @@ -523,7 +522,7 @@ void isoLocalDateTime(String propertyValue) { propertyValues.add(propertyName, propertyValue); binder.bind(propertyValues); BindingResult bindingResult = binder.getBindingResult(); - assertThat(bindingResult.getErrorCount()).isEqualTo(0); + assertThat(bindingResult.getErrorCount()).isZero(); assertThat(bindingResult.getFieldValue(propertyName)).isEqualTo("2021-03-02T12:00:00"); } @@ -540,7 +539,7 @@ void patternLocalDateWithUnsupportedPattern() { assertThat(fieldError.unwrap(TypeMismatchException.class)) .hasMessageContaining("for property 'patternLocalDateWithFallbackPatterns'") .hasCauseInstanceOf(ConversionFailedException.class).cause() - .hasMessageContaining("for value '210302'") + .hasMessageContaining("for value [210302]") .hasCauseInstanceOf(IllegalArgumentException.class).cause() .hasMessageContaining("Parse attempt failed for value [210302]") .hasCauseInstanceOf(DateTimeParseException.class).cause() @@ -565,10 +564,10 @@ public static class DateTimeBean { @DateTimeFormat(style = "M-") private LocalDate styleLocalDate; - @DateTimeFormat(style = "S-", fallbackPatterns = { "yyyy-MM-dd", "yyyyMMdd", "yyyy.MM.dd" }) + @DateTimeFormat(style = "S-", fallbackPatterns = {"yyyy-MM-dd", "yyyyMMdd", "yyyy.MM.dd"}) private LocalDate styleLocalDateWithFallbackPatterns; - @DateTimeFormat(pattern = "yyyy-MM-dd", fallbackPatterns = { "M/d/yy", "yyyyMMdd", "yyyy.MM.dd" }) + @DateTimeFormat(pattern = "yyyy-MM-dd", fallbackPatterns = {"M/d/yy", "yyyyMMdd", "yyyy.MM.dd"}) private LocalDate patternLocalDateWithFallbackPatterns; private LocalTime localTime; @@ -576,7 +575,7 @@ public static class DateTimeBean { @DateTimeFormat(style = "-M") private LocalTime styleLocalTime; - @DateTimeFormat(style = "-M", fallbackPatterns = { "HH:mm:ss", "HH:mm"}) + @DateTimeFormat(style = "-M", fallbackPatterns = {"HH:mm:ss", "HH:mm"}) private LocalTime styleLocalTimeWithFallbackPatterns; private LocalDateTime localDateTime; @@ -596,7 +595,7 @@ public static class DateTimeBean { @DateTimeFormat(iso = ISO.DATE_TIME) private LocalDateTime isoLocalDateTime; - @DateTimeFormat(iso = ISO.DATE_TIME, fallbackPatterns = { "yyyy-MM-dd HH:mm:ss", "M/d/yy HH:mm"}) + @DateTimeFormat(iso = ISO.DATE_TIME, fallbackPatterns = {"yyyy-MM-dd HH:mm:ss", "M/d/yy HH:mm"}) private LocalDateTime isoLocalDateTimeWithFallbackPatterns; private Instant instant; @@ -615,7 +614,6 @@ public static class DateTimeBean { private final List children = new ArrayList<>(); - public LocalDate getLocalDate() { return this.localDate; } diff --git a/spring-context/src/test/java/org/springframework/format/datetime/standard/InstantFormatterTests.java b/spring-context/src/test/java/org/springframework/format/datetime/standard/InstantFormatterTests.java index 16ba2cbd5e5c..bd4058fdcae1 100644 --- a/spring-context/src/test/java/org/springframework/format/datetime/standard/InstantFormatterTests.java +++ b/spring-context/src/test/java/org/springframework/format/datetime/standard/InstantFormatterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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.text.ParseException; import java.time.Instant; import java.time.format.DateTimeFormatter; +import java.util.Locale; import java.util.Random; import java.util.stream.Stream; @@ -49,13 +50,12 @@ class InstantFormatterTests { private final InstantFormatter instantFormatter = new InstantFormatter(); + @ParameterizedTest @ArgumentsSource(ISOSerializedInstantProvider.class) void should_parse_an_ISO_formatted_string_representation_of_an_Instant(String input) throws ParseException { Instant expected = DateTimeFormatter.ISO_INSTANT.parse(input, Instant::from); - - Instant actual = instantFormatter.parse(input, null); - + Instant actual = instantFormatter.parse(input, Locale.US); assertThat(actual).isEqualTo(expected); } @@ -63,9 +63,7 @@ void should_parse_an_ISO_formatted_string_representation_of_an_Instant(String in @ArgumentsSource(RFC1123SerializedInstantProvider.class) void should_parse_an_RFC1123_formatted_string_representation_of_an_Instant(String input) throws ParseException { Instant expected = DateTimeFormatter.RFC_1123_DATE_TIME.parse(input, Instant::from); - - Instant actual = instantFormatter.parse(input, null); - + Instant actual = instantFormatter.parse(input, Locale.US); assertThat(actual).isEqualTo(expected); } @@ -73,12 +71,11 @@ void should_parse_an_RFC1123_formatted_string_representation_of_an_Instant(Strin @ArgumentsSource(RandomInstantProvider.class) void should_serialize_an_Instant_using_ISO_format_and_ignoring_Locale(Instant input) { String expected = DateTimeFormatter.ISO_INSTANT.format(input); - - String actual = instantFormatter.print(input, null); - + String actual = instantFormatter.print(input, Locale.US); assertThat(actual).isEqualTo(expected); } + private static class RandomInstantProvider implements ArgumentsProvider { private static final long DATA_SET_SIZE = 10; @@ -100,6 +97,7 @@ Stream randomInstantStream(Instant min, Instant max) { } } + private static class ISOSerializedInstantProvider extends RandomInstantProvider { @Override @@ -108,6 +106,7 @@ Stream provideArguments() { } } + private static class RFC1123SerializedInstantProvider extends RandomInstantProvider { // RFC-1123 supports only 4-digit years diff --git a/spring-context/src/test/java/org/springframework/instrument/classloading/ReflectiveLoadTimeWeaverTests.java b/spring-context/src/test/java/org/springframework/instrument/classloading/ReflectiveLoadTimeWeaverTests.java index 448f71572ac6..989b1e196de2 100644 --- a/spring-context/src/test/java/org/springframework/instrument/classloading/ReflectiveLoadTimeWeaverTests.java +++ b/spring-context/src/test/java/org/springframework/instrument/classloading/ReflectiveLoadTimeWeaverTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 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. @@ -85,16 +85,13 @@ public static class JustAddTransformerClassLoader extends ClassLoader { private int numTimesAddTransformerCalled = 0; - public int getNumTimesGetThrowawayClassLoaderCalled() { return this.numTimesAddTransformerCalled; } - public void addTransformer(ClassFileTransformer transformer) { ++this.numTimesAddTransformerCalled; } - } @@ -102,18 +99,15 @@ public static final class TotallyCompliantClassLoader extends JustAddTransformer private int numTimesGetThrowawayClassLoaderCalled = 0; - @Override public int getNumTimesGetThrowawayClassLoaderCalled() { return this.numTimesGetThrowawayClassLoaderCalled; } - public ClassLoader getThrowawayClassLoader() { ++this.numTimesGetThrowawayClassLoaderCalled; return getClass().getClassLoader(); } - } } diff --git a/spring-context/src/test/java/org/springframework/jmx/AbstractMBeanServerTests.java b/spring-context/src/test/java/org/springframework/jmx/AbstractMBeanServerTests.java index b43ec1709e57..6078982e1381 100644 --- a/spring-context/src/test/java/org/springframework/jmx/AbstractMBeanServerTests.java +++ b/spring-context/src/test/java/org/springframework/jmx/AbstractMBeanServerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2022 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-context/src/test/java/org/springframework/jmx/support/JmxUtilsTests.java b/spring-context/src/test/java/org/springframework/jmx/support/JmxUtilsTests.java index f8ef4348bc00..30b8eb122c4a 100644 --- a/spring-context/src/test/java/org/springframework/jmx/support/JmxUtilsTests.java +++ b/spring-context/src/test/java/org/springframework/jmx/support/JmxUtilsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2022 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,7 +20,6 @@ import javax.management.DynamicMBean; import javax.management.MBeanServer; -import javax.management.MBeanServerFactory; import javax.management.MalformedObjectNameException; import javax.management.NotCompliantMBeanException; import javax.management.ObjectName; @@ -32,6 +31,7 @@ import org.springframework.jmx.IJmxTestBean; import org.springframework.jmx.JmxTestBean; import org.springframework.jmx.export.TestDynamicMBean; +import org.springframework.util.MBeanTestUtils; import org.springframework.util.ObjectUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -41,6 +41,7 @@ * * @author Rob Harrop * @author Juergen Hoeller + * @author Sam Brannen */ class JmxUtilsTests { @@ -131,10 +132,11 @@ void locatePlatformMBeanServer() { MBeanServer server = null; try { server = JmxUtils.locateMBeanServer(); + assertThat(server).isNotNull(); } finally { if (server != null) { - MBeanServerFactory.releaseMBeanServer(server); + MBeanTestUtils.releaseMBeanServer(server); } } } diff --git a/spring-context/src/test/java/org/springframework/jndi/JndiLocatorDelegateTests.java b/spring-context/src/test/java/org/springframework/jndi/JndiLocatorDelegateTests.java index 700860e11d6d..5e139967ad54 100644 --- a/spring-context/src/test/java/org/springframework/jndi/JndiLocatorDelegateTests.java +++ b/spring-context/src/test/java/org/springframework/jndi/JndiLocatorDelegateTests.java @@ -21,21 +21,25 @@ import javax.naming.spi.NamingManager; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledForJreRange; import static org.assertj.core.api.Assertions.assertThat; - - +import static org.junit.jupiter.api.condition.JRE.JAVA_16; /** * Tests for {@link JndiLocatorDelegate}. * * @author Phillip Webb * @author Juergen Hoeller + * @author Sam Brannen */ -public class JndiLocatorDelegateTests { +@DisabledForJreRange( + min = JAVA_16, + disabledReason = "Cannot use reflection to set private static field in javax.naming.spi.NamingManager") +class JndiLocatorDelegateTests { @Test - public void isDefaultJndiEnvironmentAvailableFalse() throws Exception { + void isDefaultJndiEnvironmentAvailableFalse() throws Exception { Field builderField = NamingManager.class.getDeclaredField("initctx_factory_builder"); builderField.setAccessible(true); Object oldBuilder = builderField.get(null); diff --git a/spring-context/src/test/java/org/springframework/mock/env/MockEnvironment.java b/spring-context/src/test/java/org/springframework/mock/env/MockEnvironment.java index 6397865a1f88..9a533f563570 100644 --- a/spring-context/src/test/java/org/springframework/mock/env/MockEnvironment.java +++ b/spring-context/src/test/java/org/springframework/mock/env/MockEnvironment.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2023 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-context/src/test/java/org/springframework/scheduling/annotation/EnableAsyncTests.java b/spring-context/src/test/java/org/springframework/scheduling/annotation/EnableAsyncTests.java index e60333950f40..04ba84eedd75 100644 --- a/spring-context/src/test/java/org/springframework/scheduling/annotation/EnableAsyncTests.java +++ b/spring-context/src/test/java/org/springframework/scheduling/annotation/EnableAsyncTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 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. @@ -95,9 +95,8 @@ public void proxyingOccursWithMockitoStub() { public void properExceptionForExistingProxyDependencyMismatch() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); ctx.register(AsyncConfig.class, AsyncBeanWithInterface.class, AsyncBeanUser.class); - assertThatExceptionOfType(UnsatisfiedDependencyException.class).isThrownBy( - ctx::refresh) - .withCauseInstanceOf(BeanNotOfRequiredTypeException.class); + assertThatExceptionOfType(UnsatisfiedDependencyException.class).isThrownBy(ctx::refresh) + .withCauseInstanceOf(BeanNotOfRequiredTypeException.class); ctx.close(); } @@ -105,9 +104,8 @@ public void properExceptionForExistingProxyDependencyMismatch() { public void properExceptionForResolvedProxyDependencyMismatch() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); ctx.register(AsyncConfig.class, AsyncBeanUser.class, AsyncBeanWithInterface.class); - assertThatExceptionOfType(UnsatisfiedDependencyException.class).isThrownBy( - ctx::refresh) - .withCauseInstanceOf(BeanNotOfRequiredTypeException.class); + assertThatExceptionOfType(UnsatisfiedDependencyException.class).isThrownBy(ctx::refresh) + .withCauseInstanceOf(BeanNotOfRequiredTypeException.class); ctx.close(); } @@ -182,8 +180,7 @@ public void aspectModeAspectJAttemptsToRegisterAsyncAspect() { @SuppressWarnings("resource") AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); ctx.register(AspectJAsyncAnnotationConfig.class); - assertThatExceptionOfType(BeanDefinitionStoreException.class).isThrownBy( - ctx::refresh); + assertThatExceptionOfType(BeanDefinitionStoreException.class).isThrownBy(ctx::refresh); } @Test diff --git a/spring-context/src/test/java/org/springframework/scheduling/annotation/EnableSchedulingTests.java b/spring-context/src/test/java/org/springframework/scheduling/annotation/EnableSchedulingTests.java index 28bdafd59f88..dc718fdeacef 100644 --- a/spring-context/src/test/java/org/springframework/scheduling/annotation/EnableSchedulingTests.java +++ b/spring-context/src/test/java/org/springframework/scheduling/annotation/EnableSchedulingTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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. @@ -61,9 +61,9 @@ public void tearDown() { @EnabledForTestGroups(LONG_RUNNING) public void withFixedRateTask() throws InterruptedException { ctx = new AnnotationConfigApplicationContext(FixedRateTaskConfig.class); - assertThat(ctx.getBean(ScheduledTaskHolder.class).getScheduledTasks().size()).isEqualTo(2); + assertThat(ctx.getBean(ScheduledTaskHolder.class).getScheduledTasks()).hasSize(2); - Thread.sleep(100); + Thread.sleep(110); assertThat(ctx.getBean(AtomicInteger.class).get()).isGreaterThanOrEqualTo(10); } @@ -71,9 +71,9 @@ public void withFixedRateTask() throws InterruptedException { @EnabledForTestGroups(LONG_RUNNING) public void withSubclass() throws InterruptedException { ctx = new AnnotationConfigApplicationContext(FixedRateTaskConfigSubclass.class); - assertThat(ctx.getBean(ScheduledTaskHolder.class).getScheduledTasks().size()).isEqualTo(2); + assertThat(ctx.getBean(ScheduledTaskHolder.class).getScheduledTasks()).hasSize(2); - Thread.sleep(100); + Thread.sleep(110); assertThat(ctx.getBean(AtomicInteger.class).get()).isGreaterThanOrEqualTo(10); } @@ -81,13 +81,13 @@ public void withSubclass() throws InterruptedException { @EnabledForTestGroups(LONG_RUNNING) public void withExplicitScheduler() throws InterruptedException { ctx = new AnnotationConfigApplicationContext(ExplicitSchedulerConfig.class); - assertThat(ctx.getBean(ScheduledTaskHolder.class).getScheduledTasks().size()).isEqualTo(1); + assertThat(ctx.getBean(ScheduledTaskHolder.class).getScheduledTasks()).hasSize(1); - Thread.sleep(100); + Thread.sleep(110); assertThat(ctx.getBean(AtomicInteger.class).get()).isGreaterThanOrEqualTo(10); assertThat(ctx.getBean(ExplicitSchedulerConfig.class).threadName).startsWith("explicitScheduler-"); assertThat(Arrays.asList(ctx.getDefaultListableBeanFactory().getDependentBeans("myTaskScheduler")).contains( - TaskManagementConfigUtils.SCHEDULED_ANNOTATION_PROCESSOR_BEAN_NAME)).isTrue(); + TaskManagementConfigUtils.SCHEDULED_ANNOTATION_PROCESSOR_BEAN_NAME)).isTrue(); } @Test @@ -100,9 +100,9 @@ public void withExplicitSchedulerAmbiguity_andSchedulingEnabled() { @EnabledForTestGroups(LONG_RUNNING) public void withExplicitScheduledTaskRegistrar() throws InterruptedException { ctx = new AnnotationConfigApplicationContext(ExplicitScheduledTaskRegistrarConfig.class); - assertThat(ctx.getBean(ScheduledTaskHolder.class).getScheduledTasks().size()).isEqualTo(1); + assertThat(ctx.getBean(ScheduledTaskHolder.class).getScheduledTasks()).hasSize(1); - Thread.sleep(100); + Thread.sleep(110); assertThat(ctx.getBean(AtomicInteger.class).get()).isGreaterThanOrEqualTo(10); assertThat(ctx.getBean(ExplicitScheduledTaskRegistrarConfig.class).threadName).startsWith("explicitScheduler1"); } @@ -124,7 +124,7 @@ public void withAmbiguousTaskSchedulers_andSingleTask_disambiguatedByScheduledTa ctx = new AnnotationConfigApplicationContext( SchedulingEnabled_withAmbiguousTaskSchedulers_andSingleTask_disambiguatedByScheduledTaskRegistrar.class); - Thread.sleep(100); + Thread.sleep(110); assertThat(ctx.getBean(ThreadAwareWorker.class).executedByThread).startsWith("explicitScheduler2-"); } @@ -134,7 +134,7 @@ public void withAmbiguousTaskSchedulers_andSingleTask_disambiguatedBySchedulerNa ctx = new AnnotationConfigApplicationContext( SchedulingEnabled_withAmbiguousTaskSchedulers_andSingleTask_disambiguatedBySchedulerNameAttribute.class); - Thread.sleep(100); + Thread.sleep(110); assertThat(ctx.getBean(ThreadAwareWorker.class).executedByThread).startsWith("explicitScheduler2-"); } @@ -143,7 +143,7 @@ public void withAmbiguousTaskSchedulers_andSingleTask_disambiguatedBySchedulerNa public void withTaskAddedVia_configureTasks() throws InterruptedException { ctx = new AnnotationConfigApplicationContext(SchedulingEnabled_withTaskAddedVia_configureTasks.class); - Thread.sleep(100); + Thread.sleep(110); assertThat(ctx.getBean(ThreadAwareWorker.class).executedByThread).startsWith("taskScheduler-"); } @@ -152,7 +152,7 @@ public void withTaskAddedVia_configureTasks() throws InterruptedException { public void withTriggerTask() throws InterruptedException { ctx = new AnnotationConfigApplicationContext(TriggerTaskConfig.class); - Thread.sleep(100); + Thread.sleep(110); assertThat(ctx.getBean(AtomicInteger.class).get()).isGreaterThan(1); } diff --git a/spring-context/src/test/java/org/springframework/scheduling/concurrent/AbstractSchedulingTaskExecutorTests.java b/spring-context/src/test/java/org/springframework/scheduling/concurrent/AbstractSchedulingTaskExecutorTests.java index e2bd98762ef8..e47417743f6e 100644 --- a/spring-context/src/test/java/org/springframework/scheduling/concurrent/AbstractSchedulingTaskExecutorTests.java +++ b/spring-context/src/test/java/org/springframework/scheduling/concurrent/AbstractSchedulingTaskExecutorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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. @@ -55,7 +55,7 @@ abstract class AbstractSchedulingTaskExecutorTests { @BeforeEach - void setUp(TestInfo testInfo) { + void setup(TestInfo testInfo) { this.testName = testInfo.getTestMethod().get().getName(); this.threadNamePrefix = this.testName + "-"; this.executor = buildExecutor(); @@ -84,11 +84,11 @@ void executeFailingRunnable() { TestTask task = new TestTask(this.testName, 0); executor.execute(task); Awaitility.await() - .dontCatchUncaughtExceptions() - .atMost(1, TimeUnit.SECONDS) - .pollInterval(10, TimeUnit.MILLISECONDS) - .until(() -> task.exception.get() != null && task.exception.get().getMessage().equals( - "TestTask failure for test 'executeFailingRunnable': expectedRunCount:<0>, actualRunCount:<1>")); + .dontCatchUncaughtExceptions() + .atMost(1, TimeUnit.SECONDS) + .pollInterval(10, TimeUnit.MILLISECONDS) + .until(() -> task.exception.get() != null && task.exception.get().getMessage().equals( + "TestTask failure for test 'executeFailingRunnable': expectedRunCount:<0>, actualRunCount:<1>")); } @Test @@ -101,7 +101,7 @@ void submitRunnable() throws Exception { } @Test - void submitFailingRunnable() throws Exception { + void submitFailingRunnable() { TestTask task = new TestTask(this.testName, 0); Future future = executor.submit(task); assertThatExceptionOfType(ExecutionException.class).isThrownBy(() -> @@ -121,31 +121,31 @@ void submitRunnableWithGetAfterShutdown() throws Exception { } @Test - void submitListenableRunnable() throws Exception { + void submitListenableRunnable() { TestTask task = new TestTask(this.testName, 1); // Act ListenableFuture future = executor.submitListenable(task); future.addCallback(result -> outcome = result, ex -> outcome = ex); // Assert Awaitility.await() - .atMost(1, TimeUnit.SECONDS) - .pollInterval(10, TimeUnit.MILLISECONDS) - .until(future::isDone); + .atMost(1, TimeUnit.SECONDS) + .pollInterval(10, TimeUnit.MILLISECONDS) + .until(future::isDone); assertThat(outcome).isNull(); assertThreadNamePrefix(task); } @Test - void submitFailingListenableRunnable() throws Exception { + void submitFailingListenableRunnable() { TestTask task = new TestTask(this.testName, 0); ListenableFuture future = executor.submitListenable(task); future.addCallback(result -> outcome = result, ex -> outcome = ex); Awaitility.await() - .dontCatchUncaughtExceptions() - .atMost(1, TimeUnit.SECONDS) - .pollInterval(10, TimeUnit.MILLISECONDS) - .until(() -> future.isDone() && outcome != null); + .dontCatchUncaughtExceptions() + .atMost(1, TimeUnit.SECONDS) + .pollInterval(10, TimeUnit.MILLISECONDS) + .until(() -> future.isDone() && outcome != null); assertThat(outcome.getClass()).isSameAs(RuntimeException.class); } @@ -159,14 +159,13 @@ void submitListenableRunnableWithGetAfterShutdown() throws Exception { future1.get(1000, TimeUnit.MILLISECONDS); } catch (Exception ex) { - /* ignore */ + // ignore } Awaitility.await() - .atMost(4, TimeUnit.SECONDS) - .pollInterval(10, TimeUnit.MILLISECONDS) - .untilAsserted(() -> - assertThatExceptionOfType(CancellationException.class).isThrownBy(() -> - future2.get(1000, TimeUnit.MILLISECONDS))); + .atMost(4, TimeUnit.SECONDS) + .pollInterval(10, TimeUnit.MILLISECONDS) + .untilAsserted(() -> assertThatExceptionOfType(CancellationException.class) + .isThrownBy(() -> future2.get(1000, TimeUnit.MILLISECONDS))); } @Test @@ -178,11 +177,11 @@ void submitCallable() throws Exception { } @Test - void submitFailingCallable() throws Exception { + void submitFailingCallable() { TestCallable task = new TestCallable(this.testName, 0); Future future = executor.submit(task); - assertThatExceptionOfType(ExecutionException.class).isThrownBy(() -> - future.get(1000, TimeUnit.MILLISECONDS)); + assertThatExceptionOfType(ExecutionException.class) + .isThrownBy(() -> future.get(1000, TimeUnit.MILLISECONDS)); assertThat(future.isDone()).isTrue(); } @@ -196,42 +195,41 @@ void submitCallableWithGetAfterShutdown() throws Exception { future1.get(1000, TimeUnit.MILLISECONDS); } catch (Exception ex) { - /* ignore */ + // ignore } Awaitility.await() - .atMost(4, TimeUnit.SECONDS) - .pollInterval(10, TimeUnit.MILLISECONDS) - .untilAsserted(() -> - assertThatExceptionOfType(CancellationException.class).isThrownBy(() -> - future2.get(1000, TimeUnit.MILLISECONDS))); + .atMost(4, TimeUnit.SECONDS) + .pollInterval(10, TimeUnit.MILLISECONDS) + .untilAsserted(() -> assertThatExceptionOfType(CancellationException.class) + .isThrownBy(() -> future2.get(1000, TimeUnit.MILLISECONDS))); } @Test - void submitListenableCallable() throws Exception { + void submitListenableCallable() { TestCallable task = new TestCallable(this.testName, 1); // Act ListenableFuture future = executor.submitListenable(task); future.addCallback(result -> outcome = result, ex -> outcome = ex); // Assert Awaitility.await() - .atMost(1, TimeUnit.SECONDS) - .pollInterval(10, TimeUnit.MILLISECONDS) - .until(() -> future.isDone() && outcome != null); + .atMost(1, TimeUnit.SECONDS) + .pollInterval(10, TimeUnit.MILLISECONDS) + .until(() -> future.isDone() && outcome != null); assertThat(outcome.toString().substring(0, this.threadNamePrefix.length())).isEqualTo(this.threadNamePrefix); } @Test - void submitFailingListenableCallable() throws Exception { + void submitFailingListenableCallable() { TestCallable task = new TestCallable(this.testName, 0); // Act ListenableFuture future = executor.submitListenable(task); future.addCallback(result -> outcome = result, ex -> outcome = ex); // Assert Awaitility.await() - .dontCatchUncaughtExceptions() - .atMost(1, TimeUnit.SECONDS) - .pollInterval(10, TimeUnit.MILLISECONDS) - .until(() -> future.isDone() && outcome != null); + .dontCatchUncaughtExceptions() + .atMost(1, TimeUnit.SECONDS) + .pollInterval(10, TimeUnit.MILLISECONDS) + .until(() -> future.isDone() && outcome != null); assertThat(outcome.getClass()).isSameAs(RuntimeException.class); } @@ -296,8 +294,9 @@ public void run() { } if (expectedRunCount >= 0) { if (actualRunCount.incrementAndGet() > expectedRunCount) { - RuntimeException exception = new RuntimeException(String.format("%s failure for test '%s': expectedRunCount:<%d>, actualRunCount:<%d>", - getClass().getSimpleName(), this.testName, expectedRunCount, actualRunCount.get())); + RuntimeException exception = new RuntimeException(String.format( + "%s failure for test '%s': expectedRunCount:<%d>, actualRunCount:<%d>", + getClass().getSimpleName(), this.testName, expectedRunCount, actualRunCount.get())); this.exception.set(exception); throw exception; } @@ -329,8 +328,9 @@ public String call() throws Exception { } if (expectedRunCount >= 0) { if (actualRunCount.incrementAndGet() > expectedRunCount) { - throw new RuntimeException(String.format("%s failure for test '%s': expectedRunCount:<%d>, actualRunCount:<%d>", - getClass().getSimpleName(), this.testName, expectedRunCount, actualRunCount.get())); + throw new RuntimeException(String.format( + "%s failure for test '%s': expectedRunCount:<%d>, actualRunCount:<%d>", + getClass().getSimpleName(), this.testName, expectedRunCount, actualRunCount.get())); } } return Thread.currentThread().getName(); diff --git a/spring-context/src/test/java/org/springframework/scheduling/concurrent/ConcurrentTaskExecutorTests.java b/spring-context/src/test/java/org/springframework/scheduling/concurrent/ConcurrentTaskExecutorTests.java index ca4a79d4530b..10f246757fbe 100644 --- a/spring-context/src/test/java/org/springframework/scheduling/concurrent/ConcurrentTaskExecutorTests.java +++ b/spring-context/src/test/java/org/springframework/scheduling/concurrent/ConcurrentTaskExecutorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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,8 +16,9 @@ package org.springframework.scheduling.concurrent; +import java.util.concurrent.Executor; +import java.util.concurrent.Future; import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.RunnableFuture; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; @@ -26,6 +27,8 @@ import org.springframework.core.task.AsyncListenableTaskExecutor; import org.springframework.core.task.NoOpRunnable; +import org.springframework.core.task.TaskDecorator; +import org.springframework.util.Assert; import static org.assertj.core.api.Assertions.assertThatCode; @@ -49,8 +52,8 @@ protected AsyncListenableTaskExecutor buildExecutor() { @AfterEach void shutdownExecutor() { for (Runnable task : concurrentExecutor.shutdownNow()) { - if (task instanceof RunnableFuture) { - ((RunnableFuture) task).cancel(true); + if (task instanceof Future) { + ((Future) task).cancel(true); } } } @@ -69,10 +72,45 @@ void passingNullExecutorToCtorResultsInDefaultTaskExecutorBeingUsed() { } @Test - void passingNullExecutorToSetterResultsInDefaultTaskExecutorBeingUsed() { + void earlySetConcurrentExecutorCallRespectsConfiguredTaskDecorator() { ConcurrentTaskExecutor executor = new ConcurrentTaskExecutor(); - executor.setConcurrentExecutor(null); + executor.setConcurrentExecutor(new DecoratedExecutor()); + executor.setTaskDecorator(new RunnableDecorator()); assertThatCode(() -> executor.execute(new NoOpRunnable())).doesNotThrowAnyException(); } + @Test + void lateSetConcurrentExecutorCallRespectsConfiguredTaskDecorator() { + ConcurrentTaskExecutor executor = new ConcurrentTaskExecutor(); + executor.setTaskDecorator(new RunnableDecorator()); + executor.setConcurrentExecutor(new DecoratedExecutor()); + assertThatCode(() -> executor.execute(new NoOpRunnable())).doesNotThrowAnyException(); + } + + + private static class DecoratedRunnable implements Runnable { + + @Override + public void run() { + } + } + + + private static class RunnableDecorator implements TaskDecorator { + + @Override + public Runnable decorate(Runnable runnable) { + return new DecoratedRunnable(); + } + } + + + private static class DecoratedExecutor implements Executor { + + @Override + public void execute(Runnable command) { + Assert.state(command instanceof DecoratedRunnable, "TaskDecorator not applied"); + } + } + } diff --git a/spring-context/src/test/java/org/springframework/scheduling/concurrent/ScheduledExecutorFactoryBeanTests.java b/spring-context/src/test/java/org/springframework/scheduling/concurrent/ScheduledExecutorFactoryBeanTests.java index 140fe6ebf1e8..6be90f35ce6a 100644 --- a/spring-context/src/test/java/org/springframework/scheduling/concurrent/ScheduledExecutorFactoryBeanTests.java +++ b/spring-context/src/test/java/org/springframework/scheduling/concurrent/ScheduledExecutorFactoryBeanTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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. @@ -41,14 +41,14 @@ class ScheduledExecutorFactoryBeanTests { @Test - void throwsExceptionIfPoolSizeIsLessThanZero() throws Exception { + void throwsExceptionIfPoolSizeIsLessThanZero() { ScheduledExecutorFactoryBean factory = new ScheduledExecutorFactoryBean(); assertThatIllegalArgumentException().isThrownBy(() -> factory.setPoolSize(-1)); } @Test @SuppressWarnings("serial") - void shutdownNowIsPropagatedToTheExecutorOnDestroy() throws Exception { + void shutdownNowIsPropagatedToTheExecutorOnDestroy() { final ScheduledExecutorService executor = mock(ScheduledExecutorService.class); ScheduledExecutorFactoryBean factory = new ScheduledExecutorFactoryBean() { @@ -66,7 +66,7 @@ protected ScheduledExecutorService createExecutor(int poolSize, ThreadFactory th @Test @SuppressWarnings("serial") - void shutdownIsPropagatedToTheExecutorOnDestroy() throws Exception { + void shutdownIsPropagatedToTheExecutorOnDestroy() { final ScheduledExecutorService executor = mock(ScheduledExecutorService.class); ScheduledExecutorFactoryBean factory = new ScheduledExecutorFactoryBean() { @@ -85,7 +85,7 @@ protected ScheduledExecutorService createExecutor(int poolSize, ThreadFactory th @Test @EnabledForTestGroups(LONG_RUNNING) - void oneTimeExecutionIsSetUpAndFiresCorrectly() throws Exception { + void oneTimeExecutionIsSetUpAndFiresCorrectly() { Runnable runnable = mock(Runnable.class); ScheduledExecutorFactoryBean factory = new ScheduledExecutorFactoryBean(); @@ -99,7 +99,7 @@ void oneTimeExecutionIsSetUpAndFiresCorrectly() throws Exception { @Test @EnabledForTestGroups(LONG_RUNNING) - void fixedRepeatedExecutionIsSetUpAndFiresCorrectly() throws Exception { + void fixedRepeatedExecutionIsSetUpAndFiresCorrectly() { Runnable runnable = mock(Runnable.class); ScheduledExecutorTask task = new ScheduledExecutorTask(runnable); @@ -117,7 +117,7 @@ void fixedRepeatedExecutionIsSetUpAndFiresCorrectly() throws Exception { @Test @EnabledForTestGroups(LONG_RUNNING) - void fixedRepeatedExecutionIsSetUpAndFiresCorrectlyAfterException() throws Exception { + void fixedRepeatedExecutionIsSetUpAndFiresCorrectlyAfterException() { Runnable runnable = mock(Runnable.class); willThrow(new IllegalStateException()).given(runnable).run(); @@ -137,7 +137,7 @@ void fixedRepeatedExecutionIsSetUpAndFiresCorrectlyAfterException() throws Excep @Test @EnabledForTestGroups(LONG_RUNNING) - void withInitialDelayRepeatedExecutionIsSetUpAndFiresCorrectly() throws Exception { + void withInitialDelayRepeatedExecutionIsSetUpAndFiresCorrectly() { Runnable runnable = mock(Runnable.class); ScheduledExecutorTask task = new ScheduledExecutorTask(runnable); @@ -157,7 +157,7 @@ void withInitialDelayRepeatedExecutionIsSetUpAndFiresCorrectly() throws Exceptio @Test @EnabledForTestGroups(LONG_RUNNING) - void withInitialDelayRepeatedExecutionIsSetUpAndFiresCorrectlyAfterException() throws Exception { + void withInitialDelayRepeatedExecutionIsSetUpAndFiresCorrectlyAfterException() { Runnable runnable = mock(Runnable.class); willThrow(new IllegalStateException()).given(runnable).run(); @@ -179,7 +179,7 @@ void withInitialDelayRepeatedExecutionIsSetUpAndFiresCorrectlyAfterException() t @Test @SuppressWarnings("serial") - void settingThreadFactoryToNullForcesUseOfDefaultButIsOtherwiseCool() throws Exception { + void settingThreadFactoryToNullForcesUseOfDefaultButIsOtherwiseCool() { ScheduledExecutorFactoryBean factory = new ScheduledExecutorFactoryBean() { @Override protected ScheduledExecutorService createExecutor(int poolSize, ThreadFactory threadFactory, RejectedExecutionHandler rejectedExecutionHandler) { @@ -195,7 +195,7 @@ protected ScheduledExecutorService createExecutor(int poolSize, ThreadFactory th @Test @SuppressWarnings("serial") - void settingRejectedExecutionHandlerToNullForcesUseOfDefaultButIsOtherwiseCool() throws Exception { + void settingRejectedExecutionHandlerToNullForcesUseOfDefaultButIsOtherwiseCool() { ScheduledExecutorFactoryBean factory = new ScheduledExecutorFactoryBean() { @Override protected ScheduledExecutorService createExecutor(int poolSize, ThreadFactory threadFactory, RejectedExecutionHandler rejectedExecutionHandler) { @@ -210,7 +210,7 @@ protected ScheduledExecutorService createExecutor(int poolSize, ThreadFactory th } @Test - void objectTypeReportsCorrectType() throws Exception { + void objectTypeReportsCorrectType() { ScheduledExecutorFactoryBean factory = new ScheduledExecutorFactoryBean(); assertThat(factory.getObjectType()).isEqualTo(ScheduledExecutorService.class); } diff --git a/spring-context/src/test/java/org/springframework/scheduling/concurrent/ThreadPoolTaskExecutorTests.java b/spring-context/src/test/java/org/springframework/scheduling/concurrent/ThreadPoolTaskExecutorTests.java index 0723a27abf79..eada6603a029 100644 --- a/spring-context/src/test/java/org/springframework/scheduling/concurrent/ThreadPoolTaskExecutorTests.java +++ b/spring-context/src/test/java/org/springframework/scheduling/concurrent/ThreadPoolTaskExecutorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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,8 +26,8 @@ import org.springframework.core.task.AsyncListenableTaskExecutor; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; -import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.InstanceOfAssertFactories.type; /** @@ -67,8 +67,7 @@ void modifyCorePoolSizeWithInvalidValueWhileRunning() { assertThat(executor.getCorePoolSize()).isEqualTo(1); assertThat(executor.getThreadPoolExecutor().getCorePoolSize()).isEqualTo(1); - assertThatThrownBy(() -> executor.setCorePoolSize(-1)) - .isInstanceOf(IllegalArgumentException.class); + assertThatIllegalArgumentException().isThrownBy(() -> executor.setCorePoolSize(-1)); assertThat(executor.getCorePoolSize()).isEqualTo(1); assertThat(executor.getThreadPoolExecutor().getCorePoolSize()).isEqualTo(1); @@ -90,8 +89,7 @@ void modifyMaxPoolSizeWithInvalidValueWhileRunning() { assertThat(executor.getMaxPoolSize()).isEqualTo(1); assertThat(executor.getThreadPoolExecutor().getMaximumPoolSize()).isEqualTo(1); - assertThatThrownBy(() -> executor.setMaxPoolSize(0)) - .isInstanceOf(IllegalArgumentException.class); + assertThatIllegalArgumentException().isThrownBy(() -> executor.setMaxPoolSize(0)); assertThat(executor.getMaxPoolSize()).isEqualTo(1); assertThat(executor.getThreadPoolExecutor().getMaximumPoolSize()).isEqualTo(1); @@ -113,8 +111,7 @@ void modifyKeepAliveSecondsWithInvalidValueWhileRunning() { assertThat(executor.getKeepAliveSeconds()).isEqualTo(60); assertThat(executor.getThreadPoolExecutor().getKeepAliveTime(TimeUnit.SECONDS)).isEqualTo(60); - assertThatThrownBy(() -> executor.setKeepAliveSeconds(-10)) - .isInstanceOf(IllegalArgumentException.class); + assertThatIllegalArgumentException().isThrownBy(() -> executor.setKeepAliveSeconds(-10)); assertThat(executor.getKeepAliveSeconds()).isEqualTo(60); assertThat(executor.getThreadPoolExecutor().getKeepAliveTime(TimeUnit.SECONDS)).isEqualTo(60); @@ -124,8 +121,8 @@ void modifyKeepAliveSecondsWithInvalidValueWhileRunning() { void queueCapacityDefault() { assertThat(executor.getQueueCapacity()).isEqualTo(Integer.MAX_VALUE); assertThat(executor.getThreadPoolExecutor().getQueue()) - .asInstanceOf(type(LinkedBlockingQueue.class)) - .extracting(BlockingQueue::remainingCapacity).isEqualTo(Integer.MAX_VALUE); + .asInstanceOf(type(LinkedBlockingQueue.class)) + .extracting(BlockingQueue::remainingCapacity).isEqualTo(Integer.MAX_VALUE); } @Test @@ -135,8 +132,8 @@ void queueCapacityZero() { assertThat(executor.getQueueCapacity()).isZero(); assertThat(executor.getThreadPoolExecutor().getQueue()) - .asInstanceOf(type(SynchronousQueue.class)) - .extracting(BlockingQueue::remainingCapacity).isEqualTo(0); + .asInstanceOf(type(SynchronousQueue.class)) + .extracting(BlockingQueue::remainingCapacity).isEqualTo(0); } @Test diff --git a/spring-context/src/test/java/org/springframework/scheduling/concurrent/ThreadPoolTaskSchedulerTests.java b/spring-context/src/test/java/org/springframework/scheduling/concurrent/ThreadPoolTaskSchedulerTests.java index a96544640628..259ad775f142 100644 --- a/spring-context/src/test/java/org/springframework/scheduling/concurrent/ThreadPoolTaskSchedulerTests.java +++ b/spring-context/src/test/java/org/springframework/scheduling/concurrent/ThreadPoolTaskSchedulerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -39,7 +39,7 @@ * @author Sam Brannen * @since 3.0 */ -public class ThreadPoolTaskSchedulerTests extends AbstractSchedulingTaskExecutorTests { +class ThreadPoolTaskSchedulerTests extends AbstractSchedulingTaskExecutorTests { private final ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); @@ -97,7 +97,7 @@ void scheduleOneTimeTask() throws Exception { } @Test - void scheduleOneTimeFailingTaskWithoutErrorHandler() throws Exception { + void scheduleOneTimeFailingTaskWithoutErrorHandler() { TestTask task = new TestTask(this.testName, 0); Future future = scheduler.schedule(task, new Date()); assertThatExceptionOfType(ExecutionException.class).isThrownBy(() -> future.get(1000, TimeUnit.MILLISECONDS)); @@ -149,7 +149,7 @@ private void await(CountDownLatch latch) { catch (InterruptedException ex) { throw new IllegalStateException(ex); } - assertThat(latch.getCount()).as("latch did not count down,").isEqualTo(0); + assertThat(latch.getCount()).as("latch did not count down").isEqualTo(0); } diff --git a/spring-context/src/test/java/org/springframework/scheduling/support/BitsCronFieldTests.java b/spring-context/src/test/java/org/springframework/scheduling/support/BitsCronFieldTests.java index 37d6b30d4fd3..b69afdbb3d60 100644 --- a/spring-context/src/test/java/org/springframework/scheduling/support/BitsCronFieldTests.java +++ b/spring-context/src/test/java/org/springframework/scheduling/support/BitsCronFieldTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -35,27 +35,32 @@ class BitsCronFieldTests { @Test void parse() { assertThat(BitsCronField.parseSeconds("42")).has(clearRange(0, 41)).has(set(42)).has(clearRange(43, 59)); - assertThat(BitsCronField.parseSeconds("0-4,8-12")).has(setRange(0, 4)).has(clearRange(5,7)).has(setRange(8, 12)).has(clearRange(13,59)); - assertThat(BitsCronField.parseSeconds("57/2")).has(clearRange(0, 56)).has(set(57)).has(clear(58)).has(set(59)); + assertThat(BitsCronField.parseSeconds("0-4,8-12")).has(setRange(0, 4)).has(clearRange(5,7)) + .has(setRange(8, 12)).has(clearRange(13,59)); + assertThat(BitsCronField.parseSeconds("57/2")).has(clearRange(0, 56)).has(set(57)) + .has(clear(58)).has(set(59)); assertThat(BitsCronField.parseMinutes("30")).has(set(30)).has(clearRange(1, 29)).has(clearRange(31, 59)); assertThat(BitsCronField.parseHours("23")).has(set(23)).has(clearRange(0, 23)); - assertThat(BitsCronField.parseHours("0-23/2")).has(set(0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22)).has(clear(1,3,5,7,9,11,13,15,17,19,21,23)); + assertThat(BitsCronField.parseHours("0-23/2")).has(set(0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22)) + .has(clear(1,3,5,7,9,11,13,15,17,19,21,23)); assertThat(BitsCronField.parseDaysOfMonth("1")).has(set(1)).has(clearRange(2, 31)); assertThat(BitsCronField.parseMonth("1")).has(set(1)).has(clearRange(2, 12)); assertThat(BitsCronField.parseDaysOfWeek("0")).has(set(7, 7)).has(clearRange(0, 6)); - - assertThat(BitsCronField.parseDaysOfWeek("7-5")).has(clear(0)).has(setRange(1, 5)).has(clear(6)).has(set(7)); + assertThat(BitsCronField.parseDaysOfWeek("7-5")).has(clear(0)).has(setRange(1, 5)) + .has(clear(6)).has(set(7)); } @Test void parseLists() { - assertThat(BitsCronField.parseSeconds("15,30")).has(set(15, 30)).has(clearRange(1, 15)).has(clearRange(31, 59)); - assertThat(BitsCronField.parseMinutes("1,2,5,9")).has(set(1, 2, 5, 9)).has(clear(0)).has(clearRange(3, 4)).has(clearRange(6, 8)).has(clearRange(10, 59)); + assertThat(BitsCronField.parseSeconds("15,30")).has(set(15, 30)).has(clearRange(1, 15)) + .has(clearRange(31, 59)); + assertThat(BitsCronField.parseMinutes("1,2,5,9")).has(set(1, 2, 5, 9)).has(clear(0)) + .has(clearRange(3, 4)).has(clearRange(6, 8)).has(clearRange(10, 59)); assertThat(BitsCronField.parseHours("1,2,3")).has(set(1, 2, 3)).has(clearRange(4, 23)); assertThat(BitsCronField.parseDaysOfMonth("1,2,3")).has(set(1, 2, 3)).has(clearRange(4, 31)); assertThat(BitsCronField.parseMonth("1,2,3")).has(set(1, 2, 3)).has(clearRange(4, 12)); @@ -107,6 +112,7 @@ void names() { .has(clear(0)).has(setRange(1, 7)); } + private static Condition set(int... indices) { return new Condition(String.format("set bits %s", Arrays.toString(indices))) { @Override diff --git a/spring-context/src/test/java/org/springframework/scheduling/support/CronTriggerTests.java b/spring-context/src/test/java/org/springframework/scheduling/support/CronTriggerTests.java index 1fe501b1301d..14185a30096c 100644 --- a/spring-context/src/test/java/org/springframework/scheduling/support/CronTriggerTests.java +++ b/spring-context/src/test/java/org/springframework/scheduling/support/CronTriggerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-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. @@ -848,6 +848,7 @@ void daylightSavingMissingHour(LocalDateTime localDateTime, TimeZone timeZone) { assertThat(nextExecutionTime).isEqualTo(this.calendar.getTime()); } + private static void roundup(Calendar calendar) { calendar.add(Calendar.SECOND, 1); calendar.set(Calendar.MILLISECOND, 0); @@ -861,9 +862,7 @@ private static void assertMatchesNextSecond(CronTrigger trigger, Calendar calend } private static TriggerContext getTriggerContext(Date lastCompletionTime) { - SimpleTriggerContext context = new SimpleTriggerContext(); - context.update(null, null, lastCompletionTime); - return context; + return new SimpleTriggerContext(null, null, lastCompletionTime); } diff --git a/spring-context/src/test/java/org/springframework/scheduling/support/QuartzCronFieldTests.java b/spring-context/src/test/java/org/springframework/scheduling/support/QuartzCronFieldTests.java index ec56f366b5b2..14c5907af48a 100644 --- a/spring-context/src/test/java/org/springframework/scheduling/support/QuartzCronFieldTests.java +++ b/spring-context/src/test/java/org/springframework/scheduling/support/QuartzCronFieldTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -28,6 +28,7 @@ * Unit tests for {@link QuartzCronField}. * * @author Arjen Poutsma + * @author Juergen Hoeller */ class QuartzCronFieldTests { @@ -71,6 +72,46 @@ void lastDayOfWeekOffset() { assertThat(field.nextOrSame(last)).isEqualTo(expected); } + @Test + void dayOfWeek_0(){ + // third Sunday (0) of the month + QuartzCronField field = QuartzCronField.parseDaysOfWeek("0#3"); + + LocalDate last = LocalDate.of(2024, 1, 1); + LocalDate expected = LocalDate.of(2024, 1, 21); + assertThat(field.nextOrSame(last)).isEqualTo(expected); + } + + @Test + void dayOfWeek_1(){ + // third Monday (1) of the month + QuartzCronField field = QuartzCronField.parseDaysOfWeek("1#3"); + + LocalDate last = LocalDate.of(2024, 1, 1); + LocalDate expected = LocalDate.of(2024, 1, 15); + assertThat(field.nextOrSame(last)).isEqualTo(expected); + } + + @Test + void dayOfWeek_2(){ + // third Tuesday (2) of the month + QuartzCronField field = QuartzCronField.parseDaysOfWeek("2#3"); + + LocalDate last = LocalDate.of(2024, 1, 1); + LocalDate expected = LocalDate.of(2024, 1, 16); + assertThat(field.nextOrSame(last)).isEqualTo(expected); + } + + @Test + void dayOfWeek_7() { + // third Sunday (7 as alternative to 0) of the month + QuartzCronField field = QuartzCronField.parseDaysOfWeek("7#3"); + + LocalDate last = LocalDate.of(2024, 1, 1); + LocalDate expected = LocalDate.of(2024, 1, 21); + assertThat(field.nextOrSame(last)).isEqualTo(expected); + } + @Test void invalidValues() { assertThatIllegalArgumentException().isThrownBy(() -> QuartzCronField.parseDaysOfMonth("")); diff --git a/spring-context/src/test/java/org/springframework/util/MBeanTestUtils.java b/spring-context/src/test/java/org/springframework/util/MBeanTestUtils.java index b96c56053c49..236565193423 100644 --- a/spring-context/src/test/java/org/springframework/util/MBeanTestUtils.java +++ b/spring-context/src/test/java/org/springframework/util/MBeanTestUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2022 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,37 +18,59 @@ import java.lang.management.ManagementFactory; import java.lang.reflect.Field; +import java.util.EnumSet; import javax.management.MBeanServer; import javax.management.MBeanServerFactory; +import org.junit.jupiter.api.condition.JRE; + /** * Utilities for MBean tests. * * @author Phillip Webb + * @author Sam Brannen */ public class MBeanTestUtils { /** - * Resets MBeanServerFactory and ManagementFactory to a known consistent state. - *

    This involves releasing all currently registered MBeanServers and resetting - * the platformMBeanServer to null. + * Reset the {@link MBeanServerFactory} to a known consistent state. This involves + * {@linkplain #releaseMBeanServer(MBeanServer) releasing} all currently registered + * MBeanServers. + *

    On JDK 8 - JDK 16, this method also resets the platformMBeanServer field + * in {@link ManagementFactory} to {@code null}. */ public static synchronized void resetMBeanServers() throws Exception { for (MBeanServer server : MBeanServerFactory.findMBeanServer(null)) { - try { - MBeanServerFactory.releaseMBeanServer(server); - } - catch (IllegalArgumentException ex) { - if (!ex.getMessage().contains("not in list")) { - throw ex; - } + releaseMBeanServer(server); + } + + if (!isCurrentJreWithinRange(JRE.JAVA_16, JRE.OTHER)) { + Field field = ManagementFactory.class.getDeclaredField("platformMBeanServer"); + field.setAccessible(true); + field.set(null, null); + } + } + + /** + * Attempt to release the supplied {@link MBeanServer}. + *

    Ignores any {@link IllegalArgumentException} thrown by + * {@link MBeanServerFactory#releaseMBeanServer(MBeanServer)} whose error + * message contains the text "not in list". + */ + public static void releaseMBeanServer(MBeanServer server) { + try { + MBeanServerFactory.releaseMBeanServer(server); + } + catch (IllegalArgumentException ex) { + if (!ex.getMessage().contains("not in list")) { + throw ex; } } + } - Field field = ManagementFactory.class.getDeclaredField("platformMBeanServer"); - field.setAccessible(true); - field.set(null, null); + static boolean isCurrentJreWithinRange(JRE min, JRE max) { + return EnumSet.range(min, max).contains(JRE.currentVersion()); } } diff --git a/spring-context/src/test/java/org/springframework/validation/DataBinderTests.java b/spring-context/src/test/java/org/springframework/validation/DataBinderTests.java index 546c599c01f7..037dc8d214a3 100644 --- a/spring-context/src/test/java/org/springframework/validation/DataBinderTests.java +++ b/spring-context/src/test/java/org/springframework/validation/DataBinderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -95,11 +95,11 @@ void bindingNoErrors() throws BindException { binder.bind(pvs); binder.close(); - assertThat(rod.getName().equals("Rod")).as("changed name correctly").isTrue(); - assertThat(rod.getAge() == 32).as("changed age correctly").isTrue(); + assertThat(rod.getName()).as("changed name correctly").isEqualTo("Rod"); + assertThat(rod.getAge()).as("changed age correctly").isEqualTo(32); Map map = binder.getBindingResult().getModel(); - assertThat(map.size() == 2).as("There is one element in map").isTrue(); + assertThat(map).as("There is one element in map").hasSize(2); TestBean tb = (TestBean) map.get("person"); assertThat(tb.equals(rod)).as("Same object").isTrue(); @@ -157,8 +157,9 @@ void bindingNoErrorsNotIgnoreUnknown() { pvs.add("name", "Rod"); pvs.add("age", 32); pvs.add("nonExisting", "someValue"); - assertThatExceptionOfType(NotWritablePropertyException.class).isThrownBy(() -> - binder.bind(pvs)); + + assertThatExceptionOfType(NotWritablePropertyException.class) + .isThrownBy(() -> binder.bind(pvs)); } @Test @@ -168,8 +169,9 @@ void bindingNoErrorsWithInvalidField() { MutablePropertyValues pvs = new MutablePropertyValues(); pvs.add("name", "Rod"); pvs.add("spouse.age", 32); - assertThatExceptionOfType(NullValueInNestedPathException.class).isThrownBy(() -> - binder.bind(pvs)); + + assertThatExceptionOfType(NullValueInNestedPathException.class) + .isThrownBy(() -> binder.bind(pvs)); } @Test @@ -197,57 +199,56 @@ void bindingWithErrors() { pvs.add("age", "32x"); pvs.add("touchy", "m.y"); binder.bind(pvs); - assertThatExceptionOfType(BindException.class).isThrownBy( - binder::close) - .satisfies(ex -> { - assertThat(rod.getName()).isEqualTo("Rod"); - Map map = binder.getBindingResult().getModel(); - TestBean tb = (TestBean) map.get("person"); - assertThat(tb).isSameAs(rod); - - BindingResult br = (BindingResult) map.get(BindingResult.MODEL_KEY_PREFIX + "person"); - assertThat(BindingResultUtils.getBindingResult(map, "person")).isEqualTo(br); - assertThat(BindingResultUtils.getRequiredBindingResult(map, "person")).isEqualTo(br); - - assertThat(BindingResultUtils.getBindingResult(map, "someOtherName")).isNull(); - assertThatIllegalStateException().isThrownBy(() -> - BindingResultUtils.getRequiredBindingResult(map, "someOtherName")); - - assertThat(binder.getBindingResult()).as("Added itself to map").isSameAs(br); - assertThat(br.hasErrors()).isTrue(); - assertThat(br.getErrorCount()).isEqualTo(2); - - assertThat(br.hasFieldErrors("age")).isTrue(); - assertThat(br.getFieldErrorCount("age")).isEqualTo(1); - assertThat(binder.getBindingResult().getFieldValue("age")).isEqualTo("32x"); - FieldError ageError = binder.getBindingResult().getFieldError("age"); - assertThat(ageError).isNotNull(); - assertThat(ageError.getCode()).isEqualTo("typeMismatch"); - assertThat(ageError.getRejectedValue()).isEqualTo("32x"); - assertThat(ageError.contains(TypeMismatchException.class)).isTrue(); - assertThat(ageError.contains(NumberFormatException.class)).isTrue(); - assertThat(ageError.unwrap(NumberFormatException.class).getMessage()).contains("32x"); - assertThat(tb.getAge()).isEqualTo(0); - - assertThat(br.hasFieldErrors("touchy")).isTrue(); - assertThat(br.getFieldErrorCount("touchy")).isEqualTo(1); - assertThat(binder.getBindingResult().getFieldValue("touchy")).isEqualTo("m.y"); - FieldError touchyError = binder.getBindingResult().getFieldError("touchy"); - assertThat(touchyError).isNotNull(); - assertThat(touchyError.getCode()).isEqualTo("methodInvocation"); - assertThat(touchyError.getRejectedValue()).isEqualTo("m.y"); - assertThat(touchyError.contains(MethodInvocationException.class)).isTrue(); - assertThat(touchyError.unwrap(MethodInvocationException.class).getCause().getMessage()).contains("a ."); - assertThat(tb.getTouchy()).isNull(); - - DataBinder binder2 = new DataBinder(new TestBean(), "person"); - MutablePropertyValues pvs2 = new MutablePropertyValues(); - pvs2.add("name", "Rod"); - pvs2.add("age", "32x"); - pvs2.add("touchy", "m.y"); - binder2.bind(pvs2); - assertThat(ex.getBindingResult()).isEqualTo(binder2.getBindingResult()); - }); + + assertThatExceptionOfType(BindException.class).isThrownBy(binder::close).satisfies(ex -> { + assertThat(rod.getName()).isEqualTo("Rod"); + Map map = binder.getBindingResult().getModel(); + TestBean tb = (TestBean) map.get("person"); + assertThat(tb).isSameAs(rod); + + BindingResult br = (BindingResult) map.get(BindingResult.MODEL_KEY_PREFIX + "person"); + assertThat(BindingResultUtils.getBindingResult(map, "person")).isEqualTo(br); + assertThat(BindingResultUtils.getRequiredBindingResult(map, "person")).isEqualTo(br); + + assertThat(BindingResultUtils.getBindingResult(map, "someOtherName")).isNull(); + assertThatIllegalStateException().isThrownBy(() -> + BindingResultUtils.getRequiredBindingResult(map, "someOtherName")); + + assertThat(binder.getBindingResult()).as("Added itself to map").isSameAs(br); + assertThat(br.hasErrors()).isTrue(); + assertThat(br.getErrorCount()).isEqualTo(2); + + assertThat(br.hasFieldErrors("age")).isTrue(); + assertThat(br.getFieldErrorCount("age")).isEqualTo(1); + assertThat(binder.getBindingResult().getFieldValue("age")).isEqualTo("32x"); + FieldError ageError = binder.getBindingResult().getFieldError("age"); + assertThat(ageError).isNotNull(); + assertThat(ageError.getCode()).isEqualTo("typeMismatch"); + assertThat(ageError.getRejectedValue()).isEqualTo("32x"); + assertThat(ageError.contains(TypeMismatchException.class)).isTrue(); + assertThat(ageError.contains(NumberFormatException.class)).isTrue(); + assertThat(ageError.unwrap(NumberFormatException.class).getMessage()).contains("32x"); + assertThat(tb.getAge()).isEqualTo(0); + + assertThat(br.hasFieldErrors("touchy")).isTrue(); + assertThat(br.getFieldErrorCount("touchy")).isEqualTo(1); + assertThat(binder.getBindingResult().getFieldValue("touchy")).isEqualTo("m.y"); + FieldError touchyError = binder.getBindingResult().getFieldError("touchy"); + assertThat(touchyError).isNotNull(); + assertThat(touchyError.getCode()).isEqualTo("methodInvocation"); + assertThat(touchyError.getRejectedValue()).isEqualTo("m.y"); + assertThat(touchyError.contains(MethodInvocationException.class)).isTrue(); + assertThat(touchyError.unwrap(MethodInvocationException.class).getCause().getMessage()).contains("a ."); + assertThat(tb.getTouchy()).isNull(); + + DataBinder binder2 = new DataBinder(new TestBean(), "person"); + MutablePropertyValues pvs2 = new MutablePropertyValues(); + pvs2.add("name", "Rod"); + pvs2.add("age", "32x"); + pvs2.add("touchy", "m.y"); + binder2.bind(pvs2); + assertThat(ex.getBindingResult()).isEqualTo(binder2.getBindingResult()); + }); } @Test @@ -257,15 +258,17 @@ void bindingWithSystemFieldError() { MutablePropertyValues pvs = new MutablePropertyValues(); pvs.add("class.classLoader.URLs[0]", "https://myserver"); binder.setIgnoreUnknownFields(false); - assertThatExceptionOfType(NotWritablePropertyException.class).isThrownBy(() -> - binder.bind(pvs)) - .withMessageContaining("classLoader"); + + assertThatExceptionOfType(NotWritablePropertyException.class) + .isThrownBy(() -> binder.bind(pvs)) + .withMessageContaining("classLoader"); } @Test void bindingWithErrorsAndCustomEditors() { TestBean rod = new TestBean(); DataBinder binder = new DataBinder(rod, "person"); + binder.registerCustomEditor(String.class, "touchy", new PropertyEditorSupport() { @Override public void setAsText(String text) throws IllegalArgumentException { @@ -286,6 +289,7 @@ public String getAsText() { return ((TestBean) getValue()).getName(); } }); + MutablePropertyValues pvs = new MutablePropertyValues(); pvs.add("name", "Rod"); pvs.add("age", "32x"); @@ -293,41 +297,39 @@ public String getAsText() { pvs.add("spouse", "Kerry"); binder.bind(pvs); - assertThatExceptionOfType(BindException.class).isThrownBy( - binder::close) - .satisfies(ex -> { - assertThat(rod.getName()).isEqualTo("Rod"); - Map model = binder.getBindingResult().getModel(); - TestBean tb = (TestBean) model.get("person"); - assertThat(tb).isEqualTo(rod); - - BindingResult br = (BindingResult) model.get(BindingResult.MODEL_KEY_PREFIX + "person"); - assertThat(binder.getBindingResult()).isSameAs(br); - assertThat(br.hasErrors()).isTrue(); - assertThat(br.getErrorCount()).isEqualTo(2); - - assertThat(br.hasFieldErrors("age")).isTrue(); - assertThat(br.getFieldErrorCount("age")).isEqualTo(1); - assertThat(binder.getBindingResult().getFieldValue("age")).isEqualTo("32x"); - FieldError ageError = binder.getBindingResult().getFieldError("age"); - assertThat(ageError).isNotNull(); - assertThat(ageError.getCode()).isEqualTo("typeMismatch"); - assertThat(ageError.getRejectedValue()).isEqualTo("32x"); - assertThat(tb.getAge()).isEqualTo(0); - - assertThat(br.hasFieldErrors("touchy")).isTrue(); - assertThat(br.getFieldErrorCount("touchy")).isEqualTo(1); - assertThat(binder.getBindingResult().getFieldValue("touchy")).isEqualTo("m.y"); - FieldError touchyError = binder.getBindingResult().getFieldError("touchy"); - assertThat(touchyError).isNotNull(); - assertThat(touchyError.getCode()).isEqualTo("methodInvocation"); - assertThat(touchyError.getRejectedValue()).isEqualTo("m.y"); - assertThat(tb.getTouchy()).isNull(); - - assertThat(br.hasFieldErrors("spouse")).isFalse(); - assertThat(binder.getBindingResult().getFieldValue("spouse")).isEqualTo("Kerry"); - assertThat(tb.getSpouse()).isNotNull(); - }); + assertThatExceptionOfType(BindException.class).isThrownBy(binder::close).satisfies(ex -> { + assertThat(rod.getName()).isEqualTo("Rod"); + Map model = binder.getBindingResult().getModel(); + TestBean tb = (TestBean) model.get("person"); + assertThat(tb).isEqualTo(rod); + + BindingResult br = (BindingResult) model.get(BindingResult.MODEL_KEY_PREFIX + "person"); + assertThat(binder.getBindingResult()).isSameAs(br); + assertThat(br.hasErrors()).isTrue(); + assertThat(br.getErrorCount()).isEqualTo(2); + + assertThat(br.hasFieldErrors("age")).isTrue(); + assertThat(br.getFieldErrorCount("age")).isEqualTo(1); + assertThat(binder.getBindingResult().getFieldValue("age")).isEqualTo("32x"); + FieldError ageError = binder.getBindingResult().getFieldError("age"); + assertThat(ageError).isNotNull(); + assertThat(ageError.getCode()).isEqualTo("typeMismatch"); + assertThat(ageError.getRejectedValue()).isEqualTo("32x"); + assertThat(tb.getAge()).isEqualTo(0); + + assertThat(br.hasFieldErrors("touchy")).isTrue(); + assertThat(br.getFieldErrorCount("touchy")).isEqualTo(1); + assertThat(binder.getBindingResult().getFieldValue("touchy")).isEqualTo("m.y"); + FieldError touchyError = binder.getBindingResult().getFieldError("touchy"); + assertThat(touchyError).isNotNull(); + assertThat(touchyError.getCode()).isEqualTo("methodInvocation"); + assertThat(touchyError.getRejectedValue()).isEqualTo("m.y"); + assertThat(tb.getTouchy()).isNull(); + + assertThat(br.hasFieldErrors("spouse")).isFalse(); + assertThat(binder.getBindingResult().getFieldValue("spouse")).isEqualTo("Kerry"); + assertThat(tb.getSpouse()).isNotNull(); + }); } @Test @@ -576,7 +578,7 @@ void bindingWithCustomFormatter() { editor = binder.getBindingResult().findEditor("myFloat", null); assertThat(editor).isNotNull(); editor.setAsText("1,6"); - assertThat(((Number) editor.getValue()).floatValue() == 1.6f).isTrue(); + assertThat(((Number) editor.getValue()).floatValue()).isEqualTo(1.6f); } finally { LocaleContextHolder.resetLocaleContext(); @@ -752,15 +754,15 @@ void bindingWithAllowedFieldsUsingAsterisks() throws BindException { binder.bind(pvs); binder.close(); - assertThat("Rod".equals(rod.getName())).as("changed name correctly").isTrue(); - assertThat("Rod".equals(rod.getTouchy())).as("changed touchy correctly").isTrue(); - assertThat(rod.getAge() == 0).as("did not change age").isTrue(); + assertThat(rod.getName()).as("changed name correctly").isEqualTo("Rod"); + assertThat(rod.getTouchy()).as("changed touchy correctly").isEqualTo("Rod"); + assertThat(rod.getAge()).as("did not change age").isEqualTo(0); String[] disallowedFields = binder.getBindingResult().getSuppressedFields(); assertThat(disallowedFields).hasSize(1); assertThat(disallowedFields[0]).isEqualTo("age"); Map m = binder.getBindingResult().getModel(); - assertThat(m.size() == 2).as("There is one element in map").isTrue(); + assertThat(m).as("There is one element in map").hasSize(2); TestBean tb = (TestBean) m.get("person"); assertThat(tb.equals(rod)).as("Same object").isTrue(); } @@ -914,7 +916,7 @@ public String getAsText() { binder.getBindingResult().rejectValue("touchy", "someCode", "someMessage"); binder.getBindingResult().rejectValue("spouse.name", "someCode", "someMessage"); - assertThat(binder.getBindingResult().getNestedPath()).isEqualTo(""); + assertThat(binder.getBindingResult().getNestedPath()).isEmpty(); assertThat(binder.getBindingResult().getFieldValue("name")).isEqualTo("value"); assertThat(binder.getBindingResult().getFieldError("name").getRejectedValue()).isEqualTo("prefixvalue"); assertThat(tb.getName()).isEqualTo("prefixvalue"); @@ -1010,7 +1012,7 @@ public String print(String object, Locale locale) { binder.getBindingResult().rejectValue("touchy", "someCode", "someMessage"); binder.getBindingResult().rejectValue("spouse.name", "someCode", "someMessage"); - assertThat(binder.getBindingResult().getNestedPath()).isEqualTo(""); + assertThat(binder.getBindingResult().getNestedPath()).isEmpty(); assertThat(binder.getBindingResult().getFieldValue("name")).isEqualTo("value"); assertThat(binder.getBindingResult().getFieldError("name").getRejectedValue()).isEqualTo("prefixvalue"); assertThat(tb.getName()).isEqualTo("prefixvalue"); @@ -1134,12 +1136,11 @@ void validatorNoErrors() throws Exception { tb2.setAge(34); tb.setSpouse(tb2); DataBinder db = new DataBinder(tb, "tb"); + db.setValidator(new TestBeanValidator()); MutablePropertyValues pvs = new MutablePropertyValues(); pvs.add("spouse.age", "argh"); db.bind(pvs); Errors errors = db.getBindingResult(); - Validator testValidator = new TestBeanValidator(); - testValidator.validate(tb, errors); errors.setNestedPath("spouse"); assertThat(errors.getNestedPath()).isEqualTo("spouse."); @@ -1148,7 +1149,7 @@ void validatorNoErrors() throws Exception { spouseValidator.validate(tb.getSpouse(), errors); errors.setNestedPath(""); - assertThat(errors.getNestedPath()).isEqualTo(""); + assertThat(errors.getNestedPath()).isEmpty(); errors.pushNestedPath("spouse"); assertThat(errors.getNestedPath()).isEqualTo("spouse."); errors.pushNestedPath("spouse"); @@ -1156,7 +1157,7 @@ void validatorNoErrors() throws Exception { errors.popNestedPath(); assertThat(errors.getNestedPath()).isEqualTo("spouse."); errors.popNestedPath(); - assertThat(errors.getNestedPath()).isEqualTo(""); + assertThat(errors.getNestedPath()).isEmpty(); try { errors.popNestedPath(); } @@ -1166,7 +1167,7 @@ void validatorNoErrors() throws Exception { errors.pushNestedPath("spouse"); assertThat(errors.getNestedPath()).isEqualTo("spouse."); errors.setNestedPath(""); - assertThat(errors.getNestedPath()).isEqualTo(""); + assertThat(errors.getNestedPath()).isEmpty(); try { errors.popNestedPath(); } @@ -1187,8 +1188,7 @@ void validatorNoErrors() throws Exception { void validatorWithErrors() { TestBean tb = new TestBean(); tb.setSpouse(new TestBean()); - - Errors errors = new BeanPropertyBindingResult(tb, "tb"); + Errors errors = new DataBinder(tb, "tb").getBindingResult(); Validator testValidator = new TestBeanValidator(); testValidator.validate(tb, errors); @@ -1201,7 +1201,11 @@ void validatorWithErrors() { errors.setNestedPath(""); assertThat(errors.hasErrors()).isTrue(); assertThat(errors.getErrorCount()).isEqualTo(6); + assertThat(errors.getAllErrors()) + .containsAll(errors.getGlobalErrors()) + .containsAll(errors.getFieldErrors()); + assertThat(errors.hasGlobalErrors()).isTrue(); assertThat(errors.getGlobalErrorCount()).isEqualTo(2); assertThat(errors.getGlobalError().getCode()).isEqualTo("NAME_TOUCHY_MISMATCH"); assertThat((errors.getGlobalErrors().get(0)).getCode()).isEqualTo("NAME_TOUCHY_MISMATCH"); @@ -1257,10 +1261,11 @@ void validatorWithErrorsAndCodesPrefix() { TestBean tb = new TestBean(); tb.setSpouse(new TestBean()); - BeanPropertyBindingResult errors = new BeanPropertyBindingResult(tb, "tb"); + DataBinder dataBinder = new DataBinder(tb, "tb"); DefaultMessageCodesResolver codesResolver = new DefaultMessageCodesResolver(); codesResolver.setPrefix("validation."); - errors.setMessageCodesResolver(codesResolver); + dataBinder.setMessageCodesResolver(codesResolver); + Errors errors = dataBinder.getBindingResult(); Validator testValidator = new TestBeanValidator(); testValidator.validate(tb, errors); @@ -1273,7 +1278,11 @@ void validatorWithErrorsAndCodesPrefix() { errors.setNestedPath(""); assertThat(errors.hasErrors()).isTrue(); assertThat(errors.getErrorCount()).isEqualTo(6); + assertThat(errors.getAllErrors()) + .containsAll(errors.getGlobalErrors()) + .containsAll(errors.getFieldErrors()); + assertThat(errors.hasGlobalErrors()).isTrue(); assertThat(errors.getGlobalErrorCount()).isEqualTo(2); assertThat(errors.getGlobalError().getCode()).isEqualTo("validation.NAME_TOUCHY_MISMATCH"); assertThat((errors.getGlobalErrors().get(0)).getCode()).isEqualTo("validation.NAME_TOUCHY_MISMATCH"); @@ -1327,9 +1336,11 @@ void validatorWithErrorsAndCodesPrefix() { @Test void validatorWithNestedObjectNull() { TestBean tb = new TestBean(); - Errors errors = new BeanPropertyBindingResult(tb, "tb"); + Errors errors = new DataBinder(tb, "tb").getBindingResult(); + Validator testValidator = new TestBeanValidator(); testValidator.validate(tb, errors); + errors.setNestedPath("spouse."); assertThat(errors.getNestedPath()).isEqualTo("spouse."); Validator spouseValidator = new SpouseValidator(); @@ -1724,7 +1735,7 @@ public void setAsText(String text) throws IllegalArgumentException { pvs.add("stringArray", new String[] {"a1", "b2"}); binder.bind(pvs); assertThat(binder.getBindingResult().hasErrors()).isFalse(); - assertThat(tb.getStringArray().length).isEqualTo(2); + assertThat(tb.getStringArray()).hasSize(2); assertThat(tb.getStringArray()[0]).isEqualTo("Xa1"); assertThat(tb.getStringArray()[1]).isEqualTo("Xb2"); } @@ -1800,16 +1811,16 @@ void rejectWithoutDefaultMessage() { tb.setName("myName"); tb.setAge(99); - BeanPropertyBindingResult ex = new BeanPropertyBindingResult(tb, "tb"); - ex.reject("invalid"); - ex.rejectValue("age", "invalidField"); + BeanPropertyBindingResult errors = new BeanPropertyBindingResult(tb, "tb"); + errors.reject("invalid"); + errors.rejectValue("age", "invalidField"); StaticMessageSource ms = new StaticMessageSource(); ms.addMessage("invalid", Locale.US, "general error"); ms.addMessage("invalidField", Locale.US, "invalid field"); - assertThat(ms.getMessage(ex.getGlobalError(), Locale.US)).isEqualTo("general error"); - assertThat(ms.getMessage(ex.getFieldError("age"), Locale.US)).isEqualTo("invalid field"); + assertThat(ms.getMessage(errors.getGlobalError(), Locale.US)).isEqualTo("general error"); + assertThat(ms.getMessage(errors.getFieldError("age"), Locale.US)).isEqualTo("invalid field"); } @Test @@ -1877,13 +1888,13 @@ void autoGrowWithinDefaultLimit() { void autoGrowBeyondDefaultLimit() { TestBean testBean = new TestBean(); DataBinder binder = new DataBinder(testBean, "testBean"); - MutablePropertyValues mpvs = new MutablePropertyValues(); mpvs.add("friends[256]", ""); + assertThatExceptionOfType(InvalidPropertyException.class) - .isThrownBy(() -> binder.bind(mpvs)) - .havingRootCause() - .isInstanceOf(IndexOutOfBoundsException.class); + .isThrownBy(() -> binder.bind(mpvs)) + .havingRootCause() + .isInstanceOf(IndexOutOfBoundsException.class); } @Test @@ -1904,13 +1915,13 @@ void autoGrowBeyondCustomLimit() { TestBean testBean = new TestBean(); DataBinder binder = new DataBinder(testBean, "testBean"); binder.setAutoGrowCollectionLimit(10); - MutablePropertyValues mpvs = new MutablePropertyValues(); mpvs.add("friends[16]", ""); + assertThatExceptionOfType(InvalidPropertyException.class) - .isThrownBy(() -> binder.bind(mpvs)) - .havingRootCause() - .isInstanceOf(IndexOutOfBoundsException.class); + .isThrownBy(() -> binder.bind(mpvs)) + .havingRootCause() + .isInstanceOf(IndexOutOfBoundsException.class); } @Test @@ -1926,7 +1937,7 @@ void nestedGrowingList() { List list = (List) form.getF().get("list"); assertThat(list.get(0)).isEqualTo("firstValue"); assertThat(list.get(1)).isEqualTo("secondValue"); - assertThat(list.size()).isEqualTo(2); + assertThat(list).hasSize(2); } @Test @@ -1959,7 +1970,7 @@ void setAutoGrowCollectionLimit() { pvs.add("integerList[256]", "1"); binder.bind(pvs); - assertThat(tb.getIntegerList().size()).isEqualTo(257); + assertThat(tb.getIntegerList()).hasSize(257); assertThat(tb.getIntegerList().get(256)).isEqualTo(Integer.valueOf(1)); assertThat(binder.getBindingResult().getFieldValue("integerList[256]")).isEqualTo(1); } diff --git a/spring-context/src/test/java/org/springframework/validation/ValidationUtilsTests.java b/spring-context/src/test/java/org/springframework/validation/ValidationUtilsTests.java index 0a027b95df43..1131a2d645c6 100644 --- a/spring-context/src/test/java/org/springframework/validation/ValidationUtilsTests.java +++ b/spring-context/src/test/java/org/springframework/validation/ValidationUtilsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 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. @@ -35,7 +35,7 @@ public class ValidationUtilsTests { @Test - public void testInvokeValidatorWithNullValidator() throws Exception { + public void testInvokeValidatorWithNullValidator() { TestBean tb = new TestBean(); Errors errors = new BeanPropertyBindingResult(tb, "tb"); assertThatIllegalArgumentException().isThrownBy(() -> @@ -43,14 +43,14 @@ public void testInvokeValidatorWithNullValidator() throws Exception { } @Test - public void testInvokeValidatorWithNullErrors() throws Exception { + public void testInvokeValidatorWithNullErrors() { TestBean tb = new TestBean(); assertThatIllegalArgumentException().isThrownBy(() -> ValidationUtils.invokeValidator(new EmptyValidator(), tb, null)); } @Test - public void testInvokeValidatorSunnyDay() throws Exception { + public void testInvokeValidatorSunnyDay() { TestBean tb = new TestBean(); Errors errors = new BeanPropertyBindingResult(tb, "tb"); ValidationUtils.invokeValidator(new EmptyValidator(), tb, errors); @@ -59,7 +59,7 @@ public void testInvokeValidatorSunnyDay() throws Exception { } @Test - public void testValidationUtilsSunnyDay() throws Exception { + public void testValidationUtilsSunnyDay() { TestBean tb = new TestBean(""); Validator testValidator = new EmptyValidator(); @@ -75,7 +75,7 @@ public void testValidationUtilsSunnyDay() throws Exception { } @Test - public void testValidationUtilsNull() throws Exception { + public void testValidationUtilsNull() { TestBean tb = new TestBean(); Errors errors = new BeanPropertyBindingResult(tb, "tb"); Validator testValidator = new EmptyValidator(); @@ -85,7 +85,7 @@ public void testValidationUtilsNull() throws Exception { } @Test - public void testValidationUtilsEmpty() throws Exception { + public void testValidationUtilsEmpty() { TestBean tb = new TestBean(""); Errors errors = new BeanPropertyBindingResult(tb, "tb"); Validator testValidator = new EmptyValidator(); @@ -113,7 +113,7 @@ public void testValidationUtilsEmptyVariants() { } @Test - public void testValidationUtilsEmptyOrWhitespace() throws Exception { + public void testValidationUtilsEmptyOrWhitespace() { TestBean tb = new TestBean(); Validator testValidator = new EmptyOrWhitespaceValidator(); diff --git a/spring-context/src/test/resources/org/springframework/aop/aspectj/OverloadedAdviceTests.xml b/spring-context/src/test/resources/org/springframework/aop/aspectj/OverloadedAdviceTests.xml index df9bfadc8ebc..ae175f39a197 100644 --- a/spring-context/src/test/resources/org/springframework/aop/aspectj/OverloadedAdviceTests.xml +++ b/spring-context/src/test/resources/org/springframework/aop/aspectj/OverloadedAdviceTests.xml @@ -18,4 +18,6 @@ + + \ No newline at end of file diff --git a/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-delegationOverrides.xml b/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-delegationOverrides.xml index 175408a2cb39..d1a875359d3c 100644 --- a/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-delegationOverrides.xml +++ b/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-delegationOverrides.xml @@ -3,9 +3,6 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans-2.5.xsd"> - @@ -27,39 +24,34 @@ - + - + + + + - + - + - + - + + String - - + Jenny 30 @@ -68,8 +60,7 @@ - + Simple bean, without any collections. diff --git a/spring-core/src/main/java/org/springframework/asm/AnnotationVisitor.java b/spring-core/src/main/java/org/springframework/asm/AnnotationVisitor.java index 05b4f9ba2f6b..19e93382fdbb 100644 --- a/spring-core/src/main/java/org/springframework/asm/AnnotationVisitor.java +++ b/spring-core/src/main/java/org/springframework/asm/AnnotationVisitor.java @@ -82,6 +82,17 @@ protected AnnotationVisitor(final int api, final AnnotationVisitor annotationVis this.av = annotationVisitor; } + /** + * The annotation visitor to which this visitor must delegate method calls. May be {@literal + * null}. + * + * @return the annotation visitor to which this visitor must delegate method calls, or {@literal + * null}. + */ + public AnnotationVisitor getDelegate() { + return av; + } + /** * Visits a primitive value of the annotation. * diff --git a/spring-core/src/main/java/org/springframework/asm/ClassReader.java b/spring-core/src/main/java/org/springframework/asm/ClassReader.java index 725ba340bbe7..d84485cafdf5 100644 --- a/spring-core/src/main/java/org/springframework/asm/ClassReader.java +++ b/spring-core/src/main/java/org/springframework/asm/ClassReader.java @@ -194,7 +194,7 @@ public ClassReader( this.b = classFileBuffer; // Check the class' major_version. This field is after the magic and minor_version fields, which // use 4 and 2 bytes respectively. - if (checkClassVersion && readShort(classFileOffset + 6) > Opcodes.V19) { + if (checkClassVersion && readShort(classFileOffset + 6) > Opcodes.V21) { throw new IllegalArgumentException( "Unsupported class file major version " + readShort(classFileOffset + 6)); } @@ -308,6 +308,7 @@ public ClassReader(final String className) throws IOException { * @return the content of the given input stream. * @throws IOException if a problem occurs during reading. */ + @SuppressWarnings("PMD.UseTryWithResources") private static byte[] readStream(final InputStream inputStream, final boolean close) throws IOException { if (inputStream == null) { @@ -376,7 +377,7 @@ public String getClassName() { } /** - * Returns the internal of name of the super class (see {@link Type#getInternalName()}). For + * Returns the internal name of the super class (see {@link Type#getInternalName()}). For * interfaces, the super class is {@link Object}. * * @return the internal name of the super class, or {@literal null} for {@link Object} class. diff --git a/spring-core/src/main/java/org/springframework/asm/ClassTooLargeException.java b/spring-core/src/main/java/org/springframework/asm/ClassTooLargeException.java index f679328b87e9..bc6a96dede4c 100644 --- a/spring-core/src/main/java/org/springframework/asm/ClassTooLargeException.java +++ b/spring-core/src/main/java/org/springframework/asm/ClassTooLargeException.java @@ -42,7 +42,8 @@ public final class ClassTooLargeException extends IndexOutOfBoundsException { /** * Constructs a new {@link ClassTooLargeException}. * - * @param className the internal name of the class. + * @param className the internal name of the class (see {@link + * org.objectweb.asm.Type#getInternalName()}). * @param constantPoolCount the number of constant pool items of the class. */ public ClassTooLargeException(final String className, final int constantPoolCount) { @@ -52,7 +53,7 @@ public ClassTooLargeException(final String className, final int constantPoolCoun } /** - * Returns the internal name of the class. + * Returns the internal name of the class (see {@link org.objectweb.asm.Type#getInternalName()}). * * @return the internal name of the class. */ diff --git a/spring-core/src/main/java/org/springframework/asm/ClassVisitor.java b/spring-core/src/main/java/org/springframework/asm/ClassVisitor.java index 14064e742e1e..80d241dddd09 100644 --- a/spring-core/src/main/java/org/springframework/asm/ClassVisitor.java +++ b/spring-core/src/main/java/org/springframework/asm/ClassVisitor.java @@ -81,6 +81,15 @@ protected ClassVisitor(final int api, final ClassVisitor classVisitor) { this.cv = classVisitor; } + /** + * The class visitor to which this visitor must delegate method calls. May be {@literal null}. + * + * @return the class visitor to which this visitor must delegate method calls, or {@literal null}. + */ + public ClassVisitor getDelegate() { + return cv; + } + /** * Visits the header of the class. * @@ -155,7 +164,8 @@ public ModuleVisitor visitModule(final String name, final int access, final Stri * implicitly its own nest, so it's invalid to call this method with the visited class name as * argument. * - * @param nestHost the internal name of the host class of the nest. + * @param nestHost the internal name of the host class of the nest (see {@link + * Type#getInternalName()}). */ public void visitNestHost(final String nestHost) { if (api < Opcodes.ASM7) { @@ -167,14 +177,19 @@ public void visitNestHost(final String nestHost) { } /** - * Visits the enclosing class of the class. This method must be called only if the class has an - * enclosing class. + * Visits the enclosing class of the class. This method must be called only if this class is a + * local or anonymous class. See the JVMS 4.7.7 section for more details. * - * @param owner internal name of the enclosing class of the class. + * @param owner internal name of the enclosing class of the class (see {@link + * Type#getInternalName()}). * @param name the name of the method that contains the class, or {@literal null} if the class is - * not enclosed in a method of its enclosing class. + * not enclosed in a method or constructor of its enclosing class (e.g. if it is enclosed in + * an instance initializer, static initializer, instance variable initializer, or class + * variable initializer). * @param descriptor the descriptor of the method that contains the class, or {@literal null} if - * the class is not enclosed in a method of its enclosing class. + * the class is not enclosed in a method or constructor of its enclosing class (e.g. if it is + * enclosed in an instance initializer, static initializer, instance variable initializer, or + * class variable initializer). */ public void visitOuterClass(final String owner, final String name, final String descriptor) { if (cv != null) { @@ -241,7 +256,7 @@ public void visitAttribute(final Attribute attribute) { * the visited class is the host of a nest. A nest host is implicitly a member of its own nest, so * it's invalid to call this method with the visited class name as argument. * - * @param nestMember the internal name of a nest member. + * @param nestMember the internal name of a nest member (see {@link Type#getInternalName()}). */ public void visitNestMember(final String nestMember) { if (api < Opcodes.ASM7) { @@ -256,7 +271,8 @@ public void visitNestMember(final String nestMember) { * Visits a permitted subclasses. A permitted subclass is one of the allowed subclasses of the * current class. * - * @param permittedSubclass the internal name of a permitted subclass. + * @param permittedSubclass the internal name of a permitted subclass (see {@link + * Type#getInternalName()}). */ public void visitPermittedSubclass(final String permittedSubclass) { if (api < Opcodes.ASM9) { @@ -269,15 +285,18 @@ public void visitPermittedSubclass(final String permittedSubclass) { /** * Visits information about an inner class. This inner class is not necessarily a member of the - * class being visited. + * class being visited. More precisely, every class or interface C which is referenced by this + * class and which is not a package member must be visited with this method. This class must + * reference its nested class or interface members, and its enclosing class, if any. See the JVMS + * 4.7.6 section for more details. * - * @param name the internal name of an inner class (see {@link Type#getInternalName()}). - * @param outerName the internal name of the class to which the inner class belongs (see {@link - * Type#getInternalName()}). May be {@literal null} for not member classes. - * @param innerName the (simple) name of the inner class inside its enclosing class. May be - * {@literal null} for anonymous inner classes. - * @param access the access flags of the inner class as originally declared in the enclosing - * class. + * @param name the internal name of C (see {@link Type#getInternalName()}). + * @param outerName the internal name of the class or interface C is a member of (see {@link + * Type#getInternalName()}). Must be {@literal null} if C is not the member of a class or + * interface (e.g. for local or anonymous classes). + * @param innerName the (simple) name of C. Must be {@literal null} for anonymous inner classes. + * @param access the access flags of C originally declared in the source code from which this + * class was compiled. */ public void visitInnerClass( final String name, final String outerName, final String innerName, final int access) { diff --git a/spring-core/src/main/java/org/springframework/asm/ClassWriter.java b/spring-core/src/main/java/org/springframework/asm/ClassWriter.java index 045d57131273..030d86566f61 100644 --- a/spring-core/src/main/java/org/springframework/asm/ClassWriter.java +++ b/spring-core/src/main/java/org/springframework/asm/ClassWriter.java @@ -842,7 +842,7 @@ public int newUTF8(final String value) { * constant pool already contains a similar item. This method is intended for {@link Attribute} * sub classes, and is normally not needed by class generators or adapters. * - * @param value the internal name of the class. + * @param value the internal name of the class (see {@link Type#getInternalName()}). * @return the index of a new or already existing class reference item. */ public int newClass(final String value) { @@ -894,7 +894,8 @@ public int newPackage(final String packageName) { * Opcodes#H_GETSTATIC}, {@link Opcodes#H_PUTFIELD}, {@link Opcodes#H_PUTSTATIC}, {@link * Opcodes#H_INVOKEVIRTUAL}, {@link Opcodes#H_INVOKESTATIC}, {@link Opcodes#H_INVOKESPECIAL}, * {@link Opcodes#H_NEWINVOKESPECIAL} or {@link Opcodes#H_INVOKEINTERFACE}. - * @param owner the internal name of the field or method owner class. + * @param owner the internal name of the field or method owner class (see {@link + * Type#getInternalName()}). * @param name the name of the field or method. * @param descriptor the descriptor of the field or method. * @return the index of a new or already existing method type reference item. @@ -916,7 +917,8 @@ public int newHandle( * Opcodes#H_GETSTATIC}, {@link Opcodes#H_PUTFIELD}, {@link Opcodes#H_PUTSTATIC}, {@link * Opcodes#H_INVOKEVIRTUAL}, {@link Opcodes#H_INVOKESTATIC}, {@link Opcodes#H_INVOKESPECIAL}, * {@link Opcodes#H_NEWINVOKESPECIAL} or {@link Opcodes#H_INVOKEINTERFACE}. - * @param owner the internal name of the field or method owner class. + * @param owner the internal name of the field or method owner class (see {@link + * Type#getInternalName()}). * @param name the name of the field or method. * @param descriptor the descriptor of the field or method. * @param isInterface true if the owner is an interface. @@ -978,7 +980,7 @@ public int newInvokeDynamic( * constant pool already contains a similar item. This method is intended for {@link Attribute} * sub classes, and is normally not needed by class generators or adapters. * - * @param owner the internal name of the field's owner class. + * @param owner the internal name of the field's owner class (see {@link Type#getInternalName()}). * @param name the field's name. * @param descriptor the field's descriptor. * @return the index of a new or already existing field reference item. @@ -992,7 +994,8 @@ public int newField(final String owner, final String name, final String descript * constant pool already contains a similar item. This method is intended for {@link Attribute} * sub classes, and is normally not needed by class generators or adapters. * - * @param owner the internal name of the method's owner class. + * @param owner the internal name of the method's owner class (see {@link + * Type#getInternalName()}). * @param name the method's name. * @param descriptor the method's descriptor. * @param isInterface {@literal true} if {@code owner} is an interface. @@ -1028,9 +1031,10 @@ public int newNameType(final String name, final String descriptor) { * currently being generated by this ClassWriter, which can of course not be loaded since it is * under construction. * - * @param type1 the internal name of a class. - * @param type2 the internal name of another class. - * @return the internal name of the common super class of the two given classes. + * @param type1 the internal name of a class (see {@link Type#getInternalName()}). + * @param type2 the internal name of another class (see {@link Type#getInternalName()}). + * @return the internal name of the common super class of the two given classes (see {@link + * Type#getInternalName()}). */ protected String getCommonSuperClass(final String type1, final String type2) { ClassLoader classLoader = getClassLoader(); diff --git a/spring-core/src/main/java/org/springframework/asm/FieldVisitor.java b/spring-core/src/main/java/org/springframework/asm/FieldVisitor.java index 2aa16bc8e619..676843656f23 100644 --- a/spring-core/src/main/java/org/springframework/asm/FieldVisitor.java +++ b/spring-core/src/main/java/org/springframework/asm/FieldVisitor.java @@ -78,6 +78,15 @@ protected FieldVisitor(final int api, final FieldVisitor fieldVisitor) { this.fv = fieldVisitor; } + /** + * The field visitor to which this visitor must delegate method calls. May be {@literal null}. + * + * @return the field visitor to which this visitor must delegate method calls, or {@literal null}. + */ + public FieldVisitor getDelegate() { + return fv; + } + /** * Visits an annotation of the field. * diff --git a/spring-core/src/main/java/org/springframework/asm/Frame.java b/spring-core/src/main/java/org/springframework/asm/Frame.java index 5a1dc2ffb15f..76317cc91c31 100644 --- a/spring-core/src/main/java/org/springframework/asm/Frame.java +++ b/spring-core/src/main/java/org/springframework/asm/Frame.java @@ -367,11 +367,12 @@ private static int getAbstractTypeFromDescriptor( typeValue = REFERENCE_KIND | symbolTable.addType(internalName); break; default: - throw new IllegalArgumentException(); + throw new IllegalArgumentException( + "Invalid descriptor fragment: " + buffer.substring(elementDescriptorOffset)); } return ((elementDescriptorOffset - offset) << DIM_SHIFT) | typeValue; default: - throw new IllegalArgumentException(); + throw new IllegalArgumentException("Invalid descriptor: " + buffer.substring(offset)); } } diff --git a/spring-core/src/main/java/org/springframework/asm/Handle.java b/spring-core/src/main/java/org/springframework/asm/Handle.java index 52a3e316bf98..717be9b82b4f 100644 --- a/spring-core/src/main/java/org/springframework/asm/Handle.java +++ b/spring-core/src/main/java/org/springframework/asm/Handle.java @@ -65,7 +65,7 @@ public final class Handle { * {@link Opcodes#H_INVOKESPECIAL}, {@link Opcodes#H_NEWINVOKESPECIAL} or {@link * Opcodes#H_INVOKEINTERFACE}. * @param owner the internal name of the class that owns the field or method designated by this - * handle. + * handle (see {@link Type#getInternalName()}). * @param name the name of the field or method designated by this handle. * @param descriptor the descriptor of the field or method designated by this handle. * @deprecated this constructor has been superseded by {@link #Handle(int, String, String, String, @@ -85,7 +85,7 @@ public Handle(final int tag, final String owner, final String name, final String * {@link Opcodes#H_INVOKESPECIAL}, {@link Opcodes#H_NEWINVOKESPECIAL} or {@link * Opcodes#H_INVOKEINTERFACE}. * @param owner the internal name of the class that owns the field or method designated by this - * handle. + * handle (see {@link Type#getInternalName()}). * @param name the name of the field or method designated by this handle. * @param descriptor the descriptor of the field or method designated by this handle. * @param isInterface whether the owner is an interface or not. @@ -118,7 +118,8 @@ public int getTag() { /** * Returns the internal name of the class that owns the field or method designated by this handle. * - * @return the internal name of the class that owns the field or method designated by this handle. + * @return the internal name of the class that owns the field or method designated by this handle + * (see {@link Type#getInternalName()}). */ public String getOwner() { return owner; diff --git a/spring-core/src/main/java/org/springframework/asm/MethodTooLargeException.java b/spring-core/src/main/java/org/springframework/asm/MethodTooLargeException.java index ac263c442aef..b41ec8b588ea 100644 --- a/spring-core/src/main/java/org/springframework/asm/MethodTooLargeException.java +++ b/spring-core/src/main/java/org/springframework/asm/MethodTooLargeException.java @@ -44,7 +44,7 @@ public final class MethodTooLargeException extends IndexOutOfBoundsException { /** * Constructs a new {@link MethodTooLargeException}. * - * @param className the internal name of the owner class. + * @param className the internal name of the owner class (see {@link Type#getInternalName()}). * @param methodName the name of the method. * @param descriptor the descriptor of the method. * @param codeSize the size of the method's Code attribute, in bytes. @@ -64,7 +64,7 @@ public MethodTooLargeException( /** * Returns the internal name of the owner class. * - * @return the internal name of the owner class. + * @return the internal name of the owner class (see {@link Type#getInternalName()}). */ public String getClassName() { return className; diff --git a/spring-core/src/main/java/org/springframework/asm/MethodVisitor.java b/spring-core/src/main/java/org/springframework/asm/MethodVisitor.java index 35ecb14c4e15..35ffc81576e4 100644 --- a/spring-core/src/main/java/org/springframework/asm/MethodVisitor.java +++ b/spring-core/src/main/java/org/springframework/asm/MethodVisitor.java @@ -94,6 +94,16 @@ protected MethodVisitor(final int api, final MethodVisitor methodVisitor) { this.mv = methodVisitor; } + /** + * The method visitor to which this visitor must delegate method calls. May be {@literal null}. + * + * @return the method visitor to which this visitor must delegate method calls, or {@literal + * null}. + */ + public MethodVisitor getDelegate() { + return mv; + } + // ----------------------------------------------------------------------------------------------- // Parameters, annotations and non standard attributes // ----------------------------------------------------------------------------------------------- @@ -120,7 +130,7 @@ public void visitParameter(final String name, final int access) { * @return a visitor to the visit the actual default value of this annotation interface method, or * {@literal null} if this visitor is not interested in visiting this default value. The * 'name' parameters passed to the methods of this annotation visitor are ignored. Moreover, - * exacly one visit method must be called on this annotation visitor, followed by visitEnd. + * exactly one visit method must be called on this annotation visitor, followed by visitEnd. */ public AnnotationVisitor visitAnnotationDefault() { if (mv != null) { @@ -273,15 +283,17 @@ public void visitCode() { * @param type the type of this stack map frame. Must be {@link Opcodes#F_NEW} for expanded * frames, or {@link Opcodes#F_FULL}, {@link Opcodes#F_APPEND}, {@link Opcodes#F_CHOP}, {@link * Opcodes#F_SAME} or {@link Opcodes#F_APPEND}, {@link Opcodes#F_SAME1} for compressed frames. - * @param numLocal the number of local variables in the visited frame. + * @param numLocal the number of local variables in the visited frame. Long and double values + * count for one variable. * @param local the local variable types in this frame. This array must not be modified. Primitive * types are represented by {@link Opcodes#TOP}, {@link Opcodes#INTEGER}, {@link * Opcodes#FLOAT}, {@link Opcodes#LONG}, {@link Opcodes#DOUBLE}, {@link Opcodes#NULL} or * {@link Opcodes#UNINITIALIZED_THIS} (long and double are represented by a single element). - * Reference types are represented by String objects (representing internal names), and - * uninitialized types by Label objects (this label designates the NEW instruction that - * created this uninitialized value). - * @param numStack the number of operand stack elements in the visited frame. + * Reference types are represented by String objects (representing internal names, see {@link + * Type#getInternalName()}), and uninitialized types by Label objects (this label designates + * the NEW instruction that created this uninitialized value). + * @param numStack the number of operand stack elements in the visited frame. Long and double + * values count for one stack element. * @param stack the operand stack types in this frame. This array must not be modified. Its * content has the same format as the "local" array. * @throws IllegalStateException if a frame is visited just after another one, without any @@ -360,7 +372,7 @@ public void visitVarInsn(final int opcode, final int varIndex) { /** * Visits a type instruction. A type instruction is an instruction that takes the internal name of - * a class as parameter. + * a class as parameter (see {@link Type#getInternalName()}). * * @param opcode the opcode of the type instruction to be visited. This opcode is either NEW, * ANEWARRAY, CHECKCAST or INSTANCEOF. @@ -552,12 +564,12 @@ public void visitLdcInsn(final Object value) { /** * Visits an IINC instruction. * - * @param var index of the local variable to be incremented. + * @param varIndex index of the local variable to be incremented. * @param increment amount to increment the local variable by. */ - public void visitIincInsn(final int var, final int increment) { + public void visitIincInsn(final int varIndex, final int increment) { if (mv != null) { - mv.visitIincInsn(var, increment); + mv.visitIincInsn(varIndex, increment); } } @@ -643,8 +655,9 @@ public AnnotationVisitor visitInsnAnnotation( * @param start the beginning of the exception handler's scope (inclusive). * @param end the end of the exception handler's scope (exclusive). * @param handler the beginning of the exception handler's code. - * @param type the internal name of the type of exceptions handled by the handler, or {@literal - * null} to catch any exceptions (for "finally" blocks). + * @param type the internal name of the type of exceptions handled by the handler (see {@link + * Type#getInternalName()}), or {@literal null} to catch any exceptions (for "finally" + * blocks). * @throws IllegalArgumentException if one of the labels has already been visited by this visitor * (by the {@link #visitLabel} method). */ diff --git a/spring-core/src/main/java/org/springframework/asm/Opcodes.java b/spring-core/src/main/java/org/springframework/asm/Opcodes.java index dc7bbce285c2..035aa4548477 100644 --- a/spring-core/src/main/java/org/springframework/asm/Opcodes.java +++ b/spring-core/src/main/java/org/springframework/asm/Opcodes.java @@ -284,6 +284,8 @@ public interface Opcodes { int V17 = 0 << 16 | 61; int V18 = 0 << 16 | 62; int V19 = 0 << 16 | 63; + int V20 = 0 << 16 | 64; + int V21 = 0 << 16 | 65; /** * Version flag indicating that the class is using 'preview' features. diff --git a/spring-core/src/main/java/org/springframework/asm/RecordComponentVisitor.java b/spring-core/src/main/java/org/springframework/asm/RecordComponentVisitor.java index 6e767b6e7078..9832b25cf0d1 100644 --- a/spring-core/src/main/java/org/springframework/asm/RecordComponentVisitor.java +++ b/spring-core/src/main/java/org/springframework/asm/RecordComponentVisitor.java @@ -45,7 +45,7 @@ public abstract class RecordComponentVisitor { /** * The record visitor to which this visitor must delegate method calls. May be {@literal null}. */ - /*package-private*/ RecordComponentVisitor delegate; + protected RecordComponentVisitor delegate; /** * Constructs a new {@link RecordComponentVisitor}. @@ -83,7 +83,8 @@ protected RecordComponentVisitor( /** * The record visitor to which this visitor must delegate method calls. May be {@literal null}. * - * @return the record visitor to which this visitor must delegate method calls or {@literal null}. + * @return the record visitor to which this visitor must delegate method calls, or {@literal + * null}. */ public RecordComponentVisitor getDelegate() { return delegate; diff --git a/spring-core/src/main/java/org/springframework/asm/RecordComponentWriter.java b/spring-core/src/main/java/org/springframework/asm/RecordComponentWriter.java index bd007ac26d61..c36d2054fcb3 100644 --- a/spring-core/src/main/java/org/springframework/asm/RecordComponentWriter.java +++ b/spring-core/src/main/java/org/springframework/asm/RecordComponentWriter.java @@ -37,7 +37,7 @@ final class RecordComponentWriter extends RecordComponentVisitor { /** The name_index field of the Record attribute. */ private final int nameIndex; - /** The descriptor_index field of the the Record attribute. */ + /** The descriptor_index field of the Record attribute. */ private final int descriptorIndex; /** diff --git a/spring-core/src/main/java/org/springframework/asm/Type.java b/spring-core/src/main/java/org/springframework/asm/Type.java index 24d0e679b87c..36d69ab8b08c 100644 --- a/spring-core/src/main/java/org/springframework/asm/Type.java +++ b/spring-core/src/main/java/org/springframework/asm/Type.java @@ -245,7 +245,7 @@ public Type getElementType() { /** * Returns the {@link Type} corresponding to the given internal name. * - * @param internalName an internal name. + * @param internalName an internal name (see {@link Type#getInternalName()}). * @return the {@link Type} corresponding to the given internal name. */ public static Type getObjectType(final String internalName) { @@ -708,8 +708,8 @@ public int getSize() { * * @return the size of the arguments of the method (plus one for the implicit this argument), * argumentsSize, and the size of its return value, returnSize, packed into a single int i = - * {@code (argumentsSize << 2) | returnSize} (argumentsSize is therefore equal to {@code - * i >> 2}, and returnSize to {@code i & 0x03}). + * {@code (argumentsSize << 2) | returnSize} (argumentsSize is therefore equal to {@code + * i >> 2}, and returnSize to {@code i & 0x03}). */ public int getArgumentsAndReturnSizes() { return getArgumentsAndReturnSizes(getDescriptor()); @@ -721,8 +721,8 @@ public int getArgumentsAndReturnSizes() { * @param methodDescriptor a method descriptor. * @return the size of the arguments of the method (plus one for the implicit this argument), * argumentsSize, and the size of its return value, returnSize, packed into a single int i = - * {@code (argumentsSize << 2) | returnSize} (argumentsSize is therefore equal to {@code - * i >> 2}, and returnSize to {@code i & 0x03}). + * {@code (argumentsSize << 2) | returnSize} (argumentsSize is therefore equal to {@code + * i >> 2}, and returnSize to {@code i & 0x03}). */ public static int getArgumentsAndReturnSizes(final String methodDescriptor) { int argumentsSize = 1; diff --git a/spring-core/src/main/java/org/springframework/cglib/beans/BeanCopier.java b/spring-core/src/main/java/org/springframework/cglib/beans/BeanCopier.java index 30a00e8d1d07..52b94fd5d813 100644 --- a/spring-core/src/main/java/org/springframework/cglib/beans/BeanCopier.java +++ b/spring-core/src/main/java/org/springframework/cglib/beans/BeanCopier.java @@ -16,12 +16,24 @@ package org.springframework.cglib.beans; import java.beans.PropertyDescriptor; -import java.lang.reflect.*; import java.security.ProtectionDomain; -import org.springframework.cglib.core.*; +import java.util.HashMap; +import java.util.Map; + import org.springframework.asm.ClassVisitor; import org.springframework.asm.Type; -import java.util.*; +import org.springframework.cglib.core.AbstractClassGenerator; +import org.springframework.cglib.core.ClassEmitter; +import org.springframework.cglib.core.CodeEmitter; +import org.springframework.cglib.core.Constants; +import org.springframework.cglib.core.Converter; +import org.springframework.cglib.core.EmitUtils; +import org.springframework.cglib.core.KeyFactory; +import org.springframework.cglib.core.Local; +import org.springframework.cglib.core.MethodInfo; +import org.springframework.cglib.core.ReflectUtils; +import org.springframework.cglib.core.Signature; +import org.springframework.cglib.core.TypeUtils; /** * @author Chris Nokleberg @@ -29,151 +41,154 @@ @SuppressWarnings({"rawtypes", "unchecked"}) abstract public class BeanCopier { - private static final BeanCopierKey KEY_FACTORY = - (BeanCopierKey)KeyFactory.create(BeanCopierKey.class); - private static final Type CONVERTER = - TypeUtils.parseType("org.springframework.cglib.core.Converter"); - private static final Type BEAN_COPIER = - TypeUtils.parseType("org.springframework.cglib.beans.BeanCopier"); - private static final Signature COPY = - new Signature("copy", Type.VOID_TYPE, new Type[]{ Constants.TYPE_OBJECT, Constants.TYPE_OBJECT, CONVERTER }); - private static final Signature CONVERT = - TypeUtils.parseSignature("Object convert(Object, Class, Object)"); - - interface BeanCopierKey { - public Object newInstance(String source, String target, boolean useConverter); - } - - public static BeanCopier create(Class source, Class target, boolean useConverter) { - Generator gen = new Generator(); - gen.setSource(source); - gen.setTarget(target); - gen.setUseConverter(useConverter); - return gen.create(); - } - - abstract public void copy(Object from, Object to, Converter converter); - - public static class Generator extends AbstractClassGenerator { - private static final Source SOURCE = new Source(BeanCopier.class.getName()); - private Class source; - private Class target; - private boolean useConverter; - - public Generator() { - super(SOURCE); - } - - public void setSource(Class source) { - if(!Modifier.isPublic(source.getModifiers())){ - setNamePrefix(source.getName()); - } - this.source = source; - } - - public void setTarget(Class target) { - if(!Modifier.isPublic(target.getModifiers())){ - setNamePrefix(target.getName()); - } - this.target = target; + private static final BeanCopierKey KEY_FACTORY = + (BeanCopierKey)KeyFactory.create(BeanCopierKey.class); + private static final Type CONVERTER = + TypeUtils.parseType("org.springframework.cglib.core.Converter"); + private static final Type BEAN_COPIER = + TypeUtils.parseType("org.springframework.cglib.beans.BeanCopier"); + private static final Signature COPY = + new Signature("copy", Type.VOID_TYPE, new Type[]{ Constants.TYPE_OBJECT, Constants.TYPE_OBJECT, CONVERTER }); + private static final Signature CONVERT = + TypeUtils.parseSignature("Object convert(Object, Class, Object)"); + + interface BeanCopierKey { + public Object newInstance(String source, String target, boolean useConverter); + } + + public static BeanCopier create(Class source, Class target, boolean useConverter) { + Generator gen = new Generator(); + gen.setSource(source); + gen.setTarget(target); + gen.setUseConverter(useConverter); + return gen.create(); + } + + abstract public void copy(Object from, Object to, Converter converter); + + public static class Generator extends AbstractClassGenerator { + private static final Source SOURCE = new Source(BeanCopier.class.getName()); + private Class source; + private Class target; + private boolean useConverter; + + public Generator() { + super(SOURCE); + } + + public void setSource(Class source) { + this.source = source; + // SPRING PATCH BEGIN + setContextClass(source); + setNamePrefix(source.getName()); + // SPRING PATCH END + } + + public void setTarget(Class target) { + this.target = target; // SPRING PATCH BEGIN setContextClass(target); + setNamePrefix(target.getName()); // SPRING PATCH END - } - - public void setUseConverter(boolean useConverter) { - this.useConverter = useConverter; - } - - protected ClassLoader getDefaultClassLoader() { - return source.getClassLoader(); - } - - protected ProtectionDomain getProtectionDomain() { - return ReflectUtils.getProtectionDomain(source); - } - - public BeanCopier create() { - Object key = KEY_FACTORY.newInstance(source.getName(), target.getName(), useConverter); - return (BeanCopier)super.create(key); - } - - public void generateClass(ClassVisitor v) { - Type sourceType = Type.getType(source); - Type targetType = Type.getType(target); - ClassEmitter ce = new ClassEmitter(v); - ce.begin_class(Constants.V1_8, - Constants.ACC_PUBLIC, - getClassName(), - BEAN_COPIER, - null, - Constants.SOURCE_FILE); - - EmitUtils.null_constructor(ce); - CodeEmitter e = ce.begin_method(Constants.ACC_PUBLIC, COPY, null); - PropertyDescriptor[] getters = ReflectUtils.getBeanGetters(source); - PropertyDescriptor[] setters = ReflectUtils.getBeanSetters(target); - - Map names = new HashMap(); - for (int i = 0; i < getters.length; i++) { - names.put(getters[i].getName(), getters[i]); - } - Local targetLocal = e.make_local(); - Local sourceLocal = e.make_local(); - if (useConverter) { - e.load_arg(1); - e.checkcast(targetType); - e.store_local(targetLocal); - e.load_arg(0); - e.checkcast(sourceType); - e.store_local(sourceLocal); - } else { - e.load_arg(1); - e.checkcast(targetType); - e.load_arg(0); - e.checkcast(sourceType); - } - for (int i = 0; i < setters.length; i++) { - PropertyDescriptor setter = setters[i]; - PropertyDescriptor getter = (PropertyDescriptor)names.get(setter.getName()); - if (getter != null) { - MethodInfo read = ReflectUtils.getMethodInfo(getter.getReadMethod()); - MethodInfo write = ReflectUtils.getMethodInfo(setter.getWriteMethod()); - if (useConverter) { - Type setterType = write.getSignature().getArgumentTypes()[0]; - e.load_local(targetLocal); - e.load_arg(2); - e.load_local(sourceLocal); - e.invoke(read); - e.box(read.getSignature().getReturnType()); - EmitUtils.load_class(e, setterType); - e.push(write.getSignature().getName()); - e.invoke_interface(CONVERTER, CONVERT); - e.unbox_or_zero(setterType); - e.invoke(write); - } else if (compatible(getter, setter)) { - e.dup2(); - e.invoke(read); - e.invoke(write); - } - } - } - e.return_value(); - e.end_method(); - ce.end_class(); - } - - private static boolean compatible(PropertyDescriptor getter, PropertyDescriptor setter) { - // TODO: allow automatic widening conversions? - return setter.getPropertyType().isAssignableFrom(getter.getPropertyType()); - } - - protected Object firstInstance(Class type) { - return ReflectUtils.newInstance(type); - } - - protected Object nextInstance(Object instance) { - return instance; - } - } + } + + public void setUseConverter(boolean useConverter) { + this.useConverter = useConverter; + } + + @Override + protected ClassLoader getDefaultClassLoader() { + return source.getClassLoader(); + } + + @Override + protected ProtectionDomain getProtectionDomain() { + return ReflectUtils.getProtectionDomain(source); + } + + public BeanCopier create() { + Object key = KEY_FACTORY.newInstance(source.getName(), target.getName(), useConverter); + return (BeanCopier)super.create(key); + } + + @Override + public void generateClass(ClassVisitor v) { + Type sourceType = Type.getType(source); + Type targetType = Type.getType(target); + ClassEmitter ce = new ClassEmitter(v); + ce.begin_class(Constants.V1_8, + Constants.ACC_PUBLIC, + getClassName(), + BEAN_COPIER, + null, + Constants.SOURCE_FILE); + + EmitUtils.null_constructor(ce); + CodeEmitter e = ce.begin_method(Constants.ACC_PUBLIC, COPY, null); + PropertyDescriptor[] getters = ReflectUtils.getBeanGetters(source); + PropertyDescriptor[] setters = ReflectUtils.getBeanSetters(target); + + Map names = new HashMap(); + for (PropertyDescriptor getter : getters) { + names.put(getter.getName(), getter); + } + Local targetLocal = e.make_local(); + Local sourceLocal = e.make_local(); + if (useConverter) { + e.load_arg(1); + e.checkcast(targetType); + e.store_local(targetLocal); + e.load_arg(0); + e.checkcast(sourceType); + e.store_local(sourceLocal); + } else { + e.load_arg(1); + e.checkcast(targetType); + e.load_arg(0); + e.checkcast(sourceType); + } + for (PropertyDescriptor setter : setters) { + PropertyDescriptor getter = (PropertyDescriptor)names.get(setter.getName()); + if (getter != null) { + MethodInfo read = ReflectUtils.getMethodInfo(getter.getReadMethod()); + MethodInfo write = ReflectUtils.getMethodInfo(setter.getWriteMethod()); + if (useConverter) { + Type setterType = write.getSignature().getArgumentTypes()[0]; + e.load_local(targetLocal); + e.load_arg(2); + e.load_local(sourceLocal); + e.invoke(read); + e.box(read.getSignature().getReturnType()); + EmitUtils.load_class(e, setterType); + e.push(write.getSignature().getName()); + e.invoke_interface(CONVERTER, CONVERT); + e.unbox_or_zero(setterType); + e.invoke(write); + } else if (compatible(getter, setter)) { + e.dup2(); + e.invoke(read); + e.invoke(write); + } + } + } + e.return_value(); + e.end_method(); + ce.end_class(); + } + + private static boolean compatible(PropertyDescriptor getter, PropertyDescriptor setter) { + // TODO: allow automatic widening conversions? + return setter.getPropertyType().isAssignableFrom(getter.getPropertyType()); + } + + @Override + protected Object firstInstance(Class type) { + return ReflectUtils.newInstance(type); + } + + @Override + protected Object nextInstance(Object instance) { + return instance; + } + } } diff --git a/spring-core/src/main/java/org/springframework/cglib/beans/BeanGenerator.java b/spring-core/src/main/java/org/springframework/cglib/beans/BeanGenerator.java index d4f5e7af9a0a..f303350f7dcb 100644 --- a/spring-core/src/main/java/org/springframework/cglib/beans/BeanGenerator.java +++ b/spring-core/src/main/java/org/springframework/cglib/beans/BeanGenerator.java @@ -17,10 +17,18 @@ import java.beans.PropertyDescriptor; import java.security.ProtectionDomain; -import java.util.*; -import org.springframework.cglib.core.*; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + import org.springframework.asm.ClassVisitor; import org.springframework.asm.Type; +import org.springframework.cglib.core.AbstractClassGenerator; +import org.springframework.cglib.core.ClassEmitter; +import org.springframework.cglib.core.Constants; +import org.springframework.cglib.core.EmitUtils; +import org.springframework.cglib.core.KeyFactory; +import org.springframework.cglib.core.ReflectUtils; /** * @author Juozas Baliuka, Chris Nokleberg @@ -28,126 +36,131 @@ @SuppressWarnings({"rawtypes", "unchecked"}) public class BeanGenerator extends AbstractClassGenerator { - private static final Source SOURCE = new Source(BeanGenerator.class.getName()); - private static final BeanGeneratorKey KEY_FACTORY = - (BeanGeneratorKey)KeyFactory.create(BeanGeneratorKey.class); - - interface BeanGeneratorKey { - public Object newInstance(String superclass, Map props); - } - - private Class superclass; - private Map props = new HashMap(); - private boolean classOnly; - - public BeanGenerator() { - super(SOURCE); - } - - /** - * Set the class which the generated class will extend. The class - * must not be declared as final, and must have a non-private - * no-argument constructor. - * @param superclass class to extend, or null to extend Object - */ - public void setSuperclass(Class superclass) { - if (superclass != null && superclass.equals(Object.class)) { - superclass = null; - } - this.superclass = superclass; + private static final Source SOURCE = new Source(BeanGenerator.class.getName()); + private static final BeanGeneratorKey KEY_FACTORY = + (BeanGeneratorKey)KeyFactory.create(BeanGeneratorKey.class); + + interface BeanGeneratorKey { + public Object newInstance(String superclass, Map props); + } + + private Class superclass; + private Map props = new HashMap(); + private boolean classOnly; + + public BeanGenerator() { + super(SOURCE); + } + + /** + * Set the class which the generated class will extend. The class + * must not be declared as final, and must have a non-private + * no-argument constructor. + * @param superclass class to extend, or null to extend Object + */ + public void setSuperclass(Class superclass) { + if (superclass != null && superclass.equals(Object.class)) { + superclass = null; + } + this.superclass = superclass; // SPRING PATCH BEGIN setContextClass(superclass); // SPRING PATCH END - } - - public void addProperty(String name, Class type) { - if (props.containsKey(name)) { - throw new IllegalArgumentException("Duplicate property name \"" + name + "\""); - } - props.put(name, Type.getType(type)); - } - - protected ClassLoader getDefaultClassLoader() { - if (superclass != null) { - return superclass.getClassLoader(); - } else { - return null; - } - } - - protected ProtectionDomain getProtectionDomain() { - return ReflectUtils.getProtectionDomain(superclass); - } - - public Object create() { - classOnly = false; - return createHelper(); - } - - public Object createClass() { - classOnly = true; - return createHelper(); - } - - private Object createHelper() { - if (superclass != null) { - setNamePrefix(superclass.getName()); - } - String superName = (superclass != null) ? superclass.getName() : "java.lang.Object"; - Object key = KEY_FACTORY.newInstance(superName, props); - return super.create(key); - } - - public void generateClass(ClassVisitor v) throws Exception { - int size = props.size(); - String[] names = (String[])props.keySet().toArray(new String[size]); - Type[] types = new Type[size]; - for (int i = 0; i < size; i++) { - types[i] = (Type)props.get(names[i]); - } - ClassEmitter ce = new ClassEmitter(v); - ce.begin_class(Constants.V1_8, - Constants.ACC_PUBLIC, - getClassName(), - superclass != null ? Type.getType(superclass) : Constants.TYPE_OBJECT, - null, - null); - EmitUtils.null_constructor(ce); - EmitUtils.add_properties(ce, names, types); - ce.end_class(); - } - - protected Object firstInstance(Class type) { - if (classOnly) { - return type; - } else { - return ReflectUtils.newInstance(type); - } - } - - protected Object nextInstance(Object instance) { - Class protoclass = (instance instanceof Class) ? (Class)instance : instance.getClass(); - if (classOnly) { - return protoclass; - } else { - return ReflectUtils.newInstance(protoclass); - } - } - - public static void addProperties(BeanGenerator gen, Map props) { - for (Iterator it = props.keySet().iterator(); it.hasNext();) { - String name = (String)it.next(); - gen.addProperty(name, (Class)props.get(name)); - } - } - - public static void addProperties(BeanGenerator gen, Class type) { - addProperties(gen, ReflectUtils.getBeanProperties(type)); - } - - public static void addProperties(BeanGenerator gen, PropertyDescriptor[] descriptors) { - for (int i = 0; i < descriptors.length; i++) { - gen.addProperty(descriptors[i].getName(), descriptors[i].getPropertyType()); - } - } + } + + public void addProperty(String name, Class type) { + if (props.containsKey(name)) { + throw new IllegalArgumentException("Duplicate property name \"" + name + "\""); + } + props.put(name, Type.getType(type)); + } + + @Override + protected ClassLoader getDefaultClassLoader() { + if (superclass != null) { + return superclass.getClassLoader(); + } else { + return null; + } + } + + @Override + protected ProtectionDomain getProtectionDomain() { + return ReflectUtils.getProtectionDomain(superclass); + } + + public Object create() { + classOnly = false; + return createHelper(); + } + + public Object createClass() { + classOnly = true; + return createHelper(); + } + + private Object createHelper() { + if (superclass != null) { + setNamePrefix(superclass.getName()); + } + String superName = (superclass != null) ? superclass.getName() : "java.lang.Object"; + Object key = KEY_FACTORY.newInstance(superName, props); + return super.create(key); + } + + @Override + public void generateClass(ClassVisitor v) throws Exception { + int size = props.size(); + String[] names = (String[])props.keySet().toArray(new String[size]); + Type[] types = new Type[size]; + for (int i = 0; i < size; i++) { + types[i] = (Type)props.get(names[i]); + } + ClassEmitter ce = new ClassEmitter(v); + ce.begin_class(Constants.V1_8, + Constants.ACC_PUBLIC, + getClassName(), + superclass != null ? Type.getType(superclass) : Constants.TYPE_OBJECT, + null, + null); + EmitUtils.null_constructor(ce); + EmitUtils.add_properties(ce, names, types); + ce.end_class(); + } + + @Override + protected Object firstInstance(Class type) { + if (classOnly) { + return type; + } else { + return ReflectUtils.newInstance(type); + } + } + + @Override + protected Object nextInstance(Object instance) { + Class protoclass = (instance instanceof Class) ? (Class)instance : instance.getClass(); + if (classOnly) { + return protoclass; + } else { + return ReflectUtils.newInstance(protoclass); + } + } + + public static void addProperties(BeanGenerator gen, Map props) { + for (Iterator it = props.keySet().iterator(); it.hasNext();) { + String name = (String)it.next(); + gen.addProperty(name, (Class)props.get(name)); + } + } + + public static void addProperties(BeanGenerator gen, Class type) { + addProperties(gen, ReflectUtils.getBeanProperties(type)); + } + + public static void addProperties(BeanGenerator gen, PropertyDescriptor[] descriptors) { + for (PropertyDescriptor descriptor : descriptors) { + gen.addProperty(descriptor.getName(), descriptor.getPropertyType()); + } + } } diff --git a/spring-core/src/main/java/org/springframework/cglib/beans/BeanMap.java b/spring-core/src/main/java/org/springframework/cglib/beans/BeanMap.java index f73e7e34fb39..9a7dbf68b584 100644 --- a/spring-core/src/main/java/org/springframework/cglib/beans/BeanMap.java +++ b/spring-core/src/main/java/org/springframework/cglib/beans/BeanMap.java @@ -41,293 +41,310 @@ */ @SuppressWarnings({"rawtypes", "unchecked"}) abstract public class BeanMap implements Map { - /** - * Limit the properties reflected in the key set of the map - * to readable properties. - * @see BeanMap.Generator#setRequire - */ - public static final int REQUIRE_GETTER = 1; - - /** - * Limit the properties reflected in the key set of the map - * to writable properties. - * @see BeanMap.Generator#setRequire - */ - public static final int REQUIRE_SETTER = 2; - - /** - * Helper method to create a new BeanMap. For finer - * control over the generated instance, use a new instance of - * BeanMap.Generator instead of this static method. - * @param bean the JavaBean underlying the map - * @return a new BeanMap instance - */ - public static BeanMap create(Object bean) { - Generator gen = new Generator(); - gen.setBean(bean); - return gen.create(); - } - - public static class Generator extends AbstractClassGenerator { - private static final Source SOURCE = new Source(BeanMap.class.getName()); - - private static final BeanMapKey KEY_FACTORY = - (BeanMapKey)KeyFactory.create(BeanMapKey.class, KeyFactory.CLASS_BY_NAME); - - interface BeanMapKey { - public Object newInstance(Class type, int require); - } - - private Object bean; - private Class beanClass; - private int require; - - public Generator() { - super(SOURCE); - } - - /** - * Set the bean that the generated map should reflect. The bean may be swapped - * out for another bean of the same type using {@link #setBean}. - * Calling this method overrides any value previously set using {@link #setBeanClass}. - * You must call either this method or {@link #setBeanClass} before {@link #create}. - * @param bean the initial bean - */ - public void setBean(Object bean) { - this.bean = bean; - if (bean != null) { + /** + * Limit the properties reflected in the key set of the map + * to readable properties. + * @see BeanMap.Generator#setRequire + */ + public static final int REQUIRE_GETTER = 1; + + /** + * Limit the properties reflected in the key set of the map + * to writable properties. + * @see BeanMap.Generator#setRequire + */ + public static final int REQUIRE_SETTER = 2; + + /** + * Helper method to create a new BeanMap. For finer + * control over the generated instance, use a new instance of + * BeanMap.Generator instead of this static method. + * @param bean the JavaBean underlying the map + * @return a new BeanMap instance + */ + public static BeanMap create(Object bean) { + Generator gen = new Generator(); + gen.setBean(bean); + return gen.create(); + } + + public static class Generator extends AbstractClassGenerator { + private static final Source SOURCE = new Source(BeanMap.class.getName()); + + private static final BeanMapKey KEY_FACTORY = + (BeanMapKey)KeyFactory.create(BeanMapKey.class, KeyFactory.CLASS_BY_NAME); + + interface BeanMapKey { + public Object newInstance(Class type, int require); + } + + private Object bean; + private Class beanClass; + private int require; + + public Generator() { + super(SOURCE); + } + + /** + * Set the bean that the generated map should reflect. The bean may be swapped + * out for another bean of the same type using {@link #setBean}. + * Calling this method overrides any value previously set using {@link #setBeanClass}. + * You must call either this method or {@link #setBeanClass} before {@link #create}. + * @param bean the initial bean + */ + public void setBean(Object bean) { + this.bean = bean; + if (bean != null) { beanClass = bean.getClass(); // SPRING PATCH BEGIN setContextClass(beanClass); // SPRING PATCH END } - } - - /** - * Set the class of the bean that the generated map should support. - * You must call either this method or {@link #setBeanClass} before {@link #create}. - * @param beanClass the class of the bean - */ - public void setBeanClass(Class beanClass) { - this.beanClass = beanClass; - } - - /** - * Limit the properties reflected by the generated map. - * @param require any combination of {@link #REQUIRE_GETTER} and - * {@link #REQUIRE_SETTER}; default is zero (any property allowed) - */ - public void setRequire(int require) { - this.require = require; - } - - protected ClassLoader getDefaultClassLoader() { - return beanClass.getClassLoader(); - } - - protected ProtectionDomain getProtectionDomain() { - return ReflectUtils.getProtectionDomain(beanClass); - } - - /** - * Create a new instance of the BeanMap. An existing - * generated class will be reused if possible. - */ - public BeanMap create() { - if (beanClass == null) - throw new IllegalArgumentException("Class of bean unknown"); - setNamePrefix(beanClass.getName()); - return (BeanMap)super.create(KEY_FACTORY.newInstance(beanClass, require)); - } - - public void generateClass(ClassVisitor v) throws Exception { - new BeanMapEmitter(v, getClassName(), beanClass, require); - } - - protected Object firstInstance(Class type) { - return ((BeanMap)ReflectUtils.newInstance(type)).newInstance(bean); - } - - protected Object nextInstance(Object instance) { - return ((BeanMap)instance).newInstance(bean); - } - } - - /** - * Create a new BeanMap instance using the specified bean. - * This is faster than using the {@link #create} static method. - * @param bean the JavaBean underlying the map - * @return a new BeanMap instance - */ - abstract public BeanMap newInstance(Object bean); - - /** - * Get the type of a property. - * @param name the name of the JavaBean property - * @return the type of the property, or null if the property does not exist - */ - abstract public Class getPropertyType(String name); - - protected Object bean; - - protected BeanMap() { - } - - protected BeanMap(Object bean) { - setBean(bean); - } - - public Object get(Object key) { - return get(bean, key); - } - - public Object put(Object key, Object value) { - return put(bean, key, value); - } - - /** - * Get the property of a bean. This allows a BeanMap - * to be used statically for multiple beans--the bean instance tied to the - * map is ignored and the bean passed to this method is used instead. - * @param bean the bean to query; must be compatible with the type of - * this BeanMap - * @param key must be a String - * @return the current value, or null if there is no matching property - */ - abstract public Object get(Object bean, Object key); - - /** - * Set the property of a bean. This allows a BeanMap - * to be used statically for multiple beans--the bean instance tied to the - * map is ignored and the bean passed to this method is used instead. - * @param key must be a String - * @return the old value, if there was one, or null - */ - abstract public Object put(Object bean, Object key, Object value); - - /** - * Change the underlying bean this map should use. - * @param bean the new JavaBean - * @see #getBean - */ - public void setBean(Object bean) { - this.bean = bean; - } - - /** - * Return the bean currently in use by this map. - * @return the current JavaBean - * @see #setBean - */ - public Object getBean() { - return bean; - } - - public void clear() { - throw new UnsupportedOperationException(); - } - - public boolean containsKey(Object key) { - return keySet().contains(key); - } - - public boolean containsValue(Object value) { - for (Iterator it = keySet().iterator(); it.hasNext();) { - Object v = get(it.next()); - if (((value == null) && (v == null)) || (value != null && value.equals(v))) - return true; - } - return false; - } - - public int size() { - return keySet().size(); - } - - public boolean isEmpty() { - return size() == 0; - } - - public Object remove(Object key) { - throw new UnsupportedOperationException(); - } - - public void putAll(Map t) { - for (Iterator it = t.keySet().iterator(); it.hasNext();) { - Object key = it.next(); - put(key, t.get(key)); - } - } - - public boolean equals(Object o) { - if (o == null || !(o instanceof Map)) { - return false; - } - Map other = (Map)o; - if (size() != other.size()) { - return false; - } - for (Iterator it = keySet().iterator(); it.hasNext();) { - Object key = it.next(); - if (!other.containsKey(key)) { - return false; - } - Object v1 = get(key); - Object v2 = other.get(key); - if (!((v1 == null) ? v2 == null : v1.equals(v2))) { - return false; - } - } - return true; - } - - public int hashCode() { - int code = 0; - for (Iterator it = keySet().iterator(); it.hasNext();) { - Object key = it.next(); - Object value = get(key); - code += ((key == null) ? 0 : key.hashCode()) ^ - ((value == null) ? 0 : value.hashCode()); - } - return code; - } - - // TODO: optimize - public Set entrySet() { - HashMap copy = new HashMap(); - for (Iterator it = keySet().iterator(); it.hasNext();) { - Object key = it.next(); - copy.put(key, get(key)); - } - return Collections.unmodifiableMap(copy).entrySet(); - } - - public Collection values() { - Set keys = keySet(); - List values = new ArrayList(keys.size()); - for (Iterator it = keys.iterator(); it.hasNext();) { - values.add(get(it.next())); - } - return Collections.unmodifiableCollection(values); - } - - /* - * @see java.util.AbstractMap#toString - */ - public String toString() - { - StringBuffer sb = new StringBuffer(); - sb.append('{'); - for (Iterator it = keySet().iterator(); it.hasNext();) { - Object key = it.next(); - sb.append(key); - sb.append('='); - sb.append(get(key)); - if (it.hasNext()) { - sb.append(", "); - } - } - sb.append('}'); - return sb.toString(); - } + } + + /** + * Set the class of the bean that the generated map should support. + * You must call either this method or {@link #setBeanClass} before {@link #create}. + * @param beanClass the class of the bean + */ + public void setBeanClass(Class beanClass) { + this.beanClass = beanClass; + } + + /** + * Limit the properties reflected by the generated map. + * @param require any combination of {@link #REQUIRE_GETTER} and + * {@link #REQUIRE_SETTER}; default is zero (any property allowed) + */ + public void setRequire(int require) { + this.require = require; + } + + @Override + protected ClassLoader getDefaultClassLoader() { + return beanClass.getClassLoader(); + } + + @Override + protected ProtectionDomain getProtectionDomain() { + return ReflectUtils.getProtectionDomain(beanClass); + } + + /** + * Create a new instance of the BeanMap. An existing + * generated class will be reused if possible. + */ + public BeanMap create() { + if (beanClass == null) { + throw new IllegalArgumentException("Class of bean unknown"); + } + setNamePrefix(beanClass.getName()); + return (BeanMap)super.create(KEY_FACTORY.newInstance(beanClass, require)); + } + + @Override + public void generateClass(ClassVisitor v) throws Exception { + new BeanMapEmitter(v, getClassName(), beanClass, require); + } + + @Override + protected Object firstInstance(Class type) { + return ((BeanMap)ReflectUtils.newInstance(type)).newInstance(bean); + } + + @Override + protected Object nextInstance(Object instance) { + return ((BeanMap)instance).newInstance(bean); + } + } + + /** + * Create a new BeanMap instance using the specified bean. + * This is faster than using the {@link #create} static method. + * @param bean the JavaBean underlying the map + * @return a new BeanMap instance + */ + abstract public BeanMap newInstance(Object bean); + + /** + * Get the type of a property. + * @param name the name of the JavaBean property + * @return the type of the property, or null if the property does not exist + */ + abstract public Class getPropertyType(String name); + + protected Object bean; + + protected BeanMap() { + } + + protected BeanMap(Object bean) { + setBean(bean); + } + + @Override + public Object get(Object key) { + return get(bean, key); + } + + @Override + public Object put(Object key, Object value) { + return put(bean, key, value); + } + + /** + * Get the property of a bean. This allows a BeanMap + * to be used statically for multiple beans--the bean instance tied to the + * map is ignored and the bean passed to this method is used instead. + * @param bean the bean to query; must be compatible with the type of + * this BeanMap + * @param key must be a String + * @return the current value, or null if there is no matching property + */ + abstract public Object get(Object bean, Object key); + + /** + * Set the property of a bean. This allows a BeanMap + * to be used statically for multiple beans--the bean instance tied to the + * map is ignored and the bean passed to this method is used instead. + * @param key must be a String + * @return the old value, if there was one, or null + */ + abstract public Object put(Object bean, Object key, Object value); + + /** + * Change the underlying bean this map should use. + * @param bean the new JavaBean + * @see #getBean + */ + public void setBean(Object bean) { + this.bean = bean; + } + + /** + * Return the bean currently in use by this map. + * @return the current JavaBean + * @see #setBean + */ + public Object getBean() { + return bean; + } + + @Override + public void clear() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean containsKey(Object key) { + return keySet().contains(key); + } + + @Override + public boolean containsValue(Object value) { + for (Iterator it = keySet().iterator(); it.hasNext();) { + Object v = get(it.next()); + if (((value == null) && (v == null)) || (value != null && value.equals(v))) { + return true; + } + } + return false; + } + + @Override + public int size() { + return keySet().size(); + } + + @Override + public boolean isEmpty() { + return size() == 0; + } + + @Override + public Object remove(Object key) { + throw new UnsupportedOperationException(); + } + + @Override + public void putAll(Map t) { + for (Object key : t.keySet()) { + put(key, t.get(key)); + } + } + + @Override + public boolean equals(Object o) { + if (o == null || !(o instanceof Map)) { + return false; + } + Map other = (Map)o; + if (size() != other.size()) { + return false; + } + for (Object key : keySet()) { + if (!other.containsKey(key)) { + return false; + } + Object v1 = get(key); + Object v2 = other.get(key); + if (!((v1 == null) ? v2 == null : v1.equals(v2))) { + return false; + } + } + return true; + } + + @Override + public int hashCode() { + int code = 0; + for (Object key : keySet()) { + Object value = get(key); + code += ((key == null) ? 0 : key.hashCode()) ^ + ((value == null) ? 0 : value.hashCode()); + } + return code; + } + + // TODO: optimize + @Override + public Set entrySet() { + HashMap copy = new HashMap(); + for (Object key : keySet()) { + copy.put(key, get(key)); + } + return Collections.unmodifiableMap(copy).entrySet(); + } + + @Override + public Collection values() { + Set keys = keySet(); + List values = new ArrayList(keys.size()); + for (Iterator it = keys.iterator(); it.hasNext();) { + values.add(get(it.next())); + } + return Collections.unmodifiableCollection(values); + } + + /* + * @see java.util.AbstractMap#toString + */ + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + sb.append('{'); + for (Iterator it = keySet().iterator(); it.hasNext();) { + Object key = it.next(); + sb.append(key); + sb.append('='); + sb.append(get(key)); + if (it.hasNext()) { + sb.append(", "); + } + } + sb.append('}'); + return sb.toString(); + } } diff --git a/spring-core/src/main/java/org/springframework/cglib/beans/BeanMapEmitter.java b/spring-core/src/main/java/org/springframework/cglib/beans/BeanMapEmitter.java index 986aa02fd618..b1e3596f8ff5 100644 --- a/spring-core/src/main/java/org/springframework/cglib/beans/BeanMapEmitter.java +++ b/spring-core/src/main/java/org/springframework/cglib/beans/BeanMapEmitter.java @@ -15,12 +15,23 @@ */ package org.springframework.cglib.beans; -import java.beans.*; -import java.util.*; -import org.springframework.cglib.core.*; +import java.beans.PropertyDescriptor; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + import org.springframework.asm.ClassVisitor; import org.springframework.asm.Label; import org.springframework.asm.Type; +import org.springframework.cglib.core.ClassEmitter; +import org.springframework.cglib.core.CodeEmitter; +import org.springframework.cglib.core.Constants; +import org.springframework.cglib.core.EmitUtils; +import org.springframework.cglib.core.MethodInfo; +import org.springframework.cglib.core.ObjectSwitchCallback; +import org.springframework.cglib.core.ReflectUtils; +import org.springframework.cglib.core.Signature; +import org.springframework.cglib.core.TypeUtils; @SuppressWarnings({"rawtypes", "unchecked"}) class BeanMapEmitter extends ClassEmitter { @@ -50,7 +61,7 @@ public BeanMapEmitter(ClassVisitor v, String className, Class type, int require) EmitUtils.null_constructor(this); EmitUtils.factory_method(this, NEW_INSTANCE); generateConstructor(); - + Map getters = makePropertyMap(ReflectUtils.getBeanGetters(type)); Map setters = makePropertyMap(ReflectUtils.getBeanSetters(type)); Map allProps = new HashMap(); @@ -79,8 +90,8 @@ public BeanMapEmitter(ClassVisitor v, String className, Class type, int require) private Map makePropertyMap(PropertyDescriptor[] props) { Map names = new HashMap(); - for (int i = 0; i < props.length; i++) { - names.put(props[i].getName(), props[i]); + for (PropertyDescriptor prop : props) { + names.put(prop.getName(), prop); } return names; } @@ -97,7 +108,7 @@ private void generateConstructor() { e.return_value(); e.end_method(); } - + private void generateGet(Class type, final Map getters) { final CodeEmitter e = begin_method(Constants.ACC_PUBLIC, BEAN_MAP_GET, null); e.load_arg(0); @@ -105,14 +116,16 @@ private void generateGet(Class type, final Map getters) { e.load_arg(1); e.checkcast(Constants.TYPE_STRING); EmitUtils.string_switch(e, getNames(getters), Constants.SWITCH_STYLE_HASH, new ObjectSwitchCallback() { - public void processCase(Object key, Label end) { + @Override + public void processCase(Object key, Label end) { PropertyDescriptor pd = (PropertyDescriptor)getters.get(key); MethodInfo method = ReflectUtils.getMethodInfo(pd.getReadMethod()); e.invoke(method); e.box(method.getSignature().getReturnType()); e.return_value(); } - public void processDefault() { + @Override + public void processDefault() { e.aconst_null(); e.return_value(); } @@ -127,7 +140,8 @@ private void generatePut(Class type, final Map setters) { e.load_arg(1); e.checkcast(Constants.TYPE_STRING); EmitUtils.string_switch(e, getNames(setters), Constants.SWITCH_STYLE_HASH, new ObjectSwitchCallback() { - public void processCase(Object key, Label end) { + @Override + public void processCase(Object key, Label end) { PropertyDescriptor pd = (PropertyDescriptor)setters.get(key); if (pd.getReadMethod() == null) { e.aconst_null(); @@ -144,7 +158,8 @@ public void processCase(Object key, Label end) { e.invoke(write); e.return_value(); } - public void processDefault() { + @Override + public void processDefault() { // fall-through } }); @@ -152,7 +167,7 @@ public void processDefault() { e.return_value(); e.end_method(); } - + private void generateKeySet(String[] allNames) { // static initializer declare_field(Constants.ACC_STATIC | Constants.ACC_PRIVATE, "keys", FIXED_KEY_SET, null); @@ -178,12 +193,14 @@ private void generateGetPropertyType(final Map allProps, String[] allNames) { final CodeEmitter e = begin_method(Constants.ACC_PUBLIC, GET_PROPERTY_TYPE, null); e.load_arg(0); EmitUtils.string_switch(e, allNames, Constants.SWITCH_STYLE_HASH, new ObjectSwitchCallback() { - public void processCase(Object key, Label end) { + @Override + public void processCase(Object key, Label end) { PropertyDescriptor pd = (PropertyDescriptor)allProps.get(key); EmitUtils.load_class(e, Type.getType(pd.getPropertyType())); e.return_value(); } - public void processDefault() { + @Override + public void processDefault() { e.aconst_null(); e.return_value(); } diff --git a/spring-core/src/main/java/org/springframework/cglib/beans/BulkBean.java b/spring-core/src/main/java/org/springframework/cglib/beans/BulkBean.java index 7fdd702dbdce..61ae4f076ac9 100644 --- a/spring-core/src/main/java/org/springframework/cglib/beans/BulkBean.java +++ b/spring-core/src/main/java/org/springframework/cglib/beans/BulkBean.java @@ -16,8 +16,11 @@ package org.springframework.cglib.beans; import java.security.ProtectionDomain; -import org.springframework.cglib.core.*; + import org.springframework.asm.ClassVisitor; +import org.springframework.cglib.core.AbstractClassGenerator; +import org.springframework.cglib.core.KeyFactory; +import org.springframework.cglib.core.ReflectUtils; /** * @author Juozas Baliuka @@ -25,118 +28,123 @@ @SuppressWarnings({"rawtypes", "unchecked"}) abstract public class BulkBean { - private static final BulkBeanKey KEY_FACTORY = - (BulkBeanKey)KeyFactory.create(BulkBeanKey.class); - - interface BulkBeanKey { - public Object newInstance(String target, String[] getters, String[] setters, String[] types); - } - - protected Class target; - protected String[] getters, setters; - protected Class[] types; - - protected BulkBean() { } - - abstract public void getPropertyValues(Object bean, Object[] values); - abstract public void setPropertyValues(Object bean, Object[] values); - - public Object[] getPropertyValues(Object bean) { - Object[] values = new Object[getters.length]; - getPropertyValues(bean, values); - return values; - } - - public Class[] getPropertyTypes() { - return types.clone(); - } - - public String[] getGetters() { - return getters.clone(); - } - - public String[] getSetters() { - return setters.clone(); - } - - public static BulkBean create(Class target, String[] getters, String[] setters, Class[] types) { - Generator gen = new Generator(); - gen.setTarget(target); - gen.setGetters(getters); - gen.setSetters(setters); - gen.setTypes(types); - return gen.create(); - } - - public static class Generator extends AbstractClassGenerator { - private static final Source SOURCE = new Source(BulkBean.class.getName()); - private Class target; - private String[] getters; - private String[] setters; - private Class[] types; - - public Generator() { - super(SOURCE); - } - - public void setTarget(Class target) { - this.target = target; + private static final BulkBeanKey KEY_FACTORY = + (BulkBeanKey)KeyFactory.create(BulkBeanKey.class); + + interface BulkBeanKey { + public Object newInstance(String target, String[] getters, String[] setters, String[] types); + } + + protected Class target; + protected String[] getters, setters; + protected Class[] types; + + protected BulkBean() { } + + abstract public void getPropertyValues(Object bean, Object[] values); + abstract public void setPropertyValues(Object bean, Object[] values); + + public Object[] getPropertyValues(Object bean) { + Object[] values = new Object[getters.length]; + getPropertyValues(bean, values); + return values; + } + + public Class[] getPropertyTypes() { + return types.clone(); + } + + public String[] getGetters() { + return getters.clone(); + } + + public String[] getSetters() { + return setters.clone(); + } + + public static BulkBean create(Class target, String[] getters, String[] setters, Class[] types) { + Generator gen = new Generator(); + gen.setTarget(target); + gen.setGetters(getters); + gen.setSetters(setters); + gen.setTypes(types); + return gen.create(); + } + + public static class Generator extends AbstractClassGenerator { + private static final Source SOURCE = new Source(BulkBean.class.getName()); + private Class target; + private String[] getters; + private String[] setters; + private Class[] types; + + public Generator() { + super(SOURCE); + } + + public void setTarget(Class target) { + this.target = target; // SPRING PATCH BEGIN setContextClass(target); // SPRING PATCH END - } - - public void setGetters(String[] getters) { - this.getters = getters; - } - - public void setSetters(String[] setters) { - this.setters = setters; - } - - public void setTypes(Class[] types) { - this.types = types; - } - - protected ClassLoader getDefaultClassLoader() { - return target.getClassLoader(); - } - - protected ProtectionDomain getProtectionDomain() { - return ReflectUtils.getProtectionDomain(target); - } - - public BulkBean create() { - setNamePrefix(target.getName()); - String targetClassName = target.getName(); - String[] typeClassNames = ReflectUtils.getNames(types); - Object key = KEY_FACTORY.newInstance(targetClassName, getters, setters, typeClassNames); - return (BulkBean)super.create(key); - } - - public void generateClass(ClassVisitor v) throws Exception { - new BulkBeanEmitter(v, getClassName(), target, getters, setters, types); - } - - protected Object firstInstance(Class type) { - BulkBean instance = (BulkBean)ReflectUtils.newInstance(type); - instance.target = target; - - int length = getters.length; - instance.getters = new String[length]; - System.arraycopy(getters, 0, instance.getters, 0, length); - - instance.setters = new String[length]; - System.arraycopy(setters, 0, instance.setters, 0, length); - - instance.types = new Class[types.length]; - System.arraycopy(types, 0, instance.types, 0, types.length); - - return instance; - } - - protected Object nextInstance(Object instance) { - return instance; - } - } + } + + public void setGetters(String[] getters) { + this.getters = getters; + } + + public void setSetters(String[] setters) { + this.setters = setters; + } + + public void setTypes(Class[] types) { + this.types = types; + } + + @Override + protected ClassLoader getDefaultClassLoader() { + return target.getClassLoader(); + } + + @Override + protected ProtectionDomain getProtectionDomain() { + return ReflectUtils.getProtectionDomain(target); + } + + public BulkBean create() { + setNamePrefix(target.getName()); + String targetClassName = target.getName(); + String[] typeClassNames = ReflectUtils.getNames(types); + Object key = KEY_FACTORY.newInstance(targetClassName, getters, setters, typeClassNames); + return (BulkBean)super.create(key); + } + + @Override + public void generateClass(ClassVisitor v) throws Exception { + new BulkBeanEmitter(v, getClassName(), target, getters, setters, types); + } + + @Override + protected Object firstInstance(Class type) { + BulkBean instance = (BulkBean)ReflectUtils.newInstance(type); + instance.target = target; + + int length = getters.length; + instance.getters = new String[length]; + System.arraycopy(getters, 0, instance.getters, 0, length); + + instance.setters = new String[length]; + System.arraycopy(setters, 0, instance.setters, 0, length); + + instance.types = new Class[types.length]; + System.arraycopy(types, 0, instance.types, 0, types.length); + + return instance; + } + + @Override + protected Object nextInstance(Object instance) { + return instance; + } + } } diff --git a/spring-core/src/main/java/org/springframework/cglib/beans/BulkBeanEmitter.java b/spring-core/src/main/java/org/springframework/cglib/beans/BulkBeanEmitter.java index 9502c3fa88a2..8a6198f4c519 100644 --- a/spring-core/src/main/java/org/springframework/cglib/beans/BulkBeanEmitter.java +++ b/spring-core/src/main/java/org/springframework/cglib/beans/BulkBeanEmitter.java @@ -17,9 +17,19 @@ import java.lang.reflect.Method; import java.lang.reflect.Modifier; -import org.springframework.cglib.core.*; + import org.springframework.asm.ClassVisitor; import org.springframework.asm.Type; +import org.springframework.cglib.core.Block; +import org.springframework.cglib.core.ClassEmitter; +import org.springframework.cglib.core.CodeEmitter; +import org.springframework.cglib.core.Constants; +import org.springframework.cglib.core.EmitUtils; +import org.springframework.cglib.core.Local; +import org.springframework.cglib.core.MethodInfo; +import org.springframework.cglib.core.ReflectUtils; +import org.springframework.cglib.core.Signature; +import org.springframework.cglib.core.TypeUtils; @SuppressWarnings({"rawtypes", "unchecked"}) class BulkBeanEmitter extends ClassEmitter { @@ -33,7 +43,7 @@ class BulkBeanEmitter extends ClassEmitter { TypeUtils.parseType("org.springframework.cglib.beans.BulkBean"); private static final Type BULK_BEAN_EXCEPTION = TypeUtils.parseType("org.springframework.cglib.beans.BulkBeanException"); - + public BulkBeanEmitter(ClassVisitor v, String className, Class target, @@ -116,7 +126,7 @@ private void generateSet(final Class target, final Method[] setters) { } e.end_method(); } - + private static void validate(Class target, String[] getters, String[] setters, diff --git a/spring-core/src/main/java/org/springframework/cglib/beans/BulkBeanException.java b/spring-core/src/main/java/org/springframework/cglib/beans/BulkBeanException.java index 20887f93832b..7325fdebee93 100644 --- a/spring-core/src/main/java/org/springframework/cglib/beans/BulkBeanException.java +++ b/spring-core/src/main/java/org/springframework/cglib/beans/BulkBeanException.java @@ -20,7 +20,7 @@ public class BulkBeanException extends RuntimeException { private int index; private Throwable cause; - + public BulkBeanException(String message, int index) { super(message); this.index = index; @@ -35,8 +35,9 @@ public BulkBeanException(Throwable cause, int index) { public int getIndex() { return index; } - - public Throwable getCause() { + + @Override + public Throwable getCause() { return cause; } } diff --git a/spring-core/src/main/java/org/springframework/cglib/beans/FixedKeySet.java b/spring-core/src/main/java/org/springframework/cglib/beans/FixedKeySet.java index 399da30ee5b9..92b32efe8cb5 100644 --- a/spring-core/src/main/java/org/springframework/cglib/beans/FixedKeySet.java +++ b/spring-core/src/main/java/org/springframework/cglib/beans/FixedKeySet.java @@ -15,7 +15,12 @@ */ package org.springframework.cglib.beans; -import java.util.*; +import java.util.AbstractSet; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; @SuppressWarnings({"rawtypes", "unchecked"}) public /* need it for class loading */ class FixedKeySet extends AbstractSet { @@ -27,11 +32,13 @@ public FixedKeySet(String[] keys) { set = Collections.unmodifiableSet(new HashSet(Arrays.asList(keys))); } - public Iterator iterator() { + @Override + public Iterator iterator() { return set.iterator(); } - public int size() { + @Override + public int size() { return size; } } diff --git a/spring-core/src/main/java/org/springframework/cglib/beans/ImmutableBean.java b/spring-core/src/main/java/org/springframework/cglib/beans/ImmutableBean.java index dfc03594228a..9c023394bd3a 100644 --- a/spring-core/src/main/java/org/springframework/cglib/beans/ImmutableBean.java +++ b/spring-core/src/main/java/org/springframework/cglib/beans/ImmutableBean.java @@ -18,115 +18,129 @@ import java.beans.PropertyDescriptor; import java.lang.reflect.Method; import java.security.ProtectionDomain; -import org.springframework.cglib.core.*; + import org.springframework.asm.ClassVisitor; import org.springframework.asm.Type; +import org.springframework.cglib.core.AbstractClassGenerator; +import org.springframework.cglib.core.ClassEmitter; +import org.springframework.cglib.core.CodeEmitter; +import org.springframework.cglib.core.Constants; +import org.springframework.cglib.core.EmitUtils; +import org.springframework.cglib.core.MethodInfo; +import org.springframework.cglib.core.ReflectUtils; +import org.springframework.cglib.core.Signature; +import org.springframework.cglib.core.TypeUtils; /** * @author Chris Nokleberg */ @SuppressWarnings({"rawtypes", "unchecked"}) public class ImmutableBean { - private static final Type ILLEGAL_STATE_EXCEPTION = - TypeUtils.parseType("IllegalStateException"); - private static final Signature CSTRUCT_OBJECT = - TypeUtils.parseConstructor("Object"); - private static final Class[] OBJECT_CLASSES = { Object.class }; - private static final String FIELD_NAME = "CGLIB$RWBean"; - - private ImmutableBean() { - } - - public static Object create(Object bean) { - Generator gen = new Generator(); - gen.setBean(bean); - return gen.create(); - } - - public static class Generator extends AbstractClassGenerator { - private static final Source SOURCE = new Source(ImmutableBean.class.getName()); - private Object bean; - private Class target; - - public Generator() { - super(SOURCE); - } - - public void setBean(Object bean) { - this.bean = bean; - target = bean.getClass(); + private static final Type ILLEGAL_STATE_EXCEPTION = + TypeUtils.parseType("IllegalStateException"); + private static final Signature CSTRUCT_OBJECT = + TypeUtils.parseConstructor("Object"); + private static final Class[] OBJECT_CLASSES = { Object.class }; + private static final String FIELD_NAME = "CGLIB$RWBean"; + + private ImmutableBean() { + } + + public static Object create(Object bean) { + Generator gen = new Generator(); + gen.setBean(bean); + return gen.create(); + } + + public static class Generator extends AbstractClassGenerator { + private static final Source SOURCE = new Source(ImmutableBean.class.getName()); + private Object bean; + private Class target; + + public Generator() { + super(SOURCE); + } + + public void setBean(Object bean) { + this.bean = bean; + target = bean.getClass(); // SPRING PATCH BEGIN setContextClass(target); // SPRING PATCH END - } - - protected ClassLoader getDefaultClassLoader() { - return target.getClassLoader(); - } - - protected ProtectionDomain getProtectionDomain() { - return ReflectUtils.getProtectionDomain(target); - } - - public Object create() { - String name = target.getName(); - setNamePrefix(name); - return super.create(name); - } - - public void generateClass(ClassVisitor v) { - Type targetType = Type.getType(target); - ClassEmitter ce = new ClassEmitter(v); - ce.begin_class(Constants.V1_8, - Constants.ACC_PUBLIC, - getClassName(), - targetType, - null, - Constants.SOURCE_FILE); - - ce.declare_field(Constants.ACC_FINAL | Constants.ACC_PRIVATE, FIELD_NAME, targetType, null); - - CodeEmitter e = ce.begin_method(Constants.ACC_PUBLIC, CSTRUCT_OBJECT, null); - e.load_this(); - e.super_invoke_constructor(); - e.load_this(); - e.load_arg(0); - e.checkcast(targetType); - e.putfield(FIELD_NAME); - e.return_value(); - e.end_method(); - - PropertyDescriptor[] descriptors = ReflectUtils.getBeanProperties(target); - Method[] getters = ReflectUtils.getPropertyMethods(descriptors, true, false); - Method[] setters = ReflectUtils.getPropertyMethods(descriptors, false, true); - - for (int i = 0; i < getters.length; i++) { - MethodInfo getter = ReflectUtils.getMethodInfo(getters[i]); - e = EmitUtils.begin_method(ce, getter, Constants.ACC_PUBLIC); - e.load_this(); - e.getfield(FIELD_NAME); - e.invoke(getter); - e.return_value(); - e.end_method(); - } - - for (int i = 0; i < setters.length; i++) { - MethodInfo setter = ReflectUtils.getMethodInfo(setters[i]); - e = EmitUtils.begin_method(ce, setter, Constants.ACC_PUBLIC); - e.throw_exception(ILLEGAL_STATE_EXCEPTION, "Bean is immutable"); - e.end_method(); - } - - ce.end_class(); - } - - protected Object firstInstance(Class type) { - return ReflectUtils.newInstance(type, OBJECT_CLASSES, new Object[]{ bean }); - } - - // TODO: optimize - protected Object nextInstance(Object instance) { - return firstInstance(instance.getClass()); - } - } + } + + @Override + protected ClassLoader getDefaultClassLoader() { + return target.getClassLoader(); + } + + @Override + protected ProtectionDomain getProtectionDomain() { + return ReflectUtils.getProtectionDomain(target); + } + + public Object create() { + String name = target.getName(); + setNamePrefix(name); + return super.create(name); + } + + @Override + public void generateClass(ClassVisitor v) { + Type targetType = Type.getType(target); + ClassEmitter ce = new ClassEmitter(v); + ce.begin_class(Constants.V1_8, + Constants.ACC_PUBLIC, + getClassName(), + targetType, + null, + Constants.SOURCE_FILE); + + ce.declare_field(Constants.ACC_FINAL | Constants.ACC_PRIVATE, FIELD_NAME, targetType, null); + + CodeEmitter e = ce.begin_method(Constants.ACC_PUBLIC, CSTRUCT_OBJECT, null); + e.load_this(); + e.super_invoke_constructor(); + e.load_this(); + e.load_arg(0); + e.checkcast(targetType); + e.putfield(FIELD_NAME); + e.return_value(); + e.end_method(); + + PropertyDescriptor[] descriptors = ReflectUtils.getBeanProperties(target); + Method[] getters = ReflectUtils.getPropertyMethods(descriptors, true, false); + Method[] setters = ReflectUtils.getPropertyMethods(descriptors, false, true); + + for (Method getter2 : getters) { + MethodInfo getter = ReflectUtils.getMethodInfo(getter2); + e = EmitUtils.begin_method(ce, getter, Constants.ACC_PUBLIC); + e.load_this(); + e.getfield(FIELD_NAME); + e.invoke(getter); + e.return_value(); + e.end_method(); + } + + for (Method setter2 : setters) { + MethodInfo setter = ReflectUtils.getMethodInfo(setter2); + e = EmitUtils.begin_method(ce, setter, Constants.ACC_PUBLIC); + e.throw_exception(ILLEGAL_STATE_EXCEPTION, "Bean is immutable"); + e.end_method(); + } + + ce.end_class(); + } + + @Override + protected Object firstInstance(Class type) { + return ReflectUtils.newInstance(type, OBJECT_CLASSES, new Object[]{ bean }); + } + + // TODO: optimize + @Override + protected Object nextInstance(Object instance) { + return firstInstance(instance.getClass()); + } + } } diff --git a/spring-core/src/main/java/org/springframework/cglib/core/ReflectUtils.java b/spring-core/src/main/java/org/springframework/cglib/core/ReflectUtils.java index 9467b9355077..cbf9ae8b7624 100644 --- a/spring-core/src/main/java/org/springframework/cglib/core/ReflectUtils.java +++ b/spring-core/src/main/java/org/springframework/cglib/core/ReflectUtils.java @@ -576,15 +576,17 @@ public static Class defineClass(String className, byte[] b, ClassLoader loader, c = (Class) lookupDefineClassMethod.invoke(lookup, b); } catch (InvocationTargetException ex) { - throw new CodeGenerationException(ex.getTargetException()); - } - catch (IllegalAccessException ex) { - throw new CodeGenerationException(ex) { + Throwable target = ex.getTargetException(); + if (target.getClass() != LinkageError.class && target.getClass() != IllegalAccessException.class) { + throw new CodeGenerationException(target); + } + throw new CodeGenerationException(target) { @Override public String getMessage() { return "ClassLoader mismatch for [" + contextClass.getName() + "]: JVM should be started with --add-opens=java.base/java.lang=ALL-UNNAMED " + - "for ClassLoader.defineClass to be accessible on " + loader.getClass().getName(); + "for ClassLoader.defineClass to be accessible on " + loader.getClass().getName() + + "; consider co-locating the affected class in that target ClassLoader instead."; } }; } diff --git a/spring-core/src/main/java/org/springframework/core/BridgeMethodResolver.java b/spring-core/src/main/java/org/springframework/core/BridgeMethodResolver.java index 016f0587ed77..811816478dd6 100644 --- a/spring-core/src/main/java/org/springframework/core/BridgeMethodResolver.java +++ b/spring-core/src/main/java/org/springframework/core/BridgeMethodResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-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. @@ -57,11 +57,11 @@ private BridgeMethodResolver() { /** - * Find the original method for the supplied {@link Method bridge Method}. + * Find the local original method for the supplied {@link Method bridge Method}. *

    It is safe to call this method passing in a non-bridge {@link Method} instance. * In such a case, the supplied {@link Method} instance is returned directly to the caller. * Callers are not required to check for bridging before calling this method. - * @param bridgeMethod the method to introspect + * @param bridgeMethod the method to introspect against its declaring class * @return the original method (either the bridged method or the passed-in method * if no more specific one could be found) */ @@ -73,8 +73,7 @@ public static Method findBridgedMethod(Method bridgeMethod) { if (bridgedMethod == null) { // Gather all methods with matching name and parameter size. List candidateMethods = new ArrayList<>(); - MethodFilter filter = candidateMethod -> - isBridgedCandidateFor(candidateMethod, bridgeMethod); + MethodFilter filter = (candidateMethod -> isBridgedCandidateFor(candidateMethod, bridgeMethod)); ReflectionUtils.doWithMethods(bridgeMethod.getDeclaringClass(), candidateMethods::add, filter); if (!candidateMethods.isEmpty()) { bridgedMethod = candidateMethods.size() == 1 ? @@ -95,10 +94,10 @@ public static Method findBridgedMethod(Method bridgeMethod) { * Returns {@code true} if the supplied '{@code candidateMethod}' can be * considered a valid candidate for the {@link Method} that is {@link Method#isBridge() bridged} * by the supplied {@link Method bridge Method}. This method performs inexpensive - * checks and can be used quickly to filter for a set of possible matches. + * checks and can be used to quickly filter for a set of possible matches. */ private static boolean isBridgedCandidateFor(Method candidateMethod, Method bridgeMethod) { - return (!candidateMethod.isBridge() && !candidateMethod.equals(bridgeMethod) && + return (!candidateMethod.isBridge() && candidateMethod.getName().equals(bridgeMethod.getName()) && candidateMethod.getParameterCount() == bridgeMethod.getParameterCount()); } @@ -121,8 +120,8 @@ private static Method searchCandidates(List candidateMethods, Method bri return candidateMethod; } else if (previousMethod != null) { - sameSig = sameSig && - Arrays.equals(candidateMethod.getGenericParameterTypes(), previousMethod.getGenericParameterTypes()); + sameSig = sameSig && Arrays.equals( + candidateMethod.getGenericParameterTypes(), previousMethod.getGenericParameterTypes()); } previousMethod = candidateMethod; } @@ -163,7 +162,8 @@ private static boolean isResolvedTypeMatch(Method genericMethod, Method candidat } } // A non-array type: compare the type itself. - if (!ClassUtils.resolvePrimitiveIfNecessary(candidateParameter).equals(ClassUtils.resolvePrimitiveIfNecessary(genericParameter.toClass()))) { + if (!ClassUtils.resolvePrimitiveIfNecessary(candidateParameter).equals( + ClassUtils.resolvePrimitiveIfNecessary(genericParameter.toClass()))) { return false; } } @@ -226,8 +226,8 @@ private static Method searchForMatch(Class type, Method bridgeMethod) { /** * Compare the signatures of the bridge method and the method which it bridges. If * the parameter and return types are the same, it is a 'visibility' bridge method - * introduced in Java 6 to fix https://bugs.openjdk.org/browse/JDK-6342411. - * See also https://stas-blogspot.blogspot.com/2010/03/java-bridge-methods-explained.html + * introduced in Java 6 to fix + * JDK-6342411. * @return whether signatures match as described */ public static boolean isVisibilityBridgeMethodPair(Method bridgeMethod, Method bridgedMethod) { diff --git a/spring-core/src/main/java/org/springframework/core/CollectionFactory.java b/spring-core/src/main/java/org/springframework/core/CollectionFactory.java index ffe3b580932b..242753e168af 100644 --- a/spring-core/src/main/java/org/springframework/core/CollectionFactory.java +++ b/spring-core/src/main/java/org/springframework/core/CollectionFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -68,6 +68,7 @@ public final class CollectionFactory { approximableCollectionTypes.add(SortedSet.class); approximableCollectionTypes.add(NavigableSet.class); approximableMapTypes.add(Map.class); + approximableMapTypes.add(MultiValueMap.class); approximableMapTypes.add(SortedMap.class); approximableMapTypes.add(NavigableMap.class); @@ -80,6 +81,7 @@ public final class CollectionFactory { approximableCollectionTypes.add(EnumSet.class); approximableMapTypes.add(HashMap.class); approximableMapTypes.add(LinkedHashMap.class); + approximableMapTypes.add(LinkedMultiValueMap.class); approximableMapTypes.add(TreeMap.class); approximableMapTypes.add(EnumMap.class); } @@ -121,13 +123,7 @@ public static boolean isApproximableCollectionType(@Nullable Class collection */ @SuppressWarnings({"rawtypes", "unchecked", "cast"}) public static Collection createApproximateCollection(@Nullable Object collection, int capacity) { - if (collection instanceof LinkedList) { - return new LinkedList<>(); - } - else if (collection instanceof List) { - return new ArrayList<>(capacity); - } - else if (collection instanceof EnumSet) { + if (collection instanceof EnumSet) { // Cast is necessary for compilation in Eclipse 4.4.1. Collection enumSet = (Collection) EnumSet.copyOf((EnumSet) collection); enumSet.clear(); @@ -136,6 +132,12 @@ else if (collection instanceof EnumSet) { else if (collection instanceof SortedSet) { return new TreeSet<>(((SortedSet) collection).comparator()); } + if (collection instanceof LinkedList) { + return new LinkedList<>(); + } + else if (collection instanceof List) { + return new ArrayList<>(capacity); + } else { return new LinkedHashSet<>(capacity); } @@ -181,7 +183,7 @@ public static Collection createCollection(Class collectionType, int ca @SuppressWarnings({"unchecked", "cast"}) public static Collection createCollection(Class collectionType, @Nullable Class elementType, int capacity) { Assert.notNull(collectionType, "Collection type must not be null"); - if (LinkedHashSet.class == collectionType || HashSet.class == collectionType || + if (LinkedHashSet.class == collectionType || Set.class == collectionType || Collection.class == collectionType) { return new LinkedHashSet<>(capacity); } @@ -191,8 +193,8 @@ else if (ArrayList.class == collectionType || List.class == collectionType) { else if (LinkedList.class == collectionType) { return new LinkedList<>(); } - else if (TreeSet.class == collectionType || NavigableSet.class == collectionType - || SortedSet.class == collectionType) { + else if (TreeSet.class == collectionType || NavigableSet.class == collectionType || + SortedSet.class == collectionType) { return new TreeSet<>(); } else if (EnumSet.class.isAssignableFrom(collectionType)) { @@ -200,6 +202,9 @@ else if (EnumSet.class.isAssignableFrom(collectionType)) { // Cast is necessary for compilation in Eclipse 4.4.1. return (Collection) EnumSet.noneOf(asEnumType(elementType)); } + else if (HashSet.class == collectionType) { + return new HashSet<>(capacity); + } else { if (collectionType.isInterface() || !Collection.class.isAssignableFrom(collectionType)) { throw new IllegalArgumentException("Unsupported Collection type: " + collectionType.getName()); @@ -251,6 +256,9 @@ public static Map createApproximateMap(@Nullable Object map, int ca else if (map instanceof SortedMap) { return new TreeMap<>(((SortedMap) map).comparator()); } + else if (map instanceof MultiValueMap) { + return new LinkedMultiValueMap(capacity); + } else { return new LinkedHashMap<>(capacity); } @@ -297,26 +305,24 @@ public static Map createMap(Class mapType, int capacity) { @SuppressWarnings({"rawtypes", "unchecked"}) public static Map createMap(Class mapType, @Nullable Class keyType, int capacity) { Assert.notNull(mapType, "Map type must not be null"); - if (mapType.isInterface()) { - if (Map.class == mapType) { - return new LinkedHashMap<>(capacity); - } - else if (SortedMap.class == mapType || NavigableMap.class == mapType) { - return new TreeMap<>(); - } - else if (MultiValueMap.class == mapType) { - return new LinkedMultiValueMap(); - } - else { - throw new IllegalArgumentException("Unsupported Map interface: " + mapType.getName()); - } + if (LinkedHashMap.class == mapType || Map.class == mapType) { + return new LinkedHashMap<>(capacity); + } + else if (LinkedMultiValueMap.class == mapType || MultiValueMap.class == mapType) { + return new LinkedMultiValueMap(); + } + else if (TreeMap.class == mapType || SortedMap.class == mapType || NavigableMap.class == mapType) { + return new TreeMap<>(); } else if (EnumMap.class == mapType) { Assert.notNull(keyType, "Cannot create EnumMap for unknown key type"); return new EnumMap(asEnumType(keyType)); } + else if (HashMap.class == mapType) { + return new HashMap<>(capacity); + } else { - if (!Map.class.isAssignableFrom(mapType)) { + if (mapType.isInterface() || !Map.class.isAssignableFrom(mapType)) { throw new IllegalArgumentException("Unsupported Map type: " + mapType.getName()); } try { diff --git a/spring-core/src/main/java/org/springframework/core/DefaultParameterNameDiscoverer.java b/spring-core/src/main/java/org/springframework/core/DefaultParameterNameDiscoverer.java index f4dca0f99688..6d6dc7892fd3 100644 --- a/spring-core/src/main/java/org/springframework/core/DefaultParameterNameDiscoverer.java +++ b/spring-core/src/main/java/org/springframework/core/DefaultParameterNameDiscoverer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 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,9 +18,9 @@ /** * Default implementation of the {@link ParameterNameDiscoverer} strategy interface, - * using the Java 8 standard reflection mechanism (if available), and falling back - * to the ASM-based {@link LocalVariableTableParameterNameDiscoverer} for checking - * debug information in the class file. + * using the Java 8 standard reflection mechanism, and falling back to the ASM-based + * {@link LocalVariableTableParameterNameDiscoverer} for checking debug information + * in the class file (e.g. for classes compiled with earlier Java versions). * *

    If a Kotlin reflection implementation is present, * {@link KotlinReflectionParameterNameDiscoverer} is added first in the list and diff --git a/spring-core/src/main/java/org/springframework/core/KotlinReflectionParameterNameDiscoverer.java b/spring-core/src/main/java/org/springframework/core/KotlinReflectionParameterNameDiscoverer.java index 01700431612d..61799e7ddcf6 100644 --- a/spring-core/src/main/java/org/springframework/core/KotlinReflectionParameterNameDiscoverer.java +++ b/spring-core/src/main/java/org/springframework/core/KotlinReflectionParameterNameDiscoverer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2022 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. @@ -31,11 +31,13 @@ * {@link ParameterNameDiscoverer} implementation which uses Kotlin's reflection facilities * for introspecting parameter names. * - * Compared to {@link StandardReflectionParameterNameDiscoverer}, it allows in addition to + *

    Compared to {@link StandardReflectionParameterNameDiscoverer}, it allows in addition to * determine interface parameter names without requiring Java 8 -parameters compiler flag. * * @author Sebastien Deleuze * @since 5.0 + * @see StandardReflectionParameterNameDiscoverer + * @see DefaultParameterNameDiscoverer */ public class KotlinReflectionParameterNameDiscoverer implements ParameterNameDiscoverer { diff --git a/spring-core/src/main/java/org/springframework/core/LocalVariableTableParameterNameDiscoverer.java b/spring-core/src/main/java/org/springframework/core/LocalVariableTableParameterNameDiscoverer.java index f20807f1190f..7805c2a1bf3f 100644 --- a/spring-core/src/main/java/org/springframework/core/LocalVariableTableParameterNameDiscoverer.java +++ b/spring-core/src/main/java/org/springframework/core/LocalVariableTableParameterNameDiscoverer.java @@ -47,12 +47,18 @@ * caches the ASM discovered information for each introspected Class, in a thread-safe * manner. It is recommended to reuse ParameterNameDiscoverer instances as far as possible. * + *

    This discoverer variant is effectively superseded by the Java 8 based + * {@link StandardReflectionParameterNameDiscoverer} but included as a fallback still + * (for code not compiled with the standard "-parameters" compiler flag). + * * @author Adrian Colyer * @author Costin Leau * @author Juergen Hoeller * @author Chris Beams * @author Sam Brannen * @since 2.0 + * @see StandardReflectionParameterNameDiscoverer + * @see DefaultParameterNameDiscoverer */ public class LocalVariableTableParameterNameDiscoverer implements ParameterNameDiscoverer { diff --git a/spring-core/src/main/java/org/springframework/core/MethodIntrospector.java b/spring-core/src/main/java/org/springframework/core/MethodIntrospector.java index 2947945cc488..f1ceac4a7ccf 100644 --- a/spring-core/src/main/java/org/springframework/core/MethodIntrospector.java +++ b/spring-core/src/main/java/org/springframework/core/MethodIntrospector.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-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. @@ -36,6 +36,7 @@ * * @author Juergen Hoeller * @author Rossen Stoyanchev + * @author Sam Brannen * @since 4.2.3 */ public final class MethodIntrospector { @@ -74,7 +75,9 @@ public static Map selectMethods(Class targetType, final Metada T result = metadataLookup.inspect(specificMethod); if (result != null) { Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(specificMethod); - if (bridgedMethod == specificMethod || metadataLookup.inspect(bridgedMethod) == null) { + if (bridgedMethod == specificMethod || bridgedMethod == method || + bridgedMethod.equals(specificMethod) || bridgedMethod.equals(method) || + metadataLookup.inspect(bridgedMethod) == null) { methodMap.put(specificMethod, result); } } diff --git a/spring-core/src/main/java/org/springframework/core/MethodParameter.java b/spring-core/src/main/java/org/springframework/core/MethodParameter.java index 42a0dfca93fc..18ff60ecde3b 100644 --- a/spring-core/src/main/java/org/springframework/core/MethodParameter.java +++ b/spring-core/src/main/java/org/springframework/core/MethodParameter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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. @@ -781,6 +781,7 @@ public MethodParameter clone() { return new MethodParameter(this); } + /** * Create a new MethodParameter for the given method or constructor. *

    This is a convenience factory method for scenarios where a diff --git a/spring-core/src/main/java/org/springframework/core/ParameterizedTypeReference.java b/spring-core/src/main/java/org/springframework/core/ParameterizedTypeReference.java index c10dd89aee9e..8e7bd2809f43 100644 --- a/spring-core/src/main/java/org/springframework/core/ParameterizedTypeReference.java +++ b/spring-core/src/main/java/org/springframework/core/ParameterizedTypeReference.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2023 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. @@ -14,7 +14,6 @@ * limitations under the License. */ - package org.springframework.core; import java.lang.reflect.ParameterizedType; @@ -92,8 +91,7 @@ public String toString() { * @since 4.3.12 */ public static ParameterizedTypeReference forType(Type type) { - return new ParameterizedTypeReference(type) { - }; + return new ParameterizedTypeReference(type) {}; } private static Class findParameterizedTypeReferenceSubclass(Class child) { diff --git a/spring-core/src/main/java/org/springframework/core/ReactiveAdapterRegistry.java b/spring-core/src/main/java/org/springframework/core/ReactiveAdapterRegistry.java index 9e56b5d5e37d..28a1152b5876 100644 --- a/spring-core/src/main/java/org/springframework/core/ReactiveAdapterRegistry.java +++ b/spring-core/src/main/java/org/springframework/core/ReactiveAdapterRegistry.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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,8 +24,6 @@ import java.util.concurrent.CompletionStage; import java.util.function.Function; -import kotlinx.coroutines.CompletableDeferredKt; -import kotlinx.coroutines.Deferred; import org.reactivestreams.Publisher; import reactor.blockhound.BlockHound; import reactor.blockhound.integration.BlockHoundIntegration; @@ -39,13 +37,14 @@ import org.springframework.util.ReflectionUtils; /** - * A registry of adapters to adapt Reactive Streams {@link Publisher} to/from - * various async/reactive types such as {@code CompletableFuture}, RxJava - * {@code Flowable}, and others. + * A registry of adapters to adapt Reactive Streams {@link Publisher} to/from various + * async/reactive types such as {@code CompletableFuture}, RxJava {@code Flowable}, etc. + * This is designed to complement Spring's Reactor {@code Mono}/{@code Flux} support while + * also being usable without Reactor, e.g. just for {@code org.reactivestreams} bridging. * - *

    By default, depending on classpath availability, adapters are registered - * for Reactor, RxJava 3, {@link CompletableFuture}, {@code Flow.Publisher}, - * and Kotlin Coroutines' {@code Deferred} and {@code Flow}. + *

    By default, depending on classpath availability, adapters are registered for Reactor + * (including {@code CompletableFuture} and {@code Flow.Publisher} adapters), RxJava 3, + * Kotlin Coroutines' {@code Deferred} (bridged via Reactor) and SmallRye Mutiny 1.x. * *

    Note: As of Spring Framework 5.3.11, support for * RxJava 1.x and 2.x is deprecated in favor of RxJava 3. @@ -136,16 +135,45 @@ public boolean hasAdapters() { * Register a reactive type along with functions to adapt to and from a * Reactive Streams {@link Publisher}. The function arguments assume that * their input is neither {@code null} nor {@link Optional}. + *

    This variant registers the new adapter after existing adapters. + * It will be matched for the exact reactive type if no earlier adapter was + * registered for the specific type, and it will be matched for assignability + * in a second pass if no earlier adapter had an assignable type before. + * @see #registerReactiveTypeOverride + * @see #getAdapter */ public void registerReactiveType(ReactiveTypeDescriptor descriptor, Function> toAdapter, Function, Object> fromAdapter) { - if (reactorPresent) { - this.adapters.add(new ReactorAdapter(descriptor, toAdapter, fromAdapter)); - } - else { - this.adapters.add(new ReactiveAdapter(descriptor, toAdapter, fromAdapter)); - } + this.adapters.add(buildAdapter(descriptor, toAdapter, fromAdapter)); + } + + /** + * Register a reactive type along with functions to adapt to and from a + * Reactive Streams {@link Publisher}. The function arguments assume that + * their input is neither {@code null} nor {@link Optional}. + *

    This variant registers the new adapter first, effectively overriding + * any previously registered adapters for the same reactive type. This allows + * for overriding existing adapters, in particular default adapters. + *

    Note that existing adapters for specific types will still match before + * an assignability match with the new adapter. In order to override all + * existing matches, a new reactive type adapter needs to be registered + * for every specific type, not relying on subtype assignability matches. + * @since 5.3.30 + * @see #registerReactiveType + * @see #getAdapter + */ + public void registerReactiveTypeOverride(ReactiveTypeDescriptor descriptor, + Function> toAdapter, Function, Object> fromAdapter) { + + this.adapters.add(0, buildAdapter(descriptor, toAdapter, fromAdapter)); + } + + private ReactiveAdapter buildAdapter(ReactiveTypeDescriptor descriptor, + Function> toAdapter, Function, Object> fromAdapter) { + + return (reactorPresent ? new ReactorAdapter(descriptor, toAdapter, fromAdapter) : + new ReactiveAdapter(descriptor, toAdapter, fromAdapter)); } /** @@ -401,9 +429,9 @@ private static class CoroutinesRegistrar { @SuppressWarnings("KotlinInternalInJava") void registerAdapters(ReactiveAdapterRegistry registry) { registry.registerReactiveType( - ReactiveTypeDescriptor.singleOptionalValue(Deferred.class, - () -> CompletableDeferredKt.CompletableDeferred(null)), - source -> CoroutinesUtils.deferredToMono((Deferred) source), + ReactiveTypeDescriptor.singleOptionalValue(kotlinx.coroutines.Deferred.class, + () -> kotlinx.coroutines.CompletableDeferredKt.CompletableDeferred(null)), + source -> CoroutinesUtils.deferredToMono((kotlinx.coroutines.Deferred) source), source -> CoroutinesUtils.monoToDeferred(Mono.from(source))); registry.registerReactiveType( diff --git a/spring-core/src/main/java/org/springframework/core/ReactiveTypeDescriptor.java b/spring-core/src/main/java/org/springframework/core/ReactiveTypeDescriptor.java index f1d76c330e24..433e2bf3b715 100644 --- a/spring-core/src/main/java/org/springframework/core/ReactiveTypeDescriptor.java +++ b/spring-core/src/main/java/org/springframework/core/ReactiveTypeDescriptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -37,7 +37,7 @@ public final class ReactiveTypeDescriptor { private final boolean noValue; @Nullable - private final Supplier emptyValueSupplier; + private final Supplier emptySupplier; private final boolean deferred; @@ -55,7 +55,7 @@ private ReactiveTypeDescriptor(Class reactiveType, boolean multiValue, boolea this.reactiveType = reactiveType; this.multiValue = multiValue; this.noValue = noValue; - this.emptyValueSupplier = emptySupplier; + this.emptySupplier = emptySupplier; this.deferred = deferred; } @@ -89,16 +89,18 @@ public boolean isNoValue() { * Return {@code true} if the reactive type can complete with no values. */ public boolean supportsEmpty() { - return (this.emptyValueSupplier != null); + return (this.emptySupplier != null); } /** * Return an empty-value instance for the underlying reactive or async type. - * Use of this type implies {@link #supportsEmpty()} is {@code true}. + *

    Use of this type implies {@link #supportsEmpty()} is {@code true}. */ public Object getEmptyValue() { - Assert.state(this.emptyValueSupplier != null, "Empty values not supported"); - return this.emptyValueSupplier.get(); + Assert.state(this.emptySupplier != null, "Empty values not supported"); + Object emptyValue = this.emptySupplier.get(); + Assert.notNull(emptyValue, "Invalid null return value from emptySupplier"); + return emptyValue; } /** @@ -130,7 +132,7 @@ public int hashCode() { /** - * Descriptor for a reactive type that can produce 0..N values. + * Descriptor for a reactive type that can produce {@code 0..N} values. * @param type the reactive type * @param emptySupplier a supplier of an empty-value instance of the reactive type */ diff --git a/spring-core/src/main/java/org/springframework/core/ResolvableType.java b/spring-core/src/main/java/org/springframework/core/ResolvableType.java index fcf99a3e713a..7e96f292e165 100644 --- a/spring-core/src/main/java/org/springframework/core/ResolvableType.java +++ b/spring-core/src/main/java/org/springframework/core/ResolvableType.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-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. @@ -133,9 +133,12 @@ public class ResolvableType implements Serializable { @Nullable private volatile ResolvableType[] generics; + @Nullable + private volatile Boolean unresolvableGenerics; + /** - * Private constructor used to create a new {@link ResolvableType} for cache key purposes, + * Private constructor used to create a new {@code ResolvableType} for cache key purposes, * with no upfront resolution. */ private ResolvableType( @@ -150,7 +153,7 @@ private ResolvableType( } /** - * Private constructor used to create a new {@link ResolvableType} for cache value purposes, + * Private constructor used to create a new {@code ResolvableType} for cache value purposes, * with upfront resolution and a pre-calculated hash. * @since 4.2 */ @@ -166,7 +169,7 @@ private ResolvableType(Type type, @Nullable TypeProvider typeProvider, } /** - * Private constructor used to create a new {@link ResolvableType} for uncached purposes, + * Private constructor used to create a new {@code ResolvableType} for uncached purposes, * with upfront resolution but lazily calculated hash. */ private ResolvableType(Type type, @Nullable TypeProvider typeProvider, @@ -181,7 +184,7 @@ private ResolvableType(Type type, @Nullable TypeProvider typeProvider, } /** - * Private constructor used to create a new {@link ResolvableType} on a {@link Class} basis. + * Private constructor used to create a new {@code ResolvableType} on a {@link Class} basis. *

    Avoids all {@code instanceof} checks in order to create a straight {@link Class} wrapper. * @since 4.2 */ @@ -220,7 +223,7 @@ public Class getRawClass() { /** * Return the underlying source of the resolvable type. Will return a {@link Field}, - * {@link MethodParameter} or {@link Type} depending on how the {@link ResolvableType} + * {@link MethodParameter} or {@link Type} depending on how the {@code ResolvableType} * was constructed. This method is primarily to provide access to additional type * information or meta-data that alternative JVM languages may provide. */ @@ -338,13 +341,14 @@ private boolean isAssignableFrom(ResolvableType other, @Nullable Map } } if (ourResolved == null) { - ourResolved = resolve(Object.class); + ourResolved = toClass(); } Class otherResolved = other.toClass(); // We need an exact type match for generics // List is not assignable from List - if (exactMatch ? !ourResolved.equals(otherResolved) : !ClassUtils.isAssignable(ourResolved, otherResolved)) { + if (exactMatch ? !ourResolved.equals(otherResolved) : + !ClassUtils.isAssignable(ourResolved, otherResolved)) { return false; } @@ -355,13 +359,15 @@ private boolean isAssignableFrom(ResolvableType other, @Nullable Map if (ourGenerics.length != typeGenerics.length) { return false; } - if (matchedBefore == null) { - matchedBefore = new IdentityHashMap<>(1); - } - matchedBefore.put(this.type, other.type); - for (int i = 0; i < ourGenerics.length; i++) { - if (!ourGenerics[i].isAssignableFrom(typeGenerics[i], matchedBefore)) { - return false; + if (ourGenerics.length > 0) { + if (matchedBefore == null) { + matchedBefore = new IdentityHashMap<>(1); + } + matchedBefore.put(this.type, other.type); + for (int i = 0; i < ourGenerics.length; i++) { + if (!ourGenerics[i].isAssignableFrom(typeGenerics[i], matchedBefore)) { + return false; + } } } } @@ -426,12 +432,12 @@ public ResolvableType asMap() { } /** - * Return this type as a {@link ResolvableType} of the specified class. Searches + * Return this type as a {@code ResolvableType} of the specified class. Searches * {@link #getSuperType() supertype} and {@link #getInterfaces() interface} * hierarchies to find a match, returning {@link #NONE} if this type does not * implement or extend the specified class. * @param type the required type (typically narrowed) - * @return a {@link ResolvableType} representing this object as the specified + * @return a {@code ResolvableType} representing this object as the specified * type, or {@link #NONE} if not resolvable as that type * @see #asCollection() * @see #asMap() @@ -456,9 +462,9 @@ public ResolvableType as(Class type) { } /** - * Return a {@link ResolvableType} representing the direct supertype of this type. + * Return a {@code ResolvableType} representing the direct supertype of this type. *

    If no supertype is available this method returns {@link #NONE}. - *

    Note: The resulting {@link ResolvableType} instance may not be {@link Serializable}. + *

    Note: The resulting {@code ResolvableType} instance may not be {@link Serializable}. * @see #getInterfaces() */ public ResolvableType getSuperType() { @@ -485,10 +491,10 @@ public ResolvableType getSuperType() { } /** - * Return a {@link ResolvableType} array representing the direct interfaces + * Return a {@code ResolvableType} array representing the direct interfaces * implemented by this type. If this type does not implement any interfaces an * empty array is returned. - *

    Note: The resulting {@link ResolvableType} instances may not be {@link Serializable}. + *

    Note: The resulting {@code ResolvableType} instances may not be {@link Serializable}. * @see #getSuperType() */ public ResolvableType[] getInterfaces() { @@ -545,6 +551,15 @@ public boolean hasUnresolvableGenerics() { if (this == NONE) { return false; } + Boolean unresolvableGenerics = this.unresolvableGenerics; + if (unresolvableGenerics == null) { + unresolvableGenerics = determineUnresolvableGenerics(); + this.unresolvableGenerics = unresolvableGenerics; + } + return unresolvableGenerics; + } + + private boolean determineUnresolvableGenerics() { ResolvableType[] generics = getGenerics(); for (ResolvableType generic : generics) { if (generic.isUnresolvableTypeVariable() || generic.isWildcardWithoutBounds()) { @@ -556,7 +571,7 @@ public boolean hasUnresolvableGenerics() { try { for (Type genericInterface : resolved.getGenericInterfaces()) { if (genericInterface instanceof Class) { - if (forClass((Class) genericInterface).hasGenerics()) { + if (((Class) genericInterface).getTypeParameters().length > 0) { return true; } } @@ -565,7 +580,10 @@ public boolean hasUnresolvableGenerics() { catch (TypeNotPresentException ex) { // Ignore non-present types in generic signature } - return getSuperType().hasUnresolvableGenerics(); + Class superclass = resolved.getSuperclass(); + if (superclass != null && superclass != Object.class) { + return getSuperType().hasUnresolvableGenerics(); + } } return false; } @@ -606,17 +624,17 @@ private boolean isWildcardWithoutBounds() { } /** - * Return a {@link ResolvableType} for the specified nesting level. + * Return a {@code ResolvableType} for the specified nesting level. *

    See {@link #getNested(int, Map)} for details. * @param nestingLevel the nesting level - * @return the {@link ResolvableType} type, or {@code #NONE} + * @return the {@code ResolvableType} type, or {@code #NONE} */ public ResolvableType getNested(int nestingLevel) { return getNested(nestingLevel, null); } /** - * Return a {@link ResolvableType} for the specified nesting level. + * Return a {@code ResolvableType} for the specified nesting level. *

    The nesting level refers to the specific generic parameter that should be returned. * A nesting level of 1 indicates this type; 2 indicates the first nested generic; * 3 the second; and so on. For example, given {@code List>} level 1 refers @@ -633,7 +651,7 @@ public ResolvableType getNested(int nestingLevel) { * current type, 2 for the first nested generic, 3 for the second and so on * @param typeIndexesPerLevel a map containing the generic index for a given * nesting level (may be {@code null}) - * @return a {@link ResolvableType} for the nested level, or {@link #NONE} + * @return a {@code ResolvableType} for the nested level, or {@link #NONE} */ public ResolvableType getNested(int nestingLevel, @Nullable Map typeIndexesPerLevel) { ResolvableType result = this; @@ -655,7 +673,7 @@ public ResolvableType getNested(int nestingLevel, @Nullable Map>}, {@code getGeneric(0)} will access the * {@code Integer}. Nested generics can be accessed by specifying multiple indexes; @@ -665,7 +683,7 @@ public ResolvableType getNested(int nestingLevel, @Nullable MapIf no generic is available at the specified indexes {@link #NONE} is returned. * @param indexes the indexes that refer to the generic parameter * (may be omitted to return the first generic) - * @return a {@link ResolvableType} for the specified generic, or {@link #NONE} + * @return a {@code ResolvableType} for the specified generic, or {@link #NONE} * @see #hasGenerics() * @see #getGenerics() * @see #resolveGeneric(int...) @@ -688,12 +706,12 @@ public ResolvableType getGeneric(@Nullable int... indexes) { } /** - * Return an array of {@link ResolvableType ResolvableTypes} representing the generic parameters of + * Return an array of {@code ResolvableType ResolvableTypes} representing the generic parameters of * this type. If no generics are available an empty array is returned. If you need to * access a specific generic consider using the {@link #getGeneric(int...)} method as * it allows access to nested generics and protects against * {@code IndexOutOfBoundsExceptions}. - * @return an array of {@link ResolvableType ResolvableTypes} representing the generic parameters + * @return an array of {@code ResolvableType ResolvableTypes} representing the generic parameters * (never {@code null}) * @see #hasGenerics() * @see #getGeneric(int...) @@ -732,7 +750,7 @@ else if (this.type instanceof ParameterizedType) { * Convenience method that will {@link #getGenerics() get} and * {@link #resolve() resolve} generic parameters. * @return an array of resolved generic parameters (the resulting array - * will never be {@code null}, but it may contain {@code null} elements}) + * will never be {@code null}, but it may contain {@code null} elements) * @see #getGenerics() * @see #resolve() */ @@ -765,7 +783,7 @@ public Class[] resolveGenerics(Class fallback) { /** * Convenience method that will {@link #getGeneric(int...) get} and - * {@link #resolve() resolve} a specific generic parameters. + * {@link #resolve() resolve} a specific generic parameter. * @param indexes the indexes that refer to the generic parameter * (may be omitted to return the first generic) * @return a resolved {@link Class} or {@code null} @@ -827,7 +845,7 @@ private Class resolveClass() { /** * Resolve this type by a single level, returning the resolved value or {@link #NONE}. - *

    Note: The returned {@link ResolvableType} should only be used as an intermediary + *

    Note: The returned {@code ResolvableType} should only be used as an intermediary * as it cannot be serialized. */ ResolvableType resolveType() { @@ -949,7 +967,7 @@ private int calculateHashCode() { } /** - * Adapts this {@link ResolvableType} to a {@link VariableResolver}. + * Adapts this {@code ResolvableType} to a {@link VariableResolver}. */ @Nullable VariableResolver asVariableResolver() { @@ -996,12 +1014,12 @@ public String toString() { // Factory methods /** - * Return a {@link ResolvableType} for the specified {@link Class}, + * Return a {@code ResolvableType} for the specified {@link Class}, * using the full generic type information for assignability checks. *

    For example: {@code ResolvableType.forClass(MyArrayList.class)}. * @param clazz the class to introspect ({@code null} is semantically * equivalent to {@code Object.class} for typical use cases here) - * @return a {@link ResolvableType} for the specified class + * @return a {@code ResolvableType} for the specified class * @see #forClass(Class, Class) * @see #forClassWithGenerics(Class, Class...) */ @@ -1010,13 +1028,13 @@ public static ResolvableType forClass(@Nullable Class clazz) { } /** - * Return a {@link ResolvableType} for the specified {@link Class}, + * Return a {@code ResolvableType} for the specified {@link Class}, * doing assignability checks against the raw class only (analogous to * {@link Class#isAssignableFrom}, which this serves as a wrapper for). *

    For example: {@code ResolvableType.forRawClass(List.class)}. * @param clazz the class to introspect ({@code null} is semantically * equivalent to {@code Object.class} for typical use cases here) - * @return a {@link ResolvableType} for the specified class + * @return a {@code ResolvableType} for the specified class * @since 4.2 * @see #forClass(Class) * @see #getRawClass() @@ -1040,12 +1058,12 @@ public boolean isAssignableFrom(ResolvableType other) { } /** - * Return a {@link ResolvableType} for the specified base type + * Return a {@code ResolvableType} for the specified base type * (interface or base class) with a given implementation class. *

    For example: {@code ResolvableType.forClass(List.class, MyArrayList.class)}. * @param baseType the base type (must not be {@code null}) * @param implementationClass the implementation class - * @return a {@link ResolvableType} for the specified base type backed by the + * @return a {@code ResolvableType} for the specified base type backed by the * given implementation class * @see #forClass(Class) * @see #forClassWithGenerics(Class, Class...) @@ -1057,10 +1075,10 @@ public static ResolvableType forClass(Class baseType, Class implementation } /** - * Return a {@link ResolvableType} for the specified {@link Class} with pre-declared generics. + * Return a {@code ResolvableType} for the specified {@link Class} with pre-declared generics. * @param clazz the class (or interface) to introspect * @param generics the generics of the class - * @return a {@link ResolvableType} for the specific class and generics + * @return a {@code ResolvableType} for the specific class and generics * @see #forClassWithGenerics(Class, ResolvableType...) */ public static ResolvableType forClassWithGenerics(Class clazz, Class... generics) { @@ -1074,10 +1092,10 @@ public static ResolvableType forClassWithGenerics(Class clazz, Class... ge } /** - * Return a {@link ResolvableType} for the specified {@link Class} with pre-declared generics. + * Return a {@code ResolvableType} for the specified {@link Class} with pre-declared generics. * @param clazz the class (or interface) to introspect * @param generics the generics of the class - * @return a {@link ResolvableType} for the specific class and generics + * @return a {@code ResolvableType} for the specific class and generics * @see #forClassWithGenerics(Class, Class...) */ public static ResolvableType forClassWithGenerics(Class clazz, ResolvableType... generics) { @@ -1098,12 +1116,12 @@ public static ResolvableType forClassWithGenerics(Class clazz, ResolvableType } /** - * Return a {@link ResolvableType} for the specified instance. The instance does not + * Return a {@code ResolvableType} for the specified instance. The instance does not * convey generic information but if it implements {@link ResolvableTypeProvider} a - * more precise {@link ResolvableType} can be used than the simple one based on + * more precise {@code ResolvableType} can be used than the simple one based on * the {@link #forClass(Class) Class instance}. * @param instance the instance (possibly {@code null}) - * @return a {@link ResolvableType} for the specified instance, + * @return a {@code ResolvableType} for the specified instance, * or {@code NONE} for {@code null} * @since 4.2 * @see ResolvableTypeProvider @@ -1119,9 +1137,9 @@ public static ResolvableType forInstance(@Nullable Object instance) { } /** - * Return a {@link ResolvableType} for the specified {@link Field}. + * Return a {@code ResolvableType} for the specified {@link Field}. * @param field the source field - * @return a {@link ResolvableType} for the specified field + * @return a {@code ResolvableType} for the specified field * @see #forField(Field, Class) */ public static ResolvableType forField(Field field) { @@ -1130,13 +1148,13 @@ public static ResolvableType forField(Field field) { } /** - * Return a {@link ResolvableType} for the specified {@link Field} with a given + * Return a {@code ResolvableType} for the specified {@link Field} with a given * implementation. *

    Use this variant when the class that declares the field includes generic * parameter variables that are satisfied by the implementation class. * @param field the source field * @param implementationClass the implementation class - * @return a {@link ResolvableType} for the specified field + * @return a {@code ResolvableType} for the specified field * @see #forField(Field) */ public static ResolvableType forField(Field field, Class implementationClass) { @@ -1146,13 +1164,13 @@ public static ResolvableType forField(Field field, Class implementationClass) } /** - * Return a {@link ResolvableType} for the specified {@link Field} with a given + * Return a {@code ResolvableType} for the specified {@link Field} with a given * implementation. *

    Use this variant when the class that declares the field includes generic * parameter variables that are satisfied by the implementation type. * @param field the source field * @param implementationType the implementation type - * @return a {@link ResolvableType} for the specified field + * @return a {@code ResolvableType} for the specified field * @see #forField(Field) */ public static ResolvableType forField(Field field, @Nullable ResolvableType implementationType) { @@ -1163,7 +1181,7 @@ public static ResolvableType forField(Field field, @Nullable ResolvableType impl } /** - * Return a {@link ResolvableType} for the specified {@link Field} with the + * Return a {@code ResolvableType} for the specified {@link Field} with the * given nesting level. * @param field the source field * @param nestingLevel the nesting level (1 for the outer level; 2 for a nested @@ -1176,7 +1194,7 @@ public static ResolvableType forField(Field field, int nestingLevel) { } /** - * Return a {@link ResolvableType} for the specified {@link Field} with a given + * Return a {@code ResolvableType} for the specified {@link Field} with a given * implementation and the given nesting level. *

    Use this variant when the class that declares the field includes generic * parameter variables that are satisfied by the implementation class. @@ -1184,7 +1202,7 @@ public static ResolvableType forField(Field field, int nestingLevel) { * @param nestingLevel the nesting level (1 for the outer level; 2 for a nested * generic type; etc) * @param implementationClass the implementation class - * @return a {@link ResolvableType} for the specified field + * @return a {@code ResolvableType} for the specified field * @see #forField(Field) */ public static ResolvableType forField(Field field, int nestingLevel, @Nullable Class implementationClass) { @@ -1194,10 +1212,10 @@ public static ResolvableType forField(Field field, int nestingLevel, @Nullable C } /** - * Return a {@link ResolvableType} for the specified {@link Constructor} parameter. + * Return a {@code ResolvableType} for the specified {@link Constructor} parameter. * @param constructor the source constructor (must not be {@code null}) * @param parameterIndex the parameter index - * @return a {@link ResolvableType} for the specified constructor parameter + * @return a {@code ResolvableType} for the specified constructor parameter * @see #forConstructorParameter(Constructor, int, Class) */ public static ResolvableType forConstructorParameter(Constructor constructor, int parameterIndex) { @@ -1206,14 +1224,14 @@ public static ResolvableType forConstructorParameter(Constructor constructor, } /** - * Return a {@link ResolvableType} for the specified {@link Constructor} parameter + * Return a {@code ResolvableType} for the specified {@link Constructor} parameter * with a given implementation. Use this variant when the class that declares the * constructor includes generic parameter variables that are satisfied by the * implementation class. * @param constructor the source constructor (must not be {@code null}) * @param parameterIndex the parameter index * @param implementationClass the implementation class - * @return a {@link ResolvableType} for the specified constructor parameter + * @return a {@code ResolvableType} for the specified constructor parameter * @see #forConstructorParameter(Constructor, int) */ public static ResolvableType forConstructorParameter(Constructor constructor, int parameterIndex, @@ -1225,9 +1243,9 @@ public static ResolvableType forConstructorParameter(Constructor constructor, } /** - * Return a {@link ResolvableType} for the specified {@link Method} return type. + * Return a {@code ResolvableType} for the specified {@link Method} return type. * @param method the source for the method return type - * @return a {@link ResolvableType} for the specified method return + * @return a {@code ResolvableType} for the specified method return * @see #forMethodReturnType(Method, Class) */ public static ResolvableType forMethodReturnType(Method method) { @@ -1236,12 +1254,12 @@ public static ResolvableType forMethodReturnType(Method method) { } /** - * Return a {@link ResolvableType} for the specified {@link Method} return type. + * Return a {@code ResolvableType} for the specified {@link Method} return type. *

    Use this variant when the class that declares the method includes generic * parameter variables that are satisfied by the implementation class. * @param method the source for the method return type * @param implementationClass the implementation class - * @return a {@link ResolvableType} for the specified method return + * @return a {@code ResolvableType} for the specified method return * @see #forMethodReturnType(Method) */ public static ResolvableType forMethodReturnType(Method method, Class implementationClass) { @@ -1251,10 +1269,10 @@ public static ResolvableType forMethodReturnType(Method method, Class impleme } /** - * Return a {@link ResolvableType} for the specified {@link Method} parameter. + * Return a {@code ResolvableType} for the specified {@link Method} parameter. * @param method the source method (must not be {@code null}) * @param parameterIndex the parameter index - * @return a {@link ResolvableType} for the specified method parameter + * @return a {@code ResolvableType} for the specified method parameter * @see #forMethodParameter(Method, int, Class) * @see #forMethodParameter(MethodParameter) */ @@ -1264,13 +1282,13 @@ public static ResolvableType forMethodParameter(Method method, int parameterInde } /** - * Return a {@link ResolvableType} for the specified {@link Method} parameter with a + * Return a {@code ResolvableType} for the specified {@link Method} parameter with a * given implementation. Use this variant when the class that declares the method * includes generic parameter variables that are satisfied by the implementation class. * @param method the source method (must not be {@code null}) * @param parameterIndex the parameter index * @param implementationClass the implementation class - * @return a {@link ResolvableType} for the specified method parameter + * @return a {@code ResolvableType} for the specified method parameter * @see #forMethodParameter(Method, int, Class) * @see #forMethodParameter(MethodParameter) */ @@ -1281,9 +1299,9 @@ public static ResolvableType forMethodParameter(Method method, int parameterInde } /** - * Return a {@link ResolvableType} for the specified {@link MethodParameter}. + * Return a {@code ResolvableType} for the specified {@link MethodParameter}. * @param methodParameter the source method parameter (must not be {@code null}) - * @return a {@link ResolvableType} for the specified method parameter + * @return a {@code ResolvableType} for the specified method parameter * @see #forMethodParameter(Method, int) */ public static ResolvableType forMethodParameter(MethodParameter methodParameter) { @@ -1291,12 +1309,12 @@ public static ResolvableType forMethodParameter(MethodParameter methodParameter) } /** - * Return a {@link ResolvableType} for the specified {@link MethodParameter} with a + * Return a {@code ResolvableType} for the specified {@link MethodParameter} with a * given implementation type. Use this variant when the class that declares the method * includes generic parameter variables that are satisfied by the implementation type. * @param methodParameter the source method parameter (must not be {@code null}) * @param implementationType the implementation type - * @return a {@link ResolvableType} for the specified method parameter + * @return a {@code ResolvableType} for the specified method parameter * @see #forMethodParameter(MethodParameter) */ public static ResolvableType forMethodParameter(MethodParameter methodParameter, @@ -1311,11 +1329,11 @@ public static ResolvableType forMethodParameter(MethodParameter methodParameter, } /** - * Return a {@link ResolvableType} for the specified {@link MethodParameter}, + * Return a {@code ResolvableType} for the specified {@link MethodParameter}, * overriding the target type to resolve with a specific given type. * @param methodParameter the source method parameter (must not be {@code null}) * @param targetType the type to resolve (a part of the method parameter's type) - * @return a {@link ResolvableType} for the specified method parameter + * @return a {@code ResolvableType} for the specified method parameter * @see #forMethodParameter(Method, int) */ public static ResolvableType forMethodParameter(MethodParameter methodParameter, @Nullable Type targetType) { @@ -1324,13 +1342,13 @@ public static ResolvableType forMethodParameter(MethodParameter methodParameter, } /** - * Return a {@link ResolvableType} for the specified {@link MethodParameter} at + * Return a {@code ResolvableType} for the specified {@link MethodParameter} at * a specific nesting level, overriding the target type to resolve with a specific * given type. * @param methodParameter the source method parameter (must not be {@code null}) * @param targetType the type to resolve (a part of the method parameter's type) * @param nestingLevel the nesting level to use - * @return a {@link ResolvableType} for the specified method parameter + * @return a {@code ResolvableType} for the specified method parameter * @since 5.2 * @see #forMethodParameter(Method, int) */ @@ -1343,9 +1361,9 @@ static ResolvableType forMethodParameter( } /** - * Return a {@link ResolvableType} as an array of the specified {@code componentType}. + * Return a {@code ResolvableType} as an array of the specified {@code componentType}. * @param componentType the component type - * @return a {@link ResolvableType} as an array of the specified component type + * @return a {@code ResolvableType} as an array of the specified component type */ public static ResolvableType forArrayComponent(ResolvableType componentType) { Assert.notNull(componentType, "Component type must not be null"); @@ -1354,10 +1372,10 @@ public static ResolvableType forArrayComponent(ResolvableType componentType) { } /** - * Return a {@link ResolvableType} for the specified {@link Type}. - *

    Note: The resulting {@link ResolvableType} instance may not be {@link Serializable}. + * Return a {@code ResolvableType} for the specified {@link Type}. + *

    Note: The resulting {@code ResolvableType} instance may not be {@link Serializable}. * @param type the source type (potentially {@code null}) - * @return a {@link ResolvableType} for the specified {@link Type} + * @return a {@code ResolvableType} for the specified {@link Type} * @see #forType(Type, ResolvableType) */ public static ResolvableType forType(@Nullable Type type) { @@ -1365,12 +1383,12 @@ public static ResolvableType forType(@Nullable Type type) { } /** - * Return a {@link ResolvableType} for the specified {@link Type} backed by the given + * Return a {@code ResolvableType} for the specified {@link Type} backed by the given * owner type. - *

    Note: The resulting {@link ResolvableType} instance may not be {@link Serializable}. + *

    Note: The resulting {@code ResolvableType} instance may not be {@link Serializable}. * @param type the source type or {@code null} * @param owner the owner type used to resolve variables - * @return a {@link ResolvableType} for the specified {@link Type} and owner + * @return a {@code ResolvableType} for the specified {@link Type} and owner * @see #forType(Type) */ public static ResolvableType forType(@Nullable Type type, @Nullable ResolvableType owner) { @@ -1383,10 +1401,10 @@ public static ResolvableType forType(@Nullable Type type, @Nullable ResolvableTy /** - * Return a {@link ResolvableType} for the specified {@link ParameterizedTypeReference}. - *

    Note: The resulting {@link ResolvableType} instance may not be {@link Serializable}. + * Return a {@code ResolvableType} for the specified {@link ParameterizedTypeReference}. + *

    Note: The resulting {@code ResolvableType} instance may not be {@link Serializable}. * @param typeReference the reference to obtain the source type from - * @return a {@link ResolvableType} for the specified {@link ParameterizedTypeReference} + * @return a {@code ResolvableType} for the specified {@link ParameterizedTypeReference} * @since 4.3.12 * @see #forType(Type) */ @@ -1395,23 +1413,23 @@ public static ResolvableType forType(ParameterizedTypeReference typeReference } /** - * Return a {@link ResolvableType} for the specified {@link Type} backed by a given + * Return a {@code ResolvableType} for the specified {@link Type} backed by a given * {@link VariableResolver}. * @param type the source type or {@code null} * @param variableResolver the variable resolver or {@code null} - * @return a {@link ResolvableType} for the specified {@link Type} and {@link VariableResolver} + * @return a {@code ResolvableType} for the specified {@link Type} and {@link VariableResolver} */ static ResolvableType forType(@Nullable Type type, @Nullable VariableResolver variableResolver) { return forType(type, null, variableResolver); } /** - * Return a {@link ResolvableType} for the specified {@link Type} backed by a given + * Return a {@code ResolvableType} for the specified {@link Type} backed by a given * {@link VariableResolver}. * @param type the source type or {@code null} * @param typeProvider the type provider or {@code null} * @param variableResolver the variable resolver or {@code null} - * @return a {@link ResolvableType} for the specified {@link Type} and {@link VariableResolver} + * @return a {@code ResolvableType} for the specified {@link Type} and {@link VariableResolver} */ static ResolvableType forType( @Nullable Type type, @Nullable TypeProvider typeProvider, @Nullable VariableResolver variableResolver) { diff --git a/spring-core/src/main/java/org/springframework/core/SerializableTypeWrapper.java b/spring-core/src/main/java/org/springframework/core/SerializableTypeWrapper.java index 7d09fe2e0694..0946cf366743 100644 --- a/spring-core/src/main/java/org/springframework/core/SerializableTypeWrapper.java +++ b/spring-core/src/main/java/org/springframework/core/SerializableTypeWrapper.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-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,7 +22,6 @@ import java.lang.reflect.Field; import java.lang.reflect.GenericArrayType; import java.lang.reflect.InvocationHandler; -import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Proxy; @@ -160,7 +159,7 @@ interface TypeProvider extends Serializable { /** * Return the source of the type, or {@code null} if not known. - *

    The default implementations returns {@code null}. + *

    The default implementation returns {@code null}. */ @Nullable default Object getSource() { @@ -204,19 +203,23 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl return forTypeProvider(new MethodInvokeTypeProvider(this.provider, method, -1)); } else if (Type[].class == method.getReturnType() && ObjectUtils.isEmpty(args)) { - Type[] result = new Type[((Type[]) method.invoke(this.provider.getType())).length]; + Object returnValue = ReflectionUtils.invokeMethod(method, this.provider.getType()); + if (returnValue == null) { + return null; + } + Type[] result = new Type[((Type[]) returnValue).length]; for (int i = 0; i < result.length; i++) { result[i] = forTypeProvider(new MethodInvokeTypeProvider(this.provider, method, i)); } return result; } - try { - return method.invoke(this.provider.getType(), args); - } - catch (InvocationTargetException ex) { - throw ex.getTargetException(); + Type type = this.provider.getType(); + if (type instanceof TypeVariable && method.getName().equals("getName")) { + // Avoid reflection for common comparison of type variables + return ((TypeVariable) type).getName(); } + return ReflectionUtils.invokeMethod(method, type, args); } } diff --git a/spring-core/src/main/java/org/springframework/core/StandardReflectionParameterNameDiscoverer.java b/spring-core/src/main/java/org/springframework/core/StandardReflectionParameterNameDiscoverer.java index 665befa0bcbe..9bce47f435b4 100644 --- a/spring-core/src/main/java/org/springframework/core/StandardReflectionParameterNameDiscoverer.java +++ b/spring-core/src/main/java/org/springframework/core/StandardReflectionParameterNameDiscoverer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2022 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,10 +26,15 @@ * {@link ParameterNameDiscoverer} implementation which uses JDK 8's reflection facilities * for introspecting parameter names (based on the "-parameters" compiler flag). * + *

    This is a key element of {@link DefaultParameterNameDiscoverer} where it is being + * combined with {@link KotlinReflectionParameterNameDiscoverer} if Kotlin is present. + * * @author Juergen Hoeller * @since 4.0 * @see java.lang.reflect.Method#getParameters() * @see java.lang.reflect.Parameter#getName() + * @see KotlinReflectionParameterNameDiscoverer + * @see DefaultParameterNameDiscoverer */ public class StandardReflectionParameterNameDiscoverer implements ParameterNameDiscoverer { diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationUtils.java b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationUtils.java index 20868db2223d..0ccece66bf66 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationUtils.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-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. @@ -149,8 +149,8 @@ public static boolean isCandidateClass(Class clazz, Collection clazz, Class annotationType) { - return isCandidateClass(clazz, annotationType.getName()); + public static boolean isCandidateClass(Class clazz, @Nullable Class annotationType) { + return (annotationType != null && isCandidateClass(clazz, annotationType.getName())); } /** @@ -757,7 +757,7 @@ public static boolean isInJavaLangAnnotationPackage(@Nullable String annotationT * Google App Engine's late arrival of {@code TypeNotPresentExceptionProxy} for * {@code Class} values (instead of early {@code Class.getAnnotations() failure}). *

    This method not failing indicates that {@link #getAnnotationAttributes(Annotation)} - * won't failure either (when attempted later on). + * won't fail either (when attempted later on). * @param annotation the annotation to validate * @throws IllegalStateException if a declared {@code Class} attribute could not be read * @since 4.3.15 @@ -1052,17 +1052,16 @@ public static Object getValue(@Nullable Annotation annotation, @Nullable String return null; } try { - Method method = annotation.annotationType().getDeclaredMethod(attributeName); - return invokeAnnotationMethod(method, annotation); - } - catch (NoSuchMethodException ex) { - return null; + for (Method method : annotation.annotationType().getDeclaredMethods()) { + if (method.getName().equals(attributeName) && method.getParameterCount() == 0) { + return invokeAnnotationMethod(method, annotation); + } + } } catch (Throwable ex) { - rethrowAnnotationConfigurationException(ex); - handleIntrospectionFailure(annotation.getClass(), ex); - return null; + handleValueRetrievalFailure(annotation, ex); } + return null; } /** @@ -1076,14 +1075,18 @@ public static Object getValue(@Nullable Annotation annotation, @Nullable String * @return the value returned from the method invocation * @since 5.3.24 */ - static Object invokeAnnotationMethod(Method method, Object annotation) { + @Nullable + static Object invokeAnnotationMethod(Method method, @Nullable Object annotation) { + if (annotation == null) { + return null; + } if (Proxy.isProxyClass(annotation.getClass())) { try { InvocationHandler handler = Proxy.getInvocationHandler(annotation); return handler.invoke(annotation, method, null); } catch (Throwable ex) { - // ignore and fall back to reflection below + // Ignore and fall back to reflection below } } return ReflectionUtils.invokeMethod(method, annotation); @@ -1117,20 +1120,32 @@ static void rethrowAnnotationConfigurationException(Throwable ex) { * @see #rethrowAnnotationConfigurationException * @see IntrospectionFailureLogger */ - static void handleIntrospectionFailure(@Nullable AnnotatedElement element, Throwable ex) { + static void handleIntrospectionFailure(AnnotatedElement element, Throwable ex) { rethrowAnnotationConfigurationException(ex); IntrospectionFailureLogger logger = IntrospectionFailureLogger.INFO; boolean meta = false; if (element instanceof Class && Annotation.class.isAssignableFrom((Class) element)) { - // Meta-annotation or (default) value lookup on an annotation type + // Meta-annotation introspection failure logger = IntrospectionFailureLogger.DEBUG; meta = true; } if (logger.isEnabled()) { - String message = meta ? - "Failed to meta-introspect annotation " : - "Failed to introspect annotations on "; - logger.log(message + element + ": " + ex); + logger.log("Failed to " + (meta ? "meta-introspect annotation " : "introspect annotations on ") + + element + ": " + ex); + } + } + + /** + * Handle the supplied value retrieval exception. + * @param annotation the annotation instance from which to retrieve the value + * @param ex the exception that we encountered + * @see #handleIntrospectionFailure + */ + private static void handleValueRetrievalFailure(Annotation annotation, Throwable ex) { + rethrowAnnotationConfigurationException(ex); + IntrospectionFailureLogger logger = IntrospectionFailureLogger.INFO; + if (logger.isEnabled()) { + logger.log("Failed to retrieve value from " + annotation + ": " + ex); } } @@ -1321,6 +1336,9 @@ public static boolean isSynthesizedAnnotation(@Nullable Annotation annotation) { public static void clearCache() { AnnotationTypeMappings.clearCache(); AnnotationsScanner.clearCache(); + AttributeMethods.cache.clear(); + RepeatableContainers.cache.clear(); + OrderUtils.orderCache.clear(); } diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationsScanner.java b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationsScanner.java index 626413838c9b..87529f4b4767 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationsScanner.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationsScanner.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -339,11 +339,10 @@ private static Method[] getBaseTypeMethods(C context, Class baseType) { Method[] methods = baseTypeMethodsCache.get(baseType); if (methods == null) { - boolean isInterface = baseType.isInterface(); - methods = isInterface ? baseType.getMethods() : ReflectionUtils.getDeclaredMethods(baseType); + methods = ReflectionUtils.getDeclaredMethods(baseType); int cleared = 0; for (int i = 0; i < methods.length; i++) { - if ((!isInterface && Modifier.isPrivate(methods[i].getModifiers())) || + if (Modifier.isPrivate(methods[i].getModifiers()) || hasPlainJavaAnnotationsOnly(methods[i]) || getDeclaredAnnotations(methods[i], false).length == 0) { methods[i] = null; @@ -457,7 +456,7 @@ static Annotation[] getDeclaredAnnotations(AnnotatedElement source, boolean defe for (int i = 0; i < annotations.length; i++) { Annotation annotation = annotations[i]; if (isIgnorable(annotation.annotationType()) || - !AttributeMethods.forAnnotationType(annotation.annotationType()).isValid(annotation)) { + !AttributeMethods.forAnnotationType(annotation.annotationType()).canLoad(annotation)) { annotations[i] = null; } else { diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AttributeMethods.java b/spring-core/src/main/java/org/springframework/core/annotation/AttributeMethods.java index ea9b6b715d9f..c56064b77a98 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/AttributeMethods.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/AttributeMethods.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-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. @@ -39,15 +39,13 @@ final class AttributeMethods { static final AttributeMethods NONE = new AttributeMethods(null, new Method[0]); - - private static final Map, AttributeMethods> cache = - new ConcurrentReferenceHashMap<>(); + static final Map, AttributeMethods> cache = new ConcurrentReferenceHashMap<>(); private static final Comparator methodComparator = (m1, m2) -> { if (m1 != null && m2 != null) { return m1.getName().compareTo(m2.getName()); } - return m1 != null ? -1 : 1; + return (m1 != null ? -1 : 1); }; @@ -86,32 +84,29 @@ private AttributeMethods(@Nullable Class annotationType, M } - /** - * Determine if this instance only contains a single attribute named - * {@code value}. - * @return {@code true} if there is only a value attribute - */ - boolean hasOnlyValueAttribute() { - return (this.attributeMethods.length == 1 && - MergedAnnotation.VALUE.equals(this.attributeMethods[0].getName())); - } - - /** * Determine if values from the given annotation can be safely accessed without * causing any {@link TypeNotPresentException TypeNotPresentExceptions}. + *

    This method is designed to cover Google App Engine's late arrival of such + * exceptions for {@code Class} values (instead of the more typical early + * {@code Class.getAnnotations() failure} on a regular JVM). * @param annotation the annotation to check * @return {@code true} if all values are present * @see #validate(Annotation) */ - boolean isValid(Annotation annotation) { + boolean canLoad(Annotation annotation) { assertAnnotation(annotation); for (int i = 0; i < size(); i++) { if (canThrowTypeNotPresentException(i)) { try { AnnotationUtils.invokeAnnotationMethod(get(i), annotation); } + catch (IllegalStateException ex) { + // Plain invocation failure to expose -> leave up to attribute retrieval + // (if any) where such invocation failure will be logged eventually. + } catch (Throwable ex) { + // TypeNotPresentException etc. -> annotation type not actually loadable. return false; } } @@ -121,13 +116,13 @@ boolean isValid(Annotation annotation) { /** * Check if values from the given annotation can be safely accessed without causing - * any {@link TypeNotPresentException TypeNotPresentExceptions}. In particular, - * this method is designed to cover Google App Engine's late arrival of such + * any {@link TypeNotPresentException TypeNotPresentExceptions}. + *

    This method is designed to cover Google App Engine's late arrival of such * exceptions for {@code Class} values (instead of the more typical early - * {@code Class.getAnnotations() failure}). + * {@code Class.getAnnotations() failure} on a regular JVM). * @param annotation the annotation to validate * @throws IllegalStateException if a declared {@code Class} attribute could not be read - * @see #isValid(Annotation) + * @see #canLoad(Annotation) */ void validate(Annotation annotation) { assertAnnotation(annotation); @@ -136,6 +131,9 @@ void validate(Annotation annotation) { try { AnnotationUtils.invokeAnnotationMethod(get(i), annotation); } + catch (IllegalStateException ex) { + throw ex; + } catch (Throwable ex) { throw new IllegalStateException("Could not obtain annotation attribute value for " + get(i).getName() + " declared on " + annotation.annotationType(), ex); @@ -160,7 +158,7 @@ private void assertAnnotation(Annotation annotation) { @Nullable Method get(String name) { int index = indexOf(name); - return index != -1 ? this.attributeMethods[index] : null; + return (index != -1 ? this.attributeMethods[index] : null); } /** diff --git a/spring-core/src/main/java/org/springframework/core/annotation/OrderUtils.java b/spring-core/src/main/java/org/springframework/core/annotation/OrderUtils.java index 25acbc848161..62a135842b7a 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/OrderUtils.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/OrderUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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. @@ -41,7 +41,7 @@ public abstract class OrderUtils { private static final String JAVAX_PRIORITY_ANNOTATION = "javax.annotation.Priority"; /** Cache for @Order value (or NOT_ANNOTATED marker) per Class. */ - private static final Map orderCache = new ConcurrentReferenceHashMap<>(64); + static final Map orderCache = new ConcurrentReferenceHashMap<>(64); /** diff --git a/spring-core/src/main/java/org/springframework/core/annotation/RepeatableContainers.java b/spring-core/src/main/java/org/springframework/core/annotation/RepeatableContainers.java index 52adbc034bb0..8460018c5bd6 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/RepeatableContainers.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/RepeatableContainers.java @@ -43,6 +43,8 @@ */ public abstract class RepeatableContainers { + static final Map, Object> cache = new ConcurrentReferenceHashMap<>(); + @Nullable private final RepeatableContainers parent; @@ -137,8 +139,6 @@ public static RepeatableContainers none() { */ private static class StandardRepeatableContainers extends RepeatableContainers { - private static final Map, Object> cache = new ConcurrentReferenceHashMap<>(); - private static final Object NONE = new Object(); private static StandardRepeatableContainers INSTANCE = new StandardRepeatableContainers(); @@ -166,8 +166,8 @@ private static Method getRepeatedAnnotationsMethod(Class a private static Object computeRepeatedAnnotationsMethod(Class annotationType) { AttributeMethods methods = AttributeMethods.forAnnotationType(annotationType); - if (methods.hasOnlyValueAttribute()) { - Method method = methods.get(0); + Method method = methods.get(MergedAnnotation.VALUE); + if (method != null) { Class returnType = method.getReturnType(); if (returnType.isArray()) { Class componentType = returnType.getComponentType(); diff --git a/spring-core/src/main/java/org/springframework/core/annotation/TypeMappedAnnotations.java b/spring-core/src/main/java/org/springframework/core/annotation/TypeMappedAnnotations.java index ddd078b25cf3..3e789889ccac 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/TypeMappedAnnotations.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/TypeMappedAnnotations.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -410,7 +410,10 @@ private MergedAnnotation process( Annotation[] repeatedAnnotations = repeatableContainers.findRepeatedAnnotations(annotation); if (repeatedAnnotations != null) { - return doWithAnnotations(type, aggregateIndex, source, repeatedAnnotations); + MergedAnnotation result = doWithAnnotations(type, aggregateIndex, source, repeatedAnnotations); + if (result != null) { + return result; + } } AnnotationTypeMappings mappings = AnnotationTypeMappings.forAnnotationType( annotation.annotationType(), repeatableContainers, annotationFilter); diff --git a/spring-core/src/main/java/org/springframework/core/codec/Decoder.java b/spring-core/src/main/java/org/springframework/core/codec/Decoder.java index cc5271d5c01f..18f8fd8c5e62 100644 --- a/spring-core/src/main/java/org/springframework/core/codec/Decoder.java +++ b/spring-core/src/main/java/org/springframework/core/codec/Decoder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-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. @@ -95,20 +95,19 @@ default T decode(DataBuffer buffer, ResolvableType targetType, @Nullable MimeType mimeType, @Nullable Map hints) throws DecodingException { CompletableFuture future = decodeToMono(Mono.just(buffer), targetType, mimeType, hints).toFuture(); - Assert.state(future.isDone(), "DataBuffer decoding should have completed."); + Assert.state(future.isDone(), "DataBuffer decoding should have completed"); - Throwable failure; try { return future.get(); } catch (ExecutionException ex) { - failure = ex.getCause(); + Throwable cause = ex.getCause(); + throw (cause instanceof CodecException ? (CodecException) cause : + new DecodingException("Failed to decode: " + (cause != null ? cause.getMessage() : ex), cause)); } catch (InterruptedException ex) { - failure = ex; + throw new DecodingException("Interrupted during decode", ex); } - throw (failure instanceof CodecException ? (CodecException) failure : - new DecodingException("Failed to decode: " + failure.getMessage(), failure)); } /** diff --git a/spring-core/src/main/java/org/springframework/core/codec/ResourceDecoder.java b/spring-core/src/main/java/org/springframework/core/codec/ResourceDecoder.java index 4e9552a650a0..8b176c3c55e1 100644 --- a/spring-core/src/main/java/org/springframework/core/codec/ResourceDecoder.java +++ b/spring-core/src/main/java/org/springframework/core/codec/ResourceDecoder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -76,10 +76,11 @@ public Resource decode(DataBuffer dataBuffer, ResolvableType elementType, } Class clazz = elementType.toClass(); - String filename = hints != null ? (String) hints.get(FILENAME_HINT) : null; + String filename = (hints != null ? (String) hints.get(FILENAME_HINT) : null); if (clazz == InputStreamResource.class) { return new InputStreamResource(new ByteArrayInputStream(bytes)) { @Override + @Nullable public String getFilename() { return filename; } @@ -92,6 +93,7 @@ public long contentLength() { else if (Resource.class.isAssignableFrom(clazz)) { return new ByteArrayResource(bytes) { @Override + @Nullable public String getFilename() { return filename; } diff --git a/spring-core/src/main/java/org/springframework/core/convert/ConversionFailedException.java b/spring-core/src/main/java/org/springframework/core/convert/ConversionFailedException.java index 8cb7b4ee11db..9f47c8317578 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/ConversionFailedException.java +++ b/spring-core/src/main/java/org/springframework/core/convert/ConversionFailedException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2023 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. @@ -49,7 +49,7 @@ public ConversionFailedException(@Nullable TypeDescriptor sourceType, TypeDescri @Nullable Object value, Throwable cause) { super("Failed to convert from type [" + sourceType + "] to type [" + targetType + - "] for value '" + ObjectUtils.nullSafeToString(value) + "'", cause); + "] for value [" + ObjectUtils.nullSafeConciseToString(value) + "]", cause); this.sourceType = sourceType; this.targetType = targetType; this.value = value; diff --git a/spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java b/spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java index 47238dda7192..7a2ce5752cb4 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java +++ b/spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-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. @@ -52,8 +52,6 @@ @SuppressWarnings("serial") public class TypeDescriptor implements Serializable { - private static final Annotation[] EMPTY_ANNOTATION_ARRAY = new Annotation[0]; - private static final Map, TypeDescriptor> commonTypesCache = new HashMap<>(32); private static final Class[] CACHED_COMMON_TYPES = { @@ -84,7 +82,7 @@ public class TypeDescriptor implements Serializable { public TypeDescriptor(MethodParameter methodParameter) { this.resolvableType = ResolvableType.forMethodParameter(methodParameter); this.type = this.resolvableType.resolve(methodParameter.getNestedParameterType()); - this.annotatedElement = new AnnotatedElementAdapter(methodParameter.getParameterIndex() == -1 ? + this.annotatedElement = AnnotatedElementAdapter.from(methodParameter.getParameterIndex() == -1 ? methodParameter.getMethodAnnotations() : methodParameter.getParameterAnnotations()); } @@ -96,7 +94,7 @@ public TypeDescriptor(MethodParameter methodParameter) { public TypeDescriptor(Field field) { this.resolvableType = ResolvableType.forField(field); this.type = this.resolvableType.resolve(field.getType()); - this.annotatedElement = new AnnotatedElementAdapter(field.getAnnotations()); + this.annotatedElement = AnnotatedElementAdapter.from(field.getAnnotations()); } /** @@ -109,7 +107,7 @@ public TypeDescriptor(Property property) { Assert.notNull(property, "Property must not be null"); this.resolvableType = ResolvableType.forMethodParameter(property.getMethodParameter()); this.type = this.resolvableType.resolve(property.getType()); - this.annotatedElement = new AnnotatedElementAdapter(property.getAnnotations()); + this.annotatedElement = AnnotatedElementAdapter.from(property.getAnnotations()); } /** @@ -125,7 +123,7 @@ public TypeDescriptor(Property property) { public TypeDescriptor(ResolvableType resolvableType, @Nullable Class type, @Nullable Annotation[] annotations) { this.resolvableType = resolvableType; this.type = (type != null ? type : resolvableType.toClass()); - this.annotatedElement = new AnnotatedElementAdapter(annotations); + this.annotatedElement = AnnotatedElementAdapter.from(annotations); } @@ -476,7 +474,7 @@ else if (isMap()) { ObjectUtils.nullSafeEquals(getMapValueTypeDescriptor(), otherDesc.getMapValueTypeDescriptor())); } else { - return true; + return Arrays.equals(getResolvableType().getGenerics(), otherDesc.getResolvableType().getGenerics()); } } @@ -523,7 +521,7 @@ public String toString() { /** * Create a new type descriptor for an object. *

    Use this factory method to introspect a source object before asking the - * conversion system to convert it to some another type. + * conversion system to convert it to some other type. *

    If the provided object is {@code null}, returns {@code null}, else calls * {@link #valueOf(Class)} to build a TypeDescriptor from the object's class. * @param source the source object @@ -734,18 +732,26 @@ private static TypeDescriptor getRelatedIfResolvable(TypeDescriptor source, Reso * @see AnnotatedElementUtils#isAnnotated(AnnotatedElement, Class) * @see AnnotatedElementUtils#getMergedAnnotation(AnnotatedElement, Class) */ - private class AnnotatedElementAdapter implements AnnotatedElement, Serializable { + private static final class AnnotatedElementAdapter implements AnnotatedElement, Serializable { + + private static final AnnotatedElementAdapter EMPTY = new AnnotatedElementAdapter(new Annotation[0]); - @Nullable private final Annotation[] annotations; - public AnnotatedElementAdapter(@Nullable Annotation[] annotations) { + private AnnotatedElementAdapter(Annotation[] annotations) { this.annotations = annotations; } + private static AnnotatedElementAdapter from(@Nullable Annotation[] annotations) { + if (annotations == null || annotations.length == 0) { + return EMPTY; + } + return new AnnotatedElementAdapter(annotations); + } + @Override public boolean isAnnotationPresent(Class annotationClass) { - for (Annotation annotation : getAnnotations()) { + for (Annotation annotation : this.annotations) { if (annotation.annotationType() == annotationClass) { return true; } @@ -757,7 +763,7 @@ public boolean isAnnotationPresent(Class annotationClass) @Nullable @SuppressWarnings("unchecked") public T getAnnotation(Class annotationClass) { - for (Annotation annotation : getAnnotations()) { + for (Annotation annotation : this.annotations) { if (annotation.annotationType() == annotationClass) { return (T) annotation; } @@ -767,7 +773,7 @@ public T getAnnotation(Class annotationClass) { @Override public Annotation[] getAnnotations() { - return (this.annotations != null ? this.annotations.clone() : EMPTY_ANNOTATION_ARRAY); + return (isEmpty() ? this.annotations : this.annotations.clone()); } @Override @@ -776,7 +782,7 @@ public Annotation[] getDeclaredAnnotations() { } public boolean isEmpty() { - return ObjectUtils.isEmpty(this.annotations); + return (this.annotations.length == 0); } @Override @@ -792,7 +798,7 @@ public int hashCode() { @Override public String toString() { - return TypeDescriptor.this.toString(); + return Arrays.toString(this.annotations); } } diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/ArrayToArrayConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/ArrayToArrayConverter.java index a51144741ca9..5af4bd785c80 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/support/ArrayToArrayConverter.java +++ b/spring-core/src/main/java/org/springframework/core/convert/support/ArrayToArrayConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-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. @@ -34,6 +34,7 @@ * * @author Keith Donald * @author Phillip Webb + * @author Sam Brannen * @since 3.0 */ final class ArrayToArrayConverter implements ConditionalGenericConverter { @@ -64,7 +65,7 @@ public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { if (this.conversionService instanceof GenericConversionService) { TypeDescriptor targetElement = targetType.getElementTypeDescriptor(); - if (targetElement != null && + if (targetElement != null && targetType.getType().isInstance(source) && ((GenericConversionService) this.conversionService).canBypassConvert( sourceType.getElementTypeDescriptor(), targetElement)) { return source; diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/GenericConversionService.java b/spring-core/src/main/java/org/springframework/core/convert/support/GenericConversionService.java index 6065d4685247..5cff48af2101 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/support/GenericConversionService.java +++ b/spring-core/src/main/java/org/springframework/core/convert/support/GenericConversionService.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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. @@ -205,8 +205,7 @@ public Object convert(@Nullable Object source, @Nullable TypeDescriptor sourceTy * @param targetType the target type * @return the converted value * @throws ConversionException if a conversion exception occurred - * @throws IllegalArgumentException if targetType is {@code null}, - * or sourceType is {@code null} but source is not {@code null} + * @throws IllegalArgumentException if targetType is {@code null} */ @Nullable public Object convert(@Nullable Object source, TypeDescriptor targetType) { diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/ObjectToObjectConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/ObjectToObjectConverter.java index cef40a25ee5b..e1405abcf9ee 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/support/ObjectToObjectConverter.java +++ b/spring-core/src/main/java/org/springframework/core/convert/support/ObjectToObjectConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-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. @@ -138,7 +138,7 @@ static boolean hasConversionMethodOrConstructor(Class targetClass, Class s @Nullable private static Executable getValidatedExecutable(Class targetClass, Class sourceClass) { Executable executable = conversionExecutableCache.get(targetClass); - if (isApplicable(executable, sourceClass)) { + if (executable != null && isApplicable(executable, sourceClass)) { return executable; } diff --git a/spring-core/src/main/java/org/springframework/core/env/CommandLinePropertySource.java b/spring-core/src/main/java/org/springframework/core/env/CommandLinePropertySource.java index 0f80080d1d6b..c317f5e2dc72 100644 --- a/spring-core/src/main/java/org/springframework/core/env/CommandLinePropertySource.java +++ b/spring-core/src/main/java/org/springframework/core/env/CommandLinePropertySource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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. @@ -252,7 +252,7 @@ public void setNonOptionArgsPropertyName(String nonOptionArgsPropertyName) { @Override public final boolean containsProperty(String name) { if (this.nonOptionArgsPropertyName.equals(name)) { - return !this.getNonOptionArgs().isEmpty(); + return !getNonOptionArgs().isEmpty(); } return this.containsOption(name); } @@ -270,7 +270,7 @@ public final boolean containsProperty(String name) { @Nullable public final String getProperty(String name) { if (this.nonOptionArgsPropertyName.equals(name)) { - Collection nonOptionArguments = this.getNonOptionArgs(); + Collection nonOptionArguments = getNonOptionArgs(); if (nonOptionArguments.isEmpty()) { return null; } @@ -278,7 +278,7 @@ public final String getProperty(String name) { return StringUtils.collectionToCommaDelimitedString(nonOptionArguments); } } - Collection optionValues = this.getOptionValues(name); + Collection optionValues = getOptionValues(name); if (optionValues == null) { return null; } diff --git a/spring-core/src/main/java/org/springframework/core/env/Environment.java b/spring-core/src/main/java/org/springframework/core/env/Environment.java index 5a9c0348162e..02dcf34f6d96 100644 --- a/spring-core/src/main/java/org/springframework/core/env/Environment.java +++ b/spring-core/src/main/java/org/springframework/core/env/Environment.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2023 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. @@ -57,6 +57,8 @@ * of property sources prior to application context {@code refresh()}. * * @author Chris Beams + * @author Phillip Webb + * @author Sam Brannen * @since 3.1 * @see PropertyResolver * @see EnvironmentCapable @@ -95,25 +97,56 @@ public interface Environment extends PropertyResolver { String[] getDefaultProfiles(); /** - * Return whether one or more of the given profiles is active or, in the case of no - * explicit active profiles, whether one or more of the given profiles is included in - * the set of default profiles. If a profile begins with '!' the logic is inverted, - * i.e. the method will return {@code true} if the given profile is not active. - * For example, {@code env.acceptsProfiles("p1", "!p2")} will return {@code true} if - * profile 'p1' is active or 'p2' is not active. - * @throws IllegalArgumentException if called with zero arguments - * or if any profile is {@code null}, empty, or whitespace only + * Determine whether one of the given profile expressions matches the + * {@linkplain #getActiveProfiles() active profiles} — or in the case + * of no explicit active profiles, whether one of the given profile expressions + * matches the {@linkplain #getDefaultProfiles() default profiles}. + *

    Profile expressions allow for complex, boolean profile logic to be + * expressed — for example {@code "p1 & p2"}, {@code "(p1 & p2) | p3"}, + * etc. See {@link Profiles#of(String...)} for details on the supported + * expression syntax. + *

    This method is a convenient shortcut for + * {@code env.acceptsProfiles(Profiles.of(profileExpressions))}. + * @since 5.3.28 + * @see Profiles#of(String...) + * @see #acceptsProfiles(Profiles) + */ + default boolean matchesProfiles(String... profileExpressions) { + return acceptsProfiles(Profiles.of(profileExpressions)); + } + + /** + * Determine whether one or more of the given profiles is active — or + * in the case of no explicit {@linkplain #getActiveProfiles() active profiles}, + * whether one or more of the given profiles is included in the set of + * {@linkplain #getDefaultProfiles() default profiles}. + *

    If a profile begins with '!' the logic is inverted, meaning this method + * will return {@code true} if the given profile is not active. For + * example, {@code env.acceptsProfiles("p1", "!p2")} will return {@code true} + * if profile 'p1' is active or 'p2' is not active. + * @throws IllegalArgumentException if called with a {@code null} array, an + * empty array, zero arguments or if any profile is {@code null}, empty, or + * whitespace only * @see #getActiveProfiles * @see #getDefaultProfiles + * @see #matchesProfiles(String...) * @see #acceptsProfiles(Profiles) - * @deprecated as of 5.1 in favor of {@link #acceptsProfiles(Profiles)} + * @deprecated as of 5.1 in favor of {@link #acceptsProfiles(Profiles)} or + * {@link #matchesProfiles(String...)} */ @Deprecated boolean acceptsProfiles(String... profiles); /** - * Return whether the {@linkplain #getActiveProfiles() active profiles} - * match the given {@link Profiles} predicate. + * Determine whether the given {@link Profiles} predicate matches the + * {@linkplain #getActiveProfiles() active profiles} — or in the case + * of no explicit active profiles, whether the given {@code Profiles} predicate + * matches the {@linkplain #getDefaultProfiles() default profiles}. + *

    If you wish provide profile expressions directly as strings, use + * {@link #matchesProfiles(String...)} instead. + * @since 5.1 + * @see #matchesProfiles(String...) + * @see Profiles#of(String...) */ boolean acceptsProfiles(Profiles profiles); diff --git a/spring-core/src/main/java/org/springframework/core/env/Profiles.java b/spring-core/src/main/java/org/springframework/core/env/Profiles.java index 7f4fba7073cb..340bb2b181cf 100644 --- a/spring-core/src/main/java/org/springframework/core/env/Profiles.java +++ b/spring-core/src/main/java/org/springframework/core/env/Profiles.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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. @@ -43,32 +43,32 @@ public interface Profiles { /** * Create a new {@link Profiles} instance that checks for matches against - * the given profile strings. + * the given profile expressions. *

    The returned instance will {@linkplain Profiles#matches(Predicate) match} - * if any one of the given profile strings matches. - *

    A profile string may contain a simple profile name (for example - * {@code "production"}) or a profile expression. A profile expression allows + * if any one of the given profile expressions matches. + *

    A profile expression may contain a simple profile name (for example + * {@code "production"}) or a compound expression. A compound expression allows * for more complicated profile logic to be expressed, for example * {@code "production & cloud"}. *

    The following operators are supported in profile expressions. *

    *

    Please note that the {@code &} and {@code |} operators may not be mixed - * without using parentheses. For example {@code "a & b | c"} is not a valid - * expression; it must be expressed as {@code "(a & b) | c"} or + * without using parentheses. For example, {@code "a & b | c"} is not a valid + * expression: it must be expressed as {@code "(a & b) | c"} or * {@code "a & (b | c)"}. *

    As of Spring Framework 5.1.17, two {@code Profiles} instances returned * by this method are considered equivalent to each other (in terms of * {@code equals()} and {@code hashCode()} semantics) if they are created - * with identical profile strings. - * @param profiles the profile strings to include + * with identical profile expressions. + * @param profileExpressions the profile expressions to include * @return a new {@link Profiles} instance */ - static Profiles of(String... profiles) { - return ProfilesParser.parse(profiles); + static Profiles of(String... profileExpressions) { + return ProfilesParser.parse(profileExpressions); } } diff --git a/spring-core/src/main/java/org/springframework/core/env/ProfilesParser.java b/spring-core/src/main/java/org/springframework/core/env/ProfilesParser.java index 5d4dd0c86b41..9b666f91ed61 100644 --- a/spring-core/src/main/java/org/springframework/core/env/ProfilesParser.java +++ b/spring-core/src/main/java/org/springframework/core/env/ProfilesParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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. @@ -43,7 +43,7 @@ private ProfilesParser() { static Profiles parse(String... expressions) { - Assert.notEmpty(expressions, "Must specify at least one profile"); + Assert.notEmpty(expressions, "Must specify at least one profile expression"); Profiles[] parsed = new Profiles[expressions.length]; for (int i = 0; i < expressions.length; i++) { parsed[i] = parseExpression(expressions[i]); @@ -71,8 +71,8 @@ private static Profiles parseTokens(String expression, StringTokenizer tokens, C } switch (token) { case "(": - Profiles contents = parseTokens(expression, tokens, Context.BRACKET); - if (context == Context.INVERT) { + Profiles contents = parseTokens(expression, tokens, Context.PARENTHESIS); + if (context == Context.NEGATE) { return contents; } elements.add(contents); @@ -86,11 +86,11 @@ private static Profiles parseTokens(String expression, StringTokenizer tokens, C operator = Operator.OR; break; case "!": - elements.add(not(parseTokens(expression, tokens, Context.INVERT))); + elements.add(not(parseTokens(expression, tokens, Context.NEGATE))); break; case ")": Profiles merged = merge(expression, elements, operator); - if (context == Context.BRACKET) { + if (context == Context.PARENTHESIS) { return merged; } elements.clear(); @@ -99,7 +99,7 @@ private static Profiles parseTokens(String expression, StringTokenizer tokens, C break; default: Profiles value = equals(token); - if (context == Context.INVERT) { + if (context == Context.NEGATE) { return value; } elements.add(value); @@ -137,15 +137,14 @@ private static Profiles equals(String profile) { return activeProfile -> activeProfile.test(profile); } - private static Predicate isMatch(Predicate activeProfile) { - return profiles -> profiles.matches(activeProfile); + private static Predicate isMatch(Predicate activeProfiles) { + return profiles -> profiles.matches(activeProfiles); } - private enum Operator {AND, OR} + private enum Operator { AND, OR } - - private enum Context {NONE, INVERT, BRACKET} + private enum Context { NONE, NEGATE, PARENTHESIS } private static class ParsedProfiles implements Profiles { @@ -169,31 +168,27 @@ public boolean matches(Predicate activeProfiles) { return false; } - @Override - public int hashCode() { - return this.expressions.hashCode(); - } - @Override public boolean equals(Object obj) { if (this == obj) { return true; } - if (obj == null) { - return false; - } - if (getClass() != obj.getClass()) { + if (obj == null || getClass() != obj.getClass()) { return false; } ParsedProfiles that = (ParsedProfiles) obj; return this.expressions.equals(that.expressions); } + @Override + public int hashCode() { + return this.expressions.hashCode(); + } + @Override public String toString() { return StringUtils.collectionToDelimitedString(this.expressions, " or "); } - } } diff --git a/spring-core/src/main/java/org/springframework/core/env/PropertiesPropertySource.java b/spring-core/src/main/java/org/springframework/core/env/PropertiesPropertySource.java index d09c3683510e..9741c5819287 100644 --- a/spring-core/src/main/java/org/springframework/core/env/PropertiesPropertySource.java +++ b/spring-core/src/main/java/org/springframework/core/env/PropertiesPropertySource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -48,7 +48,7 @@ protected PropertiesPropertySource(String name, Map source) { @Override public String[] getPropertyNames() { synchronized (this.source) { - return super.getPropertyNames(); + return ((Map) this.source).keySet().stream().filter(k -> k instanceof String).toArray(String[]::new); } } diff --git a/spring-core/src/main/java/org/springframework/core/io/ClassPathResource.java b/spring-core/src/main/java/org/springframework/core/io/ClassPathResource.java index f99adce06ce4..9d207a431f90 100644 --- a/spring-core/src/main/java/org/springframework/core/io/ClassPathResource.java +++ b/spring-core/src/main/java/org/springframework/core/io/ClassPathResource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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. @@ -33,13 +33,15 @@ * *

    Supports resolution as {@code java.io.File} if the class path * resource resides in the file system, but not for resources in a JAR. - * Always supports resolution as URL. + * Always supports resolution as {@code java.net.URL}. * * @author Juergen Hoeller * @author Sam Brannen * @since 28.12.2003 * @see ClassLoader#getResourceAsStream(String) + * @see ClassLoader#getResource(String) * @see Class#getResourceAsStream(String) + * @see Class#getResource(String) */ public class ClassPathResource extends AbstractFileResolvingResource { @@ -124,7 +126,7 @@ public final String getPath() { } /** - * Return the ClassLoader that this resource will be obtained from. + * Return the {@link ClassLoader} that this resource will be obtained from. */ @Nullable public final ClassLoader getClassLoader() { @@ -134,8 +136,8 @@ public final ClassLoader getClassLoader() { /** * This implementation checks for the resolution of a resource URL. - * @see java.lang.ClassLoader#getResource(String) - * @see java.lang.Class#getResource(String) + * @see ClassLoader#getResource(String) + * @see Class#getResource(String) */ @Override public boolean exists() { @@ -145,8 +147,8 @@ public boolean exists() { /** * This implementation checks for the resolution of a resource URL upfront, * then proceeding with {@link AbstractFileResolvingResource}'s length check. - * @see java.lang.ClassLoader#getResource(String) - * @see java.lang.Class#getResource(String) + * @see ClassLoader#getResource(String) + * @see Class#getResource(String) */ @Override public boolean isReadable() { @@ -179,9 +181,11 @@ else if (this.classLoader != null) { } /** - * This implementation opens an InputStream for the given class path resource. - * @see java.lang.ClassLoader#getResourceAsStream(String) - * @see java.lang.Class#getResourceAsStream(String) + * This implementation opens an {@link InputStream} for the underlying class + * path resource, if available. + * @see ClassLoader#getResourceAsStream(String) + * @see Class#getResourceAsStream(String) + * @see ClassLoader#getSystemResourceAsStream(String) */ @Override public InputStream getInputStream() throws IOException { @@ -204,8 +208,8 @@ else if (this.classLoader != null) { /** * This implementation returns a URL for the underlying class path resource, * if available. - * @see java.lang.ClassLoader#getResource(String) - * @see java.lang.Class#getResource(String) + * @see ClassLoader#getResource(String) + * @see Class#getResource(String) */ @Override public URL getURL() throws IOException { @@ -217,9 +221,9 @@ public URL getURL() throws IOException { } /** - * This implementation creates a ClassPathResource, applying the given path - * relative to the path of the underlying resource of this descriptor. - * @see org.springframework.util.StringUtils#applyRelativePath(String, String) + * This implementation creates a {@code ClassPathResource}, applying the given + * path relative to the path used to create this descriptor. + * @see StringUtils#applyRelativePath(String, String) */ @Override public Resource createRelative(String relativePath) { @@ -231,7 +235,7 @@ public Resource createRelative(String relativePath) { /** * This implementation returns the name of the file that this class path * resource refers to. - * @see org.springframework.util.StringUtils#getFilename(String) + * @see StringUtils#getFilename(String) */ @Override @Nullable @@ -277,8 +281,7 @@ public boolean equals(@Nullable Object other) { } /** - * This implementation returns the hash code of the underlying - * class path location. + * This implementation returns the hash code of the underlying class path location. */ @Override public int hashCode() { diff --git a/spring-core/src/main/java/org/springframework/core/io/FileSystemResource.java b/spring-core/src/main/java/org/springframework/core/io/FileSystemResource.java index feb2f9067da3..cb17bf55bb95 100644 --- a/spring-core/src/main/java/org/springframework/core/io/FileSystemResource.java +++ b/spring-core/src/main/java/org/springframework/core/io/FileSystemResource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 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. @@ -158,6 +158,7 @@ public final String getPath() { /** * This implementation returns whether the underlying file exists. * @see java.io.File#exists() + * @see java.nio.file.Files#exists(Path, java.nio.file.LinkOption...) */ @Override public boolean exists() { @@ -169,6 +170,8 @@ public boolean exists() { * (and corresponds to an actual file with content, not to a directory). * @see java.io.File#canRead() * @see java.io.File#isDirectory() + * @see java.nio.file.Files#isReadable(Path) + * @see java.nio.file.Files#isDirectory(Path, java.nio.file.LinkOption...) */ @Override public boolean isReadable() { @@ -177,8 +180,8 @@ public boolean isReadable() { } /** - * This implementation opens a NIO file stream for the underlying file. - * @see java.io.FileInputStream + * This implementation opens an NIO file stream for the underlying file. + * @see java.nio.file.Files#newInputStream(Path, java.nio.file.OpenOption...) */ @Override public InputStream getInputStream() throws IOException { @@ -195,6 +198,8 @@ public InputStream getInputStream() throws IOException { * (and corresponds to an actual file with content, not to a directory). * @see java.io.File#canWrite() * @see java.io.File#isDirectory() + * @see java.nio.file.Files#isWritable(Path) + * @see java.nio.file.Files#isDirectory(Path, java.nio.file.LinkOption...) */ @Override public boolean isWritable() { @@ -204,7 +209,7 @@ public boolean isWritable() { /** * This implementation opens a FileOutputStream for the underlying file. - * @see java.io.FileOutputStream + * @see java.nio.file.Files#newOutputStream(Path, java.nio.file.OpenOption...) */ @Override public OutputStream getOutputStream() throws IOException { @@ -214,6 +219,7 @@ public OutputStream getOutputStream() throws IOException { /** * This implementation returns a URL for the underlying file. * @see java.io.File#toURI() + * @see java.nio.file.Path#toUri() */ @Override public URL getURL() throws IOException { @@ -223,6 +229,7 @@ public URL getURL() throws IOException { /** * This implementation returns a URI for the underlying file. * @see java.io.File#toURI() + * @see java.nio.file.Path#toUri() */ @Override public URI getURI() throws IOException { @@ -324,6 +331,7 @@ public Resource createRelative(String relativePath) { /** * This implementation returns the name of the file. * @see java.io.File#getName() + * @see java.nio.file.Path#getFileName() */ @Override public String getFilename() { @@ -334,6 +342,7 @@ public String getFilename() { * This implementation returns a description that includes the absolute * path of the file. * @see java.io.File#getAbsolutePath() + * @see java.nio.file.Path#toAbsolutePath() */ @Override public String getDescription() { @@ -342,7 +351,7 @@ public String getDescription() { /** - * This implementation compares the underlying File references. + * This implementation compares the underlying file paths. */ @Override public boolean equals(@Nullable Object other) { @@ -351,7 +360,7 @@ public boolean equals(@Nullable Object other) { } /** - * This implementation returns the hash code of the underlying File reference. + * This implementation returns the hash code of the underlying file path. */ @Override public int hashCode() { diff --git a/spring-core/src/main/java/org/springframework/core/io/PathResource.java b/spring-core/src/main/java/org/springframework/core/io/PathResource.java index 6412f001a2b4..5ca0fc7c27f4 100644 --- a/spring-core/src/main/java/org/springframework/core/io/PathResource.java +++ b/spring-core/src/main/java/org/springframework/core/io/PathResource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 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. @@ -61,7 +61,7 @@ public class PathResource extends AbstractResource implements WritableResource { /** - * Create a new PathResource from a Path handle. + * Create a new {@code PathResource} from a {@link Path} handle. *

    Note: Unlike {@link FileSystemResource}, when building relative resources * via {@link #createRelative}, the relative path will be built underneath * the given root: e.g. Paths.get("C:/dir1/"), relative path "dir2" → "C:/dir1/dir2"! @@ -73,7 +73,7 @@ public PathResource(Path path) { } /** - * Create a new PathResource from a Path handle. + * Create a new {@code PathResource} from a path string. *

    Note: Unlike {@link FileSystemResource}, when building relative resources * via {@link #createRelative}, the relative path will be built underneath * the given root: e.g. Paths.get("C:/dir1/"), relative path "dir2" → "C:/dir1/dir2"! @@ -86,7 +86,7 @@ public PathResource(String path) { } /** - * Create a new PathResource from a Path handle. + * Create a new {@code PathResource} from a {@link URI}. *

    Note: Unlike {@link FileSystemResource}, when building relative resources * via {@link #createRelative}, the relative path will be built underneath * the given root: e.g. Paths.get("C:/dir1/"), relative path "dir2" → "C:/dir1/dir2"! @@ -127,7 +127,7 @@ public boolean isReadable() { } /** - * This implementation opens a InputStream for the underlying file. + * This implementation opens an {@link InputStream} for the underlying file. * @see java.nio.file.spi.FileSystemProvider#newInputStream(Path, OpenOption...) */ @Override @@ -153,7 +153,7 @@ public boolean isWritable() { } /** - * This implementation opens a OutputStream for the underlying file. + * This implementation opens an {@link OutputStream} for the underlying file. * @see java.nio.file.spi.FileSystemProvider#newOutputStream(Path, OpenOption...) */ @Override @@ -165,7 +165,7 @@ public OutputStream getOutputStream() throws IOException { } /** - * This implementation returns a URL for the underlying file. + * This implementation returns a {@link URL} for the underlying file. * @see java.nio.file.Path#toUri() * @see java.net.URI#toURL() */ @@ -175,7 +175,7 @@ public URL getURL() throws IOException { } /** - * This implementation returns a URI for the underlying file. + * This implementation returns a {@link URI} for the underlying file. * @see java.nio.file.Path#toUri() */ @Override @@ -192,7 +192,7 @@ public boolean isFile() { } /** - * This implementation returns the underlying File reference. + * This implementation returns the underlying {@link File} reference. */ @Override public File getFile() throws IOException { @@ -207,7 +207,7 @@ public File getFile() throws IOException { } /** - * This implementation opens a Channel for the underlying file. + * This implementation opens a {@link ReadableByteChannel} for the underlying file. * @see Files#newByteChannel(Path, OpenOption...) */ @Override @@ -221,7 +221,7 @@ public ReadableByteChannel readableChannel() throws IOException { } /** - * This implementation opens a Channel for the underlying file. + * This implementation opens a {@link WritableByteChannel} for the underlying file. * @see Files#newByteChannel(Path, OpenOption...) */ @Override @@ -238,7 +238,7 @@ public long contentLength() throws IOException { } /** - * This implementation returns the underlying File's timestamp. + * This implementation returns the underlying file's timestamp. * @see java.nio.file.Files#getLastModifiedTime(Path, java.nio.file.LinkOption...) */ @Override @@ -249,7 +249,7 @@ public long lastModified() throws IOException { } /** - * This implementation creates a PathResource, applying the given path + * This implementation creates a {@link PathResource}, applying the given path * relative to the path of the underlying file of this resource descriptor. * @see java.nio.file.Path#resolve(String) */ @@ -274,7 +274,7 @@ public String getDescription() { /** - * This implementation compares the underlying Path references. + * This implementation compares the underlying {@link Path} references. */ @Override public boolean equals(@Nullable Object other) { @@ -283,7 +283,7 @@ public boolean equals(@Nullable Object other) { } /** - * This implementation returns the hash code of the underlying Path reference. + * This implementation returns the hash code of the underlying {@link Path} reference. */ @Override public int hashCode() { diff --git a/spring-core/src/main/java/org/springframework/core/io/buffer/DataBufferUtils.java b/spring-core/src/main/java/org/springframework/core/io/buffer/DataBufferUtils.java index 7bb0cc910548..c9b9a9a9f942 100644 --- a/spring-core/src/main/java/org/springframework/core/io/buffer/DataBufferUtils.java +++ b/spring-core/src/main/java/org/springframework/core/io/buffer/DataBufferUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-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. @@ -62,7 +62,7 @@ */ public abstract class DataBufferUtils { - private final static Log logger = LogFactory.getLog(DataBufferUtils.class); + private static final Log logger = LogFactory.getLog(DataBufferUtils.class); private static final Consumer RELEASE_CONSUMER = DataBufferUtils::release; @@ -728,7 +728,7 @@ private interface NestedMatcher extends Matcher { */ private static class SingleByteMatcher implements NestedMatcher { - static SingleByteMatcher NEWLINE_MATCHER = new SingleByteMatcher(new byte[] {10}); + static final SingleByteMatcher NEWLINE_MATCHER = new SingleByteMatcher(new byte[] {10}); private final byte[] delimiter; @@ -767,7 +767,7 @@ public void reset() { /** * Base class for a {@link NestedMatcher}. */ - private static abstract class AbstractNestedMatcher implements NestedMatcher { + private abstract static class AbstractNestedMatcher implements NestedMatcher { private final byte[] delimiter; @@ -1005,11 +1005,11 @@ public void completed(Integer read, DataBuffer dataBuffer) { } @Override - public void failed(Throwable exc, DataBuffer dataBuffer) { + public void failed(Throwable ex, DataBuffer dataBuffer) { release(dataBuffer); closeChannel(this.channel); this.state.set(State.DISPOSED); - this.sink.error(exc); + this.sink.error(ex); } private enum State { @@ -1064,7 +1064,6 @@ protected void hookOnComplete() { public Context currentContext() { return Context.of(this.sink.contextView()); } - } @@ -1145,9 +1144,9 @@ else if (this.completed.get()) { } @Override - public void failed(Throwable exc, ByteBuffer byteBuffer) { + public void failed(Throwable ex, ByteBuffer byteBuffer) { sinkDataBuffer(); - this.sink.error(exc); + this.sink.error(ex); } private void sinkDataBuffer() { @@ -1161,7 +1160,6 @@ private void sinkDataBuffer() { public Context currentContext() { return Context.of(this.sink.contextView()); } - } } diff --git a/spring-core/src/main/java/org/springframework/core/io/support/PathMatchingResourcePatternResolver.java b/spring-core/src/main/java/org/springframework/core/io/support/PathMatchingResourcePatternResolver.java index 223c0529196a..6b83fd39781a 100644 --- a/spring-core/src/main/java/org/springframework/core/io/support/PathMatchingResourcePatternResolver.java +++ b/spring-core/src/main/java/org/springframework/core/io/support/PathMatchingResourcePatternResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -429,8 +429,9 @@ protected void addClassPathManifestEntries(Set result) { String filePath = new File(path).getAbsolutePath(); int prefixIndex = filePath.indexOf(':'); if (prefixIndex == 1) { - // Possibly "c:" drive prefix on Windows, to be upper-cased for proper duplicate detection - filePath = StringUtils.capitalize(filePath); + // Possibly a drive prefix on Windows (for example, "c:"), so we prepend a slash + // and convert the drive letter to uppercase for consistent duplicate detection. + filePath = "/" + StringUtils.capitalize(filePath); } // # can appear in directories/filenames, java.net.URL should not treat it as a fragment filePath = StringUtils.replace(filePath, "#", "%23"); @@ -495,7 +496,7 @@ protected Resource[] findPathMatchingResources(String locationPattern) throws IO String rootDirPath = determineRootDir(locationPattern); String subPattern = locationPattern.substring(rootDirPath.length()); Resource[] rootDirResources = getResources(rootDirPath); - Set result = new LinkedHashSet<>(16); + Set result = new LinkedHashSet<>(64); for (Resource rootDirResource : rootDirResources) { rootDirResource = resolveRootDirResource(rootDirResource); URL rootDirUrl = rootDirResource.getURL(); @@ -647,7 +648,7 @@ protected Set doFindPathMatchingJarResources(Resource rootDirResource, // The Sun JRE does not return a slash here, but BEA JRockit does. rootEntryPath = rootEntryPath + "/"; } - Set result = new LinkedHashSet<>(8); + Set result = new LinkedHashSet<>(64); for (Enumeration entries = jarFile.entries(); entries.hasMoreElements();) { JarEntry entry = entries.nextElement(); String entryPath = entry.getName(); @@ -863,7 +864,7 @@ private static class PatternVirtualFileVisitor implements InvocationHandler { private final String rootPath; - private final Set resources = new LinkedHashSet<>(); + private final Set resources = new LinkedHashSet<>(64); public PatternVirtualFileVisitor(String rootPath, String subPattern, PathMatcher pathMatcher) { this.subPattern = subPattern; @@ -894,7 +895,6 @@ else if ("visit".equals(methodName)) { else if ("toString".equals(methodName)) { return toString(); } - throw new IllegalStateException("Unexpected method invocation: " + method); } diff --git a/spring-core/src/main/java/org/springframework/core/io/support/ResourcePropertiesPersister.java b/spring-core/src/main/java/org/springframework/core/io/support/ResourcePropertiesPersister.java index 1388c1c175a2..112776baeba6 100644 --- a/spring-core/src/main/java/org/springframework/core/io/support/ResourcePropertiesPersister.java +++ b/spring-core/src/main/java/org/springframework/core/io/support/ResourcePropertiesPersister.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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,6 +30,8 @@ * "spring.xml.ignore" property. * *

    This is the standard implementation used in Spring's resource support. + * Only intended for internal use within the framework. For other purposes, + * use its base class {@link DefaultPropertiesPersister} instead. * * @author Juergen Hoeller * @author Sebastien Deleuze @@ -40,7 +42,6 @@ public class ResourcePropertiesPersister extends DefaultPropertiesPersister { /** * A convenient constant for a default {@code ResourcePropertiesPersister} instance, * as used in Spring's common resource support. - * @since 5.3 */ public static final ResourcePropertiesPersister INSTANCE = new ResourcePropertiesPersister(); diff --git a/spring-core/src/main/java/org/springframework/core/log/LogFormatUtils.java b/spring-core/src/main/java/org/springframework/core/log/LogFormatUtils.java index 550a202255c2..786680bb2a3a 100644 --- a/spring-core/src/main/java/org/springframework/core/log/LogFormatUtils.java +++ b/spring-core/src/main/java/org/springframework/core/log/LogFormatUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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 org.springframework.lang.Nullable; import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; /** * Utility methods for formatting and logging messages. @@ -78,7 +79,7 @@ public static String formatValue( result = ObjectUtils.nullSafeToString(ex); } if (maxLength != -1) { - result = (result.length() > maxLength ? result.substring(0, maxLength) + " (truncated)..." : result); + result = StringUtils.truncate(result, maxLength); } if (replaceNewlinesAndControlCharacters) { result = NEWLINE_PATTERN.matcher(result).replaceAll(""); diff --git a/spring-core/src/main/java/org/springframework/core/log/LogMessage.java b/spring-core/src/main/java/org/springframework/core/log/LogMessage.java index ee7dd65f2814..be325db7bfd9 100644 --- a/spring-core/src/main/java/org/springframework/core/log/LogMessage.java +++ b/spring-core/src/main/java/org/springframework/core/log/LogMessage.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 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,12 +22,13 @@ import org.springframework.util.Assert; /** - * A simple log message type for use with Commons Logging, allowing - * for convenient lazy resolution of a given {@link Supplier} instance - * (typically bound to a Java 8 lambda expression) or a printf-style - * format string ({@link String#format}) in its {@link #toString()}. + * A simple log message type for use with Commons Logging, allowing for convenient + * lazy resolution of a given {@link Supplier} instance (typically bound to a lambda + * expression) or a printf-style format string ({@link String#format}) in its + * {@link #toString()}. * * @author Juergen Hoeller + * @author Sebastien Deleuze * @since 5.2 * @see #of(Supplier) * @see #format(String, Object) @@ -77,7 +78,7 @@ public String toString() { /** * Build a lazily resolving message from the given supplier. - * @param supplier the supplier (typically bound to a Java 8 lambda expression) + * @param supplier the supplier (typically bound to a lambda expression) * @see #toString() */ public static LogMessage of(Supplier supplier) { @@ -87,63 +88,68 @@ public static LogMessage of(Supplier supplier) { /** * Build a lazily formatted message from the given format string and argument. * @param format the format string (following {@link String#format} rules) - * @param arg1 the argument + * @param arg1 the argument (can be {@code null}) * @see String#format(String, Object...) */ - public static LogMessage format(String format, Object arg1) { + public static LogMessage format(String format, @Nullable Object arg1) { return new FormatMessage1(format, arg1); } /** * Build a lazily formatted message from the given format string and arguments. * @param format the format string (following {@link String#format} rules) - * @param arg1 the first argument - * @param arg2 the second argument + * @param arg1 the first argument (can be {@code null}) + * @param arg2 the second argument (can be {@code null}) * @see String#format(String, Object...) */ - public static LogMessage format(String format, Object arg1, Object arg2) { + public static LogMessage format(String format, @Nullable Object arg1, @Nullable Object arg2) { return new FormatMessage2(format, arg1, arg2); } /** * Build a lazily formatted message from the given format string and arguments. * @param format the format string (following {@link String#format} rules) - * @param arg1 the first argument - * @param arg2 the second argument - * @param arg3 the third argument + * @param arg1 the first argument (can be {@code null}) + * @param arg2 the second argument (can be {@code null}) + * @param arg3 the third argument (can be {@code null}) * @see String#format(String, Object...) */ - public static LogMessage format(String format, Object arg1, Object arg2, Object arg3) { + public static LogMessage format(String format, @Nullable Object arg1, @Nullable Object arg2, @Nullable Object arg3) { return new FormatMessage3(format, arg1, arg2, arg3); } /** * Build a lazily formatted message from the given format string and arguments. * @param format the format string (following {@link String#format} rules) - * @param arg1 the first argument - * @param arg2 the second argument - * @param arg3 the third argument - * @param arg4 the fourth argument + * @param arg1 the first argument (can be {@code null}) + * @param arg2 the second argument (can be {@code null}) + * @param arg3 the third argument (can be {@code null}) + * @param arg4 the fourth argument (can be {@code null}) * @see String#format(String, Object...) */ - public static LogMessage format(String format, Object arg1, Object arg2, Object arg3, Object arg4) { + public static LogMessage format(String format, @Nullable Object arg1, @Nullable Object arg2, @Nullable Object arg3, + @Nullable Object arg4) { return new FormatMessage4(format, arg1, arg2, arg3, arg4); } /** * Build a lazily formatted message from the given format string and varargs. + *

    This varargs {@code format()} variant may be costly. You should therefore + * use the individual argument variants whenever possible: + * {@link #format(String, Object)}, {@link #format(String, Object, Object)}, etc. * @param format the format string (following {@link String#format} rules) - * @param args the varargs array (costly, prefer individual arguments) + * @param args the varargs array (can be {@code null} and can contain {@code null} + * elements) * @see String#format(String, Object...) */ - public static LogMessage format(String format, Object... args) { + public static LogMessage format(String format, @Nullable Object... args) { return new FormatMessageX(format, args); } private static final class SupplierMessage extends LogMessage { - private Supplier supplier; + private final Supplier supplier; SupplierMessage(Supplier supplier) { Assert.notNull(supplier, "Supplier must not be null"); @@ -170,9 +176,10 @@ private static abstract class FormatMessage extends LogMessage { private static final class FormatMessage1 extends FormatMessage { + @Nullable private final Object arg1; - FormatMessage1(String format, Object arg1) { + FormatMessage1(String format, @Nullable Object arg1) { super(format); this.arg1 = arg1; } @@ -186,11 +193,13 @@ protected String buildString() { private static final class FormatMessage2 extends FormatMessage { + @Nullable private final Object arg1; + @Nullable private final Object arg2; - FormatMessage2(String format, Object arg1, Object arg2) { + FormatMessage2(String format, @Nullable Object arg1, @Nullable Object arg2) { super(format); this.arg1 = arg1; this.arg2 = arg2; @@ -205,13 +214,16 @@ String buildString() { private static final class FormatMessage3 extends FormatMessage { + @Nullable private final Object arg1; + @Nullable private final Object arg2; + @Nullable private final Object arg3; - FormatMessage3(String format, Object arg1, Object arg2, Object arg3) { + FormatMessage3(String format, @Nullable Object arg1, @Nullable Object arg2, @Nullable Object arg3) { super(format); this.arg1 = arg1; this.arg2 = arg2; @@ -227,15 +239,20 @@ String buildString() { private static final class FormatMessage4 extends FormatMessage { + @Nullable private final Object arg1; + @Nullable private final Object arg2; + @Nullable private final Object arg3; + @Nullable private final Object arg4; - FormatMessage4(String format, Object arg1, Object arg2, Object arg3, Object arg4) { + FormatMessage4(String format, @Nullable Object arg1, @Nullable Object arg2, @Nullable Object arg3, + @Nullable Object arg4) { super(format); this.arg1 = arg1; this.arg2 = arg2; @@ -252,9 +269,10 @@ String buildString() { private static final class FormatMessageX extends FormatMessage { + @Nullable private final Object[] args; - FormatMessageX(String format, Object... args) { + FormatMessageX(String format, @Nullable Object... args) { super(format); this.args = args; } diff --git a/spring-core/src/main/java/org/springframework/core/metrics/DefaultApplicationStartup.java b/spring-core/src/main/java/org/springframework/core/metrics/DefaultApplicationStartup.java index 9fdb24ac9ed3..9a497d3eb9ee 100644 --- a/spring-core/src/main/java/org/springframework/core/metrics/DefaultApplicationStartup.java +++ b/spring-core/src/main/java/org/springframework/core/metrics/DefaultApplicationStartup.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-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,8 @@ import java.util.Iterator; import java.util.function.Supplier; +import org.springframework.lang.Nullable; + /** * Default "no op" {@code ApplicationStartup} implementation. * @@ -52,6 +54,7 @@ public long getId() { } @Override + @Nullable public Long getParentId() { return null; } @@ -73,7 +76,6 @@ public StartupStep tag(String key, Supplier value) { @Override public void end() { - } diff --git a/spring-core/src/main/java/org/springframework/core/metrics/jfr/FlightRecorderStartupStep.java b/spring-core/src/main/java/org/springframework/core/metrics/jfr/FlightRecorderStartupStep.java index 915a52833c9d..c3d56b757d1e 100644 --- a/spring-core/src/main/java/org/springframework/core/metrics/jfr/FlightRecorderStartupStep.java +++ b/spring-core/src/main/java/org/springframework/core/metrics/jfr/FlightRecorderStartupStep.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. @@ -21,10 +21,10 @@ import java.util.function.Supplier; import org.springframework.core.metrics.StartupStep; -import org.springframework.lang.NonNull; /** * {@link StartupStep} implementation for the Java Flight Recorder. + * *

    This variant delegates to a {@link FlightRecorderStartupEvent JFR event extension} * to collect and record data in Java Flight Recorder. * @@ -114,12 +114,12 @@ public void add(String key, Supplier value) { add(key, value.get()); } - @NonNull @Override public Iterator iterator() { return new TagsIterator(); } + private class TagsIterator implements Iterator { private int idx = 0; diff --git a/spring-core/src/main/java/org/springframework/core/serializer/DefaultDeserializer.java b/spring-core/src/main/java/org/springframework/core/serializer/DefaultDeserializer.java index 45ccaa298531..ba7d821331aa 100644 --- a/spring-core/src/main/java/org/springframework/core/serializer/DefaultDeserializer.java +++ b/spring-core/src/main/java/org/springframework/core/serializer/DefaultDeserializer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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,6 +50,7 @@ public DefaultDeserializer() { /** * Create a {@code DefaultDeserializer} for using an {@link ObjectInputStream} * with the given {@code ClassLoader}. + * @param classLoader the ClassLoader to use * @since 4.2.1 * @see ConfigurableObjectInputStream#ConfigurableObjectInputStream(InputStream, ClassLoader) */ diff --git a/spring-core/src/main/java/org/springframework/core/serializer/support/DeserializingConverter.java b/spring-core/src/main/java/org/springframework/core/serializer/support/DeserializingConverter.java index 7412907159c8..782ddc901bd5 100644 --- a/spring-core/src/main/java/org/springframework/core/serializer/support/DeserializingConverter.java +++ b/spring-core/src/main/java/org/springframework/core/serializer/support/DeserializingConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2023 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,6 +21,7 @@ import org.springframework.core.convert.converter.Converter; import org.springframework.core.serializer.DefaultDeserializer; import org.springframework.core.serializer.Deserializer; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -50,10 +51,11 @@ public DeserializingConverter() { /** * Create a {@code DeserializingConverter} for using an {@link java.io.ObjectInputStream} * with the given {@code ClassLoader}. + * @param classLoader the ClassLoader to use * @since 4.2.1 * @see DefaultDeserializer#DefaultDeserializer(ClassLoader) */ - public DeserializingConverter(ClassLoader classLoader) { + public DeserializingConverter(@Nullable ClassLoader classLoader) { this.deserializer = new DefaultDeserializer(classLoader); } diff --git a/spring-core/src/main/java/org/springframework/core/task/SimpleAsyncTaskExecutor.java b/spring-core/src/main/java/org/springframework/core/task/SimpleAsyncTaskExecutor.java index 072502a868c9..8540697a52b3 100644 --- a/spring-core/src/main/java/org/springframework/core/task/SimpleAsyncTaskExecutor.java +++ b/spring-core/src/main/java/org/springframework/core/task/SimpleAsyncTaskExecutor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -33,8 +33,8 @@ * {@link TaskExecutor} implementation that fires up a new Thread for each task, * executing it asynchronously. * - *

    Supports limiting concurrent threads through the "concurrencyLimit" - * bean property. By default, the number of concurrent threads is unlimited. + *

    Supports limiting concurrent threads through {@link #setConcurrencyLimit}. + * By default, the number of concurrent task executions is unlimited. * *

    NOTE: This implementation does not reuse threads! Consider a * thread-pooling TaskExecutor implementation instead, in particular for @@ -133,33 +133,31 @@ public final ThreadFactory getThreadFactory() { * have to cast it and call {@code Future#get} to evaluate exceptions. * @since 4.3 */ - public final void setTaskDecorator(TaskDecorator taskDecorator) { + public void setTaskDecorator(TaskDecorator taskDecorator) { this.taskDecorator = taskDecorator; } /** - * Set the maximum number of parallel accesses allowed. - * -1 indicates no concurrency limit at all. - *

    In principle, this limit can be changed at runtime, - * although it is generally designed as a config time setting. - * NOTE: Do not switch between -1 and any concrete limit at runtime, - * as this will lead to inconsistent concurrency counts: A limit - * of -1 effectively turns off concurrency counting completely. + * Set the maximum number of parallel task executions allowed. + * The default of -1 indicates no concurrency limit at all. + *

    This is the equivalent of a maximum pool size in a thread pool, + * preventing temporary overload of the thread management system. * @see #UNBOUNDED_CONCURRENCY + * @see org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor#setMaxPoolSize */ public void setConcurrencyLimit(int concurrencyLimit) { this.concurrencyThrottle.setConcurrencyLimit(concurrencyLimit); } /** - * Return the maximum number of parallel accesses allowed. + * Return the maximum number of parallel task executions allowed. */ public final int getConcurrencyLimit() { return this.concurrencyThrottle.getConcurrencyLimit(); } /** - * Return whether this throttle is currently active. + * Return whether the concurrency throttle is currently active. * @return {@code true} if the concurrency limit for this instance is active * @see #getConcurrencyLimit() * @see #setConcurrencyLimit diff --git a/spring-core/src/main/java/org/springframework/core/task/support/ExecutorServiceAdapter.java b/spring-core/src/main/java/org/springframework/core/task/support/ExecutorServiceAdapter.java index 3297ff16e11e..bfea8ffc285c 100644 --- a/spring-core/src/main/java/org/springframework/core/task/support/ExecutorServiceAdapter.java +++ b/spring-core/src/main/java/org/springframework/core/task/support/ExecutorServiceAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2023 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. @@ -90,4 +90,9 @@ public boolean isTerminated() { return false; } + // @Override on JDK 19 + public void close() { + // no-op in order to avoid container-triggered shutdown call which would lead to exception logging + } + } diff --git a/spring-core/src/main/java/org/springframework/core/type/classreading/SimpleAnnotationMetadataReadingVisitor.java b/spring-core/src/main/java/org/springframework/core/type/classreading/SimpleAnnotationMetadataReadingVisitor.java index a951194ae7dc..700fcc5138c1 100644 --- a/spring-core/src/main/java/org/springframework/core/type/classreading/SimpleAnnotationMetadataReadingVisitor.java +++ b/spring-core/src/main/java/org/springframework/core/type/classreading/SimpleAnnotationMetadataReadingVisitor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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. @@ -171,6 +171,7 @@ private boolean isInterface(int access) { return (access & Opcodes.ACC_INTERFACE) != 0; } + /** * {@link MergedAnnotation} source. */ @@ -182,11 +183,6 @@ private static final class Source { this.className = className; } - @Override - public int hashCode() { - return this.className.hashCode(); - } - @Override public boolean equals(@Nullable Object obj) { if (this == obj) { @@ -198,11 +194,15 @@ public boolean equals(@Nullable Object obj) { return this.className.equals(((Source) obj).className); } + @Override + public int hashCode() { + return this.className.hashCode(); + } + @Override public String toString() { return this.className; } - } } diff --git a/spring-core/src/main/java/org/springframework/core/type/filter/AbstractTypeHierarchyTraversingFilter.java b/spring-core/src/main/java/org/springframework/core/type/filter/AbstractTypeHierarchyTraversingFilter.java index ad8df6836628..ee7d6d5046e2 100644 --- a/spring-core/src/main/java/org/springframework/core/type/filter/AbstractTypeHierarchyTraversingFilter.java +++ b/spring-core/src/main/java/org/springframework/core/type/filter/AbstractTypeHierarchyTraversingFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -73,20 +73,20 @@ public boolean match(MetadataReader metadataReader, MetadataReaderFactory metada // Optimization to avoid creating ClassReader for superclass. Boolean superClassMatch = matchSuperClass(superClassName); if (superClassMatch != null) { - if (superClassMatch.booleanValue()) { + if (superClassMatch) { return true; } } else { // Need to read superclass to determine a match... try { - if (match(metadata.getSuperClassName(), metadataReaderFactory)) { + if (match(superClassName, metadataReaderFactory)) { return true; } } catch (IOException ex) { if (logger.isDebugEnabled()) { - logger.debug("Could not read superclass [" + metadata.getSuperClassName() + + logger.debug("Could not read superclass [" + superClassName + "] of type-filtered class [" + metadata.getClassName() + "]"); } } @@ -99,7 +99,7 @@ public boolean match(MetadataReader metadataReader, MetadataReaderFactory metada // Optimization to avoid creating ClassReader for superclass Boolean interfaceMatch = matchInterface(ifc); if (interfaceMatch != null) { - if (interfaceMatch.booleanValue()) { + if (interfaceMatch) { return true; } } diff --git a/spring-core/src/main/java/org/springframework/util/ClassUtils.java b/spring-core/src/main/java/org/springframework/util/ClassUtils.java index d5858c7399c7..262c544472fe 100644 --- a/spring-core/src/main/java/org/springframework/util/ClassUtils.java +++ b/spring-core/src/main/java/org/springframework/util/ClassUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-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. @@ -44,12 +44,14 @@ /** * Miscellaneous {@code java.lang.Class} utility methods. - * Mainly for internal use within the framework. + * + *

    Mainly for internal use within the framework. * * @author Juergen Hoeller * @author Keith Donald * @author Rob Harrop * @author Sam Brannen + * @author Sebastien Deleuze * @since 1.1 * @see TypeUtils * @see ReflectionUtils @@ -83,6 +85,12 @@ public abstract class ClassUtils { /** The ".class" file suffix. */ public static final String CLASS_FILE_SUFFIX = ".class"; + /** Precomputed value for the combination of private, static and final modifiers. */ + private static final int NON_OVERRIDABLE_MODIFIER = Modifier.PRIVATE | Modifier.STATIC | Modifier.FINAL; + + /** Precomputed value for the combination of public and protected modifiers. */ + private static final int OVERRIDABLE_MODIFIER = Modifier.PUBLIC | Modifier.PROTECTED; + /** * Map with primitive wrapper type as key and corresponding primitive @@ -236,7 +244,7 @@ public static ClassLoader overrideThreadContextClassLoader(@Nullable ClassLoader * style (e.g. "java.lang.Thread.State" instead of "java.lang.Thread$State"). * @param name the name of the Class * @param classLoader the class loader to use - * (may be {@code null}, which indicates the default class loader) + * (can be {@code null}, which indicates the default class loader) * @return a class instance for the supplied name * @throws ClassNotFoundException if the class was not found * @throws LinkageError if the class file could not be loaded @@ -307,7 +315,7 @@ public static Class forName(String name, @Nullable ClassLoader classLoader) * the exceptions thrown in case of class loading failure. * @param className the name of the Class * @param classLoader the class loader to use - * (may be {@code null}, which indicates the default class loader) + * (can be {@code null}, which indicates the default class loader) * @return a class instance for the supplied name * @throws IllegalArgumentException if the class name was not resolvable * (that is, the class could not be found or the class file could not be loaded) @@ -341,7 +349,7 @@ public static Class resolveClassName(String className, @Nullable ClassLoader * one of its dependencies is not present or cannot be loaded. * @param className the name of the class to check * @param classLoader the class loader to use - * (may be {@code null} which indicates the default class loader) + * (can be {@code null} which indicates the default class loader) * @return whether the specified class is present (including all of its * superclasses and interfaces) * @throws IllegalStateException if the corresponding class is resolvable but @@ -368,7 +376,7 @@ public static boolean isPresent(String className, @Nullable ClassLoader classLoa * Check whether the given class is visible in the given ClassLoader. * @param clazz the class to check (typically an interface) * @param classLoader the ClassLoader to check against - * (may be {@code null} in which case this method will always return {@code true}) + * (can be {@code null} in which case this method will always return {@code true}) */ public static boolean isVisible(Class clazz, @Nullable ClassLoader classLoader) { if (classLoader == null) { @@ -392,7 +400,7 @@ public static boolean isVisible(Class clazz, @Nullable ClassLoader classLoade * i.e. whether it is loaded by the given ClassLoader or a parent of it. * @param clazz the class to analyze * @param classLoader the ClassLoader to potentially cache metadata in - * (may be {@code null} which indicates the system class loader) + * (can be {@code null} which indicates the system class loader) */ public static boolean isCacheSafe(Class clazz, @Nullable ClassLoader classLoader) { Assert.notNull(clazz, "Class must not be null"); @@ -532,9 +540,10 @@ public static Class resolvePrimitiveIfNecessary(Class clazz) { * Check if the right-hand side type may be assigned to the left-hand side * type, assuming setting by reflection. Considers primitive wrapper * classes as assignable to the corresponding primitive types. - * @param lhsType the target type - * @param rhsType the value type that should be assigned to the target type - * @return if the target type is assignable from the value type + * @param lhsType the target type (left-hand side (LHS) type) + * @param rhsType the value type (right-hand side (RHS) type) that should + * be assigned to the target type + * @return {@code true} if {@code rhsType} is assignable to {@code lhsType} * @see TypeUtils#isAssignable(java.lang.reflect.Type, java.lang.reflect.Type) */ public static boolean isAssignable(Class lhsType, Class rhsType) { @@ -655,7 +664,7 @@ public static String classNamesToString(Class... classes) { * in the given collection. *

    Basically like {@code AbstractCollection.toString()}, but stripping * the "class "/"interface " prefix before every class name. - * @param classes a Collection of Class objects (may be {@code null}) + * @param classes a Collection of Class objects (can be {@code null}) * @return a String of form "[com.foo.Bar, com.foo.Baz]" * @see java.util.AbstractCollection#toString() */ @@ -710,7 +719,7 @@ public static Class[] getAllInterfacesForClass(Class clazz) { *

    If the class itself is an interface, it gets returned as sole interface. * @param clazz the class to analyze for interfaces * @param classLoader the ClassLoader that the interfaces need to be visible in - * (may be {@code null} when accepting all declared interfaces) + * (can be {@code null} when accepting all declared interfaces) * @return all interfaces that the given object implements as an array */ public static Class[] getAllInterfacesForClass(Class clazz, @Nullable ClassLoader classLoader) { @@ -745,7 +754,7 @@ public static Set> getAllInterfacesForClassAsSet(Class clazz) { *

    If the class itself is an interface, it gets returned as sole interface. * @param clazz the class to analyze for interfaces * @param classLoader the ClassLoader that the interfaces need to be visible in - * (may be {@code null} when accepting all declared interfaces) + * (can be {@code null} when accepting all declared interfaces) * @return all interfaces that the given object implements as a Set */ public static Set> getAllInterfacesForClassAsSet(Class clazz, @Nullable ClassLoader classLoader) { @@ -859,9 +868,9 @@ public static boolean isLambdaClass(Class clazz) { /** * Check whether the given object is a CGLIB proxy. * @param object the object to check - * @see #isCglibProxyClass(Class) * @see org.springframework.aop.support.AopUtils#isCglibProxy(Object) * @deprecated as of 5.2, in favor of custom (possibly narrower) checks + * such as for a Spring AOP proxy */ @Deprecated public static boolean isCglibProxy(Object object) { @@ -871,8 +880,9 @@ public static boolean isCglibProxy(Object object) { /** * Check whether the specified class is a CGLIB-generated class. * @param clazz the class to check - * @see #isCglibProxyClassName(String) + * @see #getUserClass(Class) * @deprecated as of 5.2, in favor of custom (possibly narrower) checks + * or simply a check for containing {@link #CGLIB_CLASS_SEPARATOR} */ @Deprecated public static boolean isCglibProxyClass(@Nullable Class clazz) { @@ -882,7 +892,9 @@ public static boolean isCglibProxyClass(@Nullable Class clazz) { /** * Check whether the specified class name is a CGLIB-generated class. * @param className the class name to check + * @see #CGLIB_CLASS_SEPARATOR * @deprecated as of 5.2, in favor of custom (possibly narrower) checks + * or simply a check for containing {@link #CGLIB_CLASS_SEPARATOR} */ @Deprecated public static boolean isCglibProxyClassName(@Nullable String className) { @@ -906,6 +918,7 @@ public static Class getUserClass(Object instance) { * class, but the original class in case of a CGLIB-generated subclass. * @param clazz the class to check * @return the user-defined class + * @see #CGLIB_CLASS_SEPARATOR */ public static Class getUserClass(Class clazz) { if (clazz.getName().contains(CGLIB_CLASS_SEPARATOR)) { @@ -1058,7 +1071,7 @@ public static String getQualifiedMethodName(Method method) { * fully qualified interface/class name + "." + method name. * @param method the method * @param clazz the clazz that the method is being invoked on - * (may be {@code null} to indicate the method's declaring class) + * (can be {@code null} to indicate the method's declaring class) * @return the qualified name of the method * @since 4.3.4 */ @@ -1139,7 +1152,7 @@ public static boolean hasMethod(Class clazz, String methodName, Class... p * @param clazz the clazz to analyze * @param methodName the name of the method * @param paramTypes the parameter types of the method - * (may be {@code null} to indicate any signature) + * (can be {@code null} to indicate any signature) * @return the method (never {@code null}) * @throws IllegalStateException if the method has not been found * @see Class#getMethod @@ -1178,7 +1191,7 @@ else if (candidates.isEmpty()) { * @param clazz the clazz to analyze * @param methodName the name of the method * @param paramTypes the parameter types of the method - * (may be {@code null} to indicate any signature) + * (can be {@code null} to indicate any signature) * @return the method, or {@code null} if not found * @see Class#getMethod */ @@ -1254,26 +1267,27 @@ public static boolean hasAtLeastOneMethodWithName(Class clazz, String methodN /** * Given a method, which may come from an interface, and a target class used * in the current reflective invocation, find the corresponding target method - * if there is one. E.g. the method may be {@code IFoo.bar()} and the - * target class may be {@code DefaultFoo}. In this case, the method may be + * if there is one — for example, the method may be {@code IFoo.bar()}, + * and the target class may be {@code DefaultFoo}. In this case, the method may be * {@code DefaultFoo.bar()}. This enables attributes on that method to be found. *

    NOTE: In contrast to {@link org.springframework.aop.support.AopUtils#getMostSpecificMethod}, * this method does not resolve bridge methods automatically. * Call {@link org.springframework.core.BridgeMethodResolver#findBridgedMethod} - * if bridge method resolution is desirable (e.g. for obtaining metadata from - * the original method definition). - *

    NOTE: Since Spring 3.1.1, if Java security settings disallow reflective - * access (e.g. calls to {@code Class#getDeclaredMethods} etc, this implementation - * will fall back to returning the originally provided method. + * if bridge method resolution is desirable — for example, to obtain + * metadata from the original method definition. + *

    NOTE: If Java security settings disallow reflective access — + * for example, calls to {@code Class#getDeclaredMethods}, etc. — this + * implementation will fall back to returning the originally provided method. * @param method the method to be invoked, which may come from an interface * @param targetClass the target class for the current invocation - * (may be {@code null} or may not even implement the method) + * (can be {@code null} or may not even implement the method) * @return the specific target method, or the original method if the * {@code targetClass} does not implement it * @see #getInterfaceMethodIfPossible(Method, Class) */ public static Method getMostSpecificMethod(Method method, @Nullable Class targetClass) { - if (targetClass != null && targetClass != method.getDeclaringClass() && isOverridable(method, targetClass)) { + if (targetClass != null && targetClass != method.getDeclaringClass() && + (isOverridable(method, targetClass) || !method.getDeclaringClass().isAssignableFrom(targetClass))) { try { if (Modifier.isPublic(method.getModifiers())) { try { @@ -1379,10 +1393,10 @@ private static boolean isGroovyObjectMethod(Method method) { * @param targetClass the target class to check against */ private static boolean isOverridable(Method method, @Nullable Class targetClass) { - if (Modifier.isPrivate(method.getModifiers())) { + if ((method.getModifiers() & NON_OVERRIDABLE_MODIFIER) != 0) { return false; } - if (Modifier.isPublic(method.getModifiers()) || Modifier.isProtected(method.getModifiers())) { + if ((method.getModifiers() & OVERRIDABLE_MODIFIER) != 0) { return true; } return (targetClass == null || @@ -1403,7 +1417,7 @@ public static Method getStaticMethod(Class clazz, String methodName, Class Assert.notNull(methodName, "Method name must not be null"); try { Method method = clazz.getMethod(methodName, args); - return Modifier.isStatic(method.getModifiers()) ? method : null; + return (Modifier.isStatic(method.getModifiers()) ? method : null); } catch (NoSuchMethodException ex) { return null; diff --git a/spring-core/src/main/java/org/springframework/util/ConcurrencyThrottleSupport.java b/spring-core/src/main/java/org/springframework/util/ConcurrencyThrottleSupport.java index 6d7d272b4fda..370537bf9a38 100644 --- a/spring-core/src/main/java/org/springframework/util/ConcurrencyThrottleSupport.java +++ b/spring-core/src/main/java/org/springframework/util/ConcurrencyThrottleSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2023 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. @@ -69,7 +69,7 @@ public abstract class ConcurrencyThrottleSupport implements Serializable { /** * Set the maximum number of concurrent access attempts allowed. - * -1 indicates unbounded concurrency. + * The default of -1 indicates no concurrency limit at all. *

    In principle, this limit can be changed at runtime, * although it is generally designed as a config time setting. *

    NOTE: Do not switch between -1 and any concrete limit at runtime, @@ -143,9 +143,10 @@ protected void beforeAccess() { */ protected void afterAccess() { if (this.concurrencyLimit >= 0) { + boolean debug = logger.isDebugEnabled(); synchronized (this.monitor) { this.concurrencyCount--; - if (logger.isDebugEnabled()) { + if (debug) { logger.debug("Returning from throttle at concurrency count " + this.concurrencyCount); } this.monitor.notify(); diff --git a/spring-core/src/main/java/org/springframework/util/ConcurrentReferenceHashMap.java b/spring-core/src/main/java/org/springframework/util/ConcurrentReferenceHashMap.java index 3291ca6e079a..75507d411d49 100644 --- a/spring-core/src/main/java/org/springframework/util/ConcurrentReferenceHashMap.java +++ b/spring-core/src/main/java/org/springframework/util/ConcurrentReferenceHashMap.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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. @@ -752,28 +752,27 @@ public V setValue(@Nullable V value) { } @Override - public String toString() { - return (this.key + "=" + this.value); - } - - @Override - @SuppressWarnings("rawtypes") - public final boolean equals(@Nullable Object other) { + public boolean equals(@Nullable Object other) { if (this == other) { return true; } - if (!(other instanceof Map.Entry)) { + if (!(other instanceof Map.Entry)) { return false; } - Map.Entry otherEntry = (Map.Entry) other; + Map.Entry otherEntry = (Map.Entry) other; return (ObjectUtils.nullSafeEquals(getKey(), otherEntry.getKey()) && ObjectUtils.nullSafeEquals(getValue(), otherEntry.getValue())); } @Override - public final int hashCode() { + public int hashCode() { return (ObjectUtils.nullSafeHashCode(this.key) ^ ObjectUtils.nullSafeHashCode(this.value)); } + + @Override + public String toString() { + return (this.key + "=" + this.value); + } } diff --git a/spring-core/src/main/java/org/springframework/util/FileSystemUtils.java b/spring-core/src/main/java/org/springframework/util/FileSystemUtils.java index 1a532aa15925..b931a0729591 100644 --- a/spring-core/src/main/java/org/springframework/util/FileSystemUtils.java +++ b/spring-core/src/main/java/org/springframework/util/FileSystemUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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. @@ -68,10 +68,10 @@ public static boolean deleteRecursively(@Nullable File root) { } /** - * Delete the supplied {@link File} — for directories, + * Delete the supplied {@link Path} — for directories, * recursively delete any nested directories or files as well. - * @param root the root {@code File} to delete - * @return {@code true} if the {@code File} existed and was deleted, + * @param root the root {@code Path} to delete + * @return {@code true} if the {@code Path} existed and was deleted, * or {@code false} if it did not exist * @throws IOException in the case of I/O errors * @since 5.0 diff --git a/spring-core/src/main/java/org/springframework/util/InstanceFilter.java b/spring-core/src/main/java/org/springframework/util/InstanceFilter.java index 3eb69149e168..08e2000f1a3d 100644 --- a/spring-core/src/main/java/org/springframework/util/InstanceFilter.java +++ b/spring-core/src/main/java/org/springframework/util/InstanceFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2023 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,7 +64,7 @@ public InstanceFilter(@Nullable Collection includes, /** - * Determine if the specified {code instance} matches this filter. + * Determine if the specified {@code instance} matches this filter. */ public boolean match(T instance) { Assert.notNull(instance, "Instance to match must not be null"); diff --git a/spring-core/src/main/java/org/springframework/util/MethodInvoker.java b/spring-core/src/main/java/org/springframework/util/MethodInvoker.java index b3e0c5554806..0443c5699ca1 100644 --- a/spring-core/src/main/java/org/springframework/util/MethodInvoker.java +++ b/spring-core/src/main/java/org/springframework/util/MethodInvoker.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 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. @@ -123,8 +123,8 @@ public String getTargetMethod() { /** * Set a fully qualified static method name to invoke, - * e.g. "example.MyExampleClass.myExampleMethod". - * Convenient alternative to specifying targetClass and targetMethod. + * e.g. "example.MyExampleClass.myExampleMethod". This is a + * convenient alternative to specifying targetClass and targetMethod. * @see #setTargetClass * @see #setTargetMethod */ @@ -157,14 +157,16 @@ public Object[] getArguments() { public void prepare() throws ClassNotFoundException, NoSuchMethodException { if (this.staticMethod != null) { int lastDotIndex = this.staticMethod.lastIndexOf('.'); - if (lastDotIndex == -1 || lastDotIndex == this.staticMethod.length()) { + if (lastDotIndex == -1 || lastDotIndex == this.staticMethod.length() - 1) { throw new IllegalArgumentException( "staticMethod must be a fully qualified class plus method name: " + "e.g. 'example.MyExampleClass.myExampleMethod'"); } String className = this.staticMethod.substring(0, lastDotIndex); String methodName = this.staticMethod.substring(lastDotIndex + 1); - this.targetClass = resolveClassName(className); + if (this.targetClass == null || !this.targetClass.getName().equals(className)) { + this.targetClass = resolveClassName(className); + } this.targetMethod = methodName; } diff --git a/spring-core/src/main/java/org/springframework/util/MultiValueMapAdapter.java b/spring-core/src/main/java/org/springframework/util/MultiValueMapAdapter.java index 0d51b56117c8..9bc9181f8947 100644 --- a/spring-core/src/main/java/org/springframework/util/MultiValueMapAdapter.java +++ b/spring-core/src/main/java/org/springframework/util/MultiValueMapAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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,7 +58,7 @@ public MultiValueMapAdapter(Map> targetMap) { @Nullable public V getFirst(K key) { List values = this.targetMap.get(key); - return (values != null && !values.isEmpty() ? values.get(0) : null); + return (!CollectionUtils.isEmpty(values) ? values.get(0) : null); } @Override @@ -69,7 +69,7 @@ public void add(K key, @Nullable V value) { @Override public void addAll(K key, List values) { - List currentValues = this.targetMap.computeIfAbsent(key, k -> new ArrayList<>(1)); + List currentValues = this.targetMap.computeIfAbsent(key, k -> new ArrayList<>(values.size())); currentValues.addAll(values); } @@ -96,7 +96,7 @@ public void setAll(Map values) { public Map toSingleValueMap() { Map singleValueMap = CollectionUtils.newLinkedHashMap(this.targetMap.size()); this.targetMap.forEach((key, values) -> { - if (values != null && !values.isEmpty()) { + if (!CollectionUtils.isEmpty(values)) { singleValueMap.put(key, values.get(0)); } }); diff --git a/spring-core/src/main/java/org/springframework/util/ObjectUtils.java b/spring-core/src/main/java/org/springframework/util/ObjectUtils.java index f5a703c30181..64aef0d25a7a 100644 --- a/spring-core/src/main/java/org/springframework/util/ObjectUtils.java +++ b/spring-core/src/main/java/org/springframework/util/ObjectUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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,12 +16,26 @@ package org.springframework.util; +import java.io.File; import java.lang.reflect.Array; +import java.net.InetAddress; +import java.net.URI; +import java.net.URL; +import java.nio.charset.Charset; +import java.nio.file.Path; +import java.time.ZoneId; +import java.time.temporal.Temporal; import java.util.Arrays; import java.util.Collection; +import java.util.Currency; +import java.util.Date; +import java.util.Locale; import java.util.Map; import java.util.Optional; import java.util.StringJoiner; +import java.util.TimeZone; +import java.util.UUID; +import java.util.regex.Pattern; import org.springframework.lang.Nullable; @@ -55,6 +69,9 @@ public abstract class ObjectUtils { private static final String EMPTY_ARRAY = ARRAY_START + ARRAY_END; private static final String ARRAY_ELEMENT_SEPARATOR = ", "; private static final Object[] EMPTY_OBJECT_ARRAY = new Object[0]; + private static final String NON_EMPTY_ARRAY = ARRAY_START + "..." + ARRAY_END; + private static final String COLLECTION = "[...]"; + private static final String MAP = NON_EMPTY_ARRAY; /** @@ -653,6 +670,7 @@ public static String nullSafeClassName(@Nullable Object obj) { * Returns a {@code "null"} String if {@code obj} is {@code null}. * @param obj the object to build a String representation for * @return a String representation of {@code obj} + * @see #nullSafeConciseToString(Object) */ public static String nullSafeToString(@Nullable Object obj) { if (obj == null) { @@ -908,4 +926,115 @@ public static String nullSafeToString(@Nullable short[] array) { return stringJoiner.toString(); } + /** + * Generate a null-safe, concise string representation of the supplied object + * as described below. + *

    Favor this method over {@link #nullSafeToString(Object)} when you need + * the length of the generated string to be limited. + *

    Returns: + *

      + *
    • {@code "null"} if {@code obj} is {@code null}
    • + *
    • {@code"Optional.empty"} if {@code obj} is an empty {@link Optional}
    • + *
    • {@code"Optional[]"} if {@code obj} is a non-empty {@code Optional}, + * where {@code } is the result of invoking {@link #nullSafeConciseToString} + * on the object contained in the {@code Optional}
    • + *
    • {@code "{}"} if {@code obj} is an empty array
    • + *
    • {@code "{...}"} if {@code obj} is a {@link Map} or a non-empty array
    • + *
    • {@code "[...]"} if {@code obj} is a {@link Collection}
    • + *
    • {@linkplain Class#getName() Class name} if {@code obj} is a {@link Class}
    • + *
    • {@linkplain Charset#name() Charset name} if {@code obj} is a {@link Charset}
    • + *
    • {@linkplain TimeZone#getID() TimeZone ID} if {@code obj} is a {@link TimeZone}
    • + *
    • {@linkplain ZoneId#getId() Zone ID} if {@code obj} is a {@link ZoneId}
    • + *
    • Potentially {@linkplain StringUtils#truncate(CharSequence) truncated string} + * if {@code obj} is a {@link String} or {@link CharSequence}
    • + *
    • Potentially {@linkplain StringUtils#truncate(CharSequence) truncated string} + * if {@code obj} is a simple value type whose {@code toString()} method + * returns a non-null value
    • + *
    • Otherwise, a string representation of the object's type name concatenated + * with {@code "@"} and a hex string form of the object's identity hash code
    • + *
    + *

    In the context of this method, a simple value type is any of the following: + * primitive wrapper (excluding {@link Void}), {@link Enum}, {@link Number}, + * {@link Date}, {@link Temporal}, {@link File}, {@link Path}, {@link URI}, + * {@link URL}, {@link InetAddress}, {@link Currency}, {@link Locale}, + * {@link UUID}, {@link Pattern}. + * @param obj the object to build a string representation for + * @return a concise string representation of the supplied object + * @since 5.3.27 + * @see #nullSafeToString(Object) + * @see StringUtils#truncate(CharSequence) + */ + public static String nullSafeConciseToString(@Nullable Object obj) { + if (obj == null) { + return "null"; + } + if (obj instanceof Optional) { + Optional optional = (Optional) obj; + return (!optional.isPresent() ? "Optional.empty" : + String.format("Optional[%s]", nullSafeConciseToString(optional.get()))); + } + if (obj.getClass().isArray()) { + return (Array.getLength(obj) == 0 ? EMPTY_ARRAY : NON_EMPTY_ARRAY); + } + if (obj instanceof Collection) { + return COLLECTION; + } + if (obj instanceof Map) { + return MAP; + } + if (obj instanceof Class) { + return ((Class) obj).getName(); + } + if (obj instanceof Charset) { + return ((Charset) obj).name(); + } + if (obj instanceof TimeZone) { + return ((TimeZone) obj).getID(); + } + if (obj instanceof ZoneId) { + return ((ZoneId) obj).getId(); + } + if (obj instanceof CharSequence) { + return StringUtils.truncate((CharSequence) obj); + } + Class type = obj.getClass(); + if (isSimpleValueType(type)) { + String str = obj.toString(); + if (str != null) { + return StringUtils.truncate(str); + } + } + return type.getTypeName() + "@" + getIdentityHexString(obj); + } + + /** + * Derived from {@link org.springframework.beans.BeanUtils#isSimpleValueType}. + *

    As of 5.3.28, considering {@link UUID} in addition to the bean-level check. + *

    As of 5.3.29, additionally considering {@link File}, {@link Path}, + * {@link InetAddress}, {@link Charset}, {@link Currency}, {@link TimeZone}, + * {@link ZoneId}, {@link Pattern}. + */ + private static boolean isSimpleValueType(Class type) { + return (Void.class != type && void.class != type && + (ClassUtils.isPrimitiveOrWrapper(type) || + Enum.class.isAssignableFrom(type) || + CharSequence.class.isAssignableFrom(type) || + Number.class.isAssignableFrom(type) || + Date.class.isAssignableFrom(type) || + Temporal.class.isAssignableFrom(type) || + ZoneId.class.isAssignableFrom(type) || + TimeZone.class.isAssignableFrom(type) || + File.class.isAssignableFrom(type) || + Path.class.isAssignableFrom(type) || + Charset.class.isAssignableFrom(type) || + Currency.class.isAssignableFrom(type) || + InetAddress.class.isAssignableFrom(type) || + URI.class == type || + URL.class == type || + UUID.class == type || + Locale.class == type || + Pattern.class == type || + Class.class == type)); + } + } diff --git a/spring-core/src/main/java/org/springframework/util/ReflectionUtils.java b/spring-core/src/main/java/org/springframework/util/ReflectionUtils.java index 274cb7188703..b90e5909d5ea 100644 --- a/spring-core/src/main/java/org/springframework/util/ReflectionUtils.java +++ b/spring-core/src/main/java/org/springframework/util/ReflectionUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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. @@ -509,16 +509,8 @@ private static List findConcreteMethodsOnInterfaces(Class clazz) { * @see java.lang.Object#equals(Object) */ public static boolean isEqualsMethod(@Nullable Method method) { - if (method == null) { - return false; - } - if (method.getParameterCount() != 1) { - return false; - } - if (!method.getName().equals("equals")) { - return false; - } - return method.getParameterTypes()[0] == Object.class; + return (method != null && method.getParameterCount() == 1 && method.getName().equals("equals") && + method.getParameterTypes()[0] == Object.class); } /** @@ -526,7 +518,7 @@ public static boolean isEqualsMethod(@Nullable Method method) { * @see java.lang.Object#hashCode() */ public static boolean isHashCodeMethod(@Nullable Method method) { - return method != null && method.getParameterCount() == 0 && method.getName().equals("hashCode"); + return (method != null && method.getParameterCount() == 0 && method.getName().equals("hashCode")); } /** diff --git a/spring-core/src/main/java/org/springframework/util/StringUtils.java b/spring-core/src/main/java/org/springframework/util/StringUtils.java index 4fe02d63892b..76111f847ab8 100644 --- a/spring-core/src/main/java/org/springframework/util/StringUtils.java +++ b/spring-core/src/main/java/org/springframework/util/StringUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-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. @@ -75,6 +75,10 @@ public abstract class StringUtils { private static final char EXTENSION_SEPARATOR = '.'; + private static final int DEFAULT_TRUNCATION_THRESHOLD = 100; + + private static final String TRUNCATION_SUFFIX = " (truncated)..."; + //--------------------------------------------------------------------- // General convenience methods for working with Strings @@ -243,26 +247,26 @@ public static String trimWhitespace(String str) { /** * Trim all whitespace from the given {@code CharSequence}: * leading, trailing, and in between characters. - * @param text the {@code CharSequence} to check + * @param str the {@code CharSequence} to check * @return the trimmed {@code CharSequence} * @since 5.3.22 * @see #trimAllWhitespace(String) * @see java.lang.Character#isWhitespace */ - public static CharSequence trimAllWhitespace(CharSequence text) { - if (!hasLength(text)) { - return text; + public static CharSequence trimAllWhitespace(CharSequence str) { + if (!hasLength(str)) { + return str; } - int len = text.length(); - StringBuilder sb = new StringBuilder(text.length()); + int len = str.length(); + StringBuilder sb = new StringBuilder(str.length()); for (int i = 0; i < len; i++) { - char c = text.charAt(i); + char c = str.charAt(i); if (!Character.isWhitespace(c)) { sb.append(c); } } - return sb.toString(); + return sb; } /** @@ -274,9 +278,10 @@ public static CharSequence trimAllWhitespace(CharSequence text) { * @see java.lang.Character#isWhitespace */ public static String trimAllWhitespace(String str) { - if (str == null) { - return null; + if (!hasLength(str)) { + return str; } + return trimAllWhitespace((CharSequence) str).toString(); } @@ -776,6 +781,7 @@ public static boolean pathEquals(String path1, String path2) { * and {@code "0"} through {@code "9"} stay the same. *

  13. Special characters {@code "-"}, {@code "_"}, {@code "."}, and {@code "*"} stay the same.
  14. *
  15. A sequence "{@code %xy}" is interpreted as a hexadecimal representation of the character.
  16. + *
  17. For all other characters (including those already decoded), the output is undefined.
  18. * * @param source the encoded String * @param charset the character set @@ -824,7 +830,7 @@ public static String uriDecode(String source, Charset charset) { * the {@link Locale#toString} format as well as BCP 47 language tags as * specified by {@link Locale#forLanguageTag}. * @param localeValue the locale value: following either {@code Locale's} - * {@code toString()} format ("en", "en_UK", etc), also accepting spaces as + * {@code toString()} format ("en", "en_UK", etc.), also accepting spaces as * separators (as an alternative to underscores), or BCP 47 (e.g. "en-UK") * @return a corresponding {@code Locale} instance, or {@code null} if none * @throws IllegalArgumentException in case of an invalid locale specification @@ -854,7 +860,7 @@ public static Locale parseLocale(String localeValue) { *

    Note: This delegate does not accept the BCP 47 language tag format. * Please use {@link #parseLocale} for lenient parsing of both formats. * @param localeString the locale {@code String}: following {@code Locale's} - * {@code toString()} format ("en", "en_UK", etc), also accepting spaces as + * {@code toString()} format ("en", "en_UK", etc.), also accepting spaces as * separators (as an alternative to underscores) * @return a corresponding {@code Locale} instance, or {@code null} if none * @throws IllegalArgumentException in case of an invalid locale specification @@ -1388,4 +1394,40 @@ public static String arrayToCommaDelimitedString(@Nullable Object[] arr) { return arrayToDelimitedString(arr, ","); } + /** + * Truncate the supplied {@link CharSequence}. + *

    Delegates to {@link #truncate(CharSequence, int)}, supplying {@code 100} + * as the threshold. + * @param charSequence the {@code CharSequence} to truncate + * @return a truncated string, or a string representation of the original + * {@code CharSequence} if its length does not exceed the threshold + * @since 5.3.27 + */ + public static String truncate(CharSequence charSequence) { + return truncate(charSequence, DEFAULT_TRUNCATION_THRESHOLD); + } + + /** + * Truncate the supplied {@link CharSequence}. + *

    If the length of the {@code CharSequence} is greater than the threshold, + * this method returns a {@linkplain CharSequence#subSequence(int, int) + * subsequence} of the {@code CharSequence} (up to the threshold) appended + * with the suffix {@code " (truncated)..."}. Otherwise, this method returns + * {@code charSequence.toString()}. + * @param charSequence the {@code CharSequence} to truncate + * @param threshold the maximum length after which to truncate; must be a + * positive number + * @return a truncated string, or a string representation of the original + * {@code CharSequence} if its length does not exceed the threshold + * @since 5.3.27 + */ + public static String truncate(CharSequence charSequence, int threshold) { + Assert.isTrue(threshold > 0, + () -> "Truncation threshold must be a positive number: " + threshold); + if (charSequence.length() > threshold) { + return charSequence.subSequence(0, threshold) + TRUNCATION_SUFFIX; + } + return charSequence.toString(); + } + } diff --git a/spring-core/src/main/java/org/springframework/util/backoff/ExponentialBackOff.java b/spring-core/src/main/java/org/springframework/util/backoff/ExponentialBackOff.java index 0331699a5b68..dceea17b91a5 100644 --- a/spring-core/src/main/java/org/springframework/util/backoff/ExponentialBackOff.java +++ b/spring-core/src/main/java/org/springframework/util/backoff/ExponentialBackOff.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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,12 +20,12 @@ /** * Implementation of {@link BackOff} that increases the back off period for each - * retry attempt. When the interval has reached the {@link #setMaxInterval(long) + * retry attempt. When the interval has reached the {@linkplain #setMaxInterval * max interval}, it is no longer increased. Stops retrying once the - * {@link #setMaxElapsedTime(long) max elapsed time} has been reached. + * {@linkplain #setMaxElapsedTime max elapsed time} has been reached. * - *

    Example: The default interval is {@value #DEFAULT_INITIAL_INTERVAL} ms, - * the default multiplier is {@value #DEFAULT_MULTIPLIER}, and the default max + *

    Example: The default interval is {@value #DEFAULT_INITIAL_INTERVAL} ms; + * the default multiplier is {@value #DEFAULT_MULTIPLIER}; and the default max * interval is {@value #DEFAULT_MAX_INTERVAL}. For 10 attempts the sequence will be * as follows: * @@ -44,10 +44,9 @@ * 10 30000 * * - *

    Note that the default max elapsed time is {@link Long#MAX_VALUE}. Use - * {@link #setMaxElapsedTime(long)} to limit the maximum length of time - * that an instance should accumulate before returning - * {@link BackOffExecution#STOP}. + *

    Note that the default max elapsed time is {@link Long#MAX_VALUE}. + * Use {@link #setMaxElapsedTime} to limit the maximum length of time that an + * instance should accumulate before returning {@link BackOffExecution#STOP}. * * @author Stephane Nicoll * @since 4.1 @@ -107,7 +106,7 @@ public ExponentialBackOff(long initialInterval, double multiplier) { /** - * The initial interval in milliseconds. + * Set the initial interval in milliseconds. */ public void setInitialInterval(long initialInterval) { this.initialInterval = initialInterval; @@ -121,7 +120,7 @@ public long getInitialInterval() { } /** - * The value to multiply the current interval by for each retry attempt. + * Set the value to multiply the current interval by for each retry attempt. */ public void setMultiplier(double multiplier) { checkMultiplier(multiplier); @@ -136,21 +135,21 @@ public double getMultiplier() { } /** - * The maximum back off time. + * Set the maximum back off time in milliseconds. */ public void setMaxInterval(long maxInterval) { this.maxInterval = maxInterval; } /** - * Return the maximum back off time. + * Return the maximum back off time in milliseconds. */ public long getMaxInterval() { return this.maxInterval; } /** - * The maximum elapsed time in milliseconds after which a call to + * Set the maximum elapsed time in milliseconds after which a call to * {@link BackOffExecution#nextBackOff()} returns {@link BackOffExecution#STOP}. */ public void setMaxElapsedTime(long maxElapsedTime) { @@ -184,10 +183,9 @@ private class ExponentialBackOffExecution implements BackOffExecution { @Override public long nextBackOff() { - if (this.currentElapsedTime >= maxElapsedTime) { + if (this.currentElapsedTime >= getMaxElapsedTime()) { return STOP; } - long nextInterval = computeNextInterval(); this.currentElapsedTime += nextInterval; return nextInterval; @@ -214,7 +212,6 @@ private long multiplyInterval(long maxInterval) { return Math.min(i, maxInterval); } - @Override public String toString() { StringBuilder sb = new StringBuilder("ExponentialBackOff{"); diff --git a/spring-core/src/main/java/org/springframework/util/comparator/NullSafeComparator.java b/spring-core/src/main/java/org/springframework/util/comparator/NullSafeComparator.java index 37933af5397d..6eae4de69b55 100644 --- a/spring-core/src/main/java/org/springframework/util/comparator/NullSafeComparator.java +++ b/spring-core/src/main/java/org/springframework/util/comparator/NullSafeComparator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2023 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. @@ -85,7 +85,7 @@ private NullSafeComparator(boolean nullsLow) { * @param nullsLow whether to treat nulls lower or higher than non-null objects */ public NullSafeComparator(Comparator comparator, boolean nullsLow) { - Assert.notNull(comparator, "Non-null Comparator is required"); + Assert.notNull(comparator, "Comparator must not be null"); this.nonNullComparator = comparator; this.nullsLow = nullsLow; } @@ -107,16 +107,16 @@ public int compare(@Nullable T o1, @Nullable T o2) { @Override - @SuppressWarnings("unchecked") public boolean equals(@Nullable Object other) { if (this == other) { return true; } - if (!(other instanceof NullSafeComparator)) { + if (!(other instanceof NullSafeComparator)) { return false; } - NullSafeComparator otherComp = (NullSafeComparator) other; - return (this.nonNullComparator.equals(otherComp.nonNullComparator) && this.nullsLow == otherComp.nullsLow); + NullSafeComparator otherComp = (NullSafeComparator) other; + return (this.nonNullComparator.equals(otherComp.nonNullComparator) && + this.nullsLow == otherComp.nullsLow); } @Override diff --git a/spring-core/src/test/java/org/springframework/SpringCoreTestSuite.java b/spring-core/src/test/java/org/springframework/SpringCoreTestSuite.java new file mode 100644 index 000000000000..a2d623435387 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/SpringCoreTestSuite.java @@ -0,0 +1,34 @@ +/* + * Copyright 2002-2022 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; + +import org.junit.platform.suite.api.IncludeClassNamePatterns; +import org.junit.platform.suite.api.SelectPackages; +import org.junit.platform.suite.api.Suite; + +/** + * JUnit Platform based test suite for tests in the spring-core module. + * + *

    This suite is only intended to be used manually within an IDE. + * + * @author Sam Brannen + */ +@Suite +@SelectPackages({"org.springframework.core", "org.springframework.util"}) +@IncludeClassNamePatterns(".*Tests?$") +class SpringCoreTestSuite { +} diff --git a/spring-core/src/test/java/org/springframework/core/CollectionFactoryTests.java b/spring-core/src/test/java/org/springframework/core/CollectionFactoryTests.java index 570099d83af8..e46ca45149ed 100644 --- a/spring-core/src/test/java/org/springframework/core/CollectionFactoryTests.java +++ b/spring-core/src/test/java/org/springframework/core/CollectionFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -209,21 +209,23 @@ void createApproximateMapFromNonEmptyEnumMap() { @Test void createsCollectionsCorrectly() { // interfaces - assertThat(createCollection(List.class, 0)).isInstanceOf(ArrayList.class); - assertThat(createCollection(Set.class, 0)).isInstanceOf(LinkedHashSet.class); - assertThat(createCollection(Collection.class, 0)).isInstanceOf(LinkedHashSet.class); - assertThat(createCollection(SortedSet.class, 0)).isInstanceOf(TreeSet.class); - assertThat(createCollection(NavigableSet.class, 0)).isInstanceOf(TreeSet.class); - - assertThat(createCollection(List.class, String.class, 0)).isInstanceOf(ArrayList.class); - assertThat(createCollection(Set.class, String.class, 0)).isInstanceOf(LinkedHashSet.class); - assertThat(createCollection(Collection.class, String.class, 0)).isInstanceOf(LinkedHashSet.class); - assertThat(createCollection(SortedSet.class, String.class, 0)).isInstanceOf(TreeSet.class); - assertThat(createCollection(NavigableSet.class, String.class, 0)).isInstanceOf(TreeSet.class); + testCollection(List.class, ArrayList.class); + testCollection(Set.class, LinkedHashSet.class); + testCollection(Collection.class, LinkedHashSet.class); + testCollection(SortedSet.class, TreeSet.class); + testCollection(NavigableSet.class, TreeSet.class); // concrete types - assertThat(createCollection(HashSet.class, 0)).isInstanceOf(HashSet.class); - assertThat(createCollection(HashSet.class, String.class, 0)).isInstanceOf(HashSet.class); + testCollection(ArrayList.class, ArrayList.class); + testCollection(HashSet.class, HashSet.class); + testCollection(LinkedHashSet.class, LinkedHashSet.class); + testCollection(TreeSet.class, TreeSet.class); + } + + private void testCollection(Class collectionType, Class resultType) { + assertThat(CollectionFactory.isApproximableCollectionType(collectionType)).isTrue(); + assertThat(createCollection(collectionType, 0)).isExactlyInstanceOf(resultType); + assertThat(createCollection(collectionType, String.class, 0)).isExactlyInstanceOf(resultType); } @Test @@ -258,20 +260,22 @@ void rejectsNullCollectionType() { @Test void createsMapsCorrectly() { // interfaces - assertThat(createMap(Map.class, 0)).isInstanceOf(LinkedHashMap.class); - assertThat(createMap(SortedMap.class, 0)).isInstanceOf(TreeMap.class); - assertThat(createMap(NavigableMap.class, 0)).isInstanceOf(TreeMap.class); - assertThat(createMap(MultiValueMap.class, 0)).isInstanceOf(LinkedMultiValueMap.class); - - assertThat(createMap(Map.class, String.class, 0)).isInstanceOf(LinkedHashMap.class); - assertThat(createMap(SortedMap.class, String.class, 0)).isInstanceOf(TreeMap.class); - assertThat(createMap(NavigableMap.class, String.class, 0)).isInstanceOf(TreeMap.class); - assertThat(createMap(MultiValueMap.class, String.class, 0)).isInstanceOf(LinkedMultiValueMap.class); + testMap(Map.class, LinkedHashMap.class); + testMap(SortedMap.class, TreeMap.class); + testMap(NavigableMap.class, TreeMap.class); + testMap(MultiValueMap.class, LinkedMultiValueMap.class); // concrete types - assertThat(createMap(HashMap.class, 0)).isInstanceOf(HashMap.class); + testMap(HashMap.class, HashMap.class); + testMap(LinkedHashMap.class, LinkedHashMap.class); + testMap(TreeMap.class, TreeMap.class); + testMap(LinkedMultiValueMap.class, LinkedMultiValueMap.class); + } - assertThat(createMap(HashMap.class, String.class, 0)).isInstanceOf(HashMap.class); + private void testMap(Class mapType, Class resultType) { + assertThat(CollectionFactory.isApproximableMapType(mapType)).isTrue(); + assertThat(createMap(mapType, 0)).isExactlyInstanceOf(resultType); + assertThat(createMap(mapType, String.class, 0)).isExactlyInstanceOf(resultType); } @Test diff --git a/spring-core/src/test/java/org/springframework/core/LocalVariableTableParameterNameDiscovererTests.java b/spring-core/src/test/java/org/springframework/core/LocalVariableTableParameterNameDiscovererTests.java index 4ab6fc86967c..db7ccb5efe76 100644 --- a/spring-core/src/test/java/org/springframework/core/LocalVariableTableParameterNameDiscovererTests.java +++ b/spring-core/src/test/java/org/springframework/core/LocalVariableTableParameterNameDiscovererTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2022 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. @@ -34,7 +34,7 @@ */ class LocalVariableTableParameterNameDiscovererTests { - private final LocalVariableTableParameterNameDiscoverer discoverer = new LocalVariableTableParameterNameDiscoverer(); + private final ParameterNameDiscoverer discoverer = new LocalVariableTableParameterNameDiscoverer(); @Test @@ -162,23 +162,23 @@ void generifiedClass() throws Exception { Constructor ctor = clazz.getDeclaredConstructor(Object.class); String[] names = discoverer.getParameterNames(ctor); - assertThat(names.length).isEqualTo(1); + assertThat(names).hasSize(1); assertThat(names[0]).isEqualTo("key"); ctor = clazz.getDeclaredConstructor(Object.class, Object.class); names = discoverer.getParameterNames(ctor); - assertThat(names.length).isEqualTo(2); + assertThat(names).hasSize(2); assertThat(names[0]).isEqualTo("key"); assertThat(names[1]).isEqualTo("value"); Method m = clazz.getMethod("generifiedStaticMethod", Object.class); names = discoverer.getParameterNames(m); - assertThat(names.length).isEqualTo(1); + assertThat(names).hasSize(1); assertThat(names[0]).isEqualTo("param"); m = clazz.getMethod("generifiedMethod", Object.class, long.class, Object.class, Object.class); names = discoverer.getParameterNames(m); - assertThat(names.length).isEqualTo(4); + assertThat(names).hasSize(4); assertThat(names[0]).isEqualTo("param"); assertThat(names[1]).isEqualTo("x"); assertThat(names[2]).isEqualTo("key"); @@ -186,21 +186,21 @@ void generifiedClass() throws Exception { m = clazz.getMethod("voidStaticMethod", Object.class, long.class, int.class); names = discoverer.getParameterNames(m); - assertThat(names.length).isEqualTo(3); + assertThat(names).hasSize(3); assertThat(names[0]).isEqualTo("obj"); assertThat(names[1]).isEqualTo("x"); assertThat(names[2]).isEqualTo("i"); m = clazz.getMethod("nonVoidStaticMethod", Object.class, long.class, int.class); names = discoverer.getParameterNames(m); - assertThat(names.length).isEqualTo(3); + assertThat(names).hasSize(3); assertThat(names[0]).isEqualTo("obj"); assertThat(names[1]).isEqualTo("x"); assertThat(names[2]).isEqualTo("i"); m = clazz.getMethod("getDate"); names = discoverer.getParameterNames(m); - assertThat(names.length).isEqualTo(0); + assertThat(names).isEmpty(); } @Disabled("Ignored because Ubuntu packages OpenJDK with debug symbols enabled. See SPR-8078.") diff --git a/spring-core/src/test/java/org/springframework/core/MethodIntrospectorTests.java b/spring-core/src/test/java/org/springframework/core/MethodIntrospectorTests.java new file mode 100644 index 000000000000..f11f8eb86598 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/MethodIntrospectorTests.java @@ -0,0 +1,112 @@ +/* + * Copyright 2002-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.core; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.reflect.Method; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.MethodIntrospector.MetadataLookup; +import org.springframework.core.annotation.MergedAnnotations; +import org.springframework.util.ReflectionUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.core.annotation.MergedAnnotations.SearchStrategy.TYPE_HIERARCHY; + +/** + * Tests for {@link MethodIntrospector}. + * + * @author Sam Brannen + * @since 5.3.34 + */ +class MethodIntrospectorTests { + + @Test // gh-32586 + void selectMethodsAndClearDeclaredMethodsCacheBetweenInvocations() { + Class targetType = ActualController.class; + + // Preconditions for this use case. + assertThat(targetType).isPublic(); + assertThat(targetType.getSuperclass()).isPackagePrivate(); + + MetadataLookup metadataLookup = (MetadataLookup) method -> { + if (MergedAnnotations.from(method, TYPE_HIERARCHY).isPresent(Mapped.class)) { + return method.getName(); + } + return null; + }; + + // Start with a clean slate. + ReflectionUtils.clearCache(); + + // Round #1 + Map methods = MethodIntrospector.selectMethods(targetType, metadataLookup); + assertThat(methods.values()).containsExactlyInAnyOrder("update", "delete"); + + // Simulate ConfigurableApplicationContext#refresh() which clears the + // ReflectionUtils#declaredMethodsCache but NOT the BridgeMethodResolver#cache. + // As a consequence, ReflectionUtils.getDeclaredMethods(...) will return a + // new set of methods that are logically equivalent to but not identical + // to (in terms of object identity) any bridged methods cached in the + // BridgeMethodResolver cache. + ReflectionUtils.clearCache(); + + // Round #2 + methods = MethodIntrospector.selectMethods(targetType, metadataLookup); + assertThat(methods.values()).containsExactlyInAnyOrder("update", "delete"); + } + + + @Retention(RetentionPolicy.RUNTIME) + @interface Mapped { + } + + interface Controller { + + void unmappedMethod(); + + @Mapped + void update(); + + @Mapped + void delete(); + } + + // Must NOT be public. + abstract static class AbstractController implements Controller { + + @Override + public void unmappedMethod() { + } + + @Override + public void delete() { + } + } + + // MUST be public. + public static class ActualController extends AbstractController { + + @Override + public void update() { + } + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/ReactiveAdapterRegistryTests.java b/spring-core/src/test/java/org/springframework/core/ReactiveAdapterRegistryTests.java index 35683ac0cbee..8d4728b2e69b 100644 --- a/spring-core/src/test/java/org/springframework/core/ReactiveAdapterRegistryTests.java +++ b/spring-core/src/test/java/org/springframework/core/ReactiveAdapterRegistryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -37,6 +37,7 @@ * Unit tests for {@link ReactiveAdapterRegistry}. * * @author Rossen Stoyanchev + * @author Juergen Hoeller */ @SuppressWarnings("unchecked") class ReactiveAdapterRegistryTests { @@ -52,14 +53,40 @@ void getAdapterForReactiveSubType() { ReactiveAdapter adapter2 = getAdapter(ExtendedFlux.class); assertThat(adapter2).isSameAs(adapter1); + // Register regular reactive type (after existing adapters) this.registry.registerReactiveType( ReactiveTypeDescriptor.multiValue(ExtendedFlux.class, ExtendedFlux::empty), o -> (ExtendedFlux) o, ExtendedFlux::from); + // Matches for ExtendedFlux itself ReactiveAdapter adapter3 = getAdapter(ExtendedFlux.class); assertThat(adapter3).isNotNull(); assertThat(adapter3).isNotSameAs(adapter1); + + // Does not match for ExtendedFlux subclass since the default Flux adapter + // is being assignability-checked first when no specific match was found + ReactiveAdapter adapter4 = getAdapter(ExtendedExtendedFlux.class); + assertThat(adapter4).isSameAs(adapter1); + + // Register reactive type override (before existing adapters) + this.registry.registerReactiveTypeOverride( + ReactiveTypeDescriptor.multiValue(Flux.class, ExtendedFlux::empty), + o -> (ExtendedFlux) o, + ExtendedFlux::from); + + // Override match for Flux + ReactiveAdapter adapter5 = getAdapter(Flux.class); + assertThat(adapter5).isNotNull(); + assertThat(adapter5).isNotSameAs(adapter1); + + // Initially registered adapter specifically matches for ExtendedFlux + ReactiveAdapter adapter6 = getAdapter(ExtendedFlux.class); + assertThat(adapter6).isSameAs(adapter3); + + // Override match for ExtendedFlux subclass + ReactiveAdapter adapter7 = getAdapter(ExtendedExtendedFlux.class); + assertThat(adapter7).isSameAs(adapter5); } @@ -79,6 +106,10 @@ public void subscribe(CoreSubscriber actual) { } + private static class ExtendedExtendedFlux extends ExtendedFlux { + } + + @Nested class Reactor { @@ -100,7 +131,7 @@ void toFlux() { List sequence = Arrays.asList(1, 2, 3); Publisher source = io.reactivex.rxjava3.core.Flowable.fromIterable(sequence); Object target = getAdapter(Flux.class).fromPublisher(source); - assertThat(target instanceof Flux).isTrue(); + assertThat(target).isInstanceOf(Flux.class); assertThat(((Flux) target).collectList().block(ONE_SECOND)).isEqualTo(sequence); } @@ -108,7 +139,7 @@ void toFlux() { void toMono() { Publisher source = io.reactivex.rxjava3.core.Flowable.fromArray(1, 2, 3); Object target = getAdapter(Mono.class).fromPublisher(source); - assertThat(target instanceof Mono).isTrue(); + assertThat(target).isInstanceOf(Mono.class); assertThat(((Mono) target).block(ONE_SECOND)).isEqualTo(Integer.valueOf(1)); } @@ -116,7 +147,7 @@ void toMono() { void toCompletableFuture() throws Exception { Publisher source = Flux.fromArray(new Integer[] {1, 2, 3}); Object target = getAdapter(CompletableFuture.class).fromPublisher(source); - assertThat(target instanceof CompletableFuture).isTrue(); + assertThat(target).isInstanceOf(CompletableFuture.class); assertThat(((CompletableFuture) target).get()).isEqualTo(Integer.valueOf(1)); } @@ -125,7 +156,7 @@ void fromCompletableFuture() { CompletableFuture future = new CompletableFuture<>(); future.complete(1); Object target = getAdapter(CompletableFuture.class).toPublisher(future); - assertThat(target instanceof Mono).as("Expected Mono Publisher: " + target.getClass().getName()).isTrue(); + assertThat(target).as("Expected Mono Publisher: " + target.getClass().getName()).isInstanceOf(Mono.class); assertThat(((Mono) target).block(ONE_SECOND)).isEqualTo(Integer.valueOf(1)); } } @@ -294,7 +325,7 @@ void toFlowable() { List sequence = Arrays.asList(1, 2, 3); Publisher source = Flux.fromIterable(sequence); Object target = getAdapter(io.reactivex.rxjava3.core.Flowable.class).fromPublisher(source); - assertThat(target instanceof io.reactivex.rxjava3.core.Flowable).isTrue(); + assertThat(target).isInstanceOf(io.reactivex.rxjava3.core.Flowable.class); assertThat(((io.reactivex.rxjava3.core.Flowable) target).toList().blockingGet()).isEqualTo(sequence); } @@ -303,7 +334,7 @@ void toObservable() { List sequence = Arrays.asList(1, 2, 3); Publisher source = Flux.fromIterable(sequence); Object target = getAdapter(io.reactivex.rxjava3.core.Observable.class).fromPublisher(source); - assertThat(target instanceof io.reactivex.rxjava3.core.Observable).isTrue(); + assertThat(target).isInstanceOf(io.reactivex.rxjava3.core.Observable.class); assertThat(((io.reactivex.rxjava3.core.Observable) target).toList().blockingGet()).isEqualTo(sequence); } @@ -311,7 +342,7 @@ void toObservable() { void toSingle() { Publisher source = Flux.fromArray(new Integer[] {1}); Object target = getAdapter(io.reactivex.rxjava3.core.Single.class).fromPublisher(source); - assertThat(target instanceof io.reactivex.rxjava3.core.Single).isTrue(); + assertThat(target).isInstanceOf(io.reactivex.rxjava3.core.Single.class); assertThat(((io.reactivex.rxjava3.core.Single) target).blockingGet()).isEqualTo(Integer.valueOf(1)); } @@ -319,7 +350,7 @@ void toSingle() { void toCompletable() { Publisher source = Flux.fromArray(new Integer[] {1, 2, 3}); Object target = getAdapter(io.reactivex.rxjava3.core.Completable.class).fromPublisher(source); - assertThat(target instanceof io.reactivex.rxjava3.core.Completable).isTrue(); + assertThat(target).isInstanceOf(io.reactivex.rxjava3.core.Completable.class); ((io.reactivex.rxjava3.core.Completable) target).blockingAwait(); } @@ -328,7 +359,7 @@ void fromFlowable() { List sequence = Arrays.asList(1, 2, 3); Object source = io.reactivex.rxjava3.core.Flowable.fromIterable(sequence); Object target = getAdapter(io.reactivex.rxjava3.core.Flowable.class).toPublisher(source); - assertThat(target instanceof Flux).as("Expected Flux Publisher: " + target.getClass().getName()).isTrue(); + assertThat(target).as("Expected Flux Publisher: " + target.getClass().getName()).isInstanceOf(Flux.class); assertThat(((Flux) target).collectList().block(ONE_SECOND)).isEqualTo(sequence); } @@ -337,7 +368,7 @@ void fromObservable() { List sequence = Arrays.asList(1, 2, 3); Object source = io.reactivex.rxjava3.core.Observable.fromIterable(sequence); Object target = getAdapter(io.reactivex.rxjava3.core.Observable.class).toPublisher(source); - assertThat(target instanceof Flux).as("Expected Flux Publisher: " + target.getClass().getName()).isTrue(); + assertThat(target).as("Expected Flux Publisher: " + target.getClass().getName()).isInstanceOf(Flux.class); assertThat(((Flux) target).collectList().block(ONE_SECOND)).isEqualTo(sequence); } @@ -345,7 +376,7 @@ void fromObservable() { void fromSingle() { Object source = io.reactivex.rxjava3.core.Single.just(1); Object target = getAdapter(io.reactivex.rxjava3.core.Single.class).toPublisher(source); - assertThat(target instanceof Mono).as("Expected Mono Publisher: " + target.getClass().getName()).isTrue(); + assertThat(target).as("Expected Mono Publisher: " + target.getClass().getName()).isInstanceOf(Mono.class); assertThat(((Mono) target).block(ONE_SECOND)).isEqualTo(Integer.valueOf(1)); } @@ -353,7 +384,7 @@ void fromSingle() { void fromCompletable() { Object source = io.reactivex.rxjava3.core.Completable.complete(); Object target = getAdapter(io.reactivex.rxjava3.core.Completable.class).toPublisher(source); - assertThat(target instanceof Mono).as("Expected Mono Publisher: " + target.getClass().getName()).isTrue(); + assertThat(target).as("Expected Mono Publisher: " + target.getClass().getName()).isInstanceOf(Mono.class); ((Mono) target).block(ONE_SECOND); } } diff --git a/spring-core/src/test/java/org/springframework/core/ResolvableTypeTests.java b/spring-core/src/test/java/org/springframework/core/ResolvableTypeTests.java index 48641a7c70c8..a3192bd91cc3 100644 --- a/spring-core/src/test/java/org/springframework/core/ResolvableTypeTests.java +++ b/spring-core/src/test/java/org/springframework/core/ResolvableTypeTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -200,8 +200,8 @@ void forPrivateField() throws Exception { @Test void forFieldMustNotBeNull() throws Exception { assertThatIllegalArgumentException() - .isThrownBy(() -> ResolvableType.forField(null)) - .withMessage("Field must not be null"); + .isThrownBy(() -> ResolvableType.forField(null)) + .withMessage("Field must not be null"); } @Test @@ -214,8 +214,8 @@ void forConstructorParameter() throws Exception { @Test void forConstructorParameterMustNotBeNull() throws Exception { assertThatIllegalArgumentException() - .isThrownBy(() -> ResolvableType.forConstructorParameter(null, 0)) - .withMessage("Constructor must not be null"); + .isThrownBy(() -> ResolvableType.forConstructorParameter(null, 0)) + .withMessage("Constructor must not be null"); } @Test @@ -228,8 +228,8 @@ void forMethodParameterByIndex() throws Exception { @Test void forMethodParameterByIndexMustNotBeNull() throws Exception { assertThatIllegalArgumentException() - .isThrownBy(() -> ResolvableType.forMethodParameter(null, 0)) - .withMessage("Method must not be null"); + .isThrownBy(() -> ResolvableType.forMethodParameter(null, 0)) + .withMessage("Method must not be null"); } @Test @@ -268,8 +268,8 @@ void forMethodParameterWithNestingAndLevels() throws Exception { @Test void forMethodParameterMustNotBeNull() throws Exception { assertThatIllegalArgumentException() - .isThrownBy(() -> ResolvableType.forMethodParameter(null)) - .withMessage("MethodParameter must not be null"); + .isThrownBy(() -> ResolvableType.forMethodParameter(null)) + .withMessage("MethodParameter must not be null"); } @Test // SPR-16210 @@ -295,8 +295,8 @@ void forMethodReturn() throws Exception { @Test void forMethodReturnMustNotBeNull() throws Exception { assertThatIllegalArgumentException() - .isThrownBy(() -> ResolvableType.forMethodReturnType(null)) - .withMessage("Method must not be null"); + .isThrownBy(() -> ResolvableType.forMethodReturnType(null)) + .withMessage("Method must not be null"); } @Test @@ -343,7 +343,7 @@ void getComponentTypeForClassArray() throws Exception { ResolvableType type = ResolvableType.forField(field); assertThat(type.isArray()).isTrue(); assertThat(type.getComponentType().getType()) - .isEqualTo(((Class) field.getGenericType()).getComponentType()); + .isEqualTo(((Class) field.getGenericType()).getComponentType()); } @Test @@ -535,7 +535,7 @@ void hasGenerics() throws Exception { void getGenericsFromParameterizedType() throws Exception { ResolvableType type = ResolvableType.forClass(List.class, ExtendsList.class); ResolvableType[] generics = type.getGenerics(); - assertThat(generics.length).isEqualTo(1); + assertThat(generics).hasSize(1); assertThat(generics[0].resolve()).isEqualTo(CharSequence.class); } @@ -543,7 +543,7 @@ void getGenericsFromParameterizedType() throws Exception { void getGenericsFromClass() throws Exception { ResolvableType type = ResolvableType.forClass(List.class); ResolvableType[] generics = type.getGenerics(); - assertThat(generics.length).isEqualTo(1); + assertThat(generics).hasSize(1); assertThat(generics[0].getType().toString()).isEqualTo("E"); } @@ -558,7 +558,7 @@ void noGetGenerics() throws Exception { void getResolvedGenerics() throws Exception { ResolvableType type = ResolvableType.forClass(List.class, ExtendsList.class); Class[] generics = type.resolveGenerics(); - assertThat(generics.length).isEqualTo(1); + assertThat(generics).hasSize(1); assertThat(generics[0]).isEqualTo(CharSequence.class); } @@ -686,7 +686,6 @@ void resolveBoundedTypeVariableResult() throws Exception { assertThat(type.resolve()).isEqualTo(CharSequence.class); } - @Test void resolveBoundedTypeVariableWildcardResult() throws Exception { ResolvableType type = ResolvableType.forMethodReturnType(Methods.class.getMethod("boundedTypeVariableWildcardResult")); @@ -701,30 +700,26 @@ void resolveVariableNotFound() throws Exception { @Test void resolveTypeVariableFromSimpleInterfaceType() { - ResolvableType type = ResolvableType.forClass( - MySimpleInterfaceType.class).as(MyInterfaceType.class); + ResolvableType type = ResolvableType.forClass(MySimpleInterfaceType.class).as(MyInterfaceType.class); assertThat(type.resolveGeneric()).isEqualTo(String.class); } @Test void resolveTypeVariableFromSimpleCollectionInterfaceType() { - ResolvableType type = ResolvableType.forClass( - MyCollectionInterfaceType.class).as(MyInterfaceType.class); + ResolvableType type = ResolvableType.forClass(MyCollectionInterfaceType.class).as(MyInterfaceType.class); assertThat(type.resolveGeneric()).isEqualTo(Collection.class); assertThat(type.resolveGeneric(0, 0)).isEqualTo(String.class); } @Test void resolveTypeVariableFromSimpleSuperclassType() { - ResolvableType type = ResolvableType.forClass( - MySimpleSuperclassType.class).as(MySuperclassType.class); + ResolvableType type = ResolvableType.forClass(MySimpleSuperclassType.class).as(MySuperclassType.class); assertThat(type.resolveGeneric()).isEqualTo(String.class); } @Test void resolveTypeVariableFromSimpleCollectionSuperclassType() { - ResolvableType type = ResolvableType.forClass( - MyCollectionSuperclassType.class).as(MySuperclassType.class); + ResolvableType type = ResolvableType.forClass(MyCollectionSuperclassType.class).as(MySuperclassType.class); assertThat(type.resolveGeneric()).isEqualTo(Collection.class); assertThat(type.resolveGeneric(0, 0)).isEqualTo(String.class); } @@ -751,8 +746,7 @@ void resolveTypeVariableFromFieldTypeWithImplementsType() throws Exception { void resolveTypeVariableFromSuperType() throws Exception { ResolvableType type = ResolvableType.forClass(ExtendsList.class); assertThat(type.resolve()).isEqualTo(ExtendsList.class); - assertThat(type.asCollection().resolveGeneric()) - .isEqualTo(CharSequence.class); + assertThat(type.asCollection().resolveGeneric()).isEqualTo(CharSequence.class); } @Test @@ -964,8 +958,8 @@ void resolveFromClassWithGenerics() throws Exception { @Test void isAssignableFromMustNotBeNull() throws Exception { assertThatIllegalArgumentException() - .isThrownBy(() -> ResolvableType.forClass(Object.class).isAssignableFrom((ResolvableType) null)) - .withMessage("ResolvableType must not be null"); + .isThrownBy(() -> ResolvableType.forClass(Object.class).isAssignableFrom((ResolvableType) null)) + .withMessage("ResolvableType must not be null"); } @Test @@ -1004,6 +998,7 @@ void isAssignableFromForClassAndClass() throws Exception { void isAssignableFromCannotBeResolved() throws Exception { ResolvableType objectType = ResolvableType.forClass(Object.class); ResolvableType unresolvableVariable = ResolvableType.forField(AssignmentBase.class.getField("o")); + assertThat(unresolvableVariable.resolve()).isNull(); assertThatResolvableType(objectType).isAssignableFrom(unresolvableVariable); assertThatResolvableType(unresolvableVariable).isAssignableFrom(objectType); @@ -1220,9 +1215,9 @@ void classWithGenericsAs() throws Exception { @Test void forClassWithMismatchedGenerics() throws Exception { assertThatIllegalArgumentException() - .isThrownBy(() -> ResolvableType.forClassWithGenerics(Map.class, Integer.class)) - .withMessageContaining("Mismatched number of generics specified for") - .withMessageContaining("java.util.Map"); + .isThrownBy(() -> ResolvableType.forClassWithGenerics(Map.class, Integer.class)) + .withMessageContaining("Mismatched number of generics specified for") + .withMessageContaining("java.util.Map"); } @Test @@ -1277,7 +1272,7 @@ void hasUnresolvableGenericsWhenSelfNotResolvable() throws Exception { } @Test - void hasUnresolvableGenericsWhenImplementesRawInterface() throws Exception { + void hasUnresolvableGenericsWhenImplementingRawInterface() throws Exception { ResolvableType type = ResolvableType.forClass(MySimpleInterfaceTypeWithImplementsRaw.class); for (ResolvableType generic : type.getGenerics()) { assertThat(generic.resolve()).isNotNull(); @@ -1307,7 +1302,7 @@ void spr12701() throws Exception { Type type = resolvableType.getType(); assertThat(type).isInstanceOf(ParameterizedType.class); assertThat(((ParameterizedType) type).getRawType()).isEqualTo(Callable.class); - assertThat(((ParameterizedType) type).getActualTypeArguments().length).isEqualTo(1); + assertThat(((ParameterizedType) type).getActualTypeArguments()).hasSize(1); assertThat(((ParameterizedType) type).getActualTypeArguments()[0]).isEqualTo(String.class); } diff --git a/spring-core/src/test/java/org/springframework/core/SpringCoreBlockHoundIntegrationTests.java b/spring-core/src/test/java/org/springframework/core/SpringCoreBlockHoundIntegrationTests.java index ea4b7fbbd552..6a9b9596c0c7 100644 --- a/spring-core/src/test/java/org/springframework/core/SpringCoreBlockHoundIntegrationTests.java +++ b/spring-core/src/test/java/org/springframework/core/SpringCoreBlockHoundIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 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. @@ -43,11 +43,10 @@ @DisabledForJreRange(min = JAVA_14) public class SpringCoreBlockHoundIntegrationTests { - @BeforeAll - static void setUp() { + static void setup() { BlockHound.builder() - .with(new ReactorBlockHoundIntegration()) // Reactor non-blocking thread predicate + .with(new ReactorBlockHoundIntegration()) // Reactor non-blocking thread predicate .with(new ReactiveAdapterRegistry.SpringCoreBlockHoundIntegration()) .install(); } diff --git a/spring-core/src/test/java/org/springframework/core/annotation/AnnotatedElementUtilsTests.java b/spring-core/src/test/java/org/springframework/core/annotation/AnnotatedElementUtilsTests.java index d90cfec05b32..336ca99b75c5 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/AnnotatedElementUtilsTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/AnnotatedElementUtilsTests.java @@ -20,6 +20,7 @@ import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Inherited; +import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @@ -77,6 +78,7 @@ * @see AnnotationUtilsTests * @see MultipleComposedAnnotationsOnSingleAnnotatedElementTests * @see ComposedRepeatableAnnotationsTests + * @see NestedRepeatableAnnotationsTests */ class AnnotatedElementUtilsTests { @@ -908,6 +910,31 @@ void getMergedAnnotationOnThreeDeepMetaWithValue() { assertThat(annotation.value()).containsExactly("FromValueAttributeMeta"); } + /** + * @since 5.3.25 + */ + @Test // gh-29685 + void getMergedRepeatableAnnotationsWithContainerWithMultipleAttributes() { + Set repeatableAnnotations = + AnnotatedElementUtils.getMergedRepeatableAnnotations( + StandardRepeatablesWithContainerWithMultipleAttributesTestCase.class, + StandardRepeatableWithContainerWithMultipleAttributes.class); + assertThat(repeatableAnnotations).map(StandardRepeatableWithContainerWithMultipleAttributes::value) + .containsExactly("a", "b"); + } + + /** + * @since 5.3.25 + */ + @Test // gh-29685 + void findMergedRepeatableAnnotationsWithContainerWithMultipleAttributes() { + Set repeatableAnnotations = + AnnotatedElementUtils.findMergedRepeatableAnnotations( + StandardRepeatablesWithContainerWithMultipleAttributesTestCase.class, + StandardRepeatableWithContainerWithMultipleAttributes.class); + assertThat(repeatableAnnotations).map(StandardRepeatableWithContainerWithMultipleAttributes::value) + .containsExactly("a", "b"); + } // ------------------------------------------------------------------------- @@ -1557,4 +1584,24 @@ class ForAnnotationsClass { static class ValueAttributeMetaMetaClass { } + @Retention(RetentionPolicy.RUNTIME) + @interface StandardContainerWithMultipleAttributes { + + StandardRepeatableWithContainerWithMultipleAttributes[] value(); + + String name() default ""; + } + + @Retention(RetentionPolicy.RUNTIME) + @Repeatable(StandardContainerWithMultipleAttributes.class) + @interface StandardRepeatableWithContainerWithMultipleAttributes { + + String value() default ""; + } + + @StandardRepeatableWithContainerWithMultipleAttributes("a") + @StandardRepeatableWithContainerWithMultipleAttributes("b") + static class StandardRepeatablesWithContainerWithMultipleAttributesTestCase { + } + } diff --git a/spring-core/src/test/java/org/springframework/core/annotation/AnnotationIntrospectionFailureTests.java b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationIntrospectionFailureTests.java index 0fa93c99e938..9750cd5ac2e8 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/AnnotationIntrospectionFailureTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationIntrospectionFailureTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -43,55 +43,45 @@ class AnnotationIntrospectionFailureTests { @Test void filteredTypeThrowsTypeNotPresentException() throws Exception { - FilteringClassLoader classLoader = new FilteringClassLoader( - getClass().getClassLoader()); - Class withExampleAnnotation = ClassUtils.forName( - WithExampleAnnotation.class.getName(), classLoader); - Annotation annotation = withExampleAnnotation.getAnnotations()[0]; + FilteringClassLoader classLoader = new FilteringClassLoader(getClass().getClassLoader()); + Class withAnnotation = ClassUtils.forName(WithExampleAnnotation.class.getName(), classLoader); + Annotation annotation = withAnnotation.getAnnotations()[0]; Method method = annotation.annotationType().getMethod("value"); method.setAccessible(true); - assertThatExceptionOfType(TypeNotPresentException.class).isThrownBy(() -> - ReflectionUtils.invokeMethod(method, annotation)) - .withCauseInstanceOf(ClassNotFoundException.class); + assertThatExceptionOfType(TypeNotPresentException.class) + .isThrownBy(() -> ReflectionUtils.invokeMethod(method, annotation)) + .withCauseInstanceOf(ClassNotFoundException.class); } @Test @SuppressWarnings("unchecked") void filteredTypeInMetaAnnotationWhenUsingAnnotatedElementUtilsHandlesException() throws Exception { - FilteringClassLoader classLoader = new FilteringClassLoader( - getClass().getClassLoader()); - Class withExampleMetaAnnotation = ClassUtils.forName( - WithExampleMetaAnnotation.class.getName(), classLoader); - Class exampleAnnotationClass = (Class) ClassUtils.forName( - ExampleAnnotation.class.getName(), classLoader); - Class exampleMetaAnnotationClass = (Class) ClassUtils.forName( - ExampleMetaAnnotation.class.getName(), classLoader); - assertThat(AnnotatedElementUtils.getMergedAnnotationAttributes( - withExampleMetaAnnotation, exampleAnnotationClass)).isNull(); - assertThat(AnnotatedElementUtils.getMergedAnnotationAttributes( - withExampleMetaAnnotation, exampleMetaAnnotationClass)).isNull(); - assertThat(AnnotatedElementUtils.hasAnnotation(withExampleMetaAnnotation, - exampleAnnotationClass)).isFalse(); - assertThat(AnnotatedElementUtils.hasAnnotation(withExampleMetaAnnotation, - exampleMetaAnnotationClass)).isFalse(); + FilteringClassLoader classLoader = new FilteringClassLoader(getClass().getClassLoader()); + Class withAnnotation = ClassUtils.forName(WithExampleMetaAnnotation.class.getName(), classLoader); + Class annotationClass = (Class) + ClassUtils.forName(ExampleAnnotation.class.getName(), classLoader); + Class metaAnnotationClass = (Class) + ClassUtils.forName(ExampleMetaAnnotation.class.getName(), classLoader); + assertThat(AnnotatedElementUtils.getMergedAnnotationAttributes(withAnnotation, annotationClass)).isNull(); + assertThat(AnnotatedElementUtils.getMergedAnnotationAttributes(withAnnotation, metaAnnotationClass)).isNull(); + assertThat(AnnotatedElementUtils.hasAnnotation(withAnnotation, annotationClass)).isFalse(); + assertThat(AnnotatedElementUtils.hasAnnotation(withAnnotation, metaAnnotationClass)).isFalse(); } @Test @SuppressWarnings("unchecked") void filteredTypeInMetaAnnotationWhenUsingMergedAnnotationsHandlesException() throws Exception { - FilteringClassLoader classLoader = new FilteringClassLoader( - getClass().getClassLoader()); - Class withExampleMetaAnnotation = ClassUtils.forName( - WithExampleMetaAnnotation.class.getName(), classLoader); - Class exampleAnnotationClass = (Class) ClassUtils.forName( - ExampleAnnotation.class.getName(), classLoader); - Class exampleMetaAnnotationClass = (Class) ClassUtils.forName( - ExampleMetaAnnotation.class.getName(), classLoader); - MergedAnnotations annotations = MergedAnnotations.from(withExampleMetaAnnotation); - assertThat(annotations.get(exampleAnnotationClass).isPresent()).isFalse(); - assertThat(annotations.get(exampleMetaAnnotationClass).isPresent()).isFalse(); - assertThat(annotations.isPresent(exampleMetaAnnotationClass)).isFalse(); - assertThat(annotations.isPresent(exampleAnnotationClass)).isFalse(); + FilteringClassLoader classLoader = new FilteringClassLoader(getClass().getClassLoader()); + Class withAnnotation = ClassUtils.forName(WithExampleMetaAnnotation.class.getName(), classLoader); + Class annotationClass = (Class) + ClassUtils.forName(ExampleAnnotation.class.getName(), classLoader); + Class metaAnnotationClass = (Class) + ClassUtils.forName(ExampleMetaAnnotation.class.getName(), classLoader); + MergedAnnotations annotations = MergedAnnotations.from(withAnnotation); + assertThat(annotations.get(annotationClass).isPresent()).isFalse(); + assertThat(annotations.get(metaAnnotationClass).isPresent()).isFalse(); + assertThat(annotations.isPresent(metaAnnotationClass)).isFalse(); + assertThat(annotations.isPresent(annotationClass)).isFalse(); } @@ -103,17 +93,16 @@ static class FilteringClassLoader extends OverridingClassLoader { @Override protected boolean isEligibleForOverriding(String className) { - return className.startsWith( - AnnotationIntrospectionFailureTests.class.getName()); + return className.startsWith(AnnotationIntrospectionFailureTests.class.getName()) || + className.startsWith("jdk.internal"); } @Override - protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { - if (name.startsWith(AnnotationIntrospectionFailureTests.class.getName()) && - name.contains("Filtered")) { + protected Class loadClassForOverriding(String name) throws ClassNotFoundException { + if (name.contains("Filtered") || name.startsWith("jdk.internal")) { throw new ClassNotFoundException(name); } - return super.loadClass(name, resolve); + return super.loadClassForOverriding(name); } } diff --git a/spring-core/src/test/java/org/springframework/core/annotation/AnnotationsScannerTests.java b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationsScannerTests.java index 1ef9cd9b003c..c10540ca0ec1 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/AnnotationsScannerTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationsScannerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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. @@ -194,7 +194,7 @@ void typeHierarchyStrategyOnClassWhenHasSuperclassScansSuperclass() { } @Test - void typeHierarchyStrategyOnClassWhenHasInterfaceDoesNotIncludeInterfaces() { + void typeHierarchyStrategyOnClassWhenHasSingleInterfaceScansInterfaces() { Class source = WithSingleInterface.class; assertThat(scan(source, SearchStrategy.TYPE_HIERARCHY)).containsExactly( "0:TestAnnotation1", "1:TestAnnotation2", "1:TestInheritedAnnotation2"); @@ -350,10 +350,19 @@ void typeHierarchyStrategyOnMethodWhenHasSuperclassScansSuperclass() { } @Test - void typeHierarchyStrategyOnMethodWhenHasInterfaceDoesNotIncludeInterfaces() { + void typeHierarchyStrategyOnMethodWhenHasInterfaceScansInterfaces() { Method source = methodFrom(WithSingleInterface.class); assertThat(scan(source, SearchStrategy.TYPE_HIERARCHY)).containsExactly( "0:TestAnnotation1", "1:TestAnnotation2", "1:TestInheritedAnnotation2"); + + source = methodFrom(Hello1Impl.class); + assertThat(scan(source, SearchStrategy.TYPE_HIERARCHY)).containsExactly("1:TestAnnotation1"); + } + + @Test // gh-31803 + void typeHierarchyStrategyOnMethodWhenHasInterfaceHierarchyScansInterfacesOnlyOnce() { + Method source = methodFrom(Hello2Impl.class); + assertThat(scan(source, SearchStrategy.TYPE_HIERARCHY)).containsExactly("1:TestAnnotation1"); } @Test @@ -663,6 +672,30 @@ public void method() { } } + interface Hello1 { + + @TestAnnotation1 + void method(); + } + + interface Hello2 extends Hello1 { + } + + static class Hello1Impl implements Hello1 { + + @Override + public void method() { + } + } + + static class Hello2Impl implements Hello2 { + + @Override + public void method() { + } + } + + @TestAnnotation2 @TestInheritedAnnotation2 static class HierarchySuperclass extends HierarchySuperSuperclass { diff --git a/spring-core/src/test/java/org/springframework/core/annotation/AttributeMethodsTests.java b/spring-core/src/test/java/org/springframework/core/annotation/AttributeMethodsTests.java index 6fb0dcdb433b..859aa47f6b2d 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/AttributeMethodsTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/AttributeMethodsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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,24 +58,6 @@ void forAnnotationTypeWhenHasMultipleAttributesReturnsAttributes() { assertThat(getAll(methods)).flatExtracting(Method::getName).containsExactly("intValue", "value"); } - @Test - void hasOnlyValueAttributeWhenHasOnlyValueAttributeReturnsTrue() { - AttributeMethods methods = AttributeMethods.forAnnotationType(ValueOnly.class); - assertThat(methods.hasOnlyValueAttribute()).isTrue(); - } - - @Test - void hasOnlyValueAttributeWhenHasOnlySingleNonValueAttributeReturnsFalse() { - AttributeMethods methods = AttributeMethods.forAnnotationType(NonValueOnly.class); - assertThat(methods.hasOnlyValueAttribute()).isFalse(); - } - - @Test - void hasOnlyValueAttributeWhenHasOnlyMultipleAttributesIncludingValueReturnsFalse() { - AttributeMethods methods = AttributeMethods.forAnnotationType(MultipleAttributes.class); - assertThat(methods.hasOnlyValueAttribute()).isFalse(); - } - @Test void indexOfNameReturnsIndex() { AttributeMethods methods = AttributeMethods.forAnnotationType(MultipleAttributes.class); @@ -130,7 +112,7 @@ void isValidWhenHasTypeNotPresentExceptionReturnsFalse() { ClassValue annotation = mockAnnotation(ClassValue.class); given(annotation.value()).willThrow(TypeNotPresentException.class); AttributeMethods attributes = AttributeMethods.forAnnotationType(annotation.annotationType()); - assertThat(attributes.isValid(annotation)).isFalse(); + assertThat(attributes.canLoad(annotation)).isFalse(); } @Test @@ -139,7 +121,7 @@ void isValidWhenDoesNotHaveTypeNotPresentExceptionReturnsTrue() { ClassValue annotation = mock(ClassValue.class); given(annotation.value()).willReturn((Class) InputStream.class); AttributeMethods attributes = AttributeMethods.forAnnotationType(annotation.annotationType()); - assertThat(attributes.isValid(annotation)).isTrue(); + assertThat(attributes.canLoad(annotation)).isTrue(); } @Test diff --git a/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsRepeatableAnnotationTests.java b/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsRepeatableAnnotationTests.java index 40b1fdf5b8f8..d3391ec35eb4 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsRepeatableAnnotationTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsRepeatableAnnotationTests.java @@ -24,6 +24,7 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.lang.reflect.AnnotatedElement; +import java.util.Arrays; import java.util.Set; import java.util.stream.Stream; @@ -175,7 +176,7 @@ void typeHierarchyWhenOnClassReturnsAnnotations() { } @Test - void typeHierarchyWhenWhenOnSuperclassReturnsAnnotations() { + void typeHierarchyWhenOnSuperclassReturnsAnnotations() { Set annotations = getAnnotations(null, PeteRepeat.class, SearchStrategy.TYPE_HIERARCHY, SubRepeatableClass.class); assertThat(annotations.stream().map(PeteRepeat::value)).containsExactly("A", "B", @@ -240,6 +241,44 @@ void typeHierarchyAnnotationsWithLocalComposedAnnotationWhoseRepeatableMetaAnnot assertThat(annotationTypes).containsExactly(WithRepeatedMetaAnnotations.class, Noninherited.class, Noninherited.class); } + @Test // gh-32731 + void searchFindsRepeatableContainerAnnotationAndRepeatedAnnotations() { + Class clazz = StandardRepeatablesWithContainerWithMultipleAttributesTestCase.class; + + // NO RepeatableContainers + MergedAnnotations mergedAnnotations = MergedAnnotations.from(clazz, SearchStrategy.TYPE_HIERARCHY, RepeatableContainers.none()); + ContainerWithMultipleAttributes container = mergedAnnotations + .get(ContainerWithMultipleAttributes.class) + .synthesize(MergedAnnotation::isPresent).orElse(null); + assertThat(container).as("container").isNotNull(); + assertThat(container.name()).isEqualTo("enigma"); + RepeatableWithContainerWithMultipleAttributes[] repeatedAnnotations = container.value(); + assertThat(Arrays.stream(repeatedAnnotations).map(RepeatableWithContainerWithMultipleAttributes::value)) + .containsExactly("A", "B"); + Set set = + mergedAnnotations.stream(RepeatableWithContainerWithMultipleAttributes.class) + .collect(MergedAnnotationCollectors.toAnnotationSet()); + // Only finds the locally declared repeated annotation. + assertThat(set.stream().map(RepeatableWithContainerWithMultipleAttributes::value)) + .containsExactly("C"); + + // Standard RepeatableContainers + mergedAnnotations = MergedAnnotations.from(clazz, SearchStrategy.TYPE_HIERARCHY, RepeatableContainers.standardRepeatables()); + container = mergedAnnotations + .get(ContainerWithMultipleAttributes.class) + .synthesize(MergedAnnotation::isPresent).orElse(null); + assertThat(container).as("container").isNotNull(); + assertThat(container.name()).isEqualTo("enigma"); + repeatedAnnotations = container.value(); + assertThat(Arrays.stream(repeatedAnnotations).map(RepeatableWithContainerWithMultipleAttributes::value)) + .containsExactly("A", "B"); + set = mergedAnnotations.stream(RepeatableWithContainerWithMultipleAttributes.class) + .collect(MergedAnnotationCollectors.toAnnotationSet()); + // Finds the locally declared repeated annotation plus the 2 in the container. + assertThat(set.stream().map(RepeatableWithContainerWithMultipleAttributes::value)) + .containsExactly("A", "B", "C"); + } + private Set getAnnotations(Class container, Class repeatable, SearchStrategy searchStrategy, AnnotatedElement element) { @@ -449,4 +488,27 @@ static class SubNoninheritedRepeatableClass extends NoninheritedRepeatableClass static class WithRepeatedMetaAnnotationsClass { } + @Retention(RetentionPolicy.RUNTIME) + @interface ContainerWithMultipleAttributes { + + RepeatableWithContainerWithMultipleAttributes[] value(); + + String name() default ""; + } + + @Retention(RetentionPolicy.RUNTIME) + @Repeatable(ContainerWithMultipleAttributes.class) + @interface RepeatableWithContainerWithMultipleAttributes { + + String value() default ""; + } + + @ContainerWithMultipleAttributes(name = "enigma", value = { + @RepeatableWithContainerWithMultipleAttributes("A"), + @RepeatableWithContainerWithMultipleAttributes("B") + }) + @RepeatableWithContainerWithMultipleAttributes("C") + static class StandardRepeatablesWithContainerWithMultipleAttributesTestCase { + } + } diff --git a/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsTests.java b/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsTests.java index 1e71dcb82dd6..ea08de6df40b 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-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,6 +42,8 @@ import org.junit.jupiter.api.Test; import org.springframework.core.Ordered; +import org.springframework.core.annotation.AnnotationsScannerTests.Hello2Impl; +import org.springframework.core.annotation.AnnotationsScannerTests.TestAnnotation1; import org.springframework.core.annotation.MergedAnnotation.Adapt; import org.springframework.core.annotation.MergedAnnotations.SearchStrategy; import org.springframework.core.annotation.subpackage.NonPublicAnnotatedClass; @@ -83,7 +85,7 @@ class ConventionBasedAnnotationAttributeOverrideTests { void getWithInheritedAnnotationsAttributesWithConventionBasedComposedAnnotation() { MergedAnnotation annotation = MergedAnnotations.from(ConventionBasedComposedContextConfigurationClass.class, - SearchStrategy.INHERITED_ANNOTATIONS).get(ContextConfiguration.class); + SearchStrategy.INHERITED_ANNOTATIONS).get(ContextConfiguration.class); assertThat(annotation.isPresent()).isTrue(); assertThat(annotation.getStringArray("locations")).containsExactly("explicitDeclaration"); assertThat(annotation.getStringArray("value")).containsExactly("explicitDeclaration"); @@ -95,7 +97,7 @@ void getWithInheritedAnnotationsFromHalfConventionBasedAndHalfAliasedComposedAnn // xmlConfigFiles can be used because it has an AliasFor annotation MergedAnnotation annotation = MergedAnnotations.from(HalfConventionBasedAndHalfAliasedComposedContextConfigurationClass1.class, - SearchStrategy.INHERITED_ANNOTATIONS).get(ContextConfiguration.class); + SearchStrategy.INHERITED_ANNOTATIONS).get(ContextConfiguration.class); assertThat(annotation.getStringArray("locations")).containsExactly("explicitDeclaration"); assertThat(annotation.getStringArray("value")).containsExactly("explicitDeclaration"); } @@ -106,7 +108,7 @@ void getWithInheritedAnnotationsFromHalfConventionBasedAndHalfAliasedComposedAnn // locations doesn't apply because it has no AliasFor annotation MergedAnnotation annotation = MergedAnnotations.from(HalfConventionBasedAndHalfAliasedComposedContextConfigurationClass2.class, - SearchStrategy.INHERITED_ANNOTATIONS).get(ContextConfiguration.class); + SearchStrategy.INHERITED_ANNOTATIONS).get(ContextConfiguration.class); assertThat(annotation.getStringArray("locations")).isEmpty(); assertThat(annotation.getStringArray("value")).isEmpty(); } @@ -127,7 +129,7 @@ void getWithTypeHierarchyWithSingleElementOverridingAnArrayViaConvention() { void getWithTypeHierarchyWithLocalAliasesThatConflictWithAttributesInMetaAnnotationByConvention() { MergedAnnotation annotation = MergedAnnotations.from(SpringApplicationConfigurationClass.class, SearchStrategy.TYPE_HIERARCHY) - .get(ContextConfiguration.class); + .get(ContextConfiguration.class); assertThat(annotation.getStringArray("locations")).isEmpty(); assertThat(annotation.getStringArray("value")).isEmpty(); assertThat(annotation.getClassArray("classes")).containsExactly(Number.class); @@ -137,33 +139,32 @@ void getWithTypeHierarchyWithLocalAliasesThatConflictWithAttributesInMetaAnnotat void getWithTypeHierarchyOnMethodWithSingleElementOverridingAnArrayViaConvention() throws Exception { testGetWithTypeHierarchyWebMapping(WebController.class.getMethod("postMappedWithPathAttribute")); } - } + @Test void fromPreconditions() { SearchStrategy strategy = SearchStrategy.DIRECT; RepeatableContainers containers = RepeatableContainers.standardRepeatables(); assertThatIllegalArgumentException() - .isThrownBy(() -> MergedAnnotations.from(getClass(), strategy, null, AnnotationFilter.PLAIN)) - .withMessage("RepeatableContainers must not be null"); + .isThrownBy(() -> MergedAnnotations.from(getClass(), strategy, null, AnnotationFilter.PLAIN)) + .withMessage("RepeatableContainers must not be null"); assertThatIllegalArgumentException() - .isThrownBy(() -> MergedAnnotations.from(getClass(), strategy, containers, null)) - .withMessage("AnnotationFilter must not be null"); + .isThrownBy(() -> MergedAnnotations.from(getClass(), strategy, containers, null)) + .withMessage("AnnotationFilter must not be null"); assertThatIllegalArgumentException() - .isThrownBy(() -> MergedAnnotations.from(getClass(), new Annotation[0], null, AnnotationFilter.PLAIN)) - .withMessage("RepeatableContainers must not be null"); + .isThrownBy(() -> MergedAnnotations.from(getClass(), new Annotation[0], null, AnnotationFilter.PLAIN)) + .withMessage("RepeatableContainers must not be null"); assertThatIllegalArgumentException() - .isThrownBy(() -> MergedAnnotations.from(getClass(), new Annotation[0], containers, null)) - .withMessage("AnnotationFilter must not be null"); + .isThrownBy(() -> MergedAnnotations.from(getClass(), new Annotation[0], containers, null)) + .withMessage("AnnotationFilter must not be null"); } @Test void streamWhenFromNonAnnotatedClass() { - assertThat(MergedAnnotations.from(NonAnnotatedClass.class). - stream(TransactionalComponent.class)).isEmpty(); + assertThat(MergedAnnotations.from(NonAnnotatedClass.class).stream(TransactionalComponent.class)).isEmpty(); } @Test @@ -183,14 +184,12 @@ void streamWhenFromClassWithMetaDepth2() { @Test void isPresentWhenFromNonAnnotatedClass() { - assertThat(MergedAnnotations.from(NonAnnotatedClass.class). - isPresent(Transactional.class)).isFalse(); + assertThat(MergedAnnotations.from(NonAnnotatedClass.class).isPresent(Transactional.class)).isFalse(); } @Test void isPresentWhenFromAnnotationClassWithMetaDepth0() { - assertThat(MergedAnnotations.from(TransactionalComponent.class). - isPresent(TransactionalComponent.class)).isFalse(); + assertThat(MergedAnnotations.from(TransactionalComponent.class).isPresent(TransactionalComponent.class)).isFalse(); } @Test @@ -202,8 +201,7 @@ void isPresentWhenFromAnnotationClassWithMetaDepth1() { @Test void isPresentWhenFromAnnotationClassWithMetaDepth2() { - MergedAnnotations annotations = MergedAnnotations.from( - ComposedTransactionalComponent.class); + MergedAnnotations annotations = MergedAnnotations.from(ComposedTransactionalComponent.class); assertThat(annotations.isPresent(Transactional.class)).isTrue(); assertThat(annotations.isPresent(Component.class)).isTrue(); assertThat(annotations.isPresent(ComposedTransactionalComponent.class)).isFalse(); @@ -211,28 +209,24 @@ void isPresentWhenFromAnnotationClassWithMetaDepth2() { @Test void isPresentWhenFromClassWithMetaDepth0() { - assertThat(MergedAnnotations.from(TransactionalComponentClass.class).isPresent( - TransactionalComponent.class)).isTrue(); + assertThat(MergedAnnotations.from(TransactionalComponentClass.class).isPresent(TransactionalComponent.class)).isTrue(); } @Test void isPresentWhenFromSubclassWithMetaDepth0() { - assertThat(MergedAnnotations.from(SubTransactionalComponentClass.class).isPresent( - TransactionalComponent.class)).isFalse(); + assertThat(MergedAnnotations.from(SubTransactionalComponentClass.class).isPresent(TransactionalComponent.class)).isFalse(); } @Test void isPresentWhenFromClassWithMetaDepth1() { - MergedAnnotations annotations = MergedAnnotations.from( - TransactionalComponentClass.class); + MergedAnnotations annotations = MergedAnnotations.from(TransactionalComponentClass.class); assertThat(annotations.isPresent(Transactional.class)).isTrue(); assertThat(annotations.isPresent(Component.class)).isTrue(); } @Test void isPresentWhenFromClassWithMetaDepth2() { - MergedAnnotations annotations = MergedAnnotations.from( - ComposedTransactionalComponentClass.class); + MergedAnnotations annotations = MergedAnnotations.from(ComposedTransactionalComponentClass.class); assertThat(annotations.isPresent(Transactional.class)).isTrue(); assertThat(annotations.isPresent(Component.class)).isTrue(); assertThat(annotations.isPresent(ComposedTransactionalComponent.class)).isTrue(); @@ -263,35 +257,31 @@ void getRootWhenDirect() { @Test void getMetaTypes() { - MergedAnnotation annotation = MergedAnnotations.from( - ComposedTransactionalComponentClass.class).get( - TransactionalComponent.class); + MergedAnnotation annotation = MergedAnnotations.from(ComposedTransactionalComponentClass.class) + .get(TransactionalComponent.class); assertThat(annotation.getMetaTypes()).containsExactly( ComposedTransactionalComponent.class, TransactionalComponent.class); } @Test void collectMultiValueMapFromNonAnnotatedClass() { - MultiValueMap map = MergedAnnotations.from( - NonAnnotatedClass.class).stream(Transactional.class).collect( - MergedAnnotationCollectors.toMultiValueMap()); + MultiValueMap map = MergedAnnotations.from(NonAnnotatedClass.class) + .stream(Transactional.class).collect(MergedAnnotationCollectors.toMultiValueMap()); assertThat(map).isEmpty(); } @Test void collectMultiValueMapFromClassWithLocalAnnotation() { - MultiValueMap map = MergedAnnotations.from(TxConfig.class).stream( - Transactional.class).collect( - MergedAnnotationCollectors.toMultiValueMap()); + MultiValueMap map = MergedAnnotations.from(TxConfig.class) + .stream(Transactional.class).collect(MergedAnnotationCollectors.toMultiValueMap()); assertThat(map).contains(entry("value", Arrays.asList("TxConfig"))); } @Test void collectMultiValueMapFromClassWithLocalComposedAnnotationAndInheritedAnnotation() { MultiValueMap map = MergedAnnotations.from( - SubClassWithInheritedAnnotation.class, - SearchStrategy.INHERITED_ANNOTATIONS).stream(Transactional.class).collect( - MergedAnnotationCollectors.toMultiValueMap()); + SubClassWithInheritedAnnotation.class, SearchStrategy.INHERITED_ANNOTATIONS) + .stream(Transactional.class).collect(MergedAnnotationCollectors.toMultiValueMap()); assertThat(map).contains( entry("qualifier", Arrays.asList("composed2", "transactionManager"))); } @@ -299,18 +289,16 @@ void collectMultiValueMapFromClassWithLocalComposedAnnotationAndInheritedAnnotat @Test void collectMultiValueMapFavorsInheritedAnnotationsOverMoreLocallyDeclaredComposedAnnotations() { MultiValueMap map = MergedAnnotations.from( - SubSubClassWithInheritedAnnotation.class, - SearchStrategy.INHERITED_ANNOTATIONS).stream(Transactional.class).collect( - MergedAnnotationCollectors.toMultiValueMap()); + SubSubClassWithInheritedAnnotation.class, SearchStrategy.INHERITED_ANNOTATIONS) + .stream(Transactional.class).collect(MergedAnnotationCollectors.toMultiValueMap()); assertThat(map).contains(entry("qualifier", Arrays.asList("transactionManager"))); } @Test void collectMultiValueMapFavorsInheritedComposedAnnotationsOverMoreLocallyDeclaredComposedAnnotations() { MultiValueMap map = MergedAnnotations.from( - SubSubClassWithInheritedComposedAnnotation.class, - SearchStrategy.INHERITED_ANNOTATIONS).stream(Transactional.class).collect( - MergedAnnotationCollectors.toMultiValueMap()); + SubSubClassWithInheritedComposedAnnotation.class, SearchStrategy.INHERITED_ANNOTATIONS) + .stream(Transactional.class).collect(MergedAnnotationCollectors.toMultiValueMap()); assertThat(map).contains(entry("qualifier", Arrays.asList("composed1"))); } @@ -323,9 +311,9 @@ void collectMultiValueMapFavorsInheritedComposedAnnotationsOverMoreLocallyDeclar */ @Test void collectMultiValueMapFromClassWithLocalAnnotationThatShadowsAnnotationFromSuperclass() { - MultiValueMap map = MergedAnnotations.from(DerivedTxConfig.class, - SearchStrategy.INHERITED_ANNOTATIONS).stream(Transactional.class).collect( - MergedAnnotationCollectors.toMultiValueMap()); + MultiValueMap map = MergedAnnotations.from( + DerivedTxConfig.class, SearchStrategy.INHERITED_ANNOTATIONS) + .stream(Transactional.class).collect(MergedAnnotationCollectors.toMultiValueMap()); assertThat(map).contains(entry("value", Arrays.asList("DerivedTxConfig"))); } @@ -336,24 +324,23 @@ void collectMultiValueMapFromClassWithLocalAnnotationThatShadowsAnnotationFromSu @Test void collectMultiValueMapFromClassWithMultipleComposedAnnotations() { MultiValueMap map = MergedAnnotations.from( - TxFromMultipleComposedAnnotations.class, - SearchStrategy.INHERITED_ANNOTATIONS).stream(Transactional.class).collect( - MergedAnnotationCollectors.toMultiValueMap()); + TxFromMultipleComposedAnnotations.class, SearchStrategy.INHERITED_ANNOTATIONS) + .stream(Transactional.class).collect(MergedAnnotationCollectors.toMultiValueMap()); assertThat(map).contains( entry("value", Arrays.asList("TxInheritedComposed", "TxComposed"))); } @Test void getWithInheritedAnnotationsFromClassWithLocalAnnotation() { - MergedAnnotation annotation = MergedAnnotations.from(TxConfig.class, - SearchStrategy.INHERITED_ANNOTATIONS).get(Transactional.class); + MergedAnnotation annotation = MergedAnnotations.from( + TxConfig.class, SearchStrategy.INHERITED_ANNOTATIONS).get(Transactional.class); assertThat(annotation.getString("value")).isEqualTo("TxConfig"); } @Test void getWithInheritedAnnotationsFromClassWithLocalAnnotationThatShadowsAnnotationFromSuperclass() { - MergedAnnotation annotation = MergedAnnotations.from(DerivedTxConfig.class, - SearchStrategy.INHERITED_ANNOTATIONS).get(Transactional.class); + MergedAnnotation annotation = MergedAnnotations.from( + DerivedTxConfig.class, SearchStrategy.INHERITED_ANNOTATIONS).get(Transactional.class); assertThat(annotation.getString("value")).isEqualTo("DerivedTxConfig"); } @@ -367,53 +354,46 @@ void getWithInheritedAnnotationsFromMetaCycleAnnotatedClassWithMissingTargetMeta @Test void getWithInheritedAnnotationsFavorsLocalComposedAnnotationOverInheritedAnnotation() { MergedAnnotation annotation = MergedAnnotations.from( - SubClassWithInheritedAnnotation.class, - SearchStrategy.INHERITED_ANNOTATIONS).get(Transactional.class); + SubClassWithInheritedAnnotation.class, SearchStrategy.INHERITED_ANNOTATIONS).get(Transactional.class); assertThat(annotation.getBoolean("readOnly")).isTrue(); } @Test void getWithInheritedAnnotationsFavorsInheritedAnnotationsOverMoreLocallyDeclaredComposedAnnotations() { MergedAnnotation annotation = MergedAnnotations.from( - SubSubClassWithInheritedAnnotation.class, - SearchStrategy.INHERITED_ANNOTATIONS).get(Transactional.class); + SubSubClassWithInheritedAnnotation.class, SearchStrategy.INHERITED_ANNOTATIONS).get(Transactional.class); assertThat(annotation.getBoolean("readOnly")).isFalse(); } @Test void getWithInheritedAnnotationsFavorsInheritedComposedAnnotationsOverMoreLocallyDeclaredComposedAnnotations() { MergedAnnotation annotation = MergedAnnotations.from( - SubSubClassWithInheritedComposedAnnotation.class, - SearchStrategy.INHERITED_ANNOTATIONS).get(Transactional.class); + SubSubClassWithInheritedComposedAnnotation.class, SearchStrategy.INHERITED_ANNOTATIONS).get(Transactional.class); assertThat(annotation.getBoolean("readOnly")).isFalse(); } @Test void getWithInheritedAnnotationsFromInterfaceImplementedBySuperclass() { MergedAnnotation annotation = MergedAnnotations.from( - ConcreteClassWithInheritedAnnotation.class, - SearchStrategy.INHERITED_ANNOTATIONS).get(Transactional.class); + ConcreteClassWithInheritedAnnotation.class, SearchStrategy.INHERITED_ANNOTATIONS).get(Transactional.class); assertThat(annotation.isPresent()).isFalse(); } @Test void getWithInheritedAnnotationsFromInheritedAnnotationInterface() { MergedAnnotation annotation = MergedAnnotations.from( - InheritedAnnotationInterface.class, - SearchStrategy.INHERITED_ANNOTATIONS).get(Transactional.class); + InheritedAnnotationInterface.class, SearchStrategy.INHERITED_ANNOTATIONS).get(Transactional.class); assertThat(annotation.isPresent()).isTrue(); } @Test void getWithInheritedAnnotationsFromNonInheritedAnnotationInterface() { MergedAnnotation annotation = MergedAnnotations.from( - NonInheritedAnnotationInterface.class, - SearchStrategy.INHERITED_ANNOTATIONS).get(Order.class); + NonInheritedAnnotationInterface.class, SearchStrategy.INHERITED_ANNOTATIONS).get(Order.class); assertThat(annotation.isPresent()).isTrue(); } - @Test void withInheritedAnnotationsFromAliasedComposedAnnotation() { MergedAnnotation annotation = MergedAnnotations.from( @@ -435,15 +415,11 @@ void withInheritedAnnotationsFromAliasedValueComposedAnnotation() { @Test void getWithInheritedAnnotationsFromImplicitAliasesInMetaAnnotationOnComposedAnnotation() { MergedAnnotation annotation = MergedAnnotations.from( - ComposedImplicitAliasesContextConfigurationClass.class, - SearchStrategy.INHERITED_ANNOTATIONS).get( - ImplicitAliasesContextConfiguration.class); - assertThat(annotation.getStringArray("groovyScripts")).containsExactly("A.xml", - "B.xml"); - assertThat(annotation.getStringArray("xmlFiles")).containsExactly("A.xml", - "B.xml"); - assertThat(annotation.getStringArray("locations")).containsExactly("A.xml", - "B.xml"); + ComposedImplicitAliasesContextConfigurationClass.class, SearchStrategy.INHERITED_ANNOTATIONS) + .get(ImplicitAliasesContextConfiguration.class); + assertThat(annotation.getStringArray("groovyScripts")).containsExactly("A.xml", "B.xml"); + assertThat(annotation.getStringArray("xmlFiles")).containsExactly("A.xml", "B.xml"); + assertThat(annotation.getStringArray("locations")).containsExactly("A.xml", "B.xml"); assertThat(annotation.getStringArray("value")).containsExactly("A.xml", "B.xml"); } @@ -483,8 +459,8 @@ void getWithInheritedAnnotationsFromTransitiveImplicitAliasesWithSkippedLevelWit } private void testGetWithInherited(Class element, String... expected) { - MergedAnnotation annotation = MergedAnnotations.from(element, - SearchStrategy.INHERITED_ANNOTATIONS).get(ContextConfiguration.class); + MergedAnnotation annotation = MergedAnnotations.from(element, SearchStrategy.INHERITED_ANNOTATIONS) + .get(ContextConfiguration.class); assertThat(annotation.getStringArray("locations")).isEqualTo(expected); assertThat(annotation.getStringArray("value")).isEqualTo(expected); assertThat(annotation.getClassArray("classes")).isEmpty(); @@ -493,8 +469,8 @@ private void testGetWithInherited(Class element, String... expected) { @Test void getWithInheritedAnnotationsFromShadowedAliasComposedAnnotation() { MergedAnnotation annotation = MergedAnnotations.from( - ShadowedAliasComposedContextConfigurationClass.class, - SearchStrategy.INHERITED_ANNOTATIONS).get(ContextConfiguration.class); + ShadowedAliasComposedContextConfigurationClass.class, SearchStrategy.INHERITED_ANNOTATIONS) + .get(ContextConfiguration.class); assertThat(annotation.getStringArray("locations")).containsExactly("test.xml"); assertThat(annotation.getStringArray("value")).containsExactly("test.xml"); } @@ -542,38 +518,39 @@ void getWithTypeHierarchyFromSubNonInheritedAnnotationInterface() { @Test void getWithTypeHierarchyFromSubSubNonInheritedAnnotationInterface() { MergedAnnotation annotation = MergedAnnotations.from( - SubSubNonInheritedAnnotationInterface.class, - SearchStrategy.TYPE_HIERARCHY).get(Order.class); + SubSubNonInheritedAnnotationInterface.class, SearchStrategy.TYPE_HIERARCHY).get(Order.class); assertThat(annotation.isPresent()).isTrue(); assertThat(annotation.getAggregateIndex()).isEqualTo(2); } @Test - void getWithTypeHierarchyInheritedFromInterfaceMethod() - throws NoSuchMethodException { - Method method = ConcreteClassWithInheritedAnnotation.class.getMethod( - "handleFromInterface"); - MergedAnnotation annotation = MergedAnnotations.from(method, - SearchStrategy.TYPE_HIERARCHY).get(Order.class); + void getWithTypeHierarchyInheritedFromInterfaceMethod() throws Exception { + Method method = ConcreteClassWithInheritedAnnotation.class.getMethod("handleFromInterface"); + MergedAnnotation annotation = MergedAnnotations.from(method, SearchStrategy.TYPE_HIERARCHY).get(Order.class); assertThat(annotation.isPresent()).isTrue(); assertThat(annotation.getAggregateIndex()).isEqualTo(1); } + @Test // gh-31803 + void streamWithTypeHierarchyInheritedFromSuperInterfaceMethod() throws Exception { + Method method = Hello2Impl.class.getMethod("method"); + long count = MergedAnnotations.from(method, SearchStrategy.TYPE_HIERARCHY) + .stream(TestAnnotation1.class).count(); + assertThat(count).isEqualTo(1); + } + @Test void getWithTypeHierarchyInheritedFromAbstractMethod() throws NoSuchMethodException { Method method = ConcreteClassWithInheritedAnnotation.class.getMethod("handle"); - MergedAnnotation annotation = MergedAnnotations.from(method, - SearchStrategy.TYPE_HIERARCHY).get(Transactional.class); + MergedAnnotation annotation = MergedAnnotations.from(method, SearchStrategy.TYPE_HIERARCHY).get(Transactional.class); assertThat(annotation.isPresent()).isTrue(); assertThat(annotation.getAggregateIndex()).isEqualTo(1); } @Test void getWithTypeHierarchyInheritedFromBridgedMethod() throws NoSuchMethodException { - Method method = ConcreteClassWithInheritedAnnotation.class.getMethod( - "handleParameterized", String.class); - MergedAnnotation annotation = MergedAnnotations.from(method, - SearchStrategy.TYPE_HIERARCHY).get(Transactional.class); + Method method = ConcreteClassWithInheritedAnnotation.class.getMethod("handleParameterized", String.class); + MergedAnnotation annotation = MergedAnnotations.from(method, SearchStrategy.TYPE_HIERARCHY).get(Transactional.class); assertThat(annotation.isPresent()).isTrue(); assertThat(annotation.getAggregateIndex()).isEqualTo(1); } @@ -601,16 +578,14 @@ void getWithTypeHierarchyFromBridgeMethod() { @Test void getWithTypeHierarchyFromClassWithMetaAndLocalTxConfig() { MergedAnnotation annotation = MergedAnnotations.from( - MetaAndLocalTxConfigClass.class, SearchStrategy.TYPE_HIERARCHY).get( - Transactional.class); + MetaAndLocalTxConfigClass.class, SearchStrategy.TYPE_HIERARCHY).get(Transactional.class); assertThat(annotation.getString("qualifier")).isEqualTo("localTxMgr"); } @Test void getWithTypeHierarchyFromClassWithAttributeAliasesInTargetAnnotation() { MergedAnnotation mergedAnnotation = MergedAnnotations.from( - AliasedTransactionalComponentClass.class, SearchStrategy.TYPE_HIERARCHY).get( - AliasedTransactional.class); + AliasedTransactionalComponentClass.class, SearchStrategy.TYPE_HIERARCHY).get(AliasedTransactional.class); AliasedTransactional synthesizedAnnotation = mergedAnnotation.synthesize(); String qualifier = "aliasForQualifier"; assertThat(mergedAnnotation.getString("value")).isEqualTo(qualifier); @@ -622,8 +597,7 @@ void getWithTypeHierarchyFromClassWithAttributeAliasesInTargetAnnotation() { @Test // gh-23767 void getWithTypeHierarchyFromClassWithComposedMetaTransactionalAnnotation() { MergedAnnotation mergedAnnotation = MergedAnnotations.from( - ComposedTransactionalClass.class, SearchStrategy.TYPE_HIERARCHY).get( - AliasedTransactional.class); + ComposedTransactionalClass.class, SearchStrategy.TYPE_HIERARCHY).get(AliasedTransactional.class); assertThat(mergedAnnotation.getString("value")).isEqualTo("anotherTransactionManager"); assertThat(mergedAnnotation.getString("qualifier")).isEqualTo("anotherTransactionManager"); } @@ -631,8 +605,7 @@ void getWithTypeHierarchyFromClassWithComposedMetaTransactionalAnnotation() { @Test // gh-23767 void getWithTypeHierarchyFromClassWithMetaMetaAliasedTransactional() { MergedAnnotation mergedAnnotation = MergedAnnotations.from( - MetaMetaAliasedTransactionalClass.class, SearchStrategy.TYPE_HIERARCHY).get( - AliasedTransactional.class); + MetaMetaAliasedTransactionalClass.class, SearchStrategy.TYPE_HIERARCHY).get(AliasedTransactional.class); assertThat(mergedAnnotation.getString("value")).isEqualTo("meta"); assertThat(mergedAnnotation.getString("qualifier")).isEqualTo("meta"); } @@ -668,51 +641,46 @@ private MergedAnnotation testGetWithTypeHierarchy(Class element, String... @Test void getWithTypeHierarchyWhenMultipleMetaAnnotationsHaveClashingAttributeNames() { MergedAnnotations annotations = MergedAnnotations.from( - AliasedComposedContextConfigurationAndTestPropertySourceClass.class, - SearchStrategy.TYPE_HIERARCHY); + AliasedComposedContextConfigurationAndTestPropertySourceClass.class, SearchStrategy.TYPE_HIERARCHY); MergedAnnotation contextConfig = annotations.get(ContextConfiguration.class); assertThat(contextConfig.getStringArray("locations")).containsExactly("test.xml"); assertThat(contextConfig.getStringArray("value")).containsExactly("test.xml"); MergedAnnotation testPropSource = annotations.get(TestPropertySource.class); - assertThat(testPropSource.getStringArray("locations")).containsExactly( - "test.properties"); - assertThat(testPropSource.getStringArray("value")).containsExactly( - "test.properties"); + assertThat(testPropSource.getStringArray("locations")).containsExactly("test.properties"); + assertThat(testPropSource.getStringArray("value")).containsExactly("test.properties"); } @Test void getWithTypeHierarchyOnMethodWithSingleElementOverridingAnArrayViaAliasFor() throws Exception { - testGetWithTypeHierarchyWebMapping( - WebController.class.getMethod("getMappedWithValueAttribute")); - testGetWithTypeHierarchyWebMapping( - WebController.class.getMethod("getMappedWithPathAttribute")); + testGetWithTypeHierarchyWebMapping(WebController.class.getMethod("getMappedWithValueAttribute")); + testGetWithTypeHierarchyWebMapping(WebController.class.getMethod("getMappedWithPathAttribute")); } private void testGetWithTypeHierarchyWebMapping(AnnotatedElement element) { - MergedAnnotation annotation = MergedAnnotations.from(element, - SearchStrategy.TYPE_HIERARCHY).get(RequestMapping.class); + MergedAnnotation annotation = MergedAnnotations.from(element, SearchStrategy.TYPE_HIERARCHY) + .get(RequestMapping.class); assertThat(annotation.getStringArray("value")).containsExactly("/test"); assertThat(annotation.getStringArray("path")).containsExactly("/test"); } @Test - void getDirectWithJavaxAnnotationType() throws Exception { - assertThat(MergedAnnotations.from(ResourceHolder.class).get( - Resource.class).getString("name")).isEqualTo("x"); + void getDirectWithJavaxAnnotationType() { + assertThat(MergedAnnotations.from(ResourceHolder.class).get(Resource.class) + .getString("name")).isEqualTo("x"); } @Test void streamInheritedFromClassWithInterface() throws Exception { Method method = TransactionalServiceImpl.class.getMethod("doIt"); - assertThat(MergedAnnotations.from(method, SearchStrategy.INHERITED_ANNOTATIONS).stream( - Transactional.class)).isEmpty(); + assertThat(MergedAnnotations.from(method, SearchStrategy.INHERITED_ANNOTATIONS) + .stream(Transactional.class)).isEmpty(); } @Test void streamTypeHierarchyFromClassWithInterface() throws Exception { Method method = TransactionalServiceImpl.class.getMethod("doIt"); - assertThat(MergedAnnotations.from(method, SearchStrategy.TYPE_HIERARCHY).stream( - Transactional.class)).hasSize(1); + assertThat(MergedAnnotations.from(method, SearchStrategy.TYPE_HIERARCHY) + .stream(Transactional.class)).hasSize(1); } @Test @@ -734,8 +702,8 @@ void getFromMethodWithMethodAnnotationOnLeaf() throws Exception { Method method = Leaf.class.getMethod("annotatedOnLeaf"); assertThat(method.getAnnotation(Order.class)).isNotNull(); assertThat(MergedAnnotations.from(method).get(Order.class).getDistance()).isEqualTo(0); - assertThat(MergedAnnotations.from(method, SearchStrategy.TYPE_HIERARCHY).get( - Order.class).getDistance()).isEqualTo(0); + assertThat(MergedAnnotations.from(method, SearchStrategy.TYPE_HIERARCHY).get(Order.class) + .getDistance()).isEqualTo(0); } @Test @@ -743,8 +711,8 @@ void getFromMethodWithAnnotationOnMethodInInterface() throws Exception { Method method = Leaf.class.getMethod("fromInterfaceImplementedByRoot"); assertThat(method.getAnnotation(Order.class)).isNull(); assertThat(MergedAnnotations.from(method).get(Order.class).getDistance()).isEqualTo(-1); - assertThat(MergedAnnotations.from(method, SearchStrategy.TYPE_HIERARCHY).get( - Order.class).getDistance()).isEqualTo(0); + assertThat(MergedAnnotations.from(method, SearchStrategy.TYPE_HIERARCHY).get(Order.class) + .getDistance()).isEqualTo(0); } @Test @@ -752,8 +720,8 @@ void getFromMethodWithMetaAnnotationOnLeaf() throws Exception { Method method = Leaf.class.getMethod("metaAnnotatedOnLeaf"); assertThat(method.getAnnotation(Order.class)).isNull(); assertThat(MergedAnnotations.from(method).get(Order.class).getDistance()).isEqualTo(1); - assertThat(MergedAnnotations.from(method, SearchStrategy.TYPE_HIERARCHY).get( - Order.class).getDistance()).isEqualTo(1); + assertThat(MergedAnnotations.from(method, SearchStrategy.TYPE_HIERARCHY).get(Order.class) + .getDistance()).isEqualTo(1); } @Test @@ -1089,7 +1057,7 @@ private Object getSuperClassSourceWithTypeIn(Class clazz, List> annotations = MergedAnnotations.from( method, SearchStrategy.TYPE_HIERARCHY).stream(MyRepeatable.class); @@ -1279,7 +1247,7 @@ void getRepeatableDeclaredOnMethod() throws Exception { @Test @SuppressWarnings("deprecation") - void getRepeatableDeclaredOnClassWithAttributeAliases() { + void streamRepeatableDeclaredOnClassWithAttributeAliases() { assertThat(MergedAnnotations.from(HierarchyClass.class).stream( TestConfiguration.class)).isEmpty(); RepeatableContainers containers = RepeatableContainers.of(TestConfiguration.class, @@ -1293,7 +1261,7 @@ void getRepeatableDeclaredOnClassWithAttributeAliases() { } @Test - void getRepeatableDeclaredOnClass() { + void streamRepeatableDeclaredOnClass() { Class element = MyRepeatableClass.class; String[] expectedValuesJava = { "A", "B", "C" }; String[] expectedValuesSpring = { "A", "B", "C", "meta1" }; @@ -1301,7 +1269,7 @@ void getRepeatableDeclaredOnClass() { } @Test - void getRepeatableDeclaredOnSuperclass() { + void streamRepeatableDeclaredOnSuperclass() { Class element = SubMyRepeatableClass.class; String[] expectedValuesJava = { "A", "B", "C" }; String[] expectedValuesSpring = { "A", "B", "C", "meta1" }; @@ -1309,7 +1277,7 @@ void getRepeatableDeclaredOnSuperclass() { } @Test - void getRepeatableDeclaredOnClassAndSuperclass() { + void streamRepeatableDeclaredOnClassAndSuperclass() { Class element = SubMyRepeatableWithAdditionalLocalDeclarationsClass.class; String[] expectedValuesJava = { "X", "Y", "Z" }; String[] expectedValuesSpring = { "X", "Y", "Z", "meta2" }; @@ -1317,7 +1285,7 @@ void getRepeatableDeclaredOnClassAndSuperclass() { } @Test - void getRepeatableDeclaredOnMultipleSuperclasses() { + void streamRepeatableDeclaredOnMultipleSuperclasses() { Class element = SubSubMyRepeatableWithAdditionalLocalDeclarationsClass.class; String[] expectedValuesJava = { "X", "Y", "Z" }; String[] expectedValuesSpring = { "X", "Y", "Z", "meta2" }; @@ -1325,7 +1293,7 @@ void getRepeatableDeclaredOnMultipleSuperclasses() { } @Test - void getDirectRepeatablesDeclaredOnClass() { + void streamDirectRepeatablesDeclaredOnClass() { Class element = MyRepeatableClass.class; String[] expectedValuesJava = { "A", "B", "C" }; String[] expectedValuesSpring = { "A", "B", "C", "meta1" }; @@ -1333,7 +1301,7 @@ void getDirectRepeatablesDeclaredOnClass() { } @Test - void getDirectRepeatablesDeclaredOnSuperclass() { + void streamDirectRepeatablesDeclaredOnSuperclass() { Class element = SubMyRepeatableClass.class; String[] expectedValuesJava = {}; String[] expectedValuesSpring = {}; @@ -1360,24 +1328,21 @@ private void testExplicitRepeatables(SearchStrategy searchStrategy, Class ele MergedAnnotations annotations = MergedAnnotations.from(element, searchStrategy, RepeatableContainers.of(MyRepeatable.class, MyRepeatableContainer.class), AnnotationFilter.PLAIN); - assertThat(annotations.stream(MyRepeatable.class).filter( - MergedAnnotationPredicates.firstRunOf( - MergedAnnotation::getAggregateIndex)).map( - annotation -> annotation.getString( - "value"))).containsExactly(expected); + Stream values = annotations.stream(MyRepeatable.class) + .filter(MergedAnnotationPredicates.firstRunOf(MergedAnnotation::getAggregateIndex)) + .map(annotation -> annotation.getString("value")); + assertThat(values).containsExactly(expected); } private void testStandardRepeatables(SearchStrategy searchStrategy, Class element, String[] expected) { - MergedAnnotations annotations = MergedAnnotations.from(element, searchStrategy); - assertThat(annotations.stream(MyRepeatable.class).filter( - MergedAnnotationPredicates.firstRunOf( - MergedAnnotation::getAggregateIndex)).map( - annotation -> annotation.getString( - "value"))).containsExactly(expected); + Stream values = MergedAnnotations.from(element, searchStrategy).stream(MyRepeatable.class) + .filter(MergedAnnotationPredicates.firstRunOf(MergedAnnotation::getAggregateIndex)) + .map(annotation -> annotation.getString("value")); + assertThat(values).containsExactly(expected); } @Test - void synthesizeWithoutAttributeAliases() throws Exception { + void synthesizeWithoutAttributeAliases() { Component component = WebController.class.getAnnotation(Component.class); assertThat(component).isNotNull(); Component synthesizedComponent = MergedAnnotation.from(component).synthesize(); @@ -1489,114 +1454,123 @@ void synthesizeShouldNotResynthesizeAlreadySynthesizedAnnotations() throws Excep } @Test - void synthesizeWhenAliasForIsMissingAttributeDeclaration() throws Exception { + void synthesizeWhenAliasForIsMissingAttributeDeclaration() { AliasForWithMissingAttributeDeclaration annotation = AliasForWithMissingAttributeDeclarationClass.class.getAnnotation( AliasForWithMissingAttributeDeclaration.class); - assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() -> - MergedAnnotation.from(annotation)) - .withMessageStartingWith("@AliasFor declaration on attribute 'foo' in annotation") - .withMessageContaining(AliasForWithMissingAttributeDeclaration.class.getName()) - .withMessageContaining("points to itself"); + + assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy( + () -> MergedAnnotation.from(annotation)) + .withMessageStartingWith("@AliasFor declaration on attribute 'foo' in annotation") + .withMessageContaining(AliasForWithMissingAttributeDeclaration.class.getName()) + .withMessageContaining("points to itself"); } @Test - void synthesizeWhenAliasForHasDuplicateAttributeDeclaration() throws Exception { - AliasForWithDuplicateAttributeDeclaration annotation = AliasForWithDuplicateAttributeDeclarationClass.class.getAnnotation( - AliasForWithDuplicateAttributeDeclaration.class); - assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() -> - MergedAnnotation.from(annotation)) - .withMessageStartingWith("In @AliasFor declared on attribute 'foo' in annotation") - .withMessageContaining(AliasForWithDuplicateAttributeDeclaration.class.getName()) - .withMessageContaining("attribute 'attribute' and its alias 'value' are present with values of 'baz' and 'bar'"); + void synthesizeWhenAliasForHasDuplicateAttributeDeclaration() { + AliasForWithDuplicateAttributeDeclaration annotation = + AliasForWithDuplicateAttributeDeclarationClass.class.getAnnotation( + AliasForWithDuplicateAttributeDeclaration.class); + + assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy( + () -> MergedAnnotation.from(annotation)) + .withMessageStartingWith("In @AliasFor declared on attribute 'foo' in annotation") + .withMessageContaining(AliasForWithDuplicateAttributeDeclaration.class.getName()) + .withMessageContaining("attribute 'attribute' and its alias 'value' are present with values of 'baz' and 'bar'"); } @Test - void synthesizeWhenAttributeAliasForNonexistentAttribute() throws Exception { + void synthesizeWhenAttributeAliasForNonexistentAttribute() { AliasForNonexistentAttribute annotation = AliasForNonexistentAttributeClass.class.getAnnotation( AliasForNonexistentAttribute.class); - assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() -> - MergedAnnotation.from(annotation)) - .withMessageStartingWith("@AliasFor declaration on attribute 'foo' in annotation") - .withMessageContaining(AliasForNonexistentAttribute.class.getName()) - .withMessageContaining("declares an alias for 'bar' which is not present"); + + assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy( + () -> MergedAnnotation.from(annotation)) + .withMessageStartingWith("@AliasFor declaration on attribute 'foo' in annotation") + .withMessageContaining(AliasForNonexistentAttribute.class.getName()) + .withMessageContaining("declares an alias for 'bar' which is not present"); } @Test - void synthesizeWhenAttributeAliasWithMirroredAliasForWrongAttribute() throws Exception { + void synthesizeWhenAttributeAliasWithMirroredAliasForWrongAttribute() { AliasForWithMirroredAliasForWrongAttribute annotation = AliasForWithMirroredAliasForWrongAttributeClass.class.getAnnotation( AliasForWithMirroredAliasForWrongAttribute.class); - assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() -> - MergedAnnotation.from(annotation)) - .withMessage("@AliasFor declaration on attribute 'bar' in annotation [" - + AliasForWithMirroredAliasForWrongAttribute.class.getName() - + "] declares an alias for 'quux' which is not present."); + + assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy( + () -> MergedAnnotation.from(annotation)) + .withMessage("@AliasFor declaration on attribute 'bar' in annotation [" + + AliasForWithMirroredAliasForWrongAttribute.class.getName() + + "] declares an alias for 'quux' which is not present."); } @Test - void synthesizeWhenAttributeAliasForAttributeOfDifferentType() throws Exception { + void synthesizeWhenAttributeAliasForAttributeOfDifferentType() { AliasForAttributeOfDifferentType annotation = AliasForAttributeOfDifferentTypeClass.class.getAnnotation( AliasForAttributeOfDifferentType.class); - assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() -> - MergedAnnotation.from(annotation)) - .withMessageStartingWith("Misconfigured aliases") - .withMessageContaining(AliasForAttributeOfDifferentType.class.getName()) - .withMessageContaining("attribute 'foo'") - .withMessageContaining("attribute 'bar'") - .withMessageContaining("same return type"); + + assertThatExceptionOfType(AnnotationConfigurationException.class) + .isThrownBy(() -> MergedAnnotation.from(annotation)) + .withMessageStartingWith("Misconfigured aliases") + .withMessageContaining(AliasForAttributeOfDifferentType.class.getName()) + .withMessageContaining("attribute 'foo'") + .withMessageContaining("attribute 'bar'") + .withMessageContaining("same return type"); } @Test - void synthesizeWhenAttributeAliasForWithMissingDefaultValues() throws Exception { + void synthesizeWhenAttributeAliasForWithMissingDefaultValues() { AliasForWithMissingDefaultValues annotation = AliasForWithMissingDefaultValuesClass.class.getAnnotation( AliasForWithMissingDefaultValues.class); - assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() -> - MergedAnnotation.from(annotation)) - .withMessageStartingWith("Misconfigured aliases") - .withMessageContaining(AliasForWithMissingDefaultValues.class.getName()) - .withMessageContaining("attribute 'foo' in annotation") - .withMessageContaining("attribute 'bar' in annotation") - .withMessageContaining("default values"); + + assertThatExceptionOfType(AnnotationConfigurationException.class) + .isThrownBy(() -> MergedAnnotation.from(annotation)) + .withMessageStartingWith("Misconfigured aliases") + .withMessageContaining(AliasForWithMissingDefaultValues.class.getName()) + .withMessageContaining("attribute 'foo' in annotation") + .withMessageContaining("attribute 'bar' in annotation") + .withMessageContaining("default values"); } @Test - void synthesizeWhenAttributeAliasForAttributeWithDifferentDefaultValue() throws Exception { + void synthesizeWhenAttributeAliasForAttributeWithDifferentDefaultValue() { AliasForAttributeWithDifferentDefaultValue annotation = AliasForAttributeWithDifferentDefaultValueClass.class.getAnnotation( AliasForAttributeWithDifferentDefaultValue.class); - assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() -> - MergedAnnotation.from(annotation)) - .withMessageStartingWith("Misconfigured aliases") - .withMessageContaining(AliasForAttributeWithDifferentDefaultValue.class.getName()) - .withMessageContaining("attribute 'foo' in annotation") - .withMessageContaining("attribute 'bar' in annotation") - .withMessageContaining("same default value"); + + assertThatExceptionOfType(AnnotationConfigurationException.class) + .isThrownBy(() -> MergedAnnotation.from(annotation)) + .withMessageStartingWith("Misconfigured aliases") + .withMessageContaining(AliasForAttributeWithDifferentDefaultValue.class.getName()) + .withMessageContaining("attribute 'foo' in annotation") + .withMessageContaining("attribute 'bar' in annotation") + .withMessageContaining("same default value"); } @Test - void synthesizeWhenAttributeAliasForMetaAnnotationThatIsNotMetaPresent() throws Exception { + void synthesizeWhenAttributeAliasForMetaAnnotationThatIsNotMetaPresent() { AliasedComposedTestConfigurationNotMetaPresent annotation = AliasedComposedTestConfigurationNotMetaPresentClass.class.getAnnotation( AliasedComposedTestConfigurationNotMetaPresent.class); - assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() -> - MergedAnnotation.from(annotation)) - .withMessageStartingWith("@AliasFor declaration on attribute 'xmlConfigFile' in annotation") - .withMessageContaining(AliasedComposedTestConfigurationNotMetaPresent.class.getName()) - .withMessageContaining("declares an alias for attribute 'location' in annotation") - .withMessageContaining(TestConfiguration.class.getName()) - .withMessageContaining("not meta-present"); + + assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy( + () -> MergedAnnotation.from(annotation)) + .withMessageStartingWith("@AliasFor declaration on attribute 'xmlConfigFile' in annotation") + .withMessageContaining(AliasedComposedTestConfigurationNotMetaPresent.class.getName()) + .withMessageContaining("declares an alias for attribute 'location' in annotation") + .withMessageContaining(TestConfiguration.class.getName()) + .withMessageContaining("not meta-present"); } @Test - void synthesizeWithImplicitAliases() throws Exception { + void synthesizeWithImplicitAliases() { testSynthesisWithImplicitAliases(ValueImplicitAliasesTestConfigurationClass.class, "value"); testSynthesisWithImplicitAliases(Location1ImplicitAliasesTestConfigurationClass.class, "location1"); testSynthesisWithImplicitAliases(XmlImplicitAliasesTestConfigurationClass.class, "xmlFile"); testSynthesisWithImplicitAliases(GroovyImplicitAliasesSimpleTestConfigurationClass.class, "groovyScript"); } - private void testSynthesisWithImplicitAliases(Class clazz, String expected) throws Exception { + private void testSynthesisWithImplicitAliases(Class clazz, String expected) { ImplicitAliasesTestConfiguration config = clazz.getAnnotation(ImplicitAliasesTestConfiguration.class); assertThat(config).isNotNull(); ImplicitAliasesTestConfiguration synthesized = MergedAnnotation.from(config).synthesize(); @@ -1608,8 +1582,7 @@ private void testSynthesisWithImplicitAliases(Class clazz, String expected) t } @Test - void synthesizeWithImplicitAliasesWithImpliedAliasNamesOmitted() - throws Exception { + void synthesizeWithImplicitAliasesWithImpliedAliasNamesOmitted() { testSynthesisWithImplicitAliasesWithImpliedAliasNamesOmitted( ValueImplicitAliasesWithImpliedAliasNamesOmittedTestConfigurationClass.class, "value"); @@ -1621,8 +1594,7 @@ void synthesizeWithImplicitAliasesWithImpliedAliasNamesOmitted() "xmlFile"); } - private void testSynthesisWithImplicitAliasesWithImpliedAliasNamesOmitted( - Class clazz, String expected) { + private void testSynthesisWithImplicitAliasesWithImpliedAliasNamesOmitted(Class clazz, String expected) { ImplicitAliasesWithImpliedAliasNamesOmittedTestConfiguration config = clazz.getAnnotation( ImplicitAliasesWithImpliedAliasNamesOmittedTestConfiguration.class); assertThat(config).isNotNull(); @@ -1635,7 +1607,7 @@ private void testSynthesisWithImplicitAliasesWithImpliedAliasNamesOmitted( } @Test - void synthesizeWithImplicitAliasesForAliasPair() throws Exception { + void synthesizeWithImplicitAliasesForAliasPair() { ImplicitAliasesForAliasPairTestConfiguration config = ImplicitAliasesForAliasPairTestConfigurationClass.class.getAnnotation( ImplicitAliasesForAliasPairTestConfiguration.class); @@ -1646,7 +1618,7 @@ void synthesizeWithImplicitAliasesForAliasPair() throws Exception { } @Test - void synthesizeWithTransitiveImplicitAliases() throws Exception { + void synthesizeWithTransitiveImplicitAliases() { TransitiveImplicitAliasesTestConfiguration config = TransitiveImplicitAliasesTestConfigurationClass.class.getAnnotation( TransitiveImplicitAliasesTestConfiguration.class); @@ -1657,70 +1629,69 @@ void synthesizeWithTransitiveImplicitAliases() throws Exception { } @Test - void synthesizeWithTransitiveImplicitAliasesForAliasPair() throws Exception { + void synthesizeWithTransitiveImplicitAliasesForAliasPair() { TransitiveImplicitAliasesForAliasPairTestConfiguration config = TransitiveImplicitAliasesForAliasPairTestConfigurationClass.class.getAnnotation( TransitiveImplicitAliasesForAliasPairTestConfiguration.class); - TransitiveImplicitAliasesForAliasPairTestConfiguration synthesized = MergedAnnotation.from( - config).synthesize(); + TransitiveImplicitAliasesForAliasPairTestConfiguration synthesized = MergedAnnotation.from(config).synthesize(); assertSynthesized(synthesized); assertThat(synthesized.xml()).isEqualTo("test.xml"); assertThat(synthesized.groovy()).isEqualTo("test.xml"); } @Test - void synthesizeWithImplicitAliasesWithMissingDefaultValues() throws Exception { + void synthesizeWithImplicitAliasesWithMissingDefaultValues() { Class clazz = ImplicitAliasesWithMissingDefaultValuesTestConfigurationClass.class; Class annotationType = ImplicitAliasesWithMissingDefaultValuesTestConfiguration.class; - ImplicitAliasesWithMissingDefaultValuesTestConfiguration config = clazz.getAnnotation( - annotationType); - assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() -> - MergedAnnotation.from(clazz, config)) - .withMessageStartingWith("Misconfigured aliases:") - .withMessageContaining("attribute 'location1' in annotation [" + annotationType.getName() + "]") - .withMessageContaining("attribute 'location2' in annotation [" + annotationType.getName() + "]") - .withMessageContaining("default values"); + ImplicitAliasesWithMissingDefaultValuesTestConfiguration config = clazz.getAnnotation(annotationType); + + assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy( + () -> MergedAnnotation.from(clazz, config)) + .withMessageStartingWith("Misconfigured aliases:") + .withMessageContaining("attribute 'location1' in annotation [" + annotationType.getName() + "]") + .withMessageContaining("attribute 'location2' in annotation [" + annotationType.getName() + "]") + .withMessageContaining("default values"); } @Test - void synthesizeWithImplicitAliasesWithDifferentDefaultValues() - throws Exception { + void synthesizeWithImplicitAliasesWithDifferentDefaultValues() { Class clazz = ImplicitAliasesWithDifferentDefaultValuesTestConfigurationClass.class; Class annotationType = ImplicitAliasesWithDifferentDefaultValuesTestConfiguration.class; - ImplicitAliasesWithDifferentDefaultValuesTestConfiguration config = clazz.getAnnotation( - annotationType); - assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() -> - MergedAnnotation.from(clazz, config)) - .withMessageStartingWith("Misconfigured aliases:") - .withMessageContaining("attribute 'location1' in annotation [" + annotationType.getName() + "]") - .withMessageContaining("attribute 'location2' in annotation [" + annotationType.getName() + "]") - .withMessageContaining("same default value"); + ImplicitAliasesWithDifferentDefaultValuesTestConfiguration config = clazz.getAnnotation(annotationType); + + assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy( + () -> MergedAnnotation.from(clazz, config)) + .withMessageStartingWith("Misconfigured aliases:") + .withMessageContaining("attribute 'location1' in annotation [" + annotationType.getName() + "]") + .withMessageContaining("attribute 'location2' in annotation [" + annotationType.getName() + "]") + .withMessageContaining("same default value"); } @Test - void synthesizeWithImplicitAliasesWithDuplicateValues() throws Exception { + void synthesizeWithImplicitAliasesWithDuplicateValues() { Class clazz = ImplicitAliasesWithDuplicateValuesTestConfigurationClass.class; Class annotationType = ImplicitAliasesWithDuplicateValuesTestConfiguration.class; - ImplicitAliasesWithDuplicateValuesTestConfiguration config = clazz.getAnnotation( - annotationType); - assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() -> - MergedAnnotation.from(clazz, config)) - .withMessageStartingWith("Different @AliasFor mirror values for annotation") - .withMessageContaining(annotationType.getName()) - .withMessageContaining("declared on class") - .withMessageContaining(clazz.getName()) - .withMessageContaining("are declared with values of"); + ImplicitAliasesWithDuplicateValuesTestConfiguration config = clazz.getAnnotation(annotationType); + + assertThatExceptionOfType(AnnotationConfigurationException.class) + .isThrownBy(() -> MergedAnnotation.from(clazz, config)) + .withMessageStartingWith("Different @AliasFor mirror values for annotation") + .withMessageContaining(annotationType.getName()) + .withMessageContaining("declared on class") + .withMessageContaining(clazz.getName()) + .withMessageContaining("are declared with values of"); } @Test - void synthesizeFromMapWithoutAttributeAliases() throws Exception { + void synthesizeFromMapWithoutAttributeAliases() { Component component = WebController.class.getAnnotation(Component.class); assertThat(component).isNotNull(); Map map = Collections.singletonMap("value", "webController"); MergedAnnotation annotation = MergedAnnotation.of(Component.class, map); + Component synthesizedComponent = annotation.synthesize(); assertSynthesized(synthesizedComponent); assertThat(synthesizedComponent.value()).isEqualTo("webController"); @@ -1728,14 +1699,13 @@ void synthesizeFromMapWithoutAttributeAliases() throws Exception { @Test @SuppressWarnings("unchecked") - void synthesizeFromMapWithNestedMap() throws Exception { + void synthesizeFromMapWithNestedMap() { ComponentScanSingleFilter componentScan = ComponentScanSingleFilterClass.class.getAnnotation( ComponentScanSingleFilter.class); assertThat(componentScan).isNotNull(); assertThat(componentScan.value().pattern()).isEqualTo("*Foo"); Map map = MergedAnnotation.from(componentScan).asMap( - annotation -> new LinkedHashMap<>(), - Adapt.ANNOTATION_TO_MAP); + annotation -> new LinkedHashMap<>(), Adapt.ANNOTATION_TO_MAP); Map filterMap = (Map) map.get("value"); assertThat(filterMap.get("pattern")).isEqualTo("*Foo"); filterMap.put("pattern", "newFoo"); @@ -1749,13 +1719,11 @@ void synthesizeFromMapWithNestedMap() throws Exception { @Test @SuppressWarnings("unchecked") - void synthesizeFromMapWithNestedArrayOfMaps() throws Exception { - ComponentScan componentScan = ComponentScanClass.class.getAnnotation( - ComponentScan.class); + void synthesizeFromMapWithNestedArrayOfMaps() { + ComponentScan componentScan = ComponentScanClass.class.getAnnotation(ComponentScan.class); assertThat(componentScan).isNotNull(); Map map = MergedAnnotation.from(componentScan).asMap( - annotation -> new LinkedHashMap<>(), - Adapt.ANNOTATION_TO_MAP); + annotation -> new LinkedHashMap<>(), Adapt.ANNOTATION_TO_MAP); Map[] filters = (Map[]) map.get("excludeFilters"); List patterns = Arrays.stream(filters).map( m -> (String) m.get("pattern")).collect(Collectors.toList()); @@ -1764,18 +1732,16 @@ void synthesizeFromMapWithNestedArrayOfMaps() throws Exception { filters[0].put("enigma", 42); filters[1].put("pattern", "newBar"); filters[1].put("enigma", 42); - MergedAnnotation annotation = MergedAnnotation.of( - ComponentScan.class, map); + MergedAnnotation annotation = MergedAnnotation.of(ComponentScan.class, map); ComponentScan synthesizedComponentScan = annotation.synthesize(); assertSynthesized(synthesizedComponentScan); - assertThat(Arrays.stream(synthesizedComponentScan.excludeFilters()).map( - Filter::pattern)).containsExactly("newFoo", "newBar"); + assertThat(Arrays.stream(synthesizedComponentScan.excludeFilters()).map(Filter::pattern)) + .containsExactly("newFoo", "newBar"); } @Test - void synthesizeFromDefaultsWithoutAttributeAliases() throws Exception { - MergedAnnotation annotation = MergedAnnotation.of( - AnnotationWithDefaults.class); + void synthesizeFromDefaultsWithoutAttributeAliases() { + MergedAnnotation annotation = MergedAnnotation.of(AnnotationWithDefaults.class); AnnotationWithDefaults synthesized = annotation.synthesize(); assertThat(synthesized.text()).isEqualTo("enigma"); assertThat(synthesized.predicate()).isTrue(); @@ -1783,51 +1749,45 @@ void synthesizeFromDefaultsWithoutAttributeAliases() throws Exception { } @Test - void synthesizeFromDefaultsWithAttributeAliases() throws Exception { - MergedAnnotation annotation = MergedAnnotation.of( - TestConfiguration.class); + void synthesizeFromDefaultsWithAttributeAliases() { + MergedAnnotation annotation = MergedAnnotation.of(TestConfiguration.class); TestConfiguration synthesized = annotation.synthesize(); assertThat(synthesized.value()).isEqualTo(""); assertThat(synthesized.location()).isEqualTo(""); } @Test - void synthesizeWhenAttributeAliasesWithDifferentValues() throws Exception { + void synthesizeWhenAttributeAliasesWithDifferentValues() { assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() -> MergedAnnotation.from(TestConfigurationMismatch.class.getAnnotation(TestConfiguration.class)).synthesize()); } @Test - void synthesizeFromMapWithMinimalAttributesWithAttributeAliases() - throws Exception { + void synthesizeFromMapWithMinimalAttributesWithAttributeAliases() { Map map = Collections.singletonMap("location", "test.xml"); - MergedAnnotation annotation = MergedAnnotation.of( - TestConfiguration.class, map); + MergedAnnotation annotation = MergedAnnotation.of(TestConfiguration.class, map); TestConfiguration synthesized = annotation.synthesize(); assertThat(synthesized.value()).isEqualTo("test.xml"); assertThat(synthesized.location()).isEqualTo("test.xml"); } @Test - void synthesizeFromMapWithAttributeAliasesThatOverrideArraysWithSingleElements() - throws Exception { + void synthesizeFromMapWithAttributeAliasesThatOverrideArraysWithSingleElements() { synthesizeFromMapWithAttributeAliasesThatOverrideArraysWithSingleElements( Collections.singletonMap("value", "/foo")); synthesizeFromMapWithAttributeAliasesThatOverrideArraysWithSingleElements( Collections.singletonMap("path", "/foo")); } - private void synthesizeFromMapWithAttributeAliasesThatOverrideArraysWithSingleElements( - Map map) { - MergedAnnotation annotation = MergedAnnotation.of(GetMapping.class, - map); + private void synthesizeFromMapWithAttributeAliasesThatOverrideArraysWithSingleElements(Map map) { + MergedAnnotation annotation = MergedAnnotation.of(GetMapping.class, map); GetMapping synthesized = annotation.synthesize(); assertThat(synthesized.value()).isEqualTo("/foo"); assertThat(synthesized.path()).isEqualTo("/foo"); } @Test - void synthesizeFromMapWithImplicitAttributeAliases() throws Exception { + void synthesizeFromMapWithImplicitAttributeAliases() { testSynthesisFromMapWithImplicitAliases("value"); testSynthesisFromMapWithImplicitAliases("location1"); testSynthesisFromMapWithImplicitAliases("location2"); @@ -1836,13 +1796,12 @@ void synthesizeFromMapWithImplicitAttributeAliases() throws Exception { testSynthesisFromMapWithImplicitAliases("groovyScript"); } - private void testSynthesisFromMapWithImplicitAliases(String attributeNameAndValue) - throws Exception { - Map map = Collections.singletonMap(attributeNameAndValue, - attributeNameAndValue); + private void testSynthesisFromMapWithImplicitAliases(String attributeNameAndValue) { + Map map = Collections.singletonMap(attributeNameAndValue, attributeNameAndValue); MergedAnnotation annotation = MergedAnnotation.of( ImplicitAliasesTestConfiguration.class, map); ImplicitAliasesTestConfiguration synthesized = annotation.synthesize(); + assertThat(synthesized.value()).isEqualTo(attributeNameAndValue); assertThat(synthesized.location1()).isEqualTo(attributeNameAndValue); assertThat(synthesized.location2()).isEqualTo(attributeNameAndValue); @@ -1852,12 +1811,12 @@ private void testSynthesisFromMapWithImplicitAliases(String attributeNameAndValu } @Test - void synthesizeFromMapWithMissingAttributeValue() throws Exception { + void synthesizeFromMapWithMissingAttributeValue() { testMissingTextAttribute(Collections.emptyMap()); } @Test - void synthesizeFromMapWithNullAttributeValue() throws Exception { + void synthesizeFromMapWithNullAttributeValue() { Map map = Collections.singletonMap("text", null); assertThat(map).containsKey("text"); testMissingTextAttribute(map); @@ -1866,12 +1825,12 @@ void synthesizeFromMapWithNullAttributeValue() throws Exception { private void testMissingTextAttribute(Map attributes) { assertThatExceptionOfType(NoSuchElementException.class).isThrownBy(() -> MergedAnnotation.of(AnnotationWithoutDefaults.class, attributes).synthesize().text()) - .withMessage("No value found for attribute named 'text' in merged annotation " + - AnnotationWithoutDefaults.class.getName()); + .withMessage("No value found for attribute named 'text' in merged annotation " + + AnnotationWithoutDefaults.class.getName()); } @Test - void synthesizeFromMapWithAttributeOfIncorrectType() throws Exception { + void synthesizeFromMapWithAttributeOfIncorrectType() { Map map = Collections.singletonMap("value", 42L); MergedAnnotation annotation = MergedAnnotation.of(Component.class, map); assertThatIllegalStateException().isThrownBy(() -> annotation.synthesize().value()) @@ -1881,10 +1840,11 @@ void synthesizeFromMapWithAttributeOfIncorrectType() throws Exception { } @Test - void synthesizeFromAnnotationAttributesWithoutAttributeAliases() throws Exception { + void synthesizeFromAnnotationAttributesWithoutAttributeAliases() { Component component = WebController.class.getAnnotation(Component.class); assertThat(component).isNotNull(); Map attributes = MergedAnnotation.from(component).asMap(); + Component synthesized = MergedAnnotation.of(Component.class, attributes).synthesize(); assertSynthesized(synthesized); assertThat(synthesized).isEqualTo(component); @@ -1918,47 +1878,41 @@ void toStringForSynthesizedAnnotations() throws Exception { private void assertToStringForWebMappingWithPathAndValue(RequestMapping webMapping) { assertThat(webMapping.toString()) - .startsWith("@org.springframework.core.annotation.MergedAnnotationsTests.RequestMapping(") - .contains( - // Strings - "value={\"/test\"}", "path={\"/test\"}", "name=\"bar\"", - // Characters - "ch='X'", "chars={'X'}", - // Enums - "method={GET, POST}", - // Classes - "clazz=org.springframework.core.annotation.MergedAnnotationsTests.RequestMethod.class", - "classes={int[][].class, org.springframework.core.annotation.MergedAnnotationsTests.RequestMethod[].class}", - // Bytes - "byteValue=(byte) 0xFF", "bytes={(byte) 0xFF}", - // Shorts - "shortValue=9876", "shorts={9876}", - // Longs - "longValue=42L", "longs={42L}", - // Floats - "floatValue=3.14f", "floats={3.14f}", - // Doubles - "doubleValue=99.999d", "doubles={99.999d}" - ) - .endsWith(")"); + .startsWith("@org.springframework.core.annotation.MergedAnnotationsTests.RequestMapping(") + .contains( + // Strings + "value={\"/test\"}", "path={\"/test\"}", "name=\"bar\"", + // Characters + "ch='X'", "chars={'X'}", + // Enums + "method={GET, POST}", + // Classes + "clazz=org.springframework.core.annotation.MergedAnnotationsTests.RequestMethod.class", + "classes={int[][].class, org.springframework.core.annotation.MergedAnnotationsTests.RequestMethod[].class}", + // Bytes + "byteValue=(byte) 0xFF", "bytes={(byte) 0xFF}", + // Shorts + "shortValue=9876", "shorts={9876}", + // Longs + "longValue=42L", "longs={42L}", + // Floats + "floatValue=3.14f", "floats={3.14f}", + // Doubles + "doubleValue=99.999d", "doubles={99.999d}" + ) + .endsWith(")"); } @Test void equalsForSynthesizedAnnotations() throws Exception { - Method methodWithPath = WebController.class.getMethod( - "handleMappedWithPathAttribute"); - RequestMapping webMappingWithAliases = methodWithPath.getAnnotation( - RequestMapping.class); + Method methodWithPath = WebController.class.getMethod("handleMappedWithPathAttribute"); + RequestMapping webMappingWithAliases = methodWithPath.getAnnotation(RequestMapping.class); assertThat(webMappingWithAliases).isNotNull(); - Method methodWithPathAndValue = WebController.class.getMethod( - "handleMappedWithSamePathAndValueAttributes"); - RequestMapping webMappingWithPathAndValue = methodWithPathAndValue.getAnnotation( - RequestMapping.class); + Method methodWithPathAndValue = WebController.class.getMethod("handleMappedWithSamePathAndValueAttributes"); + RequestMapping webMappingWithPathAndValue = methodWithPathAndValue.getAnnotation(RequestMapping.class); assertThat(webMappingWithPathAndValue).isNotNull(); - RequestMapping synthesizedWebMapping1 = MergedAnnotation.from( - webMappingWithAliases).synthesize(); - RequestMapping synthesizedWebMapping2 = MergedAnnotation.from( - webMappingWithPathAndValue).synthesize(); + RequestMapping synthesizedWebMapping1 = MergedAnnotation.from(webMappingWithAliases).synthesize(); + RequestMapping synthesizedWebMapping2 = MergedAnnotation.from(webMappingWithPathAndValue).synthesize(); // Equality amongst standard annotations assertThat(webMappingWithAliases).isEqualTo(webMappingWithAliases); assertThat(webMappingWithPathAndValue).isEqualTo(webMappingWithPathAndValue); @@ -1980,51 +1934,33 @@ void equalsForSynthesizedAnnotations() throws Exception { @Test void hashCodeForSynthesizedAnnotations() throws Exception { - Method methodWithPath = WebController.class.getMethod( - "handleMappedWithPathAttribute"); - RequestMapping webMappingWithAliases = methodWithPath.getAnnotation( - RequestMapping.class); + Method methodWithPath = WebController.class.getMethod("handleMappedWithPathAttribute"); + RequestMapping webMappingWithAliases = methodWithPath.getAnnotation(RequestMapping.class); assertThat(webMappingWithAliases).isNotNull(); - Method methodWithPathAndValue = WebController.class.getMethod( - "handleMappedWithSamePathAndValueAttributes"); - RequestMapping webMappingWithPathAndValue = methodWithPathAndValue.getAnnotation( - RequestMapping.class); + Method methodWithPathAndValue = WebController.class.getMethod("handleMappedWithSamePathAndValueAttributes"); + RequestMapping webMappingWithPathAndValue = methodWithPathAndValue.getAnnotation(RequestMapping.class); assertThat(webMappingWithPathAndValue).isNotNull(); - RequestMapping synthesizedWebMapping1 = MergedAnnotation.from( - webMappingWithAliases).synthesize(); + RequestMapping synthesizedWebMapping1 = MergedAnnotation.from(webMappingWithAliases).synthesize(); assertThat(synthesizedWebMapping1).isNotNull(); - RequestMapping synthesizedWebMapping2 = MergedAnnotation.from( - webMappingWithPathAndValue).synthesize(); + RequestMapping synthesizedWebMapping2 = MergedAnnotation.from(webMappingWithPathAndValue).synthesize(); assertThat(synthesizedWebMapping2).isNotNull(); // Equality amongst standard annotations - assertThat(webMappingWithAliases.hashCode()).isEqualTo( - webMappingWithAliases.hashCode()); - assertThat(webMappingWithPathAndValue.hashCode()).isEqualTo( - webMappingWithPathAndValue.hashCode()); + assertThat(webMappingWithAliases.hashCode()).isEqualTo(webMappingWithAliases.hashCode()); + assertThat(webMappingWithPathAndValue.hashCode()).isEqualTo(webMappingWithPathAndValue.hashCode()); // Inequality amongst standard annotations - assertThat(webMappingWithAliases.hashCode()).isNotEqualTo( - webMappingWithPathAndValue.hashCode()); - assertThat(webMappingWithPathAndValue.hashCode()).isNotEqualTo( - webMappingWithAliases.hashCode()); + assertThat(webMappingWithAliases.hashCode()).isNotEqualTo(webMappingWithPathAndValue.hashCode()); + assertThat(webMappingWithPathAndValue.hashCode()).isNotEqualTo(webMappingWithAliases.hashCode()); // Equality amongst synthesized annotations - assertThat(synthesizedWebMapping1.hashCode()).isEqualTo( - synthesizedWebMapping1.hashCode()); - assertThat(synthesizedWebMapping2.hashCode()).isEqualTo( - synthesizedWebMapping2.hashCode()); - assertThat(synthesizedWebMapping1.hashCode()).isEqualTo( - synthesizedWebMapping2.hashCode()); - assertThat(synthesizedWebMapping2.hashCode()).isEqualTo( - synthesizedWebMapping1.hashCode()); + assertThat(synthesizedWebMapping1.hashCode()).isEqualTo(synthesizedWebMapping1.hashCode()); + assertThat(synthesizedWebMapping2.hashCode()).isEqualTo(synthesizedWebMapping2.hashCode()); + assertThat(synthesizedWebMapping1.hashCode()).isEqualTo(synthesizedWebMapping2.hashCode()); + assertThat(synthesizedWebMapping2.hashCode()).isEqualTo(synthesizedWebMapping1.hashCode()); // Equality between standard and synthesized annotations - assertThat(synthesizedWebMapping1.hashCode()).isEqualTo( - webMappingWithPathAndValue.hashCode()); - assertThat(webMappingWithPathAndValue.hashCode()).isEqualTo( - synthesizedWebMapping1.hashCode()); + assertThat(synthesizedWebMapping1.hashCode()).isEqualTo(webMappingWithPathAndValue.hashCode()); + assertThat(webMappingWithPathAndValue.hashCode()).isEqualTo(synthesizedWebMapping1.hashCode()); // Inequality between standard and synthesized annotations - assertThat(synthesizedWebMapping1.hashCode()).isNotEqualTo( - webMappingWithAliases.hashCode()); - assertThat(webMappingWithAliases.hashCode()).isNotEqualTo( - synthesizedWebMapping1.hashCode()); + assertThat(synthesizedWebMapping1.hashCode()).isNotEqualTo(webMappingWithAliases.hashCode()); + assertThat(webMappingWithAliases.hashCode()).isNotEqualTo(synthesizedWebMapping1.hashCode()); } /** @@ -2052,7 +1988,7 @@ void synthesizeNonPublicWithAttributeAliasesFromDifferentPackage() throws Except } @Test - void synthesizeWithArrayOfAnnotations() throws Exception { + void synthesizeWithArrayOfAnnotations() { Hierarchy hierarchy = HierarchyClass.class.getAnnotation(Hierarchy.class); assertThat(hierarchy).isNotNull(); Hierarchy synthesizedHierarchy = MergedAnnotation.from(hierarchy).synthesize(); @@ -2074,12 +2010,10 @@ void synthesizeWithArrayOfAnnotations() throws Exception { } @Test - void synthesizeWithArrayOfChars() throws Exception { - CharsContainer charsContainer = GroupOfCharsClass.class.getAnnotation( - CharsContainer.class); + void synthesizeWithArrayOfChars() { + CharsContainer charsContainer = GroupOfCharsClass.class.getAnnotation(CharsContainer.class); assertThat(charsContainer).isNotNull(); - CharsContainer synthesizedCharsContainer = MergedAnnotation.from( - charsContainer).synthesize(); + CharsContainer synthesizedCharsContainer = MergedAnnotation.from(charsContainer).synthesize(); assertSynthesized(synthesizedCharsContainer); char[] chars = synthesizedCharsContainer.chars(); assertThat(chars).containsExactly('x', 'y', 'z'); @@ -2092,53 +2026,50 @@ void synthesizeWithArrayOfChars() throws Exception { @Test void getValueWhenHasDefaultOverride() { - MergedAnnotation annotation = MergedAnnotations.from( - DefaultOverrideClass.class).get(DefaultOverrideRoot.class); + MergedAnnotation annotation = MergedAnnotations.from(DefaultOverrideClass.class) + .get(DefaultOverrideRoot.class); assertThat(annotation.getString("text")).isEqualTo("metameta"); } @Test // gh-22654 void getValueWhenHasDefaultOverrideWithImplicitAlias() { - MergedAnnotation annotation1 = MergedAnnotations.from( - DefaultOverrideImplicitAliasMetaClass1.class).get(DefaultOverrideRoot.class); + MergedAnnotation annotation1 = MergedAnnotations.from(DefaultOverrideImplicitAliasMetaClass1.class) + .get(DefaultOverrideRoot.class); assertThat(annotation1.getString("text")).isEqualTo("alias-meta-1"); - MergedAnnotation annotation2 = MergedAnnotations.from( - DefaultOverrideImplicitAliasMetaClass2.class).get(DefaultOverrideRoot.class); + MergedAnnotation annotation2 = MergedAnnotations.from(DefaultOverrideImplicitAliasMetaClass2.class) + .get(DefaultOverrideRoot.class); assertThat(annotation2.getString("text")).isEqualTo("alias-meta-2"); } @Test // gh-22654 void getValueWhenHasDefaultOverrideWithExplicitAlias() { - MergedAnnotation annotation = MergedAnnotations.from( - DefaultOverrideExplicitAliasRootMetaMetaClass.class).get( - DefaultOverrideExplicitAliasRoot.class); + MergedAnnotation annotation = MergedAnnotations.from(DefaultOverrideExplicitAliasRootMetaMetaClass.class) + .get(DefaultOverrideExplicitAliasRoot.class); assertThat(annotation.getString("text")).isEqualTo("meta"); assertThat(annotation.getString("value")).isEqualTo("meta"); } @Test // gh-22703 void getValueWhenThreeDeepMetaWithValue() { - MergedAnnotation annotation = MergedAnnotations.from( - ValueAttributeMetaMetaClass.class).get(ValueAttribute.class); - assertThat(annotation.getStringArray(MergedAnnotation.VALUE)).containsExactly( - "FromValueAttributeMeta"); + MergedAnnotation annotation = MergedAnnotations.from(ValueAttributeMetaMetaClass.class) + .get(ValueAttribute.class); + assertThat(annotation.getStringArray(MergedAnnotation.VALUE)).containsExactly("FromValueAttributeMeta"); } @Test void asAnnotationAttributesReturnsPopulatedAnnotationAttributes() { - MergedAnnotation annotation = MergedAnnotations.from( - SpringApplicationConfigurationClass.class).get( - SpringApplicationConfiguration.class); - AnnotationAttributes attributes = annotation.asAnnotationAttributes( - Adapt.CLASS_TO_STRING); - assertThat(attributes).containsEntry("classes", new String[] { Number.class.getName() }); + MergedAnnotation annotation = MergedAnnotations.from(SpringApplicationConfigurationClass.class) + .get(SpringApplicationConfiguration.class); + AnnotationAttributes attributes = annotation.asAnnotationAttributes(Adapt.CLASS_TO_STRING); + assertThat(attributes).containsEntry("classes", new String[] {Number.class.getName()}); assertThat(attributes.annotationType()).isEqualTo(SpringApplicationConfiguration.class); } + // @formatter:off + @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) - @Target({ ElementType.TYPE, ElementType.METHOD }) @Inherited @interface Transactional { @@ -2191,8 +2122,8 @@ static class ComposedTransactionalComponentClass { static class AliasedTransactionalComponentClass { } + @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) - @Target({ ElementType.TYPE, ElementType.METHOD }) @Inherited @interface AliasedTransactional { @@ -2543,7 +2474,7 @@ interface InterfaceWithInheritedAnnotation { void handleFromInterface(); } - static abstract class AbstractClassWithInheritedAnnotation + abstract static class AbstractClassWithInheritedAnnotation implements InterfaceWithInheritedAnnotation { @Transactional @@ -2847,7 +2778,7 @@ public void overrideWithoutNewAnnotation() { } } - public static abstract class SimpleGeneric { + public abstract static class SimpleGeneric { @Order(1) public abstract void something(T arg); @@ -2937,7 +2868,7 @@ public void foo(String t) { } } - public static abstract class BaseClassWithGenericAnnotatedMethod { + public abstract static class BaseClassWithGenericAnnotatedMethod { @Order abstract void foo(T t); diff --git a/spring-core/src/test/java/org/springframework/core/annotation/RepeatableContainersTests.java b/spring-core/src/test/java/org/springframework/core/annotation/RepeatableContainersTests.java index b3a9860394a2..fd2b89da9256 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/RepeatableContainersTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/RepeatableContainersTests.java @@ -20,7 +20,9 @@ import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.Arrays; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; @@ -31,189 +33,171 @@ * Tests for {@link RepeatableContainers}. * * @author Phillip Webb + * @author Sam Brannen */ class RepeatableContainersTests { - @Test - void standardRepeatablesWhenNonRepeatableReturnsNull() { - Object[] values = findRepeatedAnnotationValues( - RepeatableContainers.standardRepeatables(), WithNonRepeatable.class, - NonRepeatable.class); - assertThat(values).isNull(); - } + @Nested + class StandardRepeatableContainersTests { - @Test - void standardRepeatablesWhenSingleReturnsNull() { - Object[] values = findRepeatedAnnotationValues( - RepeatableContainers.standardRepeatables(), - WithSingleStandardRepeatable.class, StandardRepeatable.class); - assertThat(values).isNull(); - } + @Test + void standardRepeatablesWhenNonRepeatableReturnsNull() { + Object[] values = findRepeatedAnnotationValues(RepeatableContainers.standardRepeatables(), + NonRepeatableTestCase.class, NonRepeatable.class); + assertThat(values).isNull(); + } - @Test - void standardRepeatablesWhenContainerReturnsRepeats() { - Object[] values = findRepeatedAnnotationValues( - RepeatableContainers.standardRepeatables(), WithStandardRepeatables.class, - StandardContainer.class); - assertThat(values).containsExactly("a", "b"); - } + @Test + void standardRepeatablesWhenSingleReturnsNull() { + Object[] values = findRepeatedAnnotationValues(RepeatableContainers.standardRepeatables(), + SingleStandardRepeatableTestCase.class, StandardRepeatable.class); + assertThat(values).isNull(); + } - @Test - void standardRepeatablesWhenContainerButNotRepeatableReturnsNull() { - Object[] values = findRepeatedAnnotationValues( - RepeatableContainers.standardRepeatables(), WithExplicitRepeatables.class, - ExplicitContainer.class); - assertThat(values).isNull(); - } + @Test + void standardRepeatablesWhenContainerButNotRepeatableReturnsNull() { + Object[] values = findRepeatedAnnotationValues(RepeatableContainers.standardRepeatables(), + ExplicitRepeatablesTestCase.class, ExplicitContainer.class); + assertThat(values).isNull(); + } - @Test - void ofExplicitWhenNonRepeatableReturnsNull() { - Object[] values = findRepeatedAnnotationValues( - RepeatableContainers.of(ExplicitRepeatable.class, - ExplicitContainer.class), - WithNonRepeatable.class, NonRepeatable.class); - assertThat(values).isNull(); - } + @Test + void standardRepeatablesWhenContainerReturnsRepeats() { + Object[] values = findRepeatedAnnotationValues(RepeatableContainers.standardRepeatables(), + StandardRepeatablesTestCase.class, StandardContainer.class); + assertThat(values).containsExactly("a", "b"); + } - @Test - void ofExplicitWhenStandardRepeatableContainerReturnsNull() { - Object[] values = findRepeatedAnnotationValues( - RepeatableContainers.of(ExplicitRepeatable.class, - ExplicitContainer.class), - WithStandardRepeatables.class, StandardContainer.class); - assertThat(values).isNull(); - } + @Test + void standardRepeatablesWithContainerWithMultipleAttributes() { + Object[] values = findRepeatedAnnotationValues(RepeatableContainers.standardRepeatables(), + StandardRepeatablesWithContainerWithMultipleAttributesTestCase.class, + StandardContainerWithMultipleAttributes.class); + assertThat(values).containsExactly("a", "b"); + } - @Test - void ofExplicitWhenContainerReturnsRepeats() { - Object[] values = findRepeatedAnnotationValues( - RepeatableContainers.of(ExplicitRepeatable.class, - ExplicitContainer.class), - WithExplicitRepeatables.class, ExplicitContainer.class); - assertThat(values).containsExactly("a", "b"); } - @Test - void ofExplicitWhenHasNoValueThrowsException() { - assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() -> - RepeatableContainers.of(ExplicitRepeatable.class, InvalidNoValue.class)) - .withMessageContaining("Invalid declaration of container type [" - + InvalidNoValue.class.getName() - + "] for repeatable annotation [" - + ExplicitRepeatable.class.getName() + "]"); - } + @Nested + class ExplicitRepeatableContainerTests { - @Test - void ofExplicitWhenValueIsNotArrayThrowsException() { - assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() -> - RepeatableContainers.of(ExplicitRepeatable.class, InvalidNotArray.class)) - .withMessage("Container type [" - + InvalidNotArray.class.getName() - + "] must declare a 'value' attribute for an array of type [" - + ExplicitRepeatable.class.getName() + "]"); - } + @Test + void ofExplicitWhenNonRepeatableReturnsNull() { + Object[] values = findRepeatedAnnotationValues( + RepeatableContainers.of(ExplicitRepeatable.class, ExplicitContainer.class), + NonRepeatableTestCase.class, NonRepeatable.class); + assertThat(values).isNull(); + } - @Test - void ofExplicitWhenValueIsArrayOfWrongTypeThrowsException() { - assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() -> - RepeatableContainers.of(ExplicitRepeatable.class, InvalidWrongArrayType.class)) - .withMessage("Container type [" - + InvalidWrongArrayType.class.getName() - + "] must declare a 'value' attribute for an array of type [" - + ExplicitRepeatable.class.getName() + "]"); - } + @Test + void ofExplicitWhenStandardRepeatableContainerReturnsNull() { + Object[] values = findRepeatedAnnotationValues( + RepeatableContainers.of(ExplicitRepeatable.class, ExplicitContainer.class), + StandardRepeatablesTestCase.class, StandardContainer.class); + assertThat(values).isNull(); + } - @Test - void ofExplicitWhenAnnotationIsNullThrowsException() { - assertThatIllegalArgumentException().isThrownBy(() -> - RepeatableContainers.of(null, null)) - .withMessage("Repeatable must not be null"); - } + @Test + void ofExplicitWhenContainerReturnsRepeats() { + Object[] values = findRepeatedAnnotationValues( + RepeatableContainers.of(ExplicitRepeatable.class, ExplicitContainer.class), + ExplicitRepeatablesTestCase.class, ExplicitContainer.class); + assertThat(values).containsExactly("a", "b"); + } - @Test - void ofExplicitWhenContainerIsNullDeducesContainer() { - Object[] values = findRepeatedAnnotationValues( - RepeatableContainers.of(StandardRepeatable.class, null), - WithStandardRepeatables.class, StandardContainer.class); - assertThat(values).containsExactly("a", "b"); - } + @Test + void ofExplicitWhenContainerIsNullDeducesContainer() { + Object[] values = findRepeatedAnnotationValues(RepeatableContainers.of(StandardRepeatable.class, null), + StandardRepeatablesTestCase.class, StandardContainer.class); + assertThat(values).containsExactly("a", "b"); + } + + @Test + void ofExplicitWhenHasNoValueThrowsException() { + assertThatExceptionOfType(AnnotationConfigurationException.class) + .isThrownBy(() -> RepeatableContainers.of(ExplicitRepeatable.class, InvalidNoValue.class)) + .withMessageContaining("Invalid declaration of container type [%s] for repeatable annotation [%s]", + InvalidNoValue.class.getName(), ExplicitRepeatable.class.getName()); + } + + @Test + void ofExplicitWhenValueIsNotArrayThrowsException() { + assertThatExceptionOfType(AnnotationConfigurationException.class) + .isThrownBy(() -> RepeatableContainers.of(ExplicitRepeatable.class, InvalidNotArray.class)) + .withMessage("Container type [%s] must declare a 'value' attribute for an array of type [%s]", + InvalidNotArray.class.getName(), ExplicitRepeatable.class.getName()); + } + + @Test + void ofExplicitWhenValueIsArrayOfWrongTypeThrowsException() { + assertThatExceptionOfType(AnnotationConfigurationException.class) + .isThrownBy(() -> RepeatableContainers.of(ExplicitRepeatable.class, InvalidWrongArrayType.class)) + .withMessage("Container type [%s] must declare a 'value' attribute for an array of type [%s]", + InvalidWrongArrayType.class.getName(), ExplicitRepeatable.class.getName()); + } + + @Test + void ofExplicitWhenAnnotationIsNullThrowsException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> RepeatableContainers.of(null, null)) + .withMessage("Repeatable must not be null"); + } + + @Test + void ofExplicitWhenContainerIsNullAndNotRepeatableThrowsException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> RepeatableContainers.of(ExplicitRepeatable.class, null)) + .withMessage("Annotation type must be a repeatable annotation: failed to resolve container type for %s", + ExplicitRepeatable.class.getName()); + } - @Test - void ofExplicitWhenContainerIsNullAndNotRepeatableThrowsException() { - assertThatIllegalArgumentException().isThrownBy(() -> - RepeatableContainers.of(ExplicitRepeatable.class, null)) - .withMessage("Annotation type must be a repeatable annotation: " + - "failed to resolve container type for " + - ExplicitRepeatable.class.getName()); } @Test void standardAndExplicitReturnsRepeats() { - RepeatableContainers repeatableContainers = RepeatableContainers.standardRepeatables().and( - ExplicitContainer.class, ExplicitRepeatable.class); - assertThat(findRepeatedAnnotationValues(repeatableContainers, - WithStandardRepeatables.class, StandardContainer.class)).containsExactly( - "a", "b"); - assertThat(findRepeatedAnnotationValues(repeatableContainers, - WithExplicitRepeatables.class, ExplicitContainer.class)).containsExactly( - "a", "b"); + RepeatableContainers repeatableContainers = RepeatableContainers.standardRepeatables() + .and(ExplicitContainer.class, ExplicitRepeatable.class); + assertThat(findRepeatedAnnotationValues(repeatableContainers, StandardRepeatablesTestCase.class, StandardContainer.class)) + .containsExactly("a", "b"); + assertThat(findRepeatedAnnotationValues(repeatableContainers, ExplicitRepeatablesTestCase.class, ExplicitContainer.class)) + .containsExactly("a", "b"); } @Test void noneAlwaysReturnsNull() { - Object[] values = findRepeatedAnnotationValues( - RepeatableContainers.none(), WithStandardRepeatables.class, - StandardContainer.class); + Object[] values = findRepeatedAnnotationValues(RepeatableContainers.none(), StandardRepeatablesTestCase.class, + StandardContainer.class); assertThat(values).isNull(); } @Test void equalsAndHashcode() { - RepeatableContainers c1 = RepeatableContainers.of(ExplicitRepeatable.class, - ExplicitContainer.class); - RepeatableContainers c2 = RepeatableContainers.of(ExplicitRepeatable.class, - ExplicitContainer.class); + RepeatableContainers c1 = RepeatableContainers.of(ExplicitRepeatable.class, ExplicitContainer.class); + RepeatableContainers c2 = RepeatableContainers.of(ExplicitRepeatable.class, ExplicitContainer.class); RepeatableContainers c3 = RepeatableContainers.standardRepeatables(); - RepeatableContainers c4 = RepeatableContainers.standardRepeatables().and( - ExplicitContainer.class, ExplicitRepeatable.class); - assertThat(c1.hashCode()).isEqualTo(c2.hashCode()); + RepeatableContainers c4 = RepeatableContainers.standardRepeatables().and(ExplicitContainer.class, ExplicitRepeatable.class); + assertThat(c1).hasSameHashCodeAs(c2); assertThat(c1).isEqualTo(c1).isEqualTo(c2); assertThat(c1).isNotEqualTo(c3).isNotEqualTo(c4); } - private Object[] findRepeatedAnnotationValues(RepeatableContainers containers, + + private static Object[] findRepeatedAnnotationValues(RepeatableContainers containers, Class element, Class annotationType) { - Annotation[] annotations = containers.findRepeatedAnnotations( - element.getAnnotation(annotationType)); + Annotation[] annotations = containers.findRepeatedAnnotations(element.getAnnotation(annotationType)); return extractValues(annotations); } - private Object[] extractValues(Annotation[] annotations) { - try { - if (annotations == null) { - return null; - } - Object[] result = new String[annotations.length]; - for (int i = 0; i < annotations.length; i++) { - result[i] = annotations[i].annotationType().getMethod("value").invoke( - annotations[i]); - } - return result; - } - catch (Exception ex) { - throw new RuntimeException(ex); + private static Object[] extractValues(Annotation[] annotations) { + if (annotations == null) { + return null; } + return Arrays.stream(annotations).map(AnnotationUtils::getValue).toArray(Object[]::new); } - @Retention(RetentionPolicy.RUNTIME) - @interface NonRepeatable { - - String value() default ""; - } @Retention(RetentionPolicy.RUNTIME) - @Repeatable(StandardContainer.class) - @interface StandardRepeatable { + @interface NonRepeatable { String value() default ""; } @@ -225,7 +209,8 @@ private Object[] extractValues(Annotation[] annotations) { } @Retention(RetentionPolicy.RUNTIME) - @interface ExplicitRepeatable { + @Repeatable(StandardContainer.class) + @interface StandardRepeatable { String value() default ""; } @@ -236,6 +221,12 @@ private Object[] extractValues(Annotation[] annotations) { ExplicitRepeatable[] value(); } + @Retention(RetentionPolicy.RUNTIME) + @interface ExplicitRepeatable { + + String value() default ""; + } + @Retention(RetentionPolicy.RUNTIME) @interface InvalidNoValue { } @@ -253,20 +244,40 @@ private Object[] extractValues(Annotation[] annotations) { } @NonRepeatable("a") - static class WithNonRepeatable { + static class NonRepeatableTestCase { } @StandardRepeatable("a") - static class WithSingleStandardRepeatable { + static class SingleStandardRepeatableTestCase { } @StandardRepeatable("a") @StandardRepeatable("b") - static class WithStandardRepeatables { + static class StandardRepeatablesTestCase { + } + + @Retention(RetentionPolicy.RUNTIME) + @interface StandardContainerWithMultipleAttributes { + + StandardRepeatableWithContainerWithMultipleAttributes[] value(); + + String name() default ""; + } + + @Retention(RetentionPolicy.RUNTIME) + @Repeatable(StandardContainerWithMultipleAttributes.class) + @interface StandardRepeatableWithContainerWithMultipleAttributes { + + String value() default ""; + } + + @StandardRepeatableWithContainerWithMultipleAttributes("a") + @StandardRepeatableWithContainerWithMultipleAttributes("b") + static class StandardRepeatablesWithContainerWithMultipleAttributesTestCase { } @ExplicitContainer({ @ExplicitRepeatable("a"), @ExplicitRepeatable("b") }) - static class WithExplicitRepeatables { + static class ExplicitRepeatablesTestCase { } } diff --git a/spring-core/src/test/java/org/springframework/core/codec/ResourceRegionEncoderTests.java b/spring-core/src/test/java/org/springframework/core/codec/ResourceRegionEncoderTests.java index 6412af933945..4cf45826d7a3 100644 --- a/spring-core/src/test/java/org/springframework/core/codec/ResourceRegionEncoderTests.java +++ b/spring-core/src/test/java/org/springframework/core/codec/ResourceRegionEncoderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2022 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-core/src/test/java/org/springframework/core/convert/TypeDescriptorTests.java b/spring-core/src/test/java/org/springframework/core/convert/TypeDescriptorTests.java index e77ffaa41b19..79a8daa99b7b 100644 --- a/spring-core/src/test/java/org/springframework/core/convert/TypeDescriptorTests.java +++ b/spring-core/src/test/java/org/springframework/core/convert/TypeDescriptorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 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. @@ -34,11 +34,13 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import org.junit.jupiter.api.Test; import org.springframework.core.MethodParameter; +import org.springframework.core.ResolvableType; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; @@ -65,7 +67,7 @@ void parameterPrimitive() throws Exception { assertThat(desc.getName()).isEqualTo("int"); assertThat(desc.toString()).isEqualTo("int"); assertThat(desc.isPrimitive()).isTrue(); - assertThat(desc.getAnnotations().length).isEqualTo(0); + assertThat(desc.getAnnotations()).isEmpty(); assertThat(desc.isCollection()).isFalse(); assertThat(desc.isMap()).isFalse(); } @@ -77,8 +79,8 @@ void parameterScalar() throws Exception { assertThat(desc.getObjectType()).isEqualTo(String.class); assertThat(desc.getName()).isEqualTo("java.lang.String"); assertThat(desc.toString()).isEqualTo("java.lang.String"); - assertThat(!desc.isPrimitive()).isTrue(); - assertThat(desc.getAnnotations().length).isEqualTo(0); + assertThat(desc.isPrimitive()).isFalse(); + assertThat(desc.getAnnotations()).isEmpty(); assertThat(desc.isCollection()).isFalse(); assertThat(desc.isArray()).isFalse(); assertThat(desc.isMap()).isFalse(); @@ -92,8 +94,8 @@ void parameterList() throws Exception { assertThat(desc.getObjectType()).isEqualTo(List.class); assertThat(desc.getName()).isEqualTo("java.util.List"); assertThat(desc.toString()).isEqualTo("java.util.List>>>"); - assertThat(!desc.isPrimitive()).isTrue(); - assertThat(desc.getAnnotations().length).isEqualTo(0); + assertThat(desc.isPrimitive()).isFalse(); + assertThat(desc.getAnnotations()).isEmpty(); assertThat(desc.isCollection()).isTrue(); assertThat(desc.isArray()).isFalse(); assertThat(desc.getElementTypeDescriptor().getType()).isEqualTo(List.class); @@ -113,8 +115,8 @@ void parameterListNoParamTypes() throws Exception { assertThat(desc.getObjectType()).isEqualTo(List.class); assertThat(desc.getName()).isEqualTo("java.util.List"); assertThat(desc.toString()).isEqualTo("java.util.List"); - assertThat(!desc.isPrimitive()).isTrue(); - assertThat(desc.getAnnotations().length).isEqualTo(0); + assertThat(desc.isPrimitive()).isFalse(); + assertThat(desc.getAnnotations()).isEmpty(); assertThat(desc.isCollection()).isTrue(); assertThat(desc.isArray()).isFalse(); assertThat((Object) desc.getElementTypeDescriptor()).isNull(); @@ -129,8 +131,8 @@ void parameterArray() throws Exception { assertThat(desc.getObjectType()).isEqualTo(Integer[].class); assertThat(desc.getName()).isEqualTo("java.lang.Integer[]"); assertThat(desc.toString()).isEqualTo("java.lang.Integer[]"); - assertThat(!desc.isPrimitive()).isTrue(); - assertThat(desc.getAnnotations().length).isEqualTo(0); + assertThat(desc.isPrimitive()).isFalse(); + assertThat(desc.getAnnotations()).isEmpty(); assertThat(desc.isCollection()).isFalse(); assertThat(desc.isArray()).isTrue(); assertThat(desc.getElementTypeDescriptor().getType()).isEqualTo(Integer.class); @@ -146,8 +148,8 @@ void parameterMap() throws Exception { assertThat(desc.getObjectType()).isEqualTo(Map.class); assertThat(desc.getName()).isEqualTo("java.util.Map"); assertThat(desc.toString()).isEqualTo("java.util.Map>"); - assertThat(!desc.isPrimitive()).isTrue(); - assertThat(desc.getAnnotations().length).isEqualTo(0); + assertThat(desc.isPrimitive()).isFalse(); + assertThat(desc.getAnnotations()).isEmpty(); assertThat(desc.isCollection()).isFalse(); assertThat(desc.isArray()).isFalse(); assertThat(desc.isMap()).isTrue(); @@ -162,7 +164,7 @@ void parameterMap() throws Exception { void parameterAnnotated() throws Exception { TypeDescriptor t1 = new TypeDescriptor(new MethodParameter(getClass().getMethod("testAnnotatedMethod", String.class), 0)); assertThat(t1.getType()).isEqualTo(String.class); - assertThat(t1.getAnnotations().length).isEqualTo(1); + assertThat(t1.getAnnotations()).hasSize(1); assertThat(t1.getAnnotation(ParameterAnnotation.class)).isNotNull(); assertThat(t1.hasAnnotation(ParameterAnnotation.class)).isTrue(); assertThat(t1.getAnnotation(ParameterAnnotation.class).value()).isEqualTo(123); @@ -335,7 +337,7 @@ void fieldMap() throws Exception { @Test void fieldAnnotated() throws Exception { TypeDescriptor typeDescriptor = new TypeDescriptor(getClass().getField("fieldAnnotated")); - assertThat(typeDescriptor.getAnnotations().length).isEqualTo(1); + assertThat(typeDescriptor.getAnnotations()).hasSize(1); assertThat(typeDescriptor.getAnnotation(FieldAnnotation.class)).isNotNull(); } @@ -462,8 +464,8 @@ void collection() { assertThat(desc.getObjectType()).isEqualTo(List.class); assertThat(desc.getName()).isEqualTo("java.util.List"); assertThat(desc.toString()).isEqualTo("java.util.List"); - assertThat(!desc.isPrimitive()).isTrue(); - assertThat(desc.getAnnotations().length).isEqualTo(0); + assertThat(desc.isPrimitive()).isFalse(); + assertThat(desc.getAnnotations()).isEmpty(); assertThat(desc.isCollection()).isTrue(); assertThat(desc.isArray()).isFalse(); assertThat(desc.getElementTypeDescriptor().getType()).isEqualTo(Integer.class); @@ -478,8 +480,8 @@ void collectionNested() { assertThat(desc.getObjectType()).isEqualTo(List.class); assertThat(desc.getName()).isEqualTo("java.util.List"); assertThat(desc.toString()).isEqualTo("java.util.List>"); - assertThat(!desc.isPrimitive()).isTrue(); - assertThat(desc.getAnnotations().length).isEqualTo(0); + assertThat(desc.isPrimitive()).isFalse(); + assertThat(desc.getAnnotations()).isEmpty(); assertThat(desc.isCollection()).isTrue(); assertThat(desc.isArray()).isFalse(); assertThat(desc.getElementTypeDescriptor().getType()).isEqualTo(List.class); @@ -494,8 +496,8 @@ void map() { assertThat(desc.getObjectType()).isEqualTo(Map.class); assertThat(desc.getName()).isEqualTo("java.util.Map"); assertThat(desc.toString()).isEqualTo("java.util.Map"); - assertThat(!desc.isPrimitive()).isTrue(); - assertThat(desc.getAnnotations().length).isEqualTo(0); + assertThat(desc.isPrimitive()).isFalse(); + assertThat(desc.getAnnotations()).isEmpty(); assertThat(desc.isCollection()).isFalse(); assertThat(desc.isArray()).isFalse(); assertThat(desc.isMap()).isTrue(); @@ -511,8 +513,8 @@ void mapNested() { assertThat(desc.getObjectType()).isEqualTo(Map.class); assertThat(desc.getName()).isEqualTo("java.util.Map"); assertThat(desc.toString()).isEqualTo("java.util.Map>"); - assertThat(!desc.isPrimitive()).isTrue(); - assertThat(desc.getAnnotations().length).isEqualTo(0); + assertThat(desc.isPrimitive()).isFalse(); + assertThat(desc.getAnnotations()).isEmpty(); assertThat(desc.isCollection()).isFalse(); assertThat(desc.isArray()).isFalse(); assertThat(desc.isMap()).isTrue(); @@ -524,7 +526,7 @@ void mapNested() { @Test void narrow() { TypeDescriptor desc = TypeDescriptor.valueOf(Number.class); - Integer value = Integer.valueOf(3); + Integer value = 3; desc = desc.narrow(value); assertThat(desc.getType()).isEqualTo(Integer.class); } @@ -532,7 +534,7 @@ void narrow() { @Test void elementType() { TypeDescriptor desc = TypeDescriptor.valueOf(List.class); - Integer value = Integer.valueOf(3); + Integer value = 3; desc = desc.elementTypeDescriptor(value); assertThat(desc.getType()).isEqualTo(Integer.class); } @@ -550,7 +552,7 @@ void elementTypePreserveContext() throws Exception { @Test void mapKeyType() { TypeDescriptor desc = TypeDescriptor.valueOf(Map.class); - Integer value = Integer.valueOf(3); + Integer value = 3; desc = desc.getMapKeyTypeDescriptor(value); assertThat(desc.getType()).isEqualTo(Integer.class); } @@ -568,7 +570,7 @@ void mapKeyTypePreserveContext() throws Exception { @Test void mapValueType() { TypeDescriptor desc = TypeDescriptor.valueOf(Map.class); - Integer value = Integer.valueOf(3); + Integer value = 3; desc = desc.getMapValueTypeDescriptor(value); assertThat(desc.getType()).isEqualTo(Integer.class); } @@ -663,12 +665,12 @@ void passDownGeneric() throws Exception { } @Test - void upCast() throws Exception { + void upcast() throws Exception { Property property = new Property(getClass(), getClass().getMethod("getProperty"), getClass().getMethod("setProperty", Map.class)); TypeDescriptor typeDescriptor = new TypeDescriptor(property); - TypeDescriptor upCast = typeDescriptor.upcast(Object.class); - assertThat(upCast.getAnnotation(MethodAnnotation1.class) != null).isTrue(); + TypeDescriptor upcast = typeDescriptor.upcast(Object.class); + assertThat(upcast.getAnnotation(MethodAnnotation1.class)).isNotNull(); } @Test @@ -682,7 +684,7 @@ void upCastNotSuper() throws Exception { } @Test - void elementTypeForCollectionSubclass() throws Exception { + void elementTypeForCollectionSubclass() { @SuppressWarnings("serial") class CustomSet extends HashSet { } @@ -692,7 +694,7 @@ class CustomSet extends HashSet { } @Test - void elementTypeForMapSubclass() throws Exception { + void elementTypeForMapSubclass() { @SuppressWarnings("serial") class CustomMap extends HashMap { } @@ -704,7 +706,7 @@ class CustomMap extends HashMap { } @Test - void createMapArray() throws Exception { + void createMapArray() { TypeDescriptor mapType = TypeDescriptor.map( LinkedHashMap.class, TypeDescriptor.valueOf(String.class), TypeDescriptor.valueOf(Integer.class)); TypeDescriptor arrayType = TypeDescriptor.array(mapType); @@ -713,13 +715,13 @@ void createMapArray() throws Exception { } @Test - void createStringArray() throws Exception { + void createStringArray() { TypeDescriptor arrayType = TypeDescriptor.array(TypeDescriptor.valueOf(String.class)); assertThat(TypeDescriptor.valueOf(String[].class)).isEqualTo(arrayType); } @Test - void createNullArray() throws Exception { + void createNullArray() { assertThat((Object) TypeDescriptor.array(null)).isNull(); } @@ -736,13 +738,13 @@ void serializable() throws Exception { } @Test - void createCollectionWithNullElement() throws Exception { + void createCollectionWithNullElement() { TypeDescriptor typeDescriptor = TypeDescriptor.collection(List.class, null); assertThat(typeDescriptor.getElementTypeDescriptor()).isNull(); } @Test - void createMapWithNullElements() throws Exception { + void createMapWithNullElements() { TypeDescriptor typeDescriptor = TypeDescriptor.map(LinkedHashMap.class, null, null); assertThat(typeDescriptor.getMapKeyTypeDescriptor()).isNull(); assertThat(typeDescriptor.getMapValueTypeDescriptor()).isNull(); @@ -757,6 +759,17 @@ void getSource() throws Exception { assertThat(TypeDescriptor.valueOf(Integer.class).getSource()).isEqualTo(Integer.class); } + @Test // gh-31672 + void equalityWithGenerics() { + ResolvableType rt1 = ResolvableType.forClassWithGenerics(Optional.class, Integer.class); + ResolvableType rt2 = ResolvableType.forClassWithGenerics(Optional.class, String.class); + + TypeDescriptor td1 = new TypeDescriptor(rt1, null, null); + TypeDescriptor td2 = new TypeDescriptor(rt2, null, null); + + assertThat(td1).isNotEqualTo(td2); + } + // Methods designed for test introspection diff --git a/spring-core/src/test/java/org/springframework/core/convert/converter/DefaultConversionServiceTests.java b/spring-core/src/test/java/org/springframework/core/convert/converter/DefaultConversionServiceTests.java index 5dcfa88201a7..db3de653571a 100644 --- a/spring-core/src/test/java/org/springframework/core/convert/converter/DefaultConversionServiceTests.java +++ b/spring-core/src/test/java/org/springframework/core/convert/converter/DefaultConversionServiceTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-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. @@ -342,20 +342,31 @@ void characterToNumber() { @Test void convertArrayToCollectionInterface() { - List result = conversionService.convert(new String[] {"1", "2", "3"}, List.class); - assertThat(result.get(0)).isEqualTo("1"); - assertThat(result.get(1)).isEqualTo("2"); - assertThat(result.get(2)).isEqualTo("3"); + @SuppressWarnings("unchecked") + Collection result = conversionService.convert(new String[] {"1", "2", "3"}, Collection.class); + assertThat(result).isExactlyInstanceOf(LinkedHashSet.class).containsExactly("1", "2", "3"); + } + + @Test + void convertArrayToSetInterface() { + @SuppressWarnings("unchecked") + Collection result = conversionService.convert(new String[] {"1", "2", "3"}, Set.class); + assertThat(result).isExactlyInstanceOf(LinkedHashSet.class).containsExactly("1", "2", "3"); + } + + @Test + void convertArrayToListInterface() { + @SuppressWarnings("unchecked") + List result = conversionService.convert(new String[] {"1", "2", "3"}, List.class); + assertThat(result).isExactlyInstanceOf(ArrayList.class).containsExactly("1", "2", "3"); } @Test void convertArrayToCollectionGenericTypeConversion() throws Exception { @SuppressWarnings("unchecked") - List result = (List) conversionService.convert(new String[] {"1", "2", "3"}, TypeDescriptor - .valueOf(String[].class), new TypeDescriptor(getClass().getDeclaredField("genericList"))); - assertThat((int) result.get(0)).isEqualTo((int) Integer.valueOf(1)); - assertThat((int) result.get(1)).isEqualTo((int) Integer.valueOf(2)); - assertThat((int) result.get(2)).isEqualTo((int) Integer.valueOf(3)); + List result = (List) conversionService.convert(new String[] {"1", "2", "3"}, + TypeDescriptor.valueOf(String[].class), new TypeDescriptor(getClass().getDeclaredField("genericList"))); + assertThat(result).isExactlyInstanceOf(ArrayList.class).containsExactly(1, 2, 3); } @Test @@ -383,10 +394,9 @@ void spr7766() throws Exception { @Test void convertArrayToCollectionImpl() { - ArrayList result = conversionService.convert(new String[] {"1", "2", "3"}, ArrayList.class); - assertThat(result.get(0)).isEqualTo("1"); - assertThat(result.get(1)).isEqualTo("2"); - assertThat(result.get(2)).isEqualTo("3"); + @SuppressWarnings("unchecked") + ArrayList result = conversionService.convert(new String[] {"1", "2", "3"}, ArrayList.class); + assertThat(result).isExactlyInstanceOf(ArrayList.class).containsExactly("1", "2", "3"); } @Test @@ -416,34 +426,25 @@ void convertEmptyArrayToString() { @Test void convertStringToArray() { String[] result = conversionService.convert("1,2,3", String[].class); - assertThat(result.length).isEqualTo(3); - assertThat(result[0]).isEqualTo("1"); - assertThat(result[1]).isEqualTo("2"); - assertThat(result[2]).isEqualTo("3"); + assertThat(result).containsExactly("1", "2", "3"); } @Test void convertStringToArrayWithElementConversion() { Integer[] result = conversionService.convert("1,2,3", Integer[].class); - assertThat(result.length).isEqualTo(3); - assertThat((int) result[0]).isEqualTo((int) Integer.valueOf(1)); - assertThat((int) result[1]).isEqualTo((int) Integer.valueOf(2)); - assertThat((int) result[2]).isEqualTo((int) Integer.valueOf(3)); + assertThat(result).containsExactly(1, 2, 3); } @Test void convertStringToPrimitiveArrayWithElementConversion() { int[] result = conversionService.convert("1,2,3", int[].class); - assertThat(result.length).isEqualTo(3); - assertThat(result[0]).isEqualTo(1); - assertThat(result[1]).isEqualTo(2); - assertThat(result[2]).isEqualTo(3); + assertThat(result).containsExactly(1, 2, 3); } @Test void convertEmptyStringToArray() { String[] result = conversionService.convert("", String[].class); - assertThat(result.length).isEqualTo(0); + assertThat(result).isEmpty(); } @Test @@ -457,7 +458,7 @@ void convertArrayToObject() { void convertArrayToObjectWithElementConversion() { String[] array = new String[] {"3"}; Integer result = conversionService.convert(array, Integer.class); - assertThat((int) result).isEqualTo((int) Integer.valueOf(3)); + assertThat(result).isEqualTo(3); } @Test @@ -470,39 +471,27 @@ void convertArrayToObjectAssignableTargetType() { @Test void convertObjectToArray() { Object[] result = conversionService.convert(3L, Object[].class); - assertThat(result.length).isEqualTo(1); - assertThat(result[0]).isEqualTo(3L); + assertThat(result).containsExactly(3L); } @Test void convertObjectToArrayWithElementConversion() { Integer[] result = conversionService.convert(3L, Integer[].class); - assertThat(result.length).isEqualTo(1); - assertThat((int) result[0]).isEqualTo((int) Integer.valueOf(3)); + assertThat(result).containsExactly(3); } @Test void convertCollectionToArray() { - List list = new ArrayList<>(); - list.add("1"); - list.add("2"); - list.add("3"); + List list = Arrays.asList("1", "2", "3"); String[] result = conversionService.convert(list, String[].class); - assertThat(result[0]).isEqualTo("1"); - assertThat(result[1]).isEqualTo("2"); - assertThat(result[2]).isEqualTo("3"); + assertThat(result).containsExactly("1", "2", "3"); } @Test void convertCollectionToArrayWithElementConversion() { - List list = new ArrayList<>(); - list.add("1"); - list.add("2"); - list.add("3"); + List list = Arrays.asList("1", "2", "3"); Integer[] result = conversionService.convert(list, Integer[].class); - assertThat((int) result[0]).isEqualTo((int) Integer.valueOf(1)); - assertThat((int) result[1]).isEqualTo((int) Integer.valueOf(2)); - assertThat((int) result[2]).isEqualTo((int) Integer.valueOf(3)); + assertThat(result).containsExactly(1, 2, 3); } @Test @@ -522,34 +511,30 @@ void convertCollectionToStringWithElementConversion() throws Exception { @Test void convertStringToCollection() { - List result = conversionService.convert("1,2,3", List.class); - assertThat(result.size()).isEqualTo(3); - assertThat(result.get(0)).isEqualTo("1"); - assertThat(result.get(1)).isEqualTo("2"); - assertThat(result.get(2)).isEqualTo("3"); + @SuppressWarnings("unchecked") + List result = conversionService.convert("1,2,3", List.class); + assertThat(result).containsExactly("1", "2", "3"); } @Test void convertStringToCollectionWithElementConversion() throws Exception { - List result = (List) conversionService.convert("1,2,3", TypeDescriptor.valueOf(String.class), + @SuppressWarnings("unchecked") + List result = (List) conversionService.convert("1,2,3", TypeDescriptor.valueOf(String.class), new TypeDescriptor(getClass().getField("genericList"))); - assertThat(result.size()).isEqualTo(3); - assertThat(result.get(0)).isEqualTo(1); - assertThat(result.get(1)).isEqualTo(2); - assertThat(result.get(2)).isEqualTo(3); + assertThat(result).containsExactly(1, 2, 3); } @Test void convertEmptyStringToCollection() { Collection result = conversionService.convert("", Collection.class); - assertThat(result.size()).isEqualTo(0); + assertThat(result).isEmpty(); } @Test void convertCollectionToObject() { List list = Collections.singletonList(3L); Long result = conversionService.convert(list, Long.class); - assertThat(result).isEqualTo(Long.valueOf(3)); + assertThat(result).isEqualTo(3); } @Test @@ -641,9 +626,27 @@ void convertObjectArrayToIntArray() { assertThat(result[2]).isEqualTo(3); } + @Test // gh-33212 + void convertIntArrayToObjectArray() { + Object[] result = conversionService.convert(new int[] {1, 2}, Object[].class); + assertThat(result).containsExactly(1, 2); + } + @Test - void convertByteArrayToWrapperArray() { - byte[] byteArray = new byte[] {1, 2, 3}; + void convertIntArrayToFloatArray() { + Float[] result = conversionService.convert(new int[] {1, 2}, Float[].class); + assertThat(result).containsExactly(1.0F, 2.0F); + } + + @Test + void convertIntArrayToPrimitiveFloatArray() { + float[] result = conversionService.convert(new int[] {1, 2}, float[].class); + assertThat(result).containsExactly(1.0F, 2.0F); + } + + @Test + void convertPrimitiveByteArrayToByteWrapperArray() { + byte[] byteArray = {1, 2, 3}; Byte[] converted = conversionService.convert(byteArray, Byte[].class); assertThat(converted).isEqualTo(new Byte[]{1, 2, 3}); } diff --git a/spring-core/src/test/java/org/springframework/core/convert/support/ByteBufferConverterTests.java b/spring-core/src/test/java/org/springframework/core/convert/support/ByteBufferConverterTests.java index 9c0f46ecb438..c1bcf11551b9 100644 --- a/spring-core/src/test/java/org/springframework/core/convert/support/ByteBufferConverterTests.java +++ b/spring-core/src/test/java/org/springframework/core/convert/support/ByteBufferConverterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 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. @@ -33,57 +33,56 @@ */ class ByteBufferConverterTests { - private GenericConversionService conversionService; + private final GenericConversionService conversionService = new DefaultConversionService(); @BeforeEach void setup() { - this.conversionService = new DefaultConversionService(); - this.conversionService.addConverter(new ByteArrayToOtherTypeConverter()); - this.conversionService.addConverter(new OtherTypeToByteArrayConverter()); + conversionService.addConverter(new ByteArrayToOtherTypeConverter()); + conversionService.addConverter(new OtherTypeToByteArrayConverter()); } @Test - void byteArrayToByteBuffer() throws Exception { + void byteArrayToByteBuffer() { byte[] bytes = new byte[] { 1, 2, 3 }; - ByteBuffer convert = this.conversionService.convert(bytes, ByteBuffer.class); + ByteBuffer convert = conversionService.convert(bytes, ByteBuffer.class); assertThat(convert.array()).isNotSameAs(bytes); assertThat(convert.array()).isEqualTo(bytes); } @Test - void byteBufferToByteArray() throws Exception { + void byteBufferToByteArray() { byte[] bytes = new byte[] { 1, 2, 3 }; ByteBuffer byteBuffer = ByteBuffer.wrap(bytes); - byte[] convert = this.conversionService.convert(byteBuffer, byte[].class); + byte[] convert = conversionService.convert(byteBuffer, byte[].class); assertThat(convert).isNotSameAs(bytes); assertThat(convert).isEqualTo(bytes); } @Test - void byteBufferToOtherType() throws Exception { + void byteBufferToOtherType() { byte[] bytes = new byte[] { 1, 2, 3 }; ByteBuffer byteBuffer = ByteBuffer.wrap(bytes); - OtherType convert = this.conversionService.convert(byteBuffer, OtherType.class); + OtherType convert = conversionService.convert(byteBuffer, OtherType.class); assertThat(convert.bytes).isNotSameAs(bytes); assertThat(convert.bytes).isEqualTo(bytes); } @Test - void otherTypeToByteBuffer() throws Exception { + void otherTypeToByteBuffer() { byte[] bytes = new byte[] { 1, 2, 3 }; OtherType otherType = new OtherType(bytes); - ByteBuffer convert = this.conversionService.convert(otherType, ByteBuffer.class); + ByteBuffer convert = conversionService.convert(otherType, ByteBuffer.class); assertThat(convert.array()).isNotSameAs(bytes); assertThat(convert.array()).isEqualTo(bytes); } @Test - void byteBufferToByteBuffer() throws Exception { + void byteBufferToByteBuffer() { byte[] bytes = new byte[] { 1, 2, 3 }; ByteBuffer byteBuffer = ByteBuffer.wrap(bytes); - ByteBuffer convert = this.conversionService.convert(byteBuffer, ByteBuffer.class); + ByteBuffer convert = conversionService.convert(byteBuffer, ByteBuffer.class); assertThat(convert).isNotSameAs(byteBuffer.rewind()); assertThat(convert).isEqualTo(byteBuffer.rewind()); assertThat(convert).isEqualTo(ByteBuffer.wrap(bytes)); diff --git a/spring-core/src/test/java/org/springframework/core/convert/support/CollectionToCollectionConverterTests.java b/spring-core/src/test/java/org/springframework/core/convert/support/CollectionToCollectionConverterTests.java index b7eca2c60473..dfcb5e61a436 100644 --- a/spring-core/src/test/java/org/springframework/core/convert/support/CollectionToCollectionConverterTests.java +++ b/spring-core/src/test/java/org/springframework/core/convert/support/CollectionToCollectionConverterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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. @@ -49,11 +49,11 @@ */ class CollectionToCollectionConverterTests { - private GenericConversionService conversionService = new GenericConversionService(); + private final GenericConversionService conversionService = new GenericConversionService(); @BeforeEach - void setUp() { + void setup() { conversionService.addConverter(new CollectionToCollectionConverter(conversionService)); } diff --git a/spring-core/src/test/java/org/springframework/core/convert/support/MapToMapConverterTests.java b/spring-core/src/test/java/org/springframework/core/convert/support/MapToMapConverterTests.java index fd900a79e779..14542589901e 100644 --- a/spring-core/src/test/java/org/springframework/core/convert/support/MapToMapConverterTests.java +++ b/spring-core/src/test/java/org/springframework/core/convert/support/MapToMapConverterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 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. @@ -38,7 +38,7 @@ /** * @author Keith Donald - * @author Phil Webb + * @author Phillip Webb * @author Juergen Hoeller */ class MapToMapConverterTests { @@ -47,7 +47,7 @@ class MapToMapConverterTests { @BeforeEach - void setUp() { + void setup() { conversionService.addConverter(new MapToMapConverter(conversionService)); } diff --git a/spring-core/src/test/java/org/springframework/core/convert/support/ObjectToObjectConverterTests.java b/spring-core/src/test/java/org/springframework/core/convert/support/ObjectToObjectConverterTests.java index 7565d74992aa..4af44b77f4fa 100644 --- a/spring-core/src/test/java/org/springframework/core/convert/support/ObjectToObjectConverterTests.java +++ b/spring-core/src/test/java/org/springframework/core/convert/support/ObjectToObjectConverterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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.util.Optional; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.core.convert.ConverterNotFoundException; @@ -29,15 +30,19 @@ * Unit tests for {@link ObjectToObjectConverter}. * * @author Sam Brannen - * @author Phil Webb + * @author Phillip Webb * @since 5.3.21 * @see org.springframework.core.convert.converter.DefaultConversionServiceTests#convertObjectToObjectUsingValueOfMethod() */ class ObjectToObjectConverterTests { - private final GenericConversionService conversionService = new GenericConversionService() {{ - addConverter(new ObjectToObjectConverter()); - }}; + private final GenericConversionService conversionService = new GenericConversionService(); + + + @BeforeEach + void setup() { + conversionService.addConverter(new ObjectToObjectConverter()); + } /** @@ -47,7 +52,7 @@ class ObjectToObjectConverterTests { @Test void nonStaticToTargetTypeSimpleNameMethodWithMatchingReturnType() { assertThat(conversionService.canConvert(Source.class, Data.class)) - .as("can convert Source to Data").isTrue(); + .as("can convert Source to Data").isTrue(); Data data = conversionService.convert(new Source("test"), Data.class); assertThat(data).asString().isEqualTo("test"); } @@ -55,21 +60,21 @@ void nonStaticToTargetTypeSimpleNameMethodWithMatchingReturnType() { @Test void nonStaticToTargetTypeSimpleNameMethodWithDifferentReturnType() { assertThat(conversionService.canConvert(Text.class, Data.class)) - .as("can convert Text to Data").isFalse(); + .as("can convert Text to Data").isFalse(); assertThat(conversionService.canConvert(Text.class, Optional.class)) - .as("can convert Text to Optional").isFalse(); + .as("can convert Text to Optional").isFalse(); assertThatExceptionOfType(ConverterNotFoundException.class) - .as("convert Text to Data") - .isThrownBy(() -> conversionService.convert(new Text("test"), Data.class)); + .as("convert Text to Data") + .isThrownBy(() -> conversionService.convert(new Text("test"), Data.class)); } @Test void staticValueOfFactoryMethodWithDifferentReturnType() { assertThat(conversionService.canConvert(String.class, Data.class)) - .as("can convert String to Data").isFalse(); + .as("can convert String to Data").isFalse(); assertThatExceptionOfType(ConverterNotFoundException.class) - .as("convert String to Data") - .isThrownBy(() -> conversionService.convert("test", Data.class)); + .as("convert String to Data") + .isThrownBy(() -> conversionService.convert("test", Data.class)); } @@ -84,9 +89,9 @@ private Source(String value) { public Data toData() { return new Data(this.value); } - } + static class Text { private final String value; @@ -98,9 +103,9 @@ private Text(String value) { public Optional toData() { return Optional.of(new Data(this.value)); } - } + static class Data { private final String value; @@ -115,9 +120,8 @@ public String toString() { } public static Optional valueOf(String string) { - return (string != null) ? Optional.of(new Data(string)) : Optional.empty(); + return (string != null ? Optional.of(new Data(string)) : Optional.empty()); } - } } diff --git a/spring-core/src/test/java/org/springframework/core/env/ProfilesTests.java b/spring-core/src/test/java/org/springframework/core/env/ProfilesTests.java index 01454ad10e45..421d26edb881 100644 --- a/spring-core/src/test/java/org/springframework/core/env/ProfilesTests.java +++ b/spring-core/src/test/java/org/springframework/core/env/ProfilesTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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. @@ -40,30 +40,30 @@ class ProfilesTests { @Test void ofWhenNullThrowsException() { - assertThatIllegalArgumentException().isThrownBy(() -> - Profiles.of((String[]) null)) - .withMessageContaining("Must specify at least one profile"); + assertThatIllegalArgumentException() + .isThrownBy(() -> Profiles.of((String[]) null)) + .withMessage("Must specify at least one profile expression"); } @Test void ofWhenEmptyThrowsException() { assertThatIllegalArgumentException() .isThrownBy(Profiles::of) - .withMessageContaining("Must specify at least one profile"); + .withMessage("Must specify at least one profile expression"); } @Test void ofNullElement() { - assertThatIllegalArgumentException().isThrownBy(() -> - Profiles.of((String) null)) - .withMessageContaining("must contain text"); + assertThatIllegalArgumentException() + .isThrownBy(() -> Profiles.of((String) null)) + .withMessage("Invalid profile expression [null]: must contain text"); } @Test void ofEmptyElement() { - assertThatIllegalArgumentException().isThrownBy(() -> - Profiles.of(" ")) - .withMessageContaining("must contain text"); + assertThatIllegalArgumentException() + .isThrownBy(() -> Profiles.of(" ")) + .withMessage("Invalid profile expression [ ]: must contain text"); } @Test @@ -356,7 +356,7 @@ void equalsAndHashCodeAreNotBasedOnLogicalStructureOfNodesWithinExpressionTree() private static void assertMalformed(Supplier supplier) { assertThatIllegalArgumentException() .isThrownBy(supplier::get) - .withMessageContaining("Malformed"); + .withMessageStartingWith("Malformed profile expression"); } private static Predicate activeProfiles(String... profiles) { diff --git a/spring-core/src/test/java/org/springframework/core/env/StandardEnvironmentTests.java b/spring-core/src/test/java/org/springframework/core/env/StandardEnvironmentTests.java index e711924d1d17..6bd889e51214 100644 --- a/spring-core/src/test/java/org/springframework/core/env/StandardEnvironmentTests.java +++ b/spring-core/src/test/java/org/springframework/core/env/StandardEnvironmentTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-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,8 +19,11 @@ import java.security.AccessControlException; import java.security.Permission; import java.util.Arrays; +import java.util.HashSet; import java.util.Map; +import java.util.Set; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.core.SpringProperties; @@ -40,8 +43,7 @@ * @author Juergen Hoeller * @author Sam Brannen */ -@SuppressWarnings("deprecation") -public class StandardEnvironmentTests { +class StandardEnvironmentTests { private static final String ALLOWED_PROPERTY_NAME = "theanswer"; private static final String ALLOWED_PROPERTY_VALUE = "42"; @@ -81,8 +83,8 @@ void merge() { assertThat(parent.getProperty("parentKey")).isEqualTo("parentVal"); assertThat(parent.getProperty("bothKey")).isEqualTo("parentBothVal"); - assertThat(child.getActiveProfiles()).isEqualTo(new String[]{"c1","c2"}); - assertThat(parent.getActiveProfiles()).isEqualTo(new String[]{"p1","p2"}); + assertThat(child.getActiveProfiles()).containsExactly("c1", "c2"); + assertThat(parent.getActiveProfiles()).containsExactly("p1", "p2"); child.merge(parent); @@ -94,8 +96,8 @@ void merge() { assertThat(parent.getProperty("parentKey")).isEqualTo("parentVal"); assertThat(parent.getProperty("bothKey")).isEqualTo("parentBothVal"); - assertThat(child.getActiveProfiles()).isEqualTo(new String[]{"c1","c2","p1","p2"}); - assertThat(parent.getActiveProfiles()).isEqualTo(new String[]{"p1","p2"}); + assertThat(child.getActiveProfiles()).containsExactly("c1", "c2", "p1", "p2"); + assertThat(parent.getActiveProfiles()).containsExactly("p1", "p2"); } @Test @@ -121,16 +123,14 @@ void activeProfilesIsEmptyByDefault() { @Test void defaultProfilesContainsDefaultProfileByDefault() { - assertThat(environment.getDefaultProfiles().length).isEqualTo(1); - assertThat(environment.getDefaultProfiles()[0]).isEqualTo("default"); + assertThat(environment.getDefaultProfiles()).containsExactly("default"); } @Test void setActiveProfiles() { environment.setActiveProfiles("local", "embedded"); String[] activeProfiles = environment.getActiveProfiles(); - assertThat(activeProfiles).contains("local", "embedded"); - assertThat(activeProfiles.length).isEqualTo(2); + assertThat(activeProfiles).containsExactly("local", "embedded"); } @Test @@ -177,15 +177,12 @@ void setDefaultProfiles_withNotOperator() { void addActiveProfile() { assertThat(environment.getActiveProfiles().length).isEqualTo(0); environment.setActiveProfiles("local", "embedded"); - assertThat(environment.getActiveProfiles()).contains("local", "embedded"); - assertThat(environment.getActiveProfiles().length).isEqualTo(2); + assertThat(environment.getActiveProfiles()).containsExactly("local", "embedded"); environment.addActiveProfile("p1"); - assertThat(environment.getActiveProfiles()).contains("p1"); - assertThat(environment.getActiveProfiles().length).isEqualTo(3); + assertThat(environment.getActiveProfiles()).containsExactly("local", "embedded", "p1"); environment.addActiveProfile("p2"); environment.addActiveProfile("p3"); - assertThat(environment.getActiveProfiles()).contains("p2", "p3"); - assertThat(environment.getActiveProfiles().length).isEqualTo(5); + assertThat(environment.getActiveProfiles()).containsExactly("local", "embedded", "p1", "p2", "p3"); } @Test @@ -195,65 +192,41 @@ void addActiveProfile_whenActiveProfilesPropertyIsAlreadySet() { env.getPropertySources().addFirst(new MockPropertySource().withProperty(ACTIVE_PROFILES_PROPERTY_NAME, "p1")); assertThat(env.getProperty(ACTIVE_PROFILES_PROPERTY_NAME)).isEqualTo("p1"); env.addActiveProfile("p2"); - assertThat(env.getActiveProfiles()).contains("p1", "p2"); + assertThat(env.getActiveProfiles()).containsExactly("p1", "p2"); } @Test void reservedDefaultProfile() { - assertThat(environment.getDefaultProfiles()).isEqualTo(new String[]{RESERVED_DEFAULT_PROFILE_NAME}); - System.setProperty(DEFAULT_PROFILES_PROPERTY_NAME, "d0"); - assertThat(environment.getDefaultProfiles()).isEqualTo(new String[]{"d0"}); - environment.setDefaultProfiles("d1", "d2"); - assertThat(environment.getDefaultProfiles()).isEqualTo(new String[]{"d1","d2"}); - System.clearProperty(DEFAULT_PROFILES_PROPERTY_NAME); + assertThat(environment.getDefaultProfiles()).containsExactly(RESERVED_DEFAULT_PROFILE_NAME); + try { + System.setProperty(DEFAULT_PROFILES_PROPERTY_NAME, "d0"); + assertThat(environment.getDefaultProfiles()).containsExactly("d0"); + environment.setDefaultProfiles("d1", "d2"); + assertThat(environment.getDefaultProfiles()).containsExactly("d1","d2"); + } + finally { + System.clearProperty(DEFAULT_PROFILES_PROPERTY_NAME); + } } @Test void defaultProfileWithCircularPlaceholder() { try { System.setProperty(DEFAULT_PROFILES_PROPERTY_NAME, "${spring.profiles.default}"); - assertThatIllegalArgumentException().isThrownBy(environment::getDefaultProfiles); + assertThatIllegalArgumentException() + .isThrownBy(environment::getDefaultProfiles) + .withMessage("Circular placeholder reference 'spring.profiles.default' in property definitions"); } finally { System.clearProperty(DEFAULT_PROFILES_PROPERTY_NAME); } } - @Test - void getActiveProfiles_systemPropertiesEmpty() { - assertThat(environment.getActiveProfiles().length).isEqualTo(0); - System.setProperty(ACTIVE_PROFILES_PROPERTY_NAME, ""); - assertThat(environment.getActiveProfiles().length).isEqualTo(0); - System.clearProperty(ACTIVE_PROFILES_PROPERTY_NAME); - } - - @Test - void getActiveProfiles_fromSystemProperties() { - System.setProperty(ACTIVE_PROFILES_PROPERTY_NAME, "foo"); - assertThat(Arrays.asList(environment.getActiveProfiles())).contains("foo"); - System.clearProperty(ACTIVE_PROFILES_PROPERTY_NAME); - } - - @Test - void getActiveProfiles_fromSystemProperties_withMultipleProfiles() { - System.setProperty(ACTIVE_PROFILES_PROPERTY_NAME, "foo,bar"); - assertThat(environment.getActiveProfiles()).contains("foo", "bar"); - System.clearProperty(ACTIVE_PROFILES_PROPERTY_NAME); - } - - @Test - void getActiveProfiles_fromSystemProperties_withMultipleProfiles_withWhitespace() { - System.setProperty(ACTIVE_PROFILES_PROPERTY_NAME, " bar , baz "); // notice whitespace - assertThat(environment.getActiveProfiles()).contains("bar", "baz"); - System.clearProperty(ACTIVE_PROFILES_PROPERTY_NAME); - } - @Test void getDefaultProfiles() { - assertThat(environment.getDefaultProfiles()).isEqualTo(new String[] {RESERVED_DEFAULT_PROFILE_NAME}); + assertThat(environment.getDefaultProfiles()).containsExactly(RESERVED_DEFAULT_PROFILE_NAME); environment.getPropertySources().addFirst(new MockPropertySource().withProperty(DEFAULT_PROFILES_PROPERTY_NAME, "pd1")); - assertThat(environment.getDefaultProfiles().length).isEqualTo(1); - assertThat(Arrays.asList(environment.getDefaultProfiles())).contains("pd1"); + assertThat(environment.getDefaultProfiles()).containsExactly("pd1"); } @Test @@ -261,82 +234,9 @@ void setDefaultProfiles() { environment.setDefaultProfiles(); assertThat(environment.getDefaultProfiles().length).isEqualTo(0); environment.setDefaultProfiles("pd1"); - assertThat(Arrays.asList(environment.getDefaultProfiles())).contains("pd1"); + assertThat(environment.getDefaultProfiles()).containsExactly("pd1"); environment.setDefaultProfiles("pd2", "pd3"); - assertThat(environment.getDefaultProfiles()).doesNotContain("pd1"); - assertThat(environment.getDefaultProfiles()).contains("pd2", "pd3"); - } - - @Test - void acceptsProfiles_withEmptyArgumentList() { - assertThatIllegalArgumentException().isThrownBy( - environment::acceptsProfiles); - } - - @Test - void acceptsProfiles_withNullArgumentList() { - assertThatIllegalArgumentException().isThrownBy(() -> environment.acceptsProfiles((String[]) null)); - } - - @Test - void acceptsProfiles_withNullArgument() { - assertThatIllegalArgumentException().isThrownBy(() -> environment.acceptsProfiles((String) null)); - } - - @Test - void acceptsProfiles_withEmptyArgument() { - assertThatIllegalArgumentException().isThrownBy(() -> environment.acceptsProfiles("")); - } - - @Test - void acceptsProfiles_activeProfileSetProgrammatically() { - assertThat(environment.acceptsProfiles("p1", "p2")).isFalse(); - environment.setActiveProfiles("p1"); - assertThat(environment.acceptsProfiles("p1", "p2")).isTrue(); - environment.setActiveProfiles("p2"); - assertThat(environment.acceptsProfiles("p1", "p2")).isTrue(); - environment.setActiveProfiles("p1", "p2"); - assertThat(environment.acceptsProfiles("p1", "p2")).isTrue(); - } - - @Test - void acceptsProfiles_activeProfileSetViaProperty() { - assertThat(environment.acceptsProfiles("p1")).isFalse(); - environment.getPropertySources().addFirst(new MockPropertySource().withProperty(ACTIVE_PROFILES_PROPERTY_NAME, "p1")); - assertThat(environment.acceptsProfiles("p1")).isTrue(); - } - - @Test - void acceptsProfiles_defaultProfile() { - assertThat(environment.acceptsProfiles("pd")).isFalse(); - environment.setDefaultProfiles("pd"); - assertThat(environment.acceptsProfiles("pd")).isTrue(); - environment.setActiveProfiles("p1"); - assertThat(environment.acceptsProfiles("pd")).isFalse(); - assertThat(environment.acceptsProfiles("p1")).isTrue(); - } - - @Test - void acceptsProfiles_withNotOperator() { - assertThat(environment.acceptsProfiles("p1")).isFalse(); - assertThat(environment.acceptsProfiles("!p1")).isTrue(); - environment.addActiveProfile("p1"); - assertThat(environment.acceptsProfiles("p1")).isTrue(); - assertThat(environment.acceptsProfiles("!p1")).isFalse(); - } - - @Test - void acceptsProfiles_withInvalidNotOperator() { - assertThatIllegalArgumentException().isThrownBy(() -> environment.acceptsProfiles("p1", "!")); - } - - @Test - void acceptsProfiles_withProfileExpression() { - assertThat(environment.acceptsProfiles(Profiles.of("p1 & p2"))).isFalse(); - environment.addActiveProfile("p1"); - assertThat(environment.acceptsProfiles(Profiles.of("p1 & p2"))).isFalse(); - environment.addActiveProfile("p2"); - assertThat(environment.acceptsProfiles(Profiles.of("p1 & p2"))).isTrue(); + assertThat(environment.getDefaultProfiles()).containsExactly("pd2", "pd3"); } @Test @@ -346,88 +246,105 @@ void environmentSubclass_withCustomProfileValidation() { protected void validateProfile(String profile) { super.validateProfile(profile); if (profile.contains("-")) { - throw new IllegalArgumentException( - "Invalid profile [" + profile + "]: must not contain dash character"); + throw new IllegalArgumentException("Invalid profile [" + profile + "]: must not contain dash character"); } } }; env.addActiveProfile("validProfile"); // succeeds - assertThatIllegalArgumentException().isThrownBy(() -> - env.addActiveProfile("invalid-profile")) + assertThatIllegalArgumentException() + .isThrownBy(() -> env.addActiveProfile("invalid-profile")) .withMessage("Invalid profile [invalid-profile]: must not contain dash character"); } @Test void suppressGetenvAccessThroughSystemProperty() { - System.setProperty("spring.getenv.ignore", "true"); - assertThat(environment.getSystemEnvironment().isEmpty()).isTrue(); - System.clearProperty("spring.getenv.ignore"); + try { + System.setProperty("spring.getenv.ignore", "true"); + assertThat(environment.getSystemEnvironment()).isEmpty(); + } + finally { + System.clearProperty("spring.getenv.ignore"); + } } @Test void suppressGetenvAccessThroughSpringProperty() { - SpringProperties.setProperty("spring.getenv.ignore", "true"); - assertThat(environment.getSystemEnvironment().isEmpty()).isTrue(); - SpringProperties.setProperty("spring.getenv.ignore", null); + try { + SpringProperties.setProperty("spring.getenv.ignore", "true"); + assertThat(environment.getSystemEnvironment()).isEmpty(); + } + finally { + SpringProperties.setProperty("spring.getenv.ignore", null); + } } @Test void suppressGetenvAccessThroughSpringFlag() { - SpringProperties.setFlag("spring.getenv.ignore"); - assertThat(environment.getSystemEnvironment().isEmpty()).isTrue(); - SpringProperties.setProperty("spring.getenv.ignore", null); + try { + SpringProperties.setFlag("spring.getenv.ignore"); + assertThat(environment.getSystemEnvironment()).isEmpty(); + } + finally { + SpringProperties.setProperty("spring.getenv.ignore", null); + } } @Test void getSystemProperties_withAndWithoutSecurityManager() { - System.setProperty(ALLOWED_PROPERTY_NAME, ALLOWED_PROPERTY_VALUE); - System.setProperty(DISALLOWED_PROPERTY_NAME, DISALLOWED_PROPERTY_VALUE); - System.getProperties().put(STRING_PROPERTY_NAME, NON_STRING_PROPERTY_VALUE); - System.getProperties().put(NON_STRING_PROPERTY_NAME, STRING_PROPERTY_VALUE); - - { - Map systemProperties = environment.getSystemProperties(); - assertThat(systemProperties).isNotNull(); - assertThat(System.getProperties()).isSameAs(systemProperties); - assertThat(systemProperties.get(ALLOWED_PROPERTY_NAME)).isEqualTo(ALLOWED_PROPERTY_VALUE); - assertThat(systemProperties.get(DISALLOWED_PROPERTY_NAME)).isEqualTo(DISALLOWED_PROPERTY_VALUE); - - // non-string keys and values work fine... until the security manager is introduced below - assertThat(systemProperties.get(STRING_PROPERTY_NAME)).isEqualTo(NON_STRING_PROPERTY_VALUE); - assertThat(systemProperties.get(NON_STRING_PROPERTY_NAME)).isEqualTo(STRING_PROPERTY_VALUE); - } + SecurityManager originalSecurityManager = System.getSecurityManager(); + try { + System.setProperty(ALLOWED_PROPERTY_NAME, ALLOWED_PROPERTY_VALUE); + System.setProperty(DISALLOWED_PROPERTY_NAME, DISALLOWED_PROPERTY_VALUE); + System.getProperties().put(STRING_PROPERTY_NAME, NON_STRING_PROPERTY_VALUE); + System.getProperties().put(NON_STRING_PROPERTY_NAME, STRING_PROPERTY_VALUE); - SecurityManager oldSecurityManager = System.getSecurityManager(); - SecurityManager securityManager = new SecurityManager() { - @Override - public void checkPropertiesAccess() { - // see https://download.oracle.com/javase/1.5.0/docs/api/java/lang/System.html#getProperties() - throw new AccessControlException("Accessing the system properties is disallowed"); + { + Map systemProperties = environment.getSystemProperties(); + assertThat(systemProperties).isNotNull(); + assertThat(System.getProperties()).isSameAs(systemProperties); + assertThat(systemProperties.get(ALLOWED_PROPERTY_NAME)).isEqualTo(ALLOWED_PROPERTY_VALUE); + assertThat(systemProperties.get(DISALLOWED_PROPERTY_NAME)).isEqualTo(DISALLOWED_PROPERTY_VALUE); + + // non-string keys and values work fine... until the security manager is introduced below + assertThat(systemProperties.get(STRING_PROPERTY_NAME)).isEqualTo(NON_STRING_PROPERTY_VALUE); + assertThat(systemProperties.get(NON_STRING_PROPERTY_NAME)).isEqualTo(STRING_PROPERTY_VALUE); + + PropertiesPropertySource systemPropertySource = (PropertiesPropertySource) + environment.getPropertySources().get(StandardEnvironment.SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME); + Set expectedKeys = new HashSet<>(System.getProperties().stringPropertyNames()); + expectedKeys.add(STRING_PROPERTY_NAME); // filtered out by stringPropertyNames due to non-String value + assertThat(new HashSet<>(Arrays.asList(systemPropertySource.getPropertyNames()))).isEqualTo(expectedKeys); } - @Override - public void checkPropertyAccess(String key) { - // see https://download.oracle.com/javase/1.5.0/docs/api/java/lang/System.html#getProperty(java.lang.String) - if (DISALLOWED_PROPERTY_NAME.equals(key)) { - throw new AccessControlException( - String.format("Accessing the system property [%s] is disallowed", DISALLOWED_PROPERTY_NAME)); + + SecurityManager securityManager = new SecurityManager() { + @Override + public void checkPropertiesAccess() { + // see https://download.oracle.com/javase/1.5.0/docs/api/java/lang/System.html#getProperties() + throw new AccessControlException("Accessing the system properties is disallowed"); } - } - @Override - public void checkPermission(Permission perm) { - // allow everything else - } - }; + @Override + public void checkPropertyAccess(String key) { + // see https://download.oracle.com/javase/1.5.0/docs/api/java/lang/System.html#getProperty(java.lang.String) + if (DISALLOWED_PROPERTY_NAME.equals(key)) { + throw new AccessControlException( + String.format("Accessing the system property [%s] is disallowed", DISALLOWED_PROPERTY_NAME)); + } + } + @Override + public void checkPermission(Permission perm) { + // allow everything else + } + }; - try { System.setSecurityManager(securityManager); { Map systemProperties = environment.getSystemProperties(); assertThat(systemProperties).isNotNull(); assertThat(systemProperties).isInstanceOf(ReadOnlySystemAttributesMap.class); - assertThat((String)systemProperties.get(ALLOWED_PROPERTY_NAME)).isEqualTo(ALLOWED_PROPERTY_VALUE); + assertThat(systemProperties.get(ALLOWED_PROPERTY_NAME)).isEqualTo(ALLOWED_PROPERTY_VALUE); assertThat(systemProperties.get(DISALLOWED_PROPERTY_NAME)).isNull(); // nothing we can do here in terms of warning the user that there was @@ -441,12 +358,12 @@ public void checkPermission(Permission perm) { // the user that under these very special conditions (non-object key + // SecurityManager that disallows access to system properties), they // cannot do what they're attempting. - assertThatIllegalArgumentException().as("searching with non-string key against ReadOnlySystemAttributesMap").isThrownBy(() -> - systemProperties.get(NON_STRING_PROPERTY_NAME)); + assertThatIllegalArgumentException().as("searching with non-string key against ReadOnlySystemAttributesMap") + .isThrownBy(() -> systemProperties.get(NON_STRING_PROPERTY_NAME)); } } finally { - System.setSecurityManager(oldSecurityManager); + System.setSecurityManager(originalSecurityManager); System.clearProperty(ALLOWED_PROPERTY_NAME); System.clearProperty(DISALLOWED_PROPERTY_NAME); System.getProperties().remove(STRING_PROPERTY_NAME); @@ -499,4 +416,251 @@ public void checkPermission(Permission perm) { EnvironmentTestUtils.getModifiableSystemEnvironment().remove(DISALLOWED_PROPERTY_NAME); } + + @Nested + class GetActiveProfiles { + + @Test + void systemPropertiesEmpty() { + assertThat(environment.getActiveProfiles()).isEmpty(); + try { + System.setProperty(ACTIVE_PROFILES_PROPERTY_NAME, ""); + assertThat(environment.getActiveProfiles()).isEmpty(); + } + finally { + System.clearProperty(ACTIVE_PROFILES_PROPERTY_NAME); + } + } + + @Test + void fromSystemProperties() { + try { + System.setProperty(ACTIVE_PROFILES_PROPERTY_NAME, "foo"); + assertThat(environment.getActiveProfiles()).containsExactly("foo"); + } + finally { + System.clearProperty(ACTIVE_PROFILES_PROPERTY_NAME); + } + } + + @Test + void fromSystemProperties_withMultipleProfiles() { + try { + System.setProperty(ACTIVE_PROFILES_PROPERTY_NAME, "foo,bar"); + assertThat(environment.getActiveProfiles()).containsExactly("foo", "bar"); + } + finally { + System.clearProperty(ACTIVE_PROFILES_PROPERTY_NAME); + } + } + + @Test + void fromSystemProperties_withMultipleProfiles_withWhitespace() { + try { + System.setProperty(ACTIVE_PROFILES_PROPERTY_NAME, " bar , baz "); // notice whitespace + assertThat(environment.getActiveProfiles()).containsExactly("bar", "baz"); + } + finally { + System.clearProperty(ACTIVE_PROFILES_PROPERTY_NAME); + } + } + } + + + @Nested + class AcceptsProfilesTests { + + @Test + @SuppressWarnings("deprecation") + void withEmptyArgumentList() { + assertThatIllegalArgumentException().isThrownBy(environment::acceptsProfiles); + } + + @Test + @SuppressWarnings("deprecation") + void withNullArgumentList() { + assertThatIllegalArgumentException().isThrownBy(() -> environment.acceptsProfiles((String[]) null)); + } + + @Test + @SuppressWarnings("deprecation") + void withNullArgument() { + assertThatIllegalArgumentException().isThrownBy(() -> environment.acceptsProfiles((String) null)); + } + + @Test + @SuppressWarnings("deprecation") + void withEmptyArgument() { + assertThatIllegalArgumentException().isThrownBy(() -> environment.acceptsProfiles("")); + } + + @Test + @SuppressWarnings("deprecation") + void activeProfileSetProgrammatically() { + assertThat(environment.acceptsProfiles("p1", "p2")).isFalse(); + environment.setActiveProfiles("p1"); + assertThat(environment.acceptsProfiles("p1", "p2")).isTrue(); + environment.setActiveProfiles("p2"); + assertThat(environment.acceptsProfiles("p1", "p2")).isTrue(); + environment.setActiveProfiles("p1", "p2"); + assertThat(environment.acceptsProfiles("p1", "p2")).isTrue(); + } + + @Test + @SuppressWarnings("deprecation") + void activeProfileSetViaProperty() { + assertThat(environment.acceptsProfiles("p1")).isFalse(); + environment.getPropertySources().addFirst(new MockPropertySource().withProperty(ACTIVE_PROFILES_PROPERTY_NAME, "p1")); + assertThat(environment.acceptsProfiles("p1")).isTrue(); + } + + @Test + @SuppressWarnings("deprecation") + void defaultProfile() { + assertThat(environment.acceptsProfiles("pd")).isFalse(); + environment.setDefaultProfiles("pd"); + assertThat(environment.acceptsProfiles("pd")).isTrue(); + environment.setActiveProfiles("p1"); + assertThat(environment.acceptsProfiles("pd")).isFalse(); + assertThat(environment.acceptsProfiles("p1")).isTrue(); + } + + @Test + @SuppressWarnings("deprecation") + void withNotOperator() { + assertThat(environment.acceptsProfiles("p1")).isFalse(); + assertThat(environment.acceptsProfiles("!p1")).isTrue(); + environment.addActiveProfile("p1"); + assertThat(environment.acceptsProfiles("p1")).isTrue(); + assertThat(environment.acceptsProfiles("!p1")).isFalse(); + } + + @Test + @SuppressWarnings("deprecation") + void withInvalidNotOperator() { + assertThatIllegalArgumentException().isThrownBy(() -> environment.acceptsProfiles("p1", "!")); + } + + @Test + void withProfileExpression() { + assertThat(environment.acceptsProfiles(Profiles.of("p1 & p2"))).isFalse(); + environment.addActiveProfile("p1"); + assertThat(environment.acceptsProfiles(Profiles.of("p1 & p2"))).isFalse(); + environment.addActiveProfile("p2"); + assertThat(environment.acceptsProfiles(Profiles.of("p1 & p2"))).isTrue(); + } + } + + + @Nested + class MatchesProfilesTests { + + @Test + @SuppressWarnings("deprecation") + void withEmptyArgumentList() { + assertThatIllegalArgumentException().isThrownBy(environment::matchesProfiles); + } + + @Test + @SuppressWarnings("deprecation") + void withNullArgumentList() { + assertThatIllegalArgumentException().isThrownBy(() -> environment.matchesProfiles((String[]) null)); + } + + @Test + @SuppressWarnings("deprecation") + void withNullArgument() { + assertThatIllegalArgumentException().isThrownBy(() -> environment.matchesProfiles((String) null)); + assertThatIllegalArgumentException().isThrownBy(() -> environment.matchesProfiles("p1", null)); + } + + @Test + @SuppressWarnings("deprecation") + void withEmptyArgument() { + assertThatIllegalArgumentException().isThrownBy(() -> environment.matchesProfiles("")); + assertThatIllegalArgumentException().isThrownBy(() -> environment.matchesProfiles("p1", "")); + assertThatIllegalArgumentException().isThrownBy(() -> environment.matchesProfiles("p1", " ")); + } + + @Test + @SuppressWarnings("deprecation") + void withInvalidNotOperator() { + assertThatIllegalArgumentException().isThrownBy(() -> environment.matchesProfiles("p1", "!")); + } + + @Test + @SuppressWarnings("deprecation") + void withInvalidCompoundExpressionGrouping() { + assertThatIllegalArgumentException().isThrownBy(() -> environment.matchesProfiles("p1 | p2 & p3")); + assertThatIllegalArgumentException().isThrownBy(() -> environment.matchesProfiles("p1 & p2 | p3")); + assertThatIllegalArgumentException().isThrownBy(() -> environment.matchesProfiles("p1 & (p2 | p3) | p4")); + } + + @Test + @SuppressWarnings("deprecation") + void activeProfileSetProgrammatically() { + assertThat(environment.matchesProfiles("p1", "p2")).isFalse(); + + environment.setActiveProfiles("p1"); + assertThat(environment.matchesProfiles("p1", "p2")).isTrue(); + + environment.setActiveProfiles("p2"); + assertThat(environment.matchesProfiles("p1", "p2")).isTrue(); + + environment.setActiveProfiles("p1", "p2"); + assertThat(environment.matchesProfiles("p1", "p2")).isTrue(); + } + + @Test + @SuppressWarnings("deprecation") + void activeProfileSetViaProperty() { + assertThat(environment.matchesProfiles("p1")).isFalse(); + + environment.getPropertySources().addFirst(new MockPropertySource().withProperty(ACTIVE_PROFILES_PROPERTY_NAME, "p1")); + assertThat(environment.matchesProfiles("p1")).isTrue(); + } + + @Test + @SuppressWarnings("deprecation") + void defaultProfile() { + assertThat(environment.matchesProfiles("pd")).isFalse(); + + environment.setDefaultProfiles("pd"); + assertThat(environment.matchesProfiles("pd")).isTrue(); + + environment.setActiveProfiles("p1"); + assertThat(environment.matchesProfiles("pd")).isFalse(); + assertThat(environment.matchesProfiles("p1")).isTrue(); + } + + @Test + @SuppressWarnings("deprecation") + void withNotOperator() { + assertThat(environment.matchesProfiles("p1")).isFalse(); + assertThat(environment.matchesProfiles("!p1")).isTrue(); + + environment.addActiveProfile("p1"); + assertThat(environment.matchesProfiles("p1")).isTrue(); + assertThat(environment.matchesProfiles("!p1")).isFalse(); + } + + @Test + void withProfileExpressions() { + assertThat(environment.matchesProfiles("p1 & p2")).isFalse(); + + environment.addActiveProfile("p1"); + assertThat(environment.matchesProfiles("p1 | p2")).isTrue(); + assertThat(environment.matchesProfiles("p1 & p2")).isFalse(); + + environment.addActiveProfile("p2"); + assertThat(environment.matchesProfiles("p1 & p2")).isTrue(); + assertThat(environment.matchesProfiles("p1 | p2")).isTrue(); + assertThat(environment.matchesProfiles("foo | p1", "p2")).isTrue(); + assertThat(environment.matchesProfiles("foo | p2", "p1")).isTrue(); + assertThat(environment.matchesProfiles("foo | (p2 & p1)")).isTrue(); + assertThat(environment.matchesProfiles("p2 & (foo | p1)")).isTrue(); + assertThat(environment.matchesProfiles("foo", "(p2 & p1)")).isTrue(); + } + } + } diff --git a/spring-core/src/test/java/org/springframework/core/io/support/PathMatchingResourcePatternResolverTests.java b/spring-core/src/test/java/org/springframework/core/io/support/PathMatchingResourcePatternResolverTests.java index 369b6932dcdf..1bba71a13443 100644 --- a/spring-core/src/test/java/org/springframework/core/io/support/PathMatchingResourcePatternResolverTests.java +++ b/spring-core/src/test/java/org/springframework/core/io/support/PathMatchingResourcePatternResolverTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-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. @@ -48,14 +48,14 @@ */ class PathMatchingResourcePatternResolverTests { - private static final String[] CLASSES_IN_CORE_IO_SUPPORT = { "EncodedResource.class", + private static final String[] CLASSES_IN_CORE_IO_SUPPORT = {"EncodedResource.class", "LocalizedResourceHelper.class", "PathMatchingResourcePatternResolver.class", "PropertiesLoaderSupport.class", "PropertiesLoaderUtils.class", "ResourceArrayPropertyEditor.class", "ResourcePatternResolver.class", - "ResourcePatternUtils.class", "SpringFactoriesLoader.class" }; + "ResourcePatternUtils.class", "SpringFactoriesLoader.class"}; - private static final String[] TEST_CLASSES_IN_CORE_IO_SUPPORT = { "PathMatchingResourcePatternResolverTests.class" }; + private static final String[] TEST_CLASSES_IN_CORE_IO_SUPPORT = {"PathMatchingResourcePatternResolverTests.class"}; - private static final String[] CLASSES_IN_REACTOR_UTIL_ANNOTATION = { "NonNull.class", "NonNullApi.class", "Nullable.class" }; + private static final String[] CLASSES_IN_REACTOR_UTIL_ANNOTATION = {"NonNull.class", "NonNullApi.class", "Nullable.class"}; private final PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); @@ -166,7 +166,7 @@ private List getSubPathsIgnoringClassFiles(String pattern, String pathPr } @Test - void usingFileProtocolWithoutWildcardInPatternAndEndingInSlashStarStar() { + void usingFileProtocolWithoutWildcardInPatternAndEndingInSlashStarStar() { Path testResourcesDir = Paths.get("src/test/resources").toAbsolutePath(); String pattern = String.format("file:%s/scanned-resources/**", testResourcesDir); String pathPrefix = ".+?resources/"; @@ -294,8 +294,8 @@ private void assertExactSubPaths(String pattern, String pathPrefix, String... su } private String getPath(Resource resource) { - // Tests fail if we use resouce.getURL().getPath(). They would also fail on Mac OS when - // using resouce.getURI().getPath() if the resource paths are not Unicode normalized. + // Tests fail if we use resource.getURL().getPath(). They would also fail on macOS when + // using resource.getURI().getPath() if the resource paths are not Unicode normalized. // // On the JVM, all tests should pass when using resouce.getFile().getPath(); however, // we use FileSystemResource#getPath since this test class is sometimes run within a diff --git a/spring-core/src/test/java/org/springframework/util/ConcurrentLruCacheTests.java b/spring-core/src/test/java/org/springframework/util/ConcurrentLruCacheTests.java index 31ec75944f38..c16efa958135 100644 --- a/spring-core/src/test/java/org/springframework/util/ConcurrentLruCacheTests.java +++ b/spring-core/src/test/java/org/springframework/util/ConcurrentLruCacheTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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,13 +21,39 @@ import static org.assertj.core.api.Assertions.assertThat; /** + * Tests for {@link ConcurrentLruCache}. + * * @author Juergen Hoeller + * @author Sam Brannen */ class ConcurrentLruCacheTests { private final ConcurrentLruCache cache = new ConcurrentLruCache<>(2, key -> key + "value"); + @Test + void zeroCapacity() { + ConcurrentLruCache cache = new ConcurrentLruCache<>(0, key -> key + "value"); + + assertThat(cache.sizeLimit()).isZero(); + assertThat(cache.size()).isZero(); + + assertThat(cache.get("k1")).isEqualTo("k1value"); + assertThat(cache.size()).isZero(); + assertThat(cache.contains("k1")).isFalse(); + + assertThat(cache.get("k2")).isEqualTo("k2value"); + assertThat(cache.size()).isZero(); + assertThat(cache.contains("k1")).isFalse(); + assertThat(cache.contains("k2")).isFalse(); + + assertThat(cache.get("k3")).isEqualTo("k3value"); + assertThat(cache.size()).isZero(); + assertThat(cache.contains("k1")).isFalse(); + assertThat(cache.contains("k2")).isFalse(); + assertThat(cache.contains("k3")).isFalse(); + } + @Test void getAndSize() { assertThat(this.cache.sizeLimit()).isEqualTo(2); diff --git a/spring-core/src/test/java/org/springframework/util/ObjectUtilsTests.java b/spring-core/src/test/java/org/springframework/util/ObjectUtilsTests.java index b18710a1bfde..a17f0bd10e0f 100644 --- a/spring-core/src/test/java/org/springframework/util/ObjectUtilsTests.java +++ b/spring-core/src/test/java/org/springframework/util/ObjectUtilsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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,16 +16,40 @@ package org.springframework.util; +import java.io.File; import java.io.IOException; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.net.InetAddress; +import java.net.URI; +import java.net.URL; +import java.net.UnknownHostException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.nio.file.Paths; import java.sql.SQLException; +import java.time.LocalDate; +import java.time.ZoneId; +import java.util.Arrays; import java.util.Collections; +import java.util.Currency; +import java.util.Date; import java.util.HashMap; import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; import java.util.Set; +import java.util.TimeZone; +import java.util.UUID; +import java.util.regex.Pattern; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.springframework.util.ObjectUtils.isEmpty; @@ -213,7 +237,7 @@ void addObjectToSingleNullElementArray() { } @Test - void addObjectToNullArray() throws Exception { + void addObjectToNullArray() { String newElement = "foo"; String[] newArray = ObjectUtils.addObjectToArray(null, newElement); assertThat(newArray).hasSize(1); @@ -221,14 +245,14 @@ void addObjectToNullArray() throws Exception { } @Test - void addNullObjectToNullArray() throws Exception { + void addNullObjectToNullArray() { Object[] newArray = ObjectUtils.addObjectToArray(null, null); assertThat(newArray).hasSize(1); assertThat(newArray[0]).isNull(); } @Test - void nullSafeEqualsWithArrays() throws Exception { + void nullSafeEqualsWithArrays() { assertThat(ObjectUtils.nullSafeEquals(new String[] {"a", "b", "c"}, new String[] {"a", "b", "c"})).isTrue(); assertThat(ObjectUtils.nullSafeEquals(new int[] {1, 2, 3}, new int[] {1, 2, 3})).isTrue(); } @@ -251,32 +275,28 @@ void hashCodeWithBooleanTrue() { @Deprecated void hashCodeWithDouble() { double dbl = 9830.43; - int expected = (new Double(dbl)).hashCode(); - assertThat(ObjectUtils.hashCode(dbl)).isEqualTo(expected); + assertThat(ObjectUtils.hashCode(dbl)).isEqualTo(Double.hashCode(dbl)); } @Test @Deprecated void hashCodeWithFloat() { float flt = 34.8f; - int expected = (Float.valueOf(flt)).hashCode(); - assertThat(ObjectUtils.hashCode(flt)).isEqualTo(expected); + assertThat(ObjectUtils.hashCode(flt)).isEqualTo(Float.hashCode(flt)); } @Test @Deprecated void hashCodeWithLong() { long lng = 883L; - int expected = (Long.valueOf(lng)).hashCode(); - assertThat(ObjectUtils.hashCode(lng)).isEqualTo(expected); + assertThat(ObjectUtils.hashCode(lng)).isEqualTo(Long.hashCode(lng)); } @Test void identityToString() { Object obj = new Object(); String expected = obj.getClass().getName() + "@" + ObjectUtils.getIdentityHexString(obj); - String actual = ObjectUtils.identityToString(obj); - assertThat(actual).isEqualTo(expected); + assertThat(ObjectUtils.identityToString(obj)).isEqualTo(expected); } @Test @@ -732,7 +752,7 @@ void nullSafeToStringWithPlainOldString() { @Test void nullSafeToStringWithObjectArray() { - Object[] array = {"Han", Long.valueOf(43)}; + Object[] array = {"Han", 43}; assertThat(ObjectUtils.nullSafeToString(array)).isEqualTo("{Han, 43}"); } @@ -820,13 +840,304 @@ void caseInsensitiveValueOf() { .withMessage("Constant [bogus] does not exist in enum type org.springframework.util.ObjectUtilsTests$Tropes"); } - private void assertEqualHashCodes(int expected, Object array) { + + private static void assertEqualHashCodes(int expected, Object array) { int actual = ObjectUtils.nullSafeHashCode(array); assertThat(actual).isEqualTo(expected); - assertThat(array.hashCode() != actual).isTrue(); + assertThat(array.hashCode()).isNotEqualTo(actual); } enum Tropes {FOO, BAR, baz} + + @Nested + class NullSafeConciseToStringTests { + + private final String truncated = " (truncated)..."; + private final int truncatedLength = 100 + truncated.length(); + + @Test + void nullSafeConciseToStringForNull() { + assertThat(ObjectUtils.nullSafeConciseToString(null)).isEqualTo("null"); + } + + @Test + void nullSafeConciseToStringForEmptyOptional() { + Optional optional = Optional.empty(); + assertThat(ObjectUtils.nullSafeConciseToString(optional)).isEqualTo("Optional.empty"); + } + + @Test + void nullSafeConciseToStringForNonEmptyOptionals() { + Optional optionalEnum = Optional.of(Tropes.BAR); + String expected = "Optional[BAR]"; + assertThat(ObjectUtils.nullSafeConciseToString(optionalEnum)).isEqualTo(expected); + + String repeat100 = repeat("X", 100); + String repeat101 = repeat("X", 101); + + Optional optionalString = Optional.of(repeat100); + expected = String.format("Optional[%s]", repeat100); + assertThat(ObjectUtils.nullSafeConciseToString(optionalString)).isEqualTo(expected); + + optionalString = Optional.of(repeat101); + expected = String.format("Optional[%s]", repeat100 + truncated); + assertThat(ObjectUtils.nullSafeConciseToString(optionalString)).isEqualTo(expected); + } + + @Test + void nullSafeConciseToStringForNonEmptyOptionalCustomType() { + class CustomType { + } + + CustomType customType = new CustomType(); + Optional optional = Optional.of(customType); + String expected = String.format("Optional[%s]", ObjectUtils.nullSafeConciseToString(customType)); + assertThat(ObjectUtils.nullSafeConciseToString(optional)).isEqualTo(expected); + } + + @Test + void nullSafeConciseToStringForClass() { + assertThat(ObjectUtils.nullSafeConciseToString(String.class)).isEqualTo("java.lang.String"); + } + + @Test + void nullSafeConciseToStringForStrings() { + String repeat100 = repeat("X", 100); + String repeat101 = repeat("X", 101); + + assertThat(ObjectUtils.nullSafeConciseToString("")).isEqualTo(""); + assertThat(ObjectUtils.nullSafeConciseToString("foo")).isEqualTo("foo"); + assertThat(ObjectUtils.nullSafeConciseToString(repeat100)).isEqualTo(repeat100); + assertThat(ObjectUtils.nullSafeConciseToString(repeat101)).hasSize(truncatedLength).endsWith(truncated); + } + + @Test + void nullSafeConciseToStringForStringBuilders() { + String repeat100 = repeat("X", 100); + String repeat101 = repeat("X", 101); + + assertThat(ObjectUtils.nullSafeConciseToString(new StringBuilder("foo"))).isEqualTo("foo"); + assertThat(ObjectUtils.nullSafeConciseToString(new StringBuilder(repeat100))).isEqualTo(repeat100); + assertThat(ObjectUtils.nullSafeConciseToString(new StringBuilder(repeat101))).hasSize(truncatedLength).endsWith(truncated); + } + + @Test + void nullSafeConciseToStringForEnum() { + assertThat(ObjectUtils.nullSafeConciseToString(Tropes.FOO)).isEqualTo("FOO"); + } + + @Test + void nullSafeConciseToStringForPrimitivesAndWrappers() { + assertThat(ObjectUtils.nullSafeConciseToString(true)).isEqualTo("true"); + assertThat(ObjectUtils.nullSafeConciseToString('X')).isEqualTo("X"); + assertThat(ObjectUtils.nullSafeConciseToString(42L)).isEqualTo("42"); + assertThat(ObjectUtils.nullSafeConciseToString(99.1234D)).isEqualTo("99.1234"); + } + + @Test + void nullSafeConciseToStringForBigNumbers() { + assertThat(ObjectUtils.nullSafeConciseToString(BigInteger.valueOf(42L))).isEqualTo("42"); + assertThat(ObjectUtils.nullSafeConciseToString(BigDecimal.valueOf(99.1234D))).isEqualTo("99.1234"); + } + + @Test + void nullSafeConciseToStringForDate() { + Date date = new Date(); + assertThat(ObjectUtils.nullSafeConciseToString(date)).isEqualTo(date.toString()); + } + + @Test + void nullSafeConciseToStringForTemporal() { + LocalDate localDate = LocalDate.now(); + assertThat(ObjectUtils.nullSafeConciseToString(localDate)).isEqualTo(localDate.toString()); + } + + @Test + void nullSafeConciseToStringForUUID() { + UUID id = UUID.randomUUID(); + assertThat(ObjectUtils.nullSafeConciseToString(id)).isEqualTo(id.toString()); + } + + @Test + void nullSafeConciseToStringForFile() { + String path = "/tmp/file.txt".replace('/', File.separatorChar); + assertThat(ObjectUtils.nullSafeConciseToString(new File(path))).isEqualTo(path); + + path = ("/tmp/" + repeat("xyz", 32)).replace('/', File.separatorChar); + assertThat(ObjectUtils.nullSafeConciseToString(new File(path))) + .hasSize(truncatedLength) + .startsWith(path.subSequence(0, 100)) + .endsWith(truncated); + } + + @Test + void nullSafeConciseToStringForPath() { + String path = "/tmp/file.txt".replace('/', File.separatorChar); + assertThat(ObjectUtils.nullSafeConciseToString(Paths.get(path))).isEqualTo(path); + + path = ("/tmp/" + repeat("xyz", 32)).replace('/', File.separatorChar); + assertThat(ObjectUtils.nullSafeConciseToString(Paths.get(path))) + .hasSize(truncatedLength) + .startsWith(path.subSequence(0, 100)) + .endsWith(truncated); + } + + @Test + void nullSafeConciseToStringForURI() { + String uri = "https://www.example.com/?foo=1&bar=2&baz=3"; + assertThat(ObjectUtils.nullSafeConciseToString(URI.create(uri))).isEqualTo(uri); + + uri += "&qux=" + repeat("4", 60); + assertThat(ObjectUtils.nullSafeConciseToString(URI.create(uri))) + .hasSize(truncatedLength) + .startsWith(uri.subSequence(0, 100)) + .endsWith(truncated); + } + + @Test + void nullSafeConciseToStringForURL() throws Exception { + String url = "https://www.example.com/?foo=1&bar=2&baz=3"; + assertThat(ObjectUtils.nullSafeConciseToString(new URL(url))).isEqualTo(url); + + url += "&qux=" + repeat("4", 60); + assertThat(ObjectUtils.nullSafeConciseToString(new URL(url))) + .hasSize(truncatedLength) + .startsWith(url.subSequence(0, 100)) + .endsWith(truncated); + } + + @Test + void nullSafeConciseToStringForInetAddress() { + InetAddress localhost = getLocalhost(); + assertThat(ObjectUtils.nullSafeConciseToString(localhost)).isEqualTo(localhost.toString()); + } + + private InetAddress getLocalhost() { + try { + return InetAddress.getLocalHost(); + } + catch (UnknownHostException ex) { + return InetAddress.getLoopbackAddress(); + } + } + + @Test + void nullSafeConciseToStringForCharset() { + Charset charset = StandardCharsets.UTF_8; + assertThat(ObjectUtils.nullSafeConciseToString(charset)).isEqualTo(charset.name()); + } + + @Test + void nullSafeConciseToStringForCurrency() { + Currency currency = Currency.getInstance(Locale.US); + assertThat(ObjectUtils.nullSafeConciseToString(currency)).isEqualTo(currency.toString()); + } + + @Test + void nullSafeConciseToStringForLocale() { + assertThat(ObjectUtils.nullSafeConciseToString(Locale.GERMANY)).isEqualTo("de_DE"); + } + + @Test + void nullSafeConciseToStringForRegExPattern() { + Pattern pattern = Pattern.compile("^(foo|bar)$"); + assertThat(ObjectUtils.nullSafeConciseToString(pattern)).isEqualTo(pattern.toString()); + } + + @Test + void nullSafeConciseToStringForTimeZone() { + TimeZone timeZone = TimeZone.getDefault(); + assertThat(ObjectUtils.nullSafeConciseToString(timeZone)).isEqualTo(timeZone.getID()); + } + + @Test + void nullSafeConciseToStringForZoneId() { + ZoneId zoneId = ZoneId.systemDefault(); + assertThat(ObjectUtils.nullSafeConciseToString(zoneId)).isEqualTo(zoneId.getId()); + } + + @Test + void nullSafeConciseToStringForEmptyArrays() { + assertThat(ObjectUtils.nullSafeConciseToString(new char[] {})).isEqualTo("{}"); + assertThat(ObjectUtils.nullSafeConciseToString(new int[][] {})).isEqualTo("{}"); + assertThat(ObjectUtils.nullSafeConciseToString(new String[] {})).isEqualTo("{}"); + assertThat(ObjectUtils.nullSafeConciseToString(new Integer[][] {})).isEqualTo("{}"); + } + + @Test + void nullSafeConciseToStringForNonEmptyArrays() { + assertThat(ObjectUtils.nullSafeConciseToString(new char[] {'a'})).isEqualTo("{...}"); + assertThat(ObjectUtils.nullSafeConciseToString(new int[][] {{1}, {2}})).isEqualTo("{...}"); + assertThat(ObjectUtils.nullSafeConciseToString(new String[] {"enigma"})).isEqualTo("{...}"); + assertThat(ObjectUtils.nullSafeConciseToString(new Integer[][] {{1}, {2}})).isEqualTo("{...}"); + } + + @Test + void nullSafeConciseToStringForEmptyCollections() { + List list = Collections.emptyList(); + Set set = Collections.emptySet(); + assertThat(ObjectUtils.nullSafeConciseToString(list)).isEqualTo("[...]"); + assertThat(ObjectUtils.nullSafeConciseToString(set)).isEqualTo("[...]"); + } + + @Test + void nullSafeConciseToStringForNonEmptyCollections() { + List list = Arrays.asList("a", "b"); + Set set = new HashSet<>(); + set.add("foo"); + assertThat(ObjectUtils.nullSafeConciseToString(list)).isEqualTo("[...]"); + assertThat(ObjectUtils.nullSafeConciseToString(set)).isEqualTo("[...]"); + } + + @Test + void nullSafeConciseToStringForEmptyMaps() { + Map map = Collections.emptyMap(); + assertThat(ObjectUtils.nullSafeConciseToString(map)).isEqualTo("{...}"); + } + + @Test + void nullSafeConciseToStringForNonEmptyMaps() { + HashMap map = new HashMap<>(); + map.put("foo", 42L); + assertThat(ObjectUtils.nullSafeConciseToString(map)).isEqualTo("{...}"); + } + + @Test + void nullSafeConciseToStringForCustomTypes() { + class ExplosiveType { + @Override + public String toString() { + throw new UnsupportedOperationException("no-go"); + } + } + ExplosiveType explosiveType = new ExplosiveType(); + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(explosiveType::toString); + assertThat(ObjectUtils.nullSafeConciseToString(explosiveType)).startsWith(prefix(ExplosiveType.class)); + + class WordyType { + @Override + public String toString() { + return repeat("blah blah", 20); + } + } + WordyType wordyType = new WordyType(); + assertThat(wordyType).asString().hasSizeGreaterThanOrEqualTo(180 /* 9x20 */); + assertThat(ObjectUtils.nullSafeConciseToString(wordyType)).startsWith(prefix(WordyType.class)); + } + + private String repeat(String str, int count) { + String result = ""; + for (int i = 0; i < count; i++) { + result += str; + } + return result; + } + + private String prefix(Class clazz) { + return clazz.getTypeName() + "@"; + } + } + } diff --git a/spring-core/src/test/java/org/springframework/util/StreamUtilsTests.java b/spring-core/src/test/java/org/springframework/util/StreamUtilsTests.java index 21f0829457b3..f3391d4da8ec 100644 --- a/spring-core/src/test/java/org/springframework/util/StreamUtilsTests.java +++ b/spring-core/src/test/java/org/springframework/util/StreamUtilsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -33,8 +33,6 @@ import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; /** * Tests for {@link StreamUtils}. @@ -47,6 +45,7 @@ class StreamUtilsTests { private String string = ""; + @BeforeEach void setup() { new Random().nextBytes(bytes); @@ -55,55 +54,50 @@ void setup() { } } + @Test void copyToByteArray() throws Exception { - InputStream inputStream = spy(new ByteArrayInputStream(bytes)); + InputStream inputStream = new ByteArrayInputStream(bytes); byte[] actual = StreamUtils.copyToByteArray(inputStream); assertThat(actual).isEqualTo(bytes); - verify(inputStream, never()).close(); } @Test void copyToString() throws Exception { Charset charset = Charset.defaultCharset(); - InputStream inputStream = spy(new ByteArrayInputStream(string.getBytes(charset))); + InputStream inputStream = new ByteArrayInputStream(string.getBytes(charset)); String actual = StreamUtils.copyToString(inputStream, charset); assertThat(actual).isEqualTo(string); - verify(inputStream, never()).close(); } @Test void copyBytes() throws Exception { - ByteArrayOutputStream out = spy(new ByteArrayOutputStream()); + ByteArrayOutputStream out = new ByteArrayOutputStream(); StreamUtils.copy(bytes, out); assertThat(out.toByteArray()).isEqualTo(bytes); - verify(out, never()).close(); } @Test void copyString() throws Exception { Charset charset = Charset.defaultCharset(); - ByteArrayOutputStream out = spy(new ByteArrayOutputStream()); + ByteArrayOutputStream out = new ByteArrayOutputStream(); StreamUtils.copy(string, charset, out); assertThat(out.toByteArray()).isEqualTo(string.getBytes(charset)); - verify(out, never()).close(); } @Test void copyStream() throws Exception { - ByteArrayOutputStream out = spy(new ByteArrayOutputStream()); + ByteArrayOutputStream out = new ByteArrayOutputStream(); StreamUtils.copy(new ByteArrayInputStream(bytes), out); assertThat(out.toByteArray()).isEqualTo(bytes); - verify(out, never()).close(); } @Test void copyRange() throws Exception { - ByteArrayOutputStream out = spy(new ByteArrayOutputStream()); + ByteArrayOutputStream out = new ByteArrayOutputStream(); StreamUtils.copyRange(new ByteArrayInputStream(bytes), out, 0, 100); byte[] range = Arrays.copyOfRange(bytes, 0, 101); assertThat(out.toByteArray()).isEqualTo(range); - verify(out, never()).close(); } @Test @@ -135,4 +129,5 @@ void nonClosingOutputStream() throws Exception { ordered.verify(source).write(bytes, 1, 2); ordered.verify(source, never()).close(); } + } diff --git a/spring-core/src/test/java/org/springframework/util/StringUtilsTests.java b/spring-core/src/test/java/org/springframework/util/StringUtilsTests.java index 4609e6832cb8..e76f60195016 100644 --- a/spring-core/src/test/java/org/springframework/util/StringUtilsTests.java +++ b/spring-core/src/test/java/org/springframework/util/StringUtilsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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,6 +22,8 @@ import java.util.Properties; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; @@ -785,4 +787,26 @@ void collectionToDelimitedStringWithNullValuesShouldNotFail() { assertThat(StringUtils.collectionToCommaDelimitedString(Collections.singletonList(null))).isEqualTo("null"); } + @Test + void truncatePreconditions() { + assertThatIllegalArgumentException() + .isThrownBy(() -> StringUtils.truncate("foo", 0)) + .withMessage("Truncation threshold must be a positive number: 0"); + assertThatIllegalArgumentException() + .isThrownBy(() -> StringUtils.truncate("foo", -99)) + .withMessage("Truncation threshold must be a positive number: -99"); + } + + @ParameterizedTest + @CsvSource(delimiterString = "-->", value = { + "'' --> ''", + "aardvark --> aardvark", + "aardvark12 --> aardvark12", + "aardvark123 --> aardvark12 (truncated)...", + "aardvark, bird, cat --> aardvark, (truncated)..." + }) + void truncate(String text, String truncated) { + assertThat(StringUtils.truncate(text, 10)).isEqualTo(truncated); + } + } diff --git a/spring-core/src/testFixtures/java/org/springframework/core/testfixture/env/MockPropertySource.java b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/env/MockPropertySource.java index ae4944ddccb7..a4cbd1f22ed1 100644 --- a/spring-core/src/testFixtures/java/org/springframework/core/testfixture/env/MockPropertySource.java +++ b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/env/MockPropertySource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2023 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. @@ -28,7 +28,7 @@ * * The {@link #setProperty} and {@link #withProperty} methods are exposed for * convenience, for example: - *

    + * 
      * {@code
      *   PropertySource source = new MockPropertySource().withProperty("foo", "bar");
      * }
    @@ -77,7 +77,7 @@ public MockPropertySource(Properties properties) {
     
     	/**
     	 * Create a new {@code MockPropertySource} with the given name and backed by the given
    -	 * {@link Properties} object
    +	 * {@link Properties} object.
     	 * @param name the {@linkplain #getName() name} of the property source
     	 * @param properties the properties to use
     	 */
    @@ -99,7 +99,7 @@ public void setProperty(String name, Object value) {
     	 * @return this {@link MockPropertySource} instance
     	 */
     	public MockPropertySource withProperty(String name, Object value) {
    -		this.setProperty(name, value);
    +		setProperty(name, value);
     		return this;
     	}
     
    diff --git a/spring-core/src/testFixtures/java/org/springframework/core/testfixture/io/buffer/AbstractLeakCheckingTests.java b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/io/buffer/AbstractLeakCheckingTests.java
    index 348f5dbbecbe..99525ced55c5 100644
    --- a/spring-core/src/testFixtures/java/org/springframework/core/testfixture/io/buffer/AbstractLeakCheckingTests.java
    +++ b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/io/buffer/AbstractLeakCheckingTests.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright 2002-2019 the original author or authors.
    + * Copyright 2002-2022 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-core/src/testFixtures/java/org/springframework/core/testfixture/io/buffer/LeakAwareDataBufferFactory.java b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/io/buffer/LeakAwareDataBufferFactory.java
    index 9686aa95b0a7..7fbd8f9835a4 100644
    --- a/spring-core/src/testFixtures/java/org/springframework/core/testfixture/io/buffer/LeakAwareDataBufferFactory.java
    +++ b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/io/buffer/LeakAwareDataBufferFactory.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright 2002-2019 the original author or authors.
    + * Copyright 2002-2022 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-expression/src/main/java/org/springframework/expression/EvaluationContext.java b/spring-expression/src/main/java/org/springframework/expression/EvaluationContext.java
    index 0b4d2eab971b..5bb6c46e0d65 100644
    --- a/spring-expression/src/main/java/org/springframework/expression/EvaluationContext.java
    +++ b/spring-expression/src/main/java/org/springframework/expression/EvaluationContext.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright 2002-2018 the original author or authors.
    + * Copyright 2002-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.expression;
     
     import java.util.List;
    +import java.util.function.Supplier;
     
     import org.springframework.lang.Nullable;
     
    @@ -24,12 +25,21 @@
      * Expressions are executed in an evaluation context. It is in this context that
      * references are resolved when encountered during expression evaluation.
      *
    - * 

    There is a default implementation of this EvaluationContext interface: - * {@link org.springframework.expression.spel.support.StandardEvaluationContext} - * which can be extended, rather than having to implement everything manually. + *

    There are two default implementations of this interface. + *

      + *
    • {@link org.springframework.expression.spel.support.SimpleEvaluationContext + * SimpleEvaluationContext}: a simpler builder-style {@code EvaluationContext} + * variant for data-binding purposes, which allows for opting into several SpEL + * features as needed.
    • + *
    • {@link org.springframework.expression.spel.support.StandardEvaluationContext + * StandardEvaluationContext}: a powerful and highly configurable {@code EvaluationContext} + * implementation, which can be extended, rather than having to implement everything + * manually.
    • + *
    * * @author Andy Clement * @author Juergen Hoeller + * @author Sam Brannen * @since 3.0 */ public interface EvaluationContext { @@ -85,7 +95,30 @@ public interface EvaluationContext { OperatorOverloader getOperatorOverloader(); /** - * Set a named variable within this evaluation context to a specified value. + * Assign the value created by the specified {@link Supplier} to a named variable + * within this evaluation context. + *

    In contrast to {@link #setVariable(String, Object)}, this method should only + * be invoked to support the assignment operator ({@code =}) within an expression. + *

    By default, this method delegates to {@code setVariable(String, Object)}, + * providing the value created by the {@code valueSupplier}. Concrete implementations + * may override this default method to provide different semantics. + * @param name the name of the variable to assign + * @param valueSupplier the supplier of the value to be assigned to the variable + * @return a {@link TypedValue} wrapping the assigned value + * @since 5.2.24 + */ + default TypedValue assignVariable(String name, Supplier valueSupplier) { + TypedValue typedValue = valueSupplier.get(); + setVariable(name, typedValue.getValue()); + return typedValue; + } + + /** + * Set a named variable in this evaluation context to a specified value. + *

    In contrast to {@link #assignVariable(String, Supplier)}, this method + * should only be invoked programmatically when interacting directly with the + * {@code EvaluationContext} — for example, to provide initial + * configuration for the context. * @param name the name of the variable to set * @param value the value to be placed in the variable */ @@ -93,10 +126,24 @@ public interface EvaluationContext { /** * Look up a named variable within this evaluation context. - * @param name variable to lookup + * @param name the name of the variable to look up * @return the value of the variable, or {@code null} if not found */ @Nullable Object lookupVariable(String name); + /** + * Determine if assignment is enabled within expressions evaluated by this evaluation + * context. + *

    If this method returns {@code false}, the assignment ({@code =}), increment + * ({@code ++}), and decrement ({@code --}) operators are disabled. + *

    By default, this method returns {@code true}. Concrete implementations may override + * this default method to disable assignment. + * @return {@code true} if assignment is enabled; {@code false} otherwise + * @since 5.3.38 + */ + default boolean isAssignmentEnabled() { + return true; + } + } diff --git a/spring-expression/src/main/java/org/springframework/expression/ExpressionException.java b/spring-expression/src/main/java/org/springframework/expression/ExpressionException.java index 1b8d222a84af..4fb1fe8b4225 100644 --- a/spring-expression/src/main/java/org/springframework/expression/ExpressionException.java +++ b/spring-expression/src/main/java/org/springframework/expression/ExpressionException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 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-expression/src/main/java/org/springframework/expression/common/TemplateAwareExpressionParser.java b/spring-expression/src/main/java/org/springframework/expression/common/TemplateAwareExpressionParser.java index 58bae54976bd..0f5ccbddbd27 100644 --- a/spring-expression/src/main/java/org/springframework/expression/common/TemplateAwareExpressionParser.java +++ b/spring-expression/src/main/java/org/springframework/expression/common/TemplateAwareExpressionParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2023 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 org.springframework.expression.ParseException; import org.springframework.expression.ParserContext; import org.springframework.lang.Nullable; +import org.springframework.util.Assert; /** * An expression parser that understands templates. It can be subclassed by expression @@ -34,6 +35,7 @@ * @author Keith Donald * @author Juergen Hoeller * @author Andy Clement + * @author Sam Brannen * @since 3.0 */ public abstract class TemplateAwareExpressionParser implements ExpressionParser { @@ -46,9 +48,11 @@ public Expression parseExpression(String expressionString) throws ParseException @Override public Expression parseExpression(String expressionString, @Nullable ParserContext context) throws ParseException { if (context != null && context.isTemplate()) { + Assert.notNull(expressionString, "'expressionString' must not be null"); return parseTemplate(expressionString, context); } else { + Assert.hasText(expressionString, "'expressionString' must not be null or blank"); return doParseExpression(expressionString, context); } } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ExpressionState.java b/spring-expression/src/main/java/org/springframework/expression/spel/ExpressionState.java index 252af447af31..8dadae596732 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ExpressionState.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ExpressionState.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2023 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.util.List; import java.util.Map; import java.util.NoSuchElementException; +import java.util.function.Supplier; import org.springframework.core.convert.TypeDescriptor; import org.springframework.expression.EvaluationContext; @@ -38,18 +39,19 @@ import org.springframework.util.CollectionUtils; /** - * An ExpressionState is for maintaining per-expression-evaluation state, any changes to - * it are not seen by other expressions but it gives a place to hold local variables and + * ExpressionState is for maintaining per-expression-evaluation state: any changes to + * it are not seen by other expressions, but it gives a place to hold local variables and * for component expressions in a compound expression to communicate state. This is in * contrast to the EvaluationContext, which is shared amongst expression evaluations, and * any changes to it will be seen by other expressions or any code that chooses to ask * questions of the context. * - *

    It also acts as a place for to define common utility routines that the various AST + *

    It also acts as a place to define common utility routines that the various AST * nodes might need. * * @author Andy Clement * @author Juergen Hoeller + * @author Sam Brannen * @since 3.0 */ public class ExpressionState { @@ -138,6 +140,29 @@ public TypedValue getScopeRootContextObject() { return this.scopeRootObjects.element(); } + /** + * Assign the value created by the specified {@link Supplier} to a named variable + * within the evaluation context. + *

    In contrast to {@link #setVariable(String, Object)}, this method should + * only be invoked to support assignment within an expression. + * @param name the name of the variable to assign + * @param valueSupplier the supplier of the value to be assigned to the variable + * @return a {@link TypedValue} wrapping the assigned value + * @since 5.2.24 + * @see EvaluationContext#assignVariable(String, Supplier) + */ + public TypedValue assignVariable(String name, Supplier valueSupplier) { + return this.relatedContext.assignVariable(name, valueSupplier); + } + + /** + * Set a named variable in the evaluation context to a specified value. + *

    In contrast to {@link #assignVariable(String, Supplier)}, this method + * should only be invoked programmatically. + * @param name the name of the variable to set + * @param value the value to be placed in the variable + * @see EvaluationContext#setVariable(String, Object) + */ public void setVariable(String name, @Nullable Object value) { this.relatedContext.setVariable(name, value); } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/SpelMessage.java b/spring-expression/src/main/java/org/springframework/expression/spel/SpelMessage.java index fe8dd70468d5..c5fe3bcec256 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/SpelMessage.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/SpelMessage.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -264,7 +264,27 @@ public enum SpelMessage { /** @since 5.3.17 */ MAX_ARRAY_ELEMENTS_THRESHOLD_EXCEEDED(Kind.ERROR, 1075, - "Array declares too many elements, exceeding the threshold of ''{0}''"); + "Array declares too many elements, exceeding the threshold of ''{0}''"), + + /** @since 5.2.23 */ + MAX_REPEATED_TEXT_SIZE_EXCEEDED(Kind.ERROR, 1076, + "Repeated text is too long, exceeding the threshold of ''{0}'' characters"), + + /** @since 5.2.23 */ + MAX_REGEX_LENGTH_EXCEEDED(Kind.ERROR, 1077, + "Regular expression is too long, exceeding the threshold of ''{0}'' characters"), + + /** @since 5.2.24 */ + MAX_CONCATENATED_STRING_LENGTH_EXCEEDED(Kind.ERROR, 1078, + "Concatenated string is too long, exceeding the threshold of ''{0}'' characters"), + + /** @since 5.2.24 */ + MAX_EXPRESSION_LENGTH_EXCEEDED(Kind.ERROR, 1079, + "SpEL expression is too long, exceeding the threshold of ''{0}'' characters"), + + /** @since 5.2.24 */ + VARIABLE_ASSIGNMENT_NOT_SUPPORTED(Kind.ERROR, 1080, + "Assignment to variable ''{0}'' is not supported"); private final Kind kind; diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/SpelParserConfiguration.java b/spring-expression/src/main/java/org/springframework/expression/spel/SpelParserConfiguration.java index 3a3c96eaa5f4..ae94b4c3b4fa 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/SpelParserConfiguration.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/SpelParserConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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,6 +30,12 @@ */ public class SpelParserConfiguration { + /** + * Default maximum length permitted for a SpEL expression. + * @since 5.2.24 + */ + private static final int DEFAULT_MAX_EXPRESSION_LENGTH = 10_000; + /** System property to configure the default compiler mode for SpEL expression parsers: {@value}. */ public static final String SPRING_EXPRESSION_COMPILER_MODE_PROPERTY_NAME = "spring.expression.compiler.mode"; @@ -54,6 +60,8 @@ public class SpelParserConfiguration { private final int maximumAutoGrowSize; + private final int maximumExpressionLength; + /** * Create a new {@code SpelParserConfiguration} instance with default settings. @@ -102,11 +110,30 @@ public SpelParserConfiguration(boolean autoGrowNullReferences, boolean autoGrowC public SpelParserConfiguration(@Nullable SpelCompilerMode compilerMode, @Nullable ClassLoader compilerClassLoader, boolean autoGrowNullReferences, boolean autoGrowCollections, int maximumAutoGrowSize) { + this(compilerMode, compilerClassLoader, autoGrowNullReferences, autoGrowCollections, + maximumAutoGrowSize, DEFAULT_MAX_EXPRESSION_LENGTH); + } + + /** + * Create a new {@code SpelParserConfiguration} instance. + * @param compilerMode the compiler mode that parsers using this configuration object should use + * @param compilerClassLoader the ClassLoader to use as the basis for expression compilation + * @param autoGrowNullReferences if null references should automatically grow + * @param autoGrowCollections if collections should automatically grow + * @param maximumAutoGrowSize the maximum size that a collection can auto grow + * @param maximumExpressionLength the maximum length of a SpEL expression; + * must be a positive number + * @since 5.2.25 + */ + public SpelParserConfiguration(@Nullable SpelCompilerMode compilerMode, @Nullable ClassLoader compilerClassLoader, + boolean autoGrowNullReferences, boolean autoGrowCollections, int maximumAutoGrowSize, int maximumExpressionLength) { + this.compilerMode = (compilerMode != null ? compilerMode : defaultCompilerMode); this.compilerClassLoader = compilerClassLoader; this.autoGrowNullReferences = autoGrowNullReferences; this.autoGrowCollections = autoGrowCollections; this.maximumAutoGrowSize = maximumAutoGrowSize; + this.maximumExpressionLength = maximumExpressionLength; } @@ -146,4 +173,12 @@ public int getMaximumAutoGrowSize() { return this.maximumAutoGrowSize; } + /** + * Return the maximum number of characters that a SpEL expression can contain. + * @since 5.2.25 + */ + public int getMaximumExpressionLength() { + return this.maximumExpressionLength; + } + } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Assign.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Assign.java index a009a07db512..1b47ead1607f 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Assign.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Assign.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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,8 @@ import org.springframework.expression.EvaluationException; import org.springframework.expression.TypedValue; import org.springframework.expression.spel.ExpressionState; +import org.springframework.expression.spel.SpelEvaluationException; +import org.springframework.expression.spel.SpelMessage; /** * Represents assignment. An alternative to calling {@code setValue} @@ -27,6 +29,7 @@ *

    Example: 'someNumberProperty=42' * * @author Andy Clement + * @author Sam Brannen * @since 3.0 */ public class Assign extends SpelNodeImpl { @@ -38,9 +41,10 @@ public Assign(int startPos, int endPos, SpelNodeImpl... operands) { @Override public TypedValue getValueInternal(ExpressionState state) throws EvaluationException { - TypedValue newValue = this.children[1].getValueInternal(state); - getChild(0).setValue(state, newValue.getValue()); - return newValue; + if (!state.getEvaluationContext().isAssignmentEnabled()) { + throw new SpelEvaluationException(getStartPosition(), SpelMessage.NOT_ASSIGNABLE, toStringAST()); + } + return this.children[0].setValueInternal(state, () -> this.children[1].getValueInternal(state)); } @Override diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/CompoundExpression.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/CompoundExpression.java index 0e47facfa7bf..616a503a4ec1 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/CompoundExpression.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/CompoundExpression.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 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.expression.spel.ast; import java.util.StringJoiner; +import java.util.function.Supplier; import org.springframework.asm.MethodVisitor; import org.springframework.expression.EvaluationException; @@ -24,13 +25,13 @@ import org.springframework.expression.spel.CodeFlow; import org.springframework.expression.spel.ExpressionState; import org.springframework.expression.spel.SpelEvaluationException; -import org.springframework.lang.Nullable; /** * Represents a DOT separated expression sequence, such as * {@code 'property1.property2.methodOne()'}. * * @author Andy Clement + * @author Sam Brannen * @since 3.0 */ public class CompoundExpression extends SpelNodeImpl { @@ -95,8 +96,12 @@ public TypedValue getValueInternal(ExpressionState state) throws EvaluationExcep } @Override - public void setValue(ExpressionState state, @Nullable Object value) throws EvaluationException { - getValueRef(state).setValue(value); + public TypedValue setValueInternal(ExpressionState state, Supplier valueSupplier) + throws EvaluationException { + + TypedValue typedValue = valueSupplier.get(); + getValueRef(state).setValue(typedValue.getValue()); + return typedValue; } @Override diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/ConstructorReference.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/ConstructorReference.java index 35a0b3efc3e3..286f51930e22 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/ConstructorReference.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/ConstructorReference.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-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,6 +22,7 @@ import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.List; +import java.util.StringJoiner; import org.springframework.asm.MethodVisitor; import org.springframework.core.convert.TypeDescriptor; @@ -46,10 +47,15 @@ * Represents the invocation of a constructor. Either a constructor on a regular type or * construction of an array. When an array is constructed, an initializer can be specified. * - *

    Examples:
    - * new String('hello world')
    - * new int[]{1,2,3,4}
    - * new int[3] new int[3]{1,2,3} + *

    Examples

    + *
      + *
    • new example.Foo()
    • + *
    • new String('hello world')
    • + *
    • new int[] {1,2,3,4}
    • + *
    • new String[] {'abc','xyz'}
    • + *
    • new int[5]
    • + *
    • new int[3][4]
    • + *
    * * @author Andy Clement * @author Juergen Hoeller @@ -68,7 +74,7 @@ public class ConstructorReference extends SpelNodeImpl { private final boolean isArrayConstructor; @Nullable - private SpelNodeImpl[] dimensions; + private final SpelNodeImpl[] dimensions; // TODO is this caching safe - passing the expression around will mean this executor is also being passed around /** The cached executor that may be reused on subsequent evaluations. */ @@ -83,6 +89,7 @@ public class ConstructorReference extends SpelNodeImpl { public ConstructorReference(int startPos, int endPos, SpelNodeImpl... arguments) { super(startPos, endPos, arguments); this.isArrayConstructor = false; + this.dimensions = null; } /** @@ -214,16 +221,33 @@ private ConstructorExecutor findExecutorForConstructor(String typeName, @Override public String toStringAST() { StringBuilder sb = new StringBuilder("new "); - int index = 0; - sb.append(getChild(index++).toStringAST()); - sb.append('('); - for (int i = index; i < getChildCount(); i++) { - if (i > index) { - sb.append(','); + sb.append(getChild(0).toStringAST()); // constructor or array type + + // Arrays + if (this.isArrayConstructor) { + if (hasInitializer()) { + // new int[] {1, 2, 3, 4, 5}, etc. + InlineList initializer = (InlineList) getChild(1); + sb.append("[] ").append(initializer.toStringAST()); + } + else { + // new int[3], new java.lang.String[3][4], etc. + for (SpelNodeImpl dimension : this.dimensions) { + sb.append('[').append(dimension.toStringAST()).append(']'); + } } - sb.append(getChild(i).toStringAST()); } - sb.append(')'); + // Constructors + else { + // new String('hello'), new org.example.Person('Jane', 32), etc. + StringJoiner sj = new StringJoiner(",", "(", ")"); + int count = getChildCount(); + for (int i = 1; i < count; i++) { + sj.add(getChild(i).toStringAST()); + } + sb.append(sj.toString()); + } + return sb.toString(); } @@ -244,6 +268,13 @@ private TypedValue createArray(ExpressionState state) throws EvaluationException } String type = (String) intendedArrayType; + + if (state.getEvaluationContext().getConstructorResolvers().isEmpty()) { + // No constructor resolver -> no array construction either (as of 5.3.38) + throw new SpelEvaluationException(getStartPosition(), SpelMessage.CONSTRUCTOR_NOT_FOUND, + type + "[]", "[]"); + } + Class componentType; TypeCode arrayTypeCode = TypeCode.forName(type); if (arrayTypeCode == TypeCode.OBJECT) { diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/FunctionReference.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/FunctionReference.java index 041773538647..b884aa1ed6cc 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/FunctionReference.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/FunctionReference.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 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. @@ -144,7 +144,7 @@ public String toStringAST() { for (int i = 0; i < getChildCount(); i++) { sj.add(getChild(i).toStringAST()); } - return '#' + this.name + sj.toString(); + return '#' + this.name + sj; } /** diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java index 04b502816169..2be26a4dec41 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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.List; import java.util.Map; import java.util.StringJoiner; +import java.util.function.Supplier; import org.springframework.asm.MethodVisitor; import org.springframework.core.convert.TypeDescriptor; @@ -45,11 +46,12 @@ /** * An Indexer can index into some proceeding structure to access a particular piece of it. - * Supported structures are: strings / collections (lists/sets) / arrays. + *

    Supported structures are: strings / collections (lists/sets) / arrays. * * @author Andy Clement * @author Phillip Webb * @author Stephane Nicoll + * @author Sam Brannen * @since 3.0 */ // TODO support multidimensional arrays @@ -102,8 +104,12 @@ public TypedValue getValueInternal(ExpressionState state) throws EvaluationExcep } @Override - public void setValue(ExpressionState state, @Nullable Object newValue) throws EvaluationException { - getValueRef(state).setValue(newValue); + public TypedValue setValueInternal(ExpressionState state, Supplier valueSupplier) + throws EvaluationException { + + TypedValue typedValue = valueSupplier.get(); + getValueRef(state).setValue(typedValue.getValue()); + return typedValue; } @Override @@ -220,6 +226,8 @@ public void generateCode(MethodVisitor mv, CodeFlow cf) { cf.loadTarget(mv); } + SpelNodeImpl index = this.children[0]; + if (this.indexedType == IndexedType.ARRAY) { int insn; if ("D".equals(this.exitTypeDescriptor)) { @@ -256,18 +264,14 @@ else if ("C".equals(this.exitTypeDescriptor)) { //depthPlusOne(exitTypeDescriptor)+"Ljava/lang/Object;"); insn = AALOAD; } - SpelNodeImpl index = this.children[0]; - cf.enterCompilationScope(); - index.generateCode(mv, cf); - cf.exitCompilationScope(); + + generateIndexCode(mv, cf, index, int.class); mv.visitInsn(insn); } else if (this.indexedType == IndexedType.LIST) { mv.visitTypeInsn(CHECKCAST, "java/util/List"); - cf.enterCompilationScope(); - this.children[0].generateCode(mv, cf); - cf.exitCompilationScope(); + generateIndexCode(mv, cf, index, int.class); mv.visitMethodInsn(INVOKEINTERFACE, "java/util/List", "get", "(I)Ljava/lang/Object;", true); } @@ -275,15 +279,13 @@ else if (this.indexedType == IndexedType.MAP) { mv.visitTypeInsn(CHECKCAST, "java/util/Map"); // Special case when the key is an unquoted string literal that will be parsed as // a property/field reference - if ((this.children[0] instanceof PropertyOrFieldReference)) { + if (index instanceof PropertyOrFieldReference) { PropertyOrFieldReference reference = (PropertyOrFieldReference) this.children[0]; String mapKeyName = reference.getName(); mv.visitLdcInsn(mapKeyName); } else { - cf.enterCompilationScope(); - this.children[0].generateCode(mv, cf); - cf.exitCompilationScope(); + generateIndexCode(mv, cf, index, Object.class); } mv.visitMethodInsn( INVOKEINTERFACE, "java/util/Map", "get", "(Ljava/lang/Object;)Ljava/lang/Object;", true); @@ -319,6 +321,11 @@ else if (this.indexedType == IndexedType.OBJECT) { cf.pushDescriptor(this.exitTypeDescriptor); } + private void generateIndexCode(MethodVisitor mv, CodeFlow cf, SpelNodeImpl indexNode, Class indexType) { + String indexDesc = CodeFlow.toDescriptor(indexType); + generateCodeForArgument(mv, cf, indexNode, indexDesc); + } + @Override public String toStringAST() { StringJoiner sj = new StringJoiner(",", "[", "]"); @@ -627,6 +634,8 @@ public void setValue(@Nullable Object newValue) { throw new SpelEvaluationException(getStartPosition(), ex, SpelMessage.EXCEPTION_DURING_PROPERTY_WRITE, this.name, ex.getMessage()); } + throw new SpelEvaluationException(getStartPosition(), + SpelMessage.INDEXING_NOT_SUPPORTED_FOR_TYPE, this.targetObjectTypeDescriptor.toString()); } @Override diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/MethodReference.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/MethodReference.java index 1ecb6187001a..2e3b6404b924 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/MethodReference.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/MethodReference.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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. @@ -263,7 +263,7 @@ public String toStringAST() { for (int i = 0; i < getChildCount(); i++) { sj.add(getChild(i).toStringAST()); } - return this.name + sj.toString(); + return this.name + sj; } /** @@ -288,12 +288,9 @@ public boolean isCompilable() { if (executor.didArgumentConversionOccur()) { return false; } - Class clazz = executor.getMethod().getDeclaringClass(); - if (!Modifier.isPublic(clazz.getModifiers()) && executor.getPublicDeclaringClass() == null) { - return false; - } - return true; + Class clazz = executor.getMethod().getDeclaringClass(); + return (Modifier.isPublic(clazz.getModifiers()) || executor.getPublicDeclaringClass() != null); } @Override diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpDec.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpDec.java index 9bab5a1e381b..193efe2fb89a 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpDec.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpDec.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -34,6 +34,7 @@ * @author Andy Clement * @author Juergen Hoeller * @author Giovanni Dall'Oglio Risso + * @author Sam Brannen * @since 3.2 */ public class OpDec extends Operator { @@ -50,6 +51,10 @@ public OpDec(int startPos, int endPos, boolean postfix, SpelNodeImpl... operands @Override public TypedValue getValueInternal(ExpressionState state) throws EvaluationException { + if (!state.getEvaluationContext().isAssignmentEnabled()) { + throw new SpelEvaluationException(getStartPosition(), SpelMessage.OPERAND_NOT_DECREMENTABLE, toStringAST()); + } + SpelNodeImpl operand = getLeftOperand(); // The operand is going to be read and then assigned to, we don't want to evaluate it twice. diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpInc.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpInc.java index ee19b59988ca..f35370719864 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpInc.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpInc.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -34,6 +34,7 @@ * @author Andy Clement * @author Juergen Hoeller * @author Giovanni Dall'Oglio Risso + * @author Sam Brannen * @since 3.2 */ public class OpInc extends Operator { @@ -50,6 +51,10 @@ public OpInc(int startPos, int endPos, boolean postfix, SpelNodeImpl... operands @Override public TypedValue getValueInternal(ExpressionState state) throws EvaluationException { + if (!state.getEvaluationContext().isAssignmentEnabled()) { + throw new SpelEvaluationException(getStartPosition(), SpelMessage.OPERAND_NOT_INCREMENTABLE, toStringAST()); + } + SpelNodeImpl operand = getLeftOperand(); ValueRef valueRef = operand.getValueRef(state); @@ -104,7 +109,7 @@ else if (op1 instanceof Byte) { } } - // set the name value + // set the new value try { valueRef.setValue(newValue.getValue()); } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpMultiply.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpMultiply.java index fbf28317b32b..0cff5cb0c8c0 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpMultiply.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpMultiply.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 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,8 @@ import org.springframework.expression.TypedValue; import org.springframework.expression.spel.CodeFlow; import org.springframework.expression.spel.ExpressionState; +import org.springframework.expression.spel.SpelEvaluationException; +import org.springframework.expression.spel.SpelMessage; import org.springframework.util.Assert; import org.springframework.util.NumberUtils; @@ -52,6 +54,13 @@ */ public class OpMultiply extends Operator { + /** + * Maximum number of characters permitted in repeated text. + * @since 5.2.23 + */ + private static final int MAX_REPEATED_TEXT_SIZE = 256; + + public OpMultiply(int startPos, int endPos, SpelNodeImpl... operands) { super("*", startPos, endPos, operands); } @@ -109,10 +118,13 @@ else if (CodeFlow.isIntegerForNumericOp(leftNumber) || CodeFlow.isIntegerForNume } if (leftOperand instanceof String && rightOperand instanceof Integer) { - int repeats = (Integer) rightOperand; - StringBuilder result = new StringBuilder(); - for (int i = 0; i < repeats; i++) { - result.append(leftOperand); + String text = (String) leftOperand; + int count = (Integer) rightOperand; + int requestedSize = text.length() * count; + checkRepeatedTextSize(requestedSize); + StringBuilder result = new StringBuilder(requestedSize); + for (int i = 0; i < count; i++) { + result.append(text); } return new TypedValue(result.toString()); } @@ -120,6 +132,13 @@ else if (CodeFlow.isIntegerForNumericOp(leftNumber) || CodeFlow.isIntegerForNume return state.operate(Operation.MULTIPLY, leftOperand, rightOperand); } + private void checkRepeatedTextSize(int requestedSize) { + if (requestedSize > MAX_REPEATED_TEXT_SIZE) { + throw new SpelEvaluationException(getStartPosition(), + SpelMessage.MAX_REPEATED_TEXT_SIZE_EXCEEDED, MAX_REPEATED_TEXT_SIZE); + } + } + @Override public boolean isCompilable() { if (!getLeftOperand().isCompilable()) { diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpPlus.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpPlus.java index d128f56b2deb..365aa8399b29 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpPlus.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpPlus.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 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,8 @@ import org.springframework.expression.TypedValue; import org.springframework.expression.spel.CodeFlow; import org.springframework.expression.spel.ExpressionState; +import org.springframework.expression.spel.SpelEvaluationException; +import org.springframework.expression.spel.SpelMessage; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.NumberUtils; @@ -46,10 +48,18 @@ * @author Juergen Hoeller * @author Ivo Smid * @author Giovanni Dall'Oglio Risso + * @author Sam Brannen * @since 3.0 */ public class OpPlus extends Operator { + /** + * Maximum number of characters permitted in a concatenated string. + * @since 5.2.24 + */ + private static final int MAX_CONCATENATED_STRING_LENGTH = 100_000; + + public OpPlus(int startPos, int endPos, SpelNodeImpl... operands) { super("+", startPos, endPos, operands); Assert.notEmpty(operands, "Operands must not be empty"); @@ -123,22 +133,45 @@ else if (CodeFlow.isIntegerForNumericOp(leftNumber) || CodeFlow.isIntegerForNume if (leftOperand instanceof String && rightOperand instanceof String) { this.exitTypeDescriptor = "Ljava/lang/String"; - return new TypedValue((String) leftOperand + rightOperand); + String leftString = (String) leftOperand; + String rightString = (String) rightOperand; + checkStringLength(leftString); + checkStringLength(rightString); + return concatenate(leftString, rightString); } if (leftOperand instanceof String) { - return new TypedValue( - leftOperand + (rightOperand == null ? "null" : convertTypedValueToString(operandTwoValue, state))); + String leftString = (String) leftOperand; + checkStringLength(leftString); + String rightString = (rightOperand == null ? "null" : convertTypedValueToString(operandTwoValue, state)); + checkStringLength(rightString); + return concatenate(leftString, rightString); } if (rightOperand instanceof String) { - return new TypedValue( - (leftOperand == null ? "null" : convertTypedValueToString(operandOneValue, state)) + rightOperand); + String rightString = (String) rightOperand; + checkStringLength(rightString); + String leftString = (leftOperand == null ? "null" : convertTypedValueToString(operandOneValue, state)); + checkStringLength(leftString); + return concatenate(leftString, rightString); } return state.operate(Operation.ADD, leftOperand, rightOperand); } + private void checkStringLength(String string) { + if (string.length() > MAX_CONCATENATED_STRING_LENGTH) { + throw new SpelEvaluationException(getStartPosition(), + SpelMessage.MAX_CONCATENATED_STRING_LENGTH_EXCEEDED, MAX_CONCATENATED_STRING_LENGTH); + } + } + + private TypedValue concatenate(String leftString, String rightString) { + String result = leftString + rightString; + checkStringLength(result); + return new TypedValue(result); + } + @Override public String toStringAST() { if (this.children.length < 2) { // unary plus diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/OperatorMatches.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OperatorMatches.java index 22795a928796..de277c68eaf6 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/OperatorMatches.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OperatorMatches.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 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. @@ -36,19 +36,41 @@ * * @author Andy Clement * @author Juergen Hoeller + * @author Sam Brannen * @since 3.0 */ public class OperatorMatches extends Operator { private static final int PATTERN_ACCESS_THRESHOLD = 1000000; - private final ConcurrentMap patternCache = new ConcurrentHashMap<>(); + /** + * Maximum number of characters permitted in a regular expression. + * @since 5.2.23 + */ + private static final int MAX_REGEX_LENGTH = 1000; + private final ConcurrentMap patternCache; + + /** + * Create a new {@link OperatorMatches} instance. + * @deprecated as of Spring Framework 5.2.23 in favor of invoking + * {@link #OperatorMatches(ConcurrentMap, int, int, SpelNodeImpl...)} + * with a shared pattern cache instead + */ + @Deprecated public OperatorMatches(int startPos, int endPos, SpelNodeImpl... operands) { - super("matches", startPos, endPos, operands); + this(new ConcurrentHashMap<>(), startPos, endPos, operands); } + /** + * Create a new {@link OperatorMatches} instance with a shared pattern cache. + * @since 5.2.23 + */ + public OperatorMatches(ConcurrentMap patternCache, int startPos, int endPos, SpelNodeImpl... operands) { + super("matches", startPos, endPos, operands); + this.patternCache = patternCache; + } /** * Check the first operand matches the regex specified as the second operand. @@ -62,26 +84,28 @@ public OperatorMatches(int startPos, int endPos, SpelNodeImpl... operands) { public BooleanTypedValue getValueInternal(ExpressionState state) throws EvaluationException { SpelNodeImpl leftOp = getLeftOperand(); SpelNodeImpl rightOp = getRightOperand(); - String left = leftOp.getValue(state, String.class); - Object right = getRightOperand().getValue(state); - if (left == null) { + String input = leftOp.getValue(state, String.class); + if (input == null) { throw new SpelEvaluationException(leftOp.getStartPosition(), SpelMessage.INVALID_FIRST_OPERAND_FOR_MATCHES_OPERATOR, (Object) null); } + + Object right = rightOp.getValue(state); if (!(right instanceof String)) { throw new SpelEvaluationException(rightOp.getStartPosition(), SpelMessage.INVALID_SECOND_OPERAND_FOR_MATCHES_OPERATOR, right); } + String regex = (String) right; try { - String rightString = (String) right; - Pattern pattern = this.patternCache.get(rightString); + Pattern pattern = this.patternCache.get(regex); if (pattern == null) { - pattern = Pattern.compile(rightString); - this.patternCache.putIfAbsent(rightString, pattern); + checkRegexLength(regex); + pattern = Pattern.compile(regex); + this.patternCache.putIfAbsent(regex, pattern); } - Matcher matcher = pattern.matcher(new MatcherInput(left, new AccessCount())); + Matcher matcher = pattern.matcher(new MatcherInput(input, new AccessCount())); return BooleanTypedValue.forValue(matcher.matches()); } catch (PatternSyntaxException ex) { @@ -94,6 +118,13 @@ public BooleanTypedValue getValueInternal(ExpressionState state) throws Evaluati } } + private void checkRegexLength(String regex) { + if (regex.length() > MAX_REGEX_LENGTH) { + throw new SpelEvaluationException(getStartPosition(), + SpelMessage.MAX_REGEX_LENGTH_EXCEEDED, MAX_REGEX_LENGTH); + } + } + private static class AccessCount { @@ -111,7 +142,7 @@ private static class MatcherInput implements CharSequence { private final CharSequence value; - private AccessCount access; + private final AccessCount access; public MatcherInput(CharSequence value, AccessCount access) { this.value = value; diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/PropertyOrFieldReference.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/PropertyOrFieldReference.java index ec509199eb89..69389e4b4a14 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/PropertyOrFieldReference.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/PropertyOrFieldReference.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 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,6 +21,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.function.Supplier; import org.springframework.asm.Label; import org.springframework.asm.MethodVisitor; @@ -46,6 +47,7 @@ * @author Andy Clement * @author Juergen Hoeller * @author Clark Duplichien + * @author Sam Brannen * @since 3.0 */ public class PropertyOrFieldReference extends SpelNodeImpl { @@ -147,8 +149,12 @@ else if (Map.class == resultDescriptor.getType()) { } @Override - public void setValue(ExpressionState state, @Nullable Object newValue) throws EvaluationException { - writeProperty(state.getActiveContextObject(), state.getEvaluationContext(), this.name, newValue); + public TypedValue setValueInternal(ExpressionState state, Supplier valueSupplier) + throws EvaluationException { + + TypedValue typedValue = valueSupplier.get(); + writeProperty(state.getActiveContextObject(), state.getEvaluationContext(), this.name, typedValue.getValue()); + return typedValue; } @Override diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/SpelNodeImpl.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/SpelNodeImpl.java index 3976fbf3425d..daff26757839 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/SpelNodeImpl.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/SpelNodeImpl.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 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.lang.reflect.Constructor; import java.lang.reflect.Member; import java.lang.reflect.Method; +import java.util.function.Supplier; import org.springframework.asm.MethodVisitor; import org.springframework.asm.Opcodes; @@ -40,6 +41,7 @@ * * @author Andy Clement * @author Juergen Hoeller + * @author Sam Brannen * @since 3.0 */ public abstract class SpelNodeImpl implements SpelNode, Opcodes { @@ -64,7 +66,7 @@ public abstract class SpelNodeImpl implements SpelNode, Opcodes { *

    The descriptor is like the bytecode form but is slightly easier to work with. * It does not include the trailing semicolon (for non array reference types). * Some examples: Ljava/lang/String, I, [I - */ + */ @Nullable protected volatile String exitTypeDescriptor; @@ -83,8 +85,8 @@ public SpelNodeImpl(int startPos, int endPos, SpelNodeImpl... operands) { /** - * Return {@code true} if the next child is one of the specified classes. - */ + * Return {@code true} if the next child is one of the specified classes. + */ protected boolean nextChildIs(Class... classes) { if (this.parent != null) { SpelNodeImpl[] peers = this.parent.children; @@ -125,6 +127,28 @@ public boolean isWritable(ExpressionState expressionState) throws EvaluationExce @Override public void setValue(ExpressionState expressionState, @Nullable Object newValue) throws EvaluationException { + setValueInternal(expressionState, () -> new TypedValue(newValue)); + } + + /** + * Evaluate the expression to a node and then set the new value created by the + * specified {@link Supplier} on that node. + *

    For example, if the expression evaluates to a property reference, then the + * property will be set to the new value. + *

    Favor this method over {@link #setValue(ExpressionState, Object)} when + * the value should be lazily computed. + *

    By default, this method throws a {@link SpelEvaluationException}, + * effectively disabling this feature. Subclasses may override this method to + * provide an actual implementation. + * @param expressionState the current expression state (includes the context) + * @param valueSupplier a supplier of the new value + * @throws EvaluationException if any problem occurs evaluating the expression or + * setting the new value + * @since 5.2.24 + */ + public TypedValue setValueInternal(ExpressionState expressionState, Supplier valueSupplier) + throws EvaluationException { + throw new SpelEvaluationException(getStartPosition(), SpelMessage.SETVALUE_NOT_SUPPORTED, getClass()); } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/StringLiteral.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/StringLiteral.java index 3fc939c2b5a7..5bc43729ac83 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/StringLiteral.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/StringLiteral.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2022 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 @@ * * @author Andy Clement * @author Juergen Hoeller + * @author Sam Brannen * @since 3.0 */ public class StringLiteral extends Literal { @@ -36,9 +37,19 @@ public class StringLiteral extends Literal { public StringLiteral(String payload, int startPos, int endPos, String value) { super(payload, startPos, endPos); + // The original enclosing quote character for the string literal: ' or ". + char quoteCharacter = value.charAt(0); + + // Remove enclosing quotes String valueWithinQuotes = value.substring(1, value.length() - 1); - valueWithinQuotes = StringUtils.replace(valueWithinQuotes, "''", "'"); - valueWithinQuotes = StringUtils.replace(valueWithinQuotes, "\"\"", "\""); + + // Replace escaped internal quote characters + if (quoteCharacter == '\'') { + valueWithinQuotes = StringUtils.replace(valueWithinQuotes, "''", "'"); + } + else { + valueWithinQuotes = StringUtils.replace(valueWithinQuotes, "\"\"", "\""); + } this.value = new TypedValue(valueWithinQuotes); this.exitTypeDescriptor = "Ljava/lang/String"; @@ -52,7 +63,9 @@ public TypedValue getLiteralValue() { @Override public String toString() { - return "'" + getLiteralValue().getValue() + "'"; + String ast = String.valueOf(getLiteralValue().getValue()); + ast = StringUtils.replace(ast, "'", "''"); + return "'" + ast + "'"; } @Override diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/VariableReference.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/VariableReference.java index 769e4efedbea..97dae78e902e 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/VariableReference.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/VariableReference.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 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,9 +17,11 @@ package org.springframework.expression.spel.ast; import java.lang.reflect.Modifier; +import java.util.function.Supplier; import org.springframework.asm.MethodVisitor; import org.springframework.expression.EvaluationContext; +import org.springframework.expression.EvaluationException; import org.springframework.expression.TypedValue; import org.springframework.expression.spel.CodeFlow; import org.springframework.expression.spel.ExpressionState; @@ -27,10 +29,11 @@ import org.springframework.lang.Nullable; /** - * Represents a variable reference, eg. #someVar. Note this is different to a *local* - * variable like $someVar + * Represents a variable reference — for example, {@code #someVar}. Note + * that this is different than a local variable like {@code $someVar}. * * @author Andy Clement + * @author Sam Brannen * @since 3.0 */ public class VariableReference extends SpelNodeImpl { @@ -53,14 +56,14 @@ public VariableReference(String variableName, int startPos, int endPos) { @Override public ValueRef getValueRef(ExpressionState state) throws SpelEvaluationException { if (this.name.equals(THIS)) { - return new ValueRef.TypedValueHolderValueRef(state.getActiveContextObject(),this); + return new ValueRef.TypedValueHolderValueRef(state.getActiveContextObject(), this); } if (this.name.equals(ROOT)) { - return new ValueRef.TypedValueHolderValueRef(state.getRootContextObject(),this); + return new ValueRef.TypedValueHolderValueRef(state.getRootContextObject(), this); } TypedValue result = state.lookupVariable(this.name); // a null value will mean either the value was null or the variable was not found - return new VariableRef(this.name,result,state.getEvaluationContext()); + return new VariableRef(this.name, result, state.getEvaluationContext()); } @Override @@ -90,8 +93,10 @@ public TypedValue getValueInternal(ExpressionState state) throws SpelEvaluationE } @Override - public void setValue(ExpressionState state, @Nullable Object value) throws SpelEvaluationException { - state.setVariable(this.name, value); + public TypedValue setValueInternal(ExpressionState state, Supplier valueSupplier) + throws EvaluationException { + + return state.assignVariable(this.name, valueSupplier); } @Override diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/standard/InternalSpelExpressionParser.java b/spring-expression/src/main/java/org/springframework/expression/spel/standard/InternalSpelExpressionParser.java index 33c4d9db4eb3..cf135d08ed23 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/standard/InternalSpelExpressionParser.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/standard/InternalSpelExpressionParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 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,12 +21,15 @@ import java.util.Collections; import java.util.Deque; import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; import java.util.regex.Pattern; import org.springframework.expression.ParseException; import org.springframework.expression.ParserContext; import org.springframework.expression.common.TemplateAwareExpressionParser; import org.springframework.expression.spel.InternalParseException; +import org.springframework.expression.spel.SpelEvaluationException; import org.springframework.expression.spel.SpelMessage; import org.springframework.expression.spel.SpelParseException; import org.springframework.expression.spel.SpelParserConfiguration; @@ -74,7 +77,6 @@ import org.springframework.expression.spel.ast.TypeReference; import org.springframework.expression.spel.ast.VariableReference; import org.springframework.lang.Nullable; -import org.springframework.util.Assert; import org.springframework.util.StringUtils; /** @@ -83,18 +85,21 @@ * @author Andy Clement * @author Juergen Hoeller * @author Phillip Webb + * @author Sam Brannen * @since 3.0 */ class InternalSpelExpressionParser extends TemplateAwareExpressionParser { private static final Pattern VALID_QUALIFIED_ID_PATTERN = Pattern.compile("[\\p{L}\\p{N}_$]+"); - private final SpelParserConfiguration configuration; // For rules that build nodes, they are stacked here for return private final Deque constructedNodes = new ArrayDeque<>(); + // Shared cache for compiled regex patterns + private final ConcurrentMap patternCache = new ConcurrentHashMap<>(); + // The expression being parsed private String expressionString = ""; @@ -121,6 +126,8 @@ public InternalSpelExpressionParser(SpelParserConfiguration configuration) { protected SpelExpression doParseExpression(String expressionString, @Nullable ParserContext context) throws ParseException { + checkExpressionLength(expressionString); + try { this.expressionString = expressionString; Tokenizer tokenizer = new Tokenizer(expressionString); @@ -129,12 +136,13 @@ protected SpelExpression doParseExpression(String expressionString, @Nullable Pa this.tokenStreamPointer = 0; this.constructedNodes.clear(); SpelNodeImpl ast = eatExpression(); - Assert.state(ast != null, "No node"); + if (ast == null) { + throw new SpelParseException(this.expressionString, 0, SpelMessage.OOD); + } Token t = peekToken(); if (t != null) { - throw new SpelParseException(t.startPos, SpelMessage.MORE_INPUT, toString(nextToken())); + throw new SpelParseException(this.expressionString, t.startPos, SpelMessage.MORE_INPUT, toString(nextToken())); } - Assert.isTrue(this.constructedNodes.isEmpty(), "At least one node expected"); return new SpelExpression(expressionString, ast, this.configuration); } catch (InternalParseException ex) { @@ -142,6 +150,13 @@ protected SpelExpression doParseExpression(String expressionString, @Nullable Pa } } + private void checkExpressionLength(String string) { + int maxLength = this.configuration.getMaximumExpressionLength(); + if (string.length() > maxLength) { + throw new SpelEvaluationException(SpelMessage.MAX_EXPRESSION_LENGTH_EXCEEDED, maxLength); + } + } + // expression // : logicalOrExpression // ( (ASSIGN^ logicalOrExpression) @@ -239,20 +254,20 @@ private SpelNodeImpl eatRelationalExpression() { if (tk == TokenKind.EQ) { return new OpEQ(t.startPos, t.endPos, expr, rhExpr); } - Assert.isTrue(tk == TokenKind.NE, "Not-equals token expected"); - return new OpNE(t.startPos, t.endPos, expr, rhExpr); + if (tk == TokenKind.NE) { + return new OpNE(t.startPos, t.endPos, expr, rhExpr); + } } if (tk == TokenKind.INSTANCEOF) { return new OperatorInstanceof(t.startPos, t.endPos, expr, rhExpr); } - if (tk == TokenKind.MATCHES) { - return new OperatorMatches(t.startPos, t.endPos, expr, rhExpr); + return new OperatorMatches(this.patternCache, t.startPos, t.endPos, expr, rhExpr); + } + if (tk == TokenKind.BETWEEN) { + return new OperatorBetween(t.startPos, t.endPos, expr, rhExpr); } - - Assert.isTrue(tk == TokenKind.BETWEEN, "Between token expected"); - return new OperatorBetween(t.startPos, t.endPos, expr, rhExpr); } return expr; } @@ -289,8 +304,7 @@ private SpelNodeImpl eatProductExpression() { else if (t.kind == TokenKind.DIV) { expr = new OpDivide(t.startPos, t.endPos, expr, rhExpr); } - else { - Assert.isTrue(t.kind == TokenKind.MOD, "Mod token expected"); + else if (t.kind == TokenKind.MOD) { expr = new OpModulus(t.startPos, t.endPos, expr, rhExpr); } } @@ -320,18 +334,21 @@ private SpelNodeImpl eatPowerIncDecExpression() { // unaryExpression: (PLUS^ | MINUS^ | BANG^ | INC^ | DEC^) unaryExpression | primaryExpression ; @Nullable private SpelNodeImpl eatUnaryExpression() { - if (peekToken(TokenKind.PLUS, TokenKind.MINUS, TokenKind.NOT)) { + if (peekToken(TokenKind.NOT, TokenKind.PLUS, TokenKind.MINUS)) { Token t = takeToken(); SpelNodeImpl expr = eatUnaryExpression(); - Assert.state(expr != null, "No node"); + if (expr == null) { + throw internalException(t.startPos, SpelMessage.OOD); + } if (t.kind == TokenKind.NOT) { return new OperatorNot(t.startPos, t.endPos, expr); } if (t.kind == TokenKind.PLUS) { return new OpPlus(t.startPos, t.endPos, expr); } - Assert.isTrue(t.kind == TokenKind.MINUS, "Minus token expected"); - return new OpMinus(t.startPos, t.endPos, expr); + if (t.kind == TokenKind.MINUS) { + return new OpMinus(t.startPos, t.endPos, expr); + } } if (peekToken(TokenKind.INC, TokenKind.DEC)) { Token t = takeToken(); @@ -339,7 +356,9 @@ private SpelNodeImpl eatUnaryExpression() { if (t.getKind() == TokenKind.INC) { return new OpInc(t.startPos, t.endPos, false, expr); } - return new OpDec(t.startPos, t.endPos, false, expr); + if (t.kind == TokenKind.DEC) { + return new OpDec(t.startPos, t.endPos, false, expr); + } } return eatPrimaryExpression(); } @@ -399,7 +418,6 @@ private SpelNodeImpl eatDottedNode() { return pop(); } if (peekToken() == null) { - // unexpectedly ran out of data throw internalException(t.startPos, SpelMessage.OOD); } else { @@ -445,8 +463,7 @@ private SpelNodeImpl[] maybeEatMethodArgs() { private void eatConstructorArgs(List accumulatedArguments) { if (!peekToken(TokenKind.LPAREN)) { - throw new InternalParseException(new SpelParseException(this.expressionString, - positionOf(peekToken()), SpelMessage.MISSING_CONSTRUCTOR_ARGS)); + throw internalException(positionOf(peekToken()), SpelMessage.MISSING_CONSTRUCTOR_ARGS); } consumeArguments(accumulatedArguments); eatToken(TokenKind.RPAREN); @@ -457,7 +474,9 @@ private void eatConstructorArgs(List accumulatedArguments) { */ private void consumeArguments(List accumulatedArguments) { Token t = peekToken(); - Assert.state(t != null, "Expected token"); + if (t == null) { + return; + } int pos = t.startPos; Token next; do { @@ -560,8 +579,7 @@ else if (peekToken(TokenKind.LITERAL_STRING)) { private boolean maybeEatTypeReference() { if (peekToken(TokenKind.IDENTIFIER)) { Token typeName = peekToken(); - Assert.state(typeName != null, "Expected token"); - if (!"T".equals(typeName.stringValue())) { + if (typeName == null || !"T".equals(typeName.stringValue())) { return false; } // It looks like a type reference but is T being used as a map key? @@ -590,8 +608,7 @@ private boolean maybeEatTypeReference() { private boolean maybeEatNullReference() { if (peekToken(TokenKind.IDENTIFIER)) { Token nullToken = peekToken(); - Assert.state(nullToken != null, "Expected token"); - if (!"null".equalsIgnoreCase(nullToken.stringValue())) { + if (nullToken == null || !"null".equalsIgnoreCase(nullToken.stringValue())) { return false; } nextToken(); @@ -604,12 +621,13 @@ private boolean maybeEatNullReference() { //projection: PROJECT^ expression RCURLY!; private boolean maybeEatProjection(boolean nullSafeNavigation) { Token t = peekToken(); - if (!peekToken(TokenKind.PROJECT, true)) { + if (t == null || !peekToken(TokenKind.PROJECT, true)) { return false; } - Assert.state(t != null, "No token"); SpelNodeImpl expr = eatExpression(); - Assert.state(expr != null, "No node"); + if (expr == null) { + throw internalException(t.startPos, SpelMessage.OOD); + } eatToken(TokenKind.RSQUARE); this.constructedNodes.push(new Projection(nullSafeNavigation, t.startPos, t.endPos, expr)); return true; @@ -619,15 +637,13 @@ private boolean maybeEatProjection(boolean nullSafeNavigation) { // map = LCURLY (key ':' value (COMMA key ':' value)*) RCURLY private boolean maybeEatInlineListOrMap() { Token t = peekToken(); - if (!peekToken(TokenKind.LCURLY, true)) { + if (t == null || !peekToken(TokenKind.LCURLY, true)) { return false; } - Assert.state(t != null, "No token"); SpelNodeImpl expr = null; Token closingCurly = peekToken(); - if (peekToken(TokenKind.RCURLY, true)) { + if (closingCurly != null && peekToken(TokenKind.RCURLY, true)) { // empty list '{}' - Assert.state(closingCurly != null, "No token"); expr = new InlineList(t.startPos, closingCurly.endPos); } else if (peekToken(TokenKind.COLON, true)) { @@ -680,12 +696,13 @@ else if (peekToken(TokenKind.COLON, true)) { // map! private boolean maybeEatIndexer() { Token t = peekToken(); - if (!peekToken(TokenKind.LSQUARE, true)) { + if (t == null || !peekToken(TokenKind.LSQUARE, true)) { return false; } - Assert.state(t != null, "No token"); SpelNodeImpl expr = eatExpression(); - Assert.state(expr != null, "No node"); + if (expr == null) { + throw internalException(t.startPos, SpelMessage.MISSING_SELECTION_EXPRESSION); + } eatToken(TokenKind.RSQUARE); this.constructedNodes.push(new Indexer(t.startPos, t.endPos, expr)); return true; @@ -693,10 +710,9 @@ private boolean maybeEatIndexer() { private boolean maybeEatSelection(boolean nullSafeNavigation) { Token t = peekToken(); - if (!peekSelectToken()) { + if (t == null || !peekSelectToken()) { return false; } - Assert.state(t != null, "No token"); nextToken(); SpelNodeImpl expr = eatExpression(); if (expr == null) { @@ -874,9 +890,14 @@ else if (t.kind == TokenKind.LITERAL_STRING) { //parenExpr : LPAREN! expression RPAREN!; private boolean maybeEatParenExpression() { if (peekToken(TokenKind.LPAREN)) { - nextToken(); + Token t = nextToken(); + if (t == null) { + return false; + } SpelNodeImpl expr = eatExpression(); - Assert.state(expr != null, "No node"); + if (expr == null) { + throw internalException(t.startPos, SpelMessage.OOD); + } eatToken(TokenKind.RPAREN); push(expr); return true; diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/standard/SpelExpressionParser.java b/spring-expression/src/main/java/org/springframework/expression/spel/standard/SpelExpressionParser.java index a8702b41bd65..ee64ba4da2e0 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/standard/SpelExpressionParser.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/standard/SpelExpressionParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2023 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. @@ -28,6 +28,7 @@ * * @author Andy Clement * @author Juergen Hoeller + * @author Sam Brannen * @since 3.0 */ public class SpelExpressionParser extends TemplateAwareExpressionParser { @@ -53,6 +54,7 @@ public SpelExpressionParser(SpelParserConfiguration configuration) { public SpelExpression parseRaw(String expressionString) throws ParseException { + Assert.hasText(expressionString, "'expressionString' must not be null or blank"); return doParseExpression(expressionString, null); } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/standard/Token.java b/spring-expression/src/main/java/org/springframework/expression/spel/standard/Token.java index dda0ff2152c0..f00f26a30037 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/standard/Token.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/standard/Token.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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. @@ -65,7 +65,7 @@ public boolean isIdentifier() { public boolean isNumericRelationalOperator() { return (this.kind == TokenKind.GT || this.kind == TokenKind.GE || this.kind == TokenKind.LT || - this.kind == TokenKind.LE || this.kind==TokenKind.EQ || this.kind==TokenKind.NE); + this.kind == TokenKind.LE || this.kind == TokenKind.EQ || this.kind == TokenKind.NE); } public String stringValue() { @@ -87,14 +87,14 @@ public Token asBetweenToken() { @Override public String toString() { - StringBuilder s = new StringBuilder(); - s.append('[').append(this.kind.toString()); + StringBuilder sb = new StringBuilder(); + sb.append('[').append(this.kind); if (this.kind.hasPayload()) { - s.append(':').append(this.data); + sb.append(':').append(this.data); } - s.append(']'); - s.append('(').append(this.startPos).append(',').append(this.endPos).append(')'); - return s.toString(); + sb.append(']'); + sb.append('(').append(this.startPos).append(',').append(this.endPos).append(')'); + return sb.toString(); } } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectiveMethodResolver.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectiveMethodResolver.java index 86889dca73e2..051ec108cab5 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectiveMethodResolver.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectiveMethodResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -48,6 +48,7 @@ * @author Andy Clement * @author Juergen Hoeller * @author Chris Beams + * @author Sam Brannen * @since 3.0 * @see StandardEvaluationContext#addMethodResolver(MethodResolver) */ @@ -116,6 +117,7 @@ public MethodExecutor resolve(EvaluationContext context, Object targetObject, St TypeConverter typeConverter = context.getTypeConverter(); Class type = (targetObject instanceof Class ? (Class) targetObject : targetObject.getClass()); ArrayList methods = new ArrayList<>(getMethods(type, targetObject)); + methods.removeIf(method -> !method.getName().equals(name)); // If a filter is registered for this type, call it MethodFilter filter = (this.filters != null ? this.filters.get(type) : null); @@ -159,48 +161,46 @@ else if (m1.isVarArgs() && !m2.isVarArgs()) { boolean multipleOptions = false; for (Method method : methodsToIterate) { - if (method.getName().equals(name)) { - int paramCount = method.getParameterCount(); - List paramDescriptors = new ArrayList<>(paramCount); - for (int i = 0; i < paramCount; i++) { - paramDescriptors.add(new TypeDescriptor(new MethodParameter(method, i))); - } - ReflectionHelper.ArgumentsMatchInfo matchInfo = null; - if (method.isVarArgs() && argumentTypes.size() >= (paramCount - 1)) { - // *sigh* complicated - matchInfo = ReflectionHelper.compareArgumentsVarargs(paramDescriptors, argumentTypes, typeConverter); - } - else if (paramCount == argumentTypes.size()) { - // Name and parameter number match, check the arguments - matchInfo = ReflectionHelper.compareArguments(paramDescriptors, argumentTypes, typeConverter); + int paramCount = method.getParameterCount(); + List paramDescriptors = new ArrayList<>(paramCount); + for (int i = 0; i < paramCount; i++) { + paramDescriptors.add(new TypeDescriptor(new MethodParameter(method, i))); + } + ReflectionHelper.ArgumentsMatchInfo matchInfo = null; + if (method.isVarArgs() && argumentTypes.size() >= (paramCount - 1)) { + // *sigh* complicated + matchInfo = ReflectionHelper.compareArgumentsVarargs(paramDescriptors, argumentTypes, typeConverter); + } + else if (paramCount == argumentTypes.size()) { + // Name and parameter number match, check the arguments + matchInfo = ReflectionHelper.compareArguments(paramDescriptors, argumentTypes, typeConverter); + } + if (matchInfo != null) { + if (matchInfo.isExactMatch()) { + return new ReflectiveMethodExecutor(method, type); } - if (matchInfo != null) { - if (matchInfo.isExactMatch()) { - return new ReflectiveMethodExecutor(method, type); - } - else if (matchInfo.isCloseMatch()) { - if (this.useDistance) { - int matchDistance = ReflectionHelper.getTypeDifferenceWeight(paramDescriptors, argumentTypes); - if (closeMatch == null || matchDistance < closeMatchDistance) { - // This is a better match... - closeMatch = method; - closeMatchDistance = matchDistance; - } - } - else { - // Take this as a close match if there isn't one already - if (closeMatch == null) { - closeMatch = method; - } + else if (matchInfo.isCloseMatch()) { + if (this.useDistance) { + int matchDistance = ReflectionHelper.getTypeDifferenceWeight(paramDescriptors, argumentTypes); + if (closeMatch == null || matchDistance < closeMatchDistance) { + // This is a better match... + closeMatch = method; + closeMatchDistance = matchDistance; } } - else if (matchInfo.isMatchRequiringConversion()) { - if (matchRequiringConversion != null) { - multipleOptions = true; + else { + // Take this as a close match if there isn't one already + if (closeMatch == null) { + closeMatch = method; } - matchRequiringConversion = method; } } + else if (matchInfo.isMatchRequiringConversion()) { + if (matchRequiringConversion != null) { + multipleOptions = true; + } + matchRequiringConversion = method; + } } } if (closeMatch != null) { @@ -225,8 +225,7 @@ private Set getMethods(Class type, Object targetObject) { if (targetObject instanceof Class) { Set result = new LinkedHashSet<>(); // Add these so that static methods are invocable on the type: e.g. Float.valueOf(..) - Method[] methods = getMethods(type); - for (Method method : methods) { + for (Method method : getMethods(type)) { if (Modifier.isStatic(method.getModifiers())) { result.add(method); } @@ -239,19 +238,23 @@ else if (Proxy.isProxyClass(type)) { Set result = new LinkedHashSet<>(); // Expose interface methods (not proxy-declared overrides) for proper vararg introspection for (Class ifc : type.getInterfaces()) { - Method[] methods = getMethods(ifc); - for (Method method : methods) { + for (Method method : getMethods(ifc)) { if (isCandidateForInvocation(method, type)) { result.add(method); } } } + // Ensure methods defined in java.lang.Object are exposed for JDK proxies. + for (Method method : getMethods(Object.class)) { + if (isCandidateForInvocation(method, type)) { + result.add(method); + } + } return result; } else { Set result = new LinkedHashSet<>(); - Method[] methods = getMethods(type); - for (Method method : methods) { + for (Method method : getMethods(type)) { if (isCandidateForInvocation(method, type)) { result.add(method); } @@ -276,7 +279,7 @@ protected Method[] getMethods(Class type) { * Determine whether the given {@code Method} is a candidate for method resolution * on an instance of the given target class. *

    The default implementation considers any method as a candidate, even for - * static methods sand non-user-declared methods on the {@link Object} base class. + * static methods and non-user-declared methods on the {@link Object} base class. * @param method the Method to evaluate * @param targetClass the concrete target class that is being introspected * @since 4.3.15 diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/SimpleEvaluationContext.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/SimpleEvaluationContext.java index d8826e344720..f10dfa3718d0 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/support/SimpleEvaluationContext.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/SimpleEvaluationContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-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,6 +21,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.function.Supplier; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.TypeDescriptor; @@ -50,25 +51,26 @@ * SpEL language syntax, e.g. excluding references to Java types, constructors, * and bean references. * - *

    When creating a {@code SimpleEvaluationContext} you need to choose the - * level of support that you need for property access in SpEL expressions: + *

    When creating a {@code SimpleEvaluationContext} you need to choose the level of + * support that you need for data binding in SpEL expressions: *

      - *
    • A custom {@code PropertyAccessor} (typically not reflection-based), - * potentially combined with a {@link DataBindingPropertyAccessor}
    • - *
    • Data binding properties for read-only access
    • - *
    • Data binding properties for read and write
    • + *
    • Data binding for read-only access
    • + *
    • Data binding for read and write access
    • + *
    • A custom {@code PropertyAccessor} (typically not reflection-based), potentially + * combined with a {@link DataBindingPropertyAccessor}
    • *
    * - *

    Conveniently, {@link SimpleEvaluationContext#forReadOnlyDataBinding()} - * enables read access to properties via {@link DataBindingPropertyAccessor}; - * same for {@link SimpleEvaluationContext#forReadWriteDataBinding()} when - * write access is needed as well. Alternatively, configure custom accessors - * via {@link SimpleEvaluationContext#forPropertyAccessors}, and potentially + *

    Conveniently, {@link SimpleEvaluationContext#forReadOnlyDataBinding()} enables + * read-only access to properties via {@link DataBindingPropertyAccessor}. Similarly, + * {@link SimpleEvaluationContext#forReadWriteDataBinding()} enables read and write access + * to properties. Alternatively, configure custom accessors via + * {@link SimpleEvaluationContext#forPropertyAccessors}, potentially + * {@linkplain Builder#withAssignmentDisabled() disable assignment}, and optionally * activate method resolution and/or a type converter through the builder. * *

    Note that {@code SimpleEvaluationContext} is typically not configured * with a default root object. Instead it is meant to be created once and - * used repeatedly through {@code getValue} calls on a pre-compiled + * used repeatedly through {@code getValue} calls on a predefined * {@link org.springframework.expression.Expression} with both an * {@code EvaluationContext} and a root object as arguments: * {@link org.springframework.expression.Expression#getValue(EvaluationContext, Object)}. @@ -78,10 +80,11 @@ * * @author Rossen Stoyanchev * @author Juergen Hoeller + * @author Sam Brannen * @since 4.3.15 - * @see #forPropertyAccessors * @see #forReadOnlyDataBinding() * @see #forReadWriteDataBinding() + * @see #forPropertyAccessors * @see StandardEvaluationContext * @see StandardTypeConverter * @see DataBindingPropertyAccessor @@ -107,14 +110,17 @@ public final class SimpleEvaluationContext implements EvaluationContext { private final Map variables = new HashMap<>(); + private final boolean assignmentEnabled; + private SimpleEvaluationContext(List accessors, List resolvers, - @Nullable TypeConverter converter, @Nullable TypedValue rootObject) { + @Nullable TypeConverter converter, @Nullable TypedValue rootObject, boolean assignmentEnabled) { this.propertyAccessors = accessors; this.methodResolvers = resolvers; this.typeConverter = (converter != null ? converter : new StandardTypeConverter()); this.rootObject = (rootObject != null ? rootObject : TypedValue.NULL); + this.assignmentEnabled = assignmentEnabled; } @@ -200,6 +206,17 @@ public OperatorOverloader getOperatorOverloader() { return this.operatorOverloader; } + /** + * {@code SimpleEvaluationContext} does not support variable assignment within + * expressions. + * @throws SpelEvaluationException with {@link SpelMessage#VARIABLE_ASSIGNMENT_NOT_SUPPORTED} + * @since 5.2.24 + */ + @Override + public TypedValue assignVariable(String name, Supplier valueSupplier) { + throw new SpelEvaluationException(SpelMessage.VARIABLE_ASSIGNMENT_NOT_SUPPORTED, "#" + name); + } + @Override public void setVariable(String name, @Nullable Object value) { this.variables.put(name, value); @@ -211,15 +228,35 @@ public Object lookupVariable(String name) { return this.variables.get(name); } + /** + * Determine if assignment is enabled within expressions evaluated by this evaluation + * context. + *

    If this method returns {@code false}, the assignment ({@code =}), increment + * ({@code ++}), and decrement ({@code --}) operators are disabled. + * @return {@code true} if assignment is enabled; {@code false} otherwise + * @since 5.3.38 + * @see #forReadOnlyDataBinding() + * @see Builder#withAssignmentDisabled() + */ + @Override + public boolean isAssignmentEnabled() { + return this.assignmentEnabled; + } /** * Create a {@code SimpleEvaluationContext} for the specified {@link PropertyAccessor} - * delegates: typically a custom {@code PropertyAccessor} specific to a use case - * (e.g. attribute resolution in a custom data structure), potentially combined with - * a {@link DataBindingPropertyAccessor} if property dereferences are needed as well. + * delegates: typically a custom {@code PropertyAccessor} specific to a use case — + * for example, for attribute resolution in a custom data structure — potentially + * combined with a {@link DataBindingPropertyAccessor} if property dereferences are + * needed as well. + *

    By default, assignment is enabled within expressions evaluated by the context + * created via this factory method; however, assignment can be disabled via + * {@link Builder#withAssignmentDisabled()}. * @param accessors the accessor delegates to use * @see DataBindingPropertyAccessor#forReadOnlyAccess() * @see DataBindingPropertyAccessor#forReadWriteAccess() + * @see #isAssignmentEnabled() + * @see Builder#withAssignmentDisabled() */ public static Builder forPropertyAccessors(PropertyAccessor... accessors) { for (PropertyAccessor accessor : accessors) { @@ -234,18 +271,28 @@ public static Builder forPropertyAccessors(PropertyAccessor... accessors) { /** * Create a {@code SimpleEvaluationContext} for read-only access to * public properties via {@link DataBindingPropertyAccessor}. + *

    Assignment is disabled within expressions evaluated by the context created via + * this factory method. * @see DataBindingPropertyAccessor#forReadOnlyAccess() * @see #forPropertyAccessors + * @see #isAssignmentEnabled() + * @see Builder#withAssignmentDisabled() */ public static Builder forReadOnlyDataBinding() { - return new Builder(DataBindingPropertyAccessor.forReadOnlyAccess()); + return new Builder(DataBindingPropertyAccessor.forReadOnlyAccess()).withAssignmentDisabled(); } /** * Create a {@code SimpleEvaluationContext} for read-write access to * public properties via {@link DataBindingPropertyAccessor}. + *

    By default, assignment is enabled within expressions evaluated by the context + * created via this factory method. Assignment can be disabled via + * {@link Builder#withAssignmentDisabled()}; however, it is preferable to use + * {@link #forReadOnlyDataBinding()} if you desire read-only access. * @see DataBindingPropertyAccessor#forReadWriteAccess() * @see #forPropertyAccessors + * @see #isAssignmentEnabled() + * @see Builder#withAssignmentDisabled() */ public static Builder forReadWriteDataBinding() { return new Builder(DataBindingPropertyAccessor.forReadWriteAccess()); @@ -255,7 +302,7 @@ public static Builder forReadWriteDataBinding() { /** * Builder for {@code SimpleEvaluationContext}. */ - public static class Builder { + public static final class Builder { private final List accessors; @@ -267,10 +314,24 @@ public static class Builder { @Nullable private TypedValue rootObject; - public Builder(PropertyAccessor... accessors) { + private boolean assignmentEnabled = true; + + + private Builder(PropertyAccessor... accessors) { this.accessors = Arrays.asList(accessors); } + + /** + * Disable assignment within expressions evaluated by this evaluation context. + * @since 5.3.38 + * @see SimpleEvaluationContext#isAssignmentEnabled() + */ + public Builder withAssignmentDisabled() { + this.assignmentEnabled = false; + return this; + } + /** * Register the specified {@link MethodResolver} delegates for * a combination of property access and method resolution. @@ -302,7 +363,6 @@ public Builder withInstanceMethods() { return this; } - /** * Register a custom {@link ConversionService}. *

    By default a {@link StandardTypeConverter} backed by a @@ -314,6 +374,7 @@ public Builder withConversionService(ConversionService conversionService) { this.typeConverter = new StandardTypeConverter(conversionService); return this; } + /** * Register a custom {@link TypeConverter}. *

    By default a {@link StandardTypeConverter} backed by a @@ -349,7 +410,8 @@ public Builder withTypedRootObject(Object rootObject, TypeDescriptor typeDescrip } public SimpleEvaluationContext build() { - return new SimpleEvaluationContext(this.accessors, this.resolvers, this.typeConverter, this.rootObject); + return new SimpleEvaluationContext(this.accessors, this.resolvers, this.typeConverter, this.rootObject, + this.assignmentEnabled); } } diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/AbstractExpressionTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/AbstractExpressionTests.java index 0e133522f621..1f6013d16f16 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/AbstractExpressionTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/AbstractExpressionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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. @@ -35,7 +35,7 @@ */ public abstract class AbstractExpressionTests { - private static final boolean DEBUG = false; + protected static final boolean DEBUG = false; protected static final boolean SHOULD_BE_WRITABLE = true; @@ -164,6 +164,24 @@ protected void evaluateAndCheckError(String expression, SpelMessage expectedMess */ protected void evaluateAndCheckError(String expression, Class expectedReturnType, SpelMessage expectedMessage, Object... otherProperties) { + + evaluateAndCheckError(this.parser, expression, expectedReturnType, expectedMessage, otherProperties); + } + + /** + * Evaluate the specified expression and ensure the expected message comes out. + * The message may have inserts and they will be checked if otherProperties is specified. + * The first entry in otherProperties should always be the position. + * @param parser the expression parser to use + * @param expression the expression to evaluate + * @param expectedReturnType ask the expression return value to be of this type if possible + * ({@code null} indicates don't ask for conversion) + * @param expectedMessage the expected message + * @param otherProperties the expected inserts within the message + */ + protected void evaluateAndCheckError(ExpressionParser parser, String expression, Class expectedReturnType, SpelMessage expectedMessage, + Object... otherProperties) { + assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> { Expression expr = parser.parseExpression(expression); assertThat(expr).as("expression").isNotNull(); @@ -202,7 +220,9 @@ protected void evaluateAndCheckError(String expression, Class expectedReturnT protected void parseAndCheckError(String expression, SpelMessage expectedMessage, Object... otherProperties) { assertThatExceptionOfType(SpelParseException.class).isThrownBy(() -> { Expression expr = parser.parseExpression(expression); - SpelUtilities.printAbstractSyntaxTree(System.out, expr); + if (DEBUG) { + SpelUtilities.printAbstractSyntaxTree(System.out, expr); + } }).satisfies(ex -> { assertThat(ex.getMessageCode()).isEqualTo(expectedMessage); if (otherProperties != null && otherProperties.length != 0) { diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/ArrayConstructorTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/ArrayConstructorTests.java index 252e532e748f..0fd3c394c380 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/ArrayConstructorTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/ArrayConstructorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-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,17 +18,21 @@ import org.junit.jupiter.api.Test; +import org.springframework.expression.EvaluationContext; import org.springframework.expression.Expression; import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.SimpleEvaluationContext; import org.springframework.util.ObjectUtils; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; /** * Test construction of arrays. * * @author Andy Clement * @author Sam Brannen + * @author Juergen Hoeller */ class ArrayConstructorTests extends AbstractExpressionTests { @@ -97,7 +101,7 @@ void errorCases() { void typeArrayConstructors() { evaluate("new String[]{'a','b','c','d'}[1]", "b", String.class); evaluateAndCheckError("new String[]{'a','b','c','d'}.size()", SpelMessage.METHOD_NOT_FOUND, 30, "size()", - "java.lang.String[]"); + "java.lang.String[]"); evaluate("new String[]{'a','b','c','d'}.length", 4, Integer.class); } @@ -110,10 +114,18 @@ void basicArray() { void multiDimensionalArrays() { evaluate("new String[2][2]", "[Ljava.lang.String;[2]{[2]{null,null},[2]{null,null}}", String[][].class); evaluate("new String[3][2][1]", - "[[Ljava.lang.String;[3]{[2]{[1]{null},[1]{null}},[2]{[1]{null},[1]{null}},[2]{[1]{null},[1]{null}}}", - String[][][].class); + "[[Ljava.lang.String;[3]{[2]{[1]{null},[1]{null}},[2]{[1]{null},[1]{null}},[2]{[1]{null},[1]{null}}}", + String[][][].class); } + @Test + void noArrayConstruction() { + EvaluationContext context = SimpleEvaluationContext.forReadWriteDataBinding().build(); + assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> + parser.parseExpression("new int[2]").getValue(context)); + } + + private void evaluateArrayBuildingExpression(String expression, String expectedToString) { SpelExpressionParser parser = new SpelExpressionParser(); Expression e = parser.parseExpression(expression); diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/CompilableMapAccessor.java b/spring-expression/src/test/java/org/springframework/expression/spel/CompilableMapAccessor.java new file mode 100644 index 000000000000..167929ffd629 --- /dev/null +++ b/spring-expression/src/test/java/org/springframework/expression/spel/CompilableMapAccessor.java @@ -0,0 +1,117 @@ +/* + * Copyright 2002-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.expression.spel; + +import java.util.Map; + +import org.springframework.asm.MethodVisitor; +import org.springframework.expression.AccessException; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.TypedValue; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * This is a local COPY of {@link org.springframework.context.expression.MapAccessor}. + * + * @author Juergen Hoeller + * @author Andy Clement + * @since 4.1 + */ +public class CompilableMapAccessor implements CompilablePropertyAccessor { + + @Override + public Class[] getSpecificTargetClasses() { + return new Class[] {Map.class}; + } + + @Override + public boolean canRead(EvaluationContext context, @Nullable Object target, String name) throws AccessException { + return (target instanceof Map && ((Map) target).containsKey(name)); + } + + @Override + public TypedValue read(EvaluationContext context, @Nullable Object target, String name) throws AccessException { + Assert.state(target instanceof Map, "Target must be of type Map"); + Map map = (Map) target; + Object value = map.get(name); + if (value == null && !map.containsKey(name)) { + throw new MapAccessException(name); + } + return new TypedValue(value); + } + + @Override + public boolean canWrite(EvaluationContext context, @Nullable Object target, String name) throws AccessException { + return true; + } + + @Override + @SuppressWarnings("unchecked") + public void write(EvaluationContext context, @Nullable Object target, String name, @Nullable Object newValue) + throws AccessException { + + Assert.state(target instanceof Map, "Target must be a Map"); + Map map = (Map) target; + map.put(name, newValue); + } + + @Override + public boolean isCompilable() { + return true; + } + + @Override + public Class getPropertyType() { + return Object.class; + } + + @Override + public void generateCode(String propertyName, MethodVisitor mv, CodeFlow cf) { + String descriptor = cf.lastDescriptor(); + if (descriptor == null || !descriptor.equals("Ljava/util/Map")) { + if (descriptor == null) { + cf.loadTarget(mv); + } + CodeFlow.insertCheckCast(mv, "Ljava/util/Map"); + } + mv.visitLdcInsn(propertyName); + mv.visitMethodInsn(INVOKEINTERFACE, "java/util/Map", "get","(Ljava/lang/Object;)Ljava/lang/Object;",true); + } + + + /** + * Exception thrown from {@code read} in order to reset a cached + * PropertyAccessor, allowing other accessors to have a try. + */ + @SuppressWarnings("serial") + private static class MapAccessException extends AccessException { + + private final String key; + + public MapAccessException(String key) { + super(""); + this.key = key; + } + + @Override + public String getMessage() { + return "Map does not contain a value for key '" + this.key + "'"; + } + } + +} diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/EvaluationTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/EvaluationTests.java index 166a46783e55..f3feba3d2a79 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/EvaluationTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/EvaluationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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,8 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; import org.springframework.expression.AccessException; import org.springframework.expression.BeanResolver; @@ -36,6 +38,7 @@ import org.springframework.expression.ParseException; import org.springframework.expression.spel.standard.SpelExpression; import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.SimpleEvaluationContext; import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.expression.spel.support.StandardTypeLocator; import org.springframework.expression.spel.testresources.TestPerson; @@ -60,6 +63,40 @@ class EvaluationTests extends AbstractExpressionTests { @Nested class MiscellaneousTests { + @Test + void expressionLength() { + String expression = String.format("'X' + '%s'", repeat(" ", 9_992)); + assertThat(expression).hasSize(10_000); + Expression expr = parser.parseExpression(expression); + String result = expr.getValue(context, String.class); + assertThat(result).hasSize(9_993); + assertThat(result.trim()).isEqualTo("X"); + + expression = String.format("'X' + '%s'", repeat(" ", 9_993)); + assertThat(expression).hasSize(10_001); + evaluateAndCheckError(expression, String.class, SpelMessage.MAX_EXPRESSION_LENGTH_EXCEEDED); + } + + @Test + void maxExpressionLengthIsConfigurable() { + int maximumExpressionLength = 20_000; + + String expression = String.format("'%s'", repeat("Y", 19_998)); + assertThat(expression).hasSize(maximumExpressionLength); + + SpelParserConfiguration configuration = + new SpelParserConfiguration(null, null, false, false, 0, maximumExpressionLength); + ExpressionParser parser = new SpelExpressionParser(configuration); + + Expression expr = parser.parseExpression(expression); + String result = expr.getValue(String.class); + assertThat(result).hasSize(19_998); + + expression = String.format("'%s'", repeat("Y", 25_000)); + assertThat(expression).hasSize(25_002); + evaluateAndCheckError(parser, expression, String.class, SpelMessage.MAX_EXPRESSION_LENGTH_EXCEEDED); + } + @Test void createListsOnAttemptToIndexNull01() throws EvaluationException, ParseException { ExpressionParser parser = new SpelExpressionParser(new SpelParserConfiguration(true, true)); @@ -127,17 +164,6 @@ void safeNavigation() { evaluate("null?.null?.null", null, null); } - @Test // SPR-16731 - void matchesWithPatternAccessThreshold() { - String pattern = "^(?=[a-z0-9-]{1,47})([a-z0-9]+[-]{0,1}){1,47}[a-z0-9]{1}$"; - String expression = "'abcde-fghijklmn-o42pasdfasdfasdf.qrstuvwxyz10x.xx.yyy.zasdfasfd' matches \'" + pattern + "\'"; - Expression expr = parser.parseExpression(expression); - assertThatExceptionOfType(SpelEvaluationException.class) - .isThrownBy(expr::getValue) - .withCauseInstanceOf(IllegalStateException.class) - .satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.FLAWED_PATTERN)); - } - // mixing operators @Test void mixingOperators() { @@ -146,8 +172,24 @@ void mixingOperators() { // assignment @Test - void assignmentToVariables() { - evaluate("#var1='value1'", "value1", String.class); + void assignmentToVariableWithStandardEvaluationContext() { + evaluate("#var1 = 'value1'", "value1", String.class); + } + + @ParameterizedTest + @CsvSource(delimiterString = "->", value = { + "'#var1 = \"value1\"' -> #var1", + "'true ? #myVar = 4 : 0' -> #myVar" + }) + void assignmentToVariableWithSimpleEvaluationContext(String expression, String varName) { + EvaluationContext context = SimpleEvaluationContext.forReadWriteDataBinding().build(); + Expression expr = parser.parseExpression(expression); + assertThatExceptionOfType(SpelEvaluationException.class) + .isThrownBy(() -> expr.getValue(context)) + .satisfies(ex -> { + assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.VARIABLE_ASSIGNMENT_NOT_SUPPORTED); + assertThat(ex.getInserts()).as("inserts").containsExactly(varName); + }); } @Test @@ -364,6 +406,53 @@ void limitCollectionGrowing() { } + @Nested + class StringLiterals { + + @Test + void insideSingleQuotes() { + evaluate("'hello'", "hello", String.class); + evaluate("'hello world'", "hello world", String.class); + } + + @Test + void insideDoubleQuotes() { + evaluate("\"hello\"", "hello", String.class); + evaluate("\"hello world\"", "hello world", String.class); + } + + @Test + void singleQuotesInsideSingleQuotes() { + evaluate("'Tony''s Pizza'", "Tony's Pizza", String.class); + evaluate("'big ''''pizza'''' parlor'", "big ''pizza'' parlor", String.class); + } + + @Test + void doubleQuotesInsideDoubleQuotes() { + evaluate("\"big \"\"pizza\"\" parlor\"", "big \"pizza\" parlor", String.class); + evaluate("\"big \"\"\"\"pizza\"\"\"\" parlor\"", "big \"\"pizza\"\" parlor", String.class); + } + + @Test + void singleQuotesInsideDoubleQuotes() { + evaluate("\"Tony's Pizza\"", "Tony's Pizza", String.class); + evaluate("\"big ''pizza'' parlor\"", "big ''pizza'' parlor", String.class); + } + + @Test + void doubleQuotesInsideSingleQuotes() { + evaluate("'big \"pizza\" parlor'", "big \"pizza\" parlor", String.class); + evaluate("'two double \"\" quotes'", "two double \"\" quotes", String.class); + } + + @Test + void inCompoundExpressions() { + evaluate("'123''4' == '123''4'", true, Boolean.class); + evaluate("\"123\"\"4\" == \"123\"\"4\"", true, Boolean.class); + } + + } + @Nested class RelationalOperatorTests { @@ -413,28 +502,47 @@ void relOperatorsInstanceof06() { } @Test - void relOperatorsMatches01() { + void matchesTrue() { + evaluate("'5.00' matches '^-?\\d+(\\.\\d{2})?$'", "true", Boolean.class); + } + + @Test + void matchesFalse() { evaluate("'5.0067' matches '^-?\\d+(\\.\\d{2})?$'", "false", Boolean.class); } @Test - void relOperatorsMatches02() { - evaluate("'5.00' matches '^-?\\d+(\\.\\d{2})?$'", "true", Boolean.class); + void matchesWithInputConversion() { + evaluate("27 matches '^.*2.*$'", true, Boolean.class); // conversion int --> string } @Test - void relOperatorsMatches03() { + void matchesWithNullInput() { evaluateAndCheckError("null matches '^.*$'", SpelMessage.INVALID_FIRST_OPERAND_FOR_MATCHES_OPERATOR, 0, null); } @Test - void relOperatorsMatches04() { + void matchesWithNullPattern() { evaluateAndCheckError("'abc' matches null", SpelMessage.INVALID_SECOND_OPERAND_FOR_MATCHES_OPERATOR, 14, null); } + @Test // SPR-16731 + void matchesWithPatternAccessThreshold() { + String pattern = "^(?=[a-z0-9-]{1,47})([a-z0-9]+[-]{0,1}){1,47}[a-z0-9]{1}$"; + String expression = "'abcde-fghijklmn-o42pasdfasdfasdf.qrstuvwxyz10x.xx.yyy.zasdfasfd' matches '" + pattern + "'"; + evaluateAndCheckError(expression, SpelMessage.FLAWED_PATTERN); + } + @Test - void relOperatorsMatches05() { - evaluate("27 matches '^.*2.*$'", true, Boolean.class); // conversion int>string + void matchesWithPatternLengthThreshold() { + String pattern = String.format("^(%s|X)", repeat("12345", 199)); + assertThat(pattern).hasSize(1000); + Expression expr = parser.parseExpression("'X' matches '" + pattern + "'"); + assertThat(expr.getValue(context, Boolean.class)).isTrue(); + + pattern += "?"; + assertThat(pattern).hasSize(1001); + evaluateAndCheckError("'abc' matches '" + pattern + "'", Boolean.class, SpelMessage.MAX_REGEX_LENGTH_EXCEEDED); } } @@ -1401,7 +1509,9 @@ private void expectFailNotDecrementable(ExpressionParser parser, EvaluationConte private void expectFail(ExpressionParser parser, EvaluationContext eContext, String expressionString, SpelMessage messageCode) { assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> { Expression e = parser.parseExpression(expressionString); - SpelUtilities.printAbstractSyntaxTree(System.out, e); + if (DEBUG) { + SpelUtilities.printAbstractSyntaxTree(System.out, e); + } e.getValue(eContext); }).satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(messageCode)); } @@ -1409,6 +1519,15 @@ private void expectFail(ExpressionParser parser, EvaluationContext eContext, Str } + private static String repeat(String str, int count) { + String result = ""; + for (int i = 0; i < count; i++) { + result += str; + } + return result; + } + + @SuppressWarnings("rawtypes") static class TestClass { diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/LiteralExpressionTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/LiteralExpressionTests.java index 4c5990237886..544aca05ffaa 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/LiteralExpressionTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/LiteralExpressionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2022 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,52 +29,70 @@ /** * @author Andy Clement */ -public class LiteralExpressionTests { +class LiteralExpressionTests { + + private final LiteralExpression lEx = new LiteralExpression("somevalue"); + + + @Test + void getValue() throws Exception { + assertThat(lEx.getValue()).isEqualTo("somevalue"); + assertThat(lEx.getValue(String.class)).isEqualTo("somevalue"); + assertThat(lEx.getValue(new Rooty())).isEqualTo("somevalue"); + assertThat(lEx.getValue(new Rooty(), String.class)).isEqualTo("somevalue"); + } @Test - public void testGetValue() throws Exception { - LiteralExpression lEx = new LiteralExpression("somevalue"); - assertThat(lEx.getValue()).isInstanceOf(String.class).isEqualTo("somevalue"); - assertThat(lEx.getValue(String.class)).isInstanceOf(String.class).isEqualTo("somevalue"); + void getValueWithSuppliedEvaluationContext() throws Exception { EvaluationContext ctx = new StandardEvaluationContext(); - assertThat(lEx.getValue(ctx)).isInstanceOf(String.class).isEqualTo("somevalue"); - assertThat(lEx.getValue(ctx, String.class)).isInstanceOf(String.class).isEqualTo("somevalue"); - assertThat(lEx.getValue(new Rooty())).isInstanceOf(String.class).isEqualTo("somevalue"); - assertThat(lEx.getValue(new Rooty(), String.class)).isInstanceOf(String.class).isEqualTo("somevalue"); - assertThat(lEx.getValue(ctx, new Rooty())).isInstanceOf(String.class).isEqualTo("somevalue"); - assertThat(lEx.getValue(ctx, new Rooty(),String.class)).isInstanceOf(String.class).isEqualTo("somevalue"); + assertThat(lEx.getValue(ctx)).isEqualTo("somevalue"); + assertThat(lEx.getValue(ctx, String.class)).isEqualTo("somevalue"); + assertThat(lEx.getValue(ctx, new Rooty())).isEqualTo("somevalue"); + assertThat(lEx.getValue(ctx, new Rooty(), String.class)).isEqualTo("somevalue"); + } + + @Test + void getExpressionString() { assertThat(lEx.getExpressionString()).isEqualTo("somevalue"); + } + + @Test + void isWritable() throws Exception { assertThat(lEx.isWritable(new StandardEvaluationContext())).isFalse(); assertThat(lEx.isWritable(new Rooty())).isFalse(); assertThat(lEx.isWritable(new StandardEvaluationContext(), new Rooty())).isFalse(); } - static class Rooty {} - @Test - public void testSetValue() { - assertThatExceptionOfType(EvaluationException.class).isThrownBy(() -> - new LiteralExpression("somevalue").setValue(new StandardEvaluationContext(), "flibble")) + void setValue() { + assertThatExceptionOfType(EvaluationException.class) + .isThrownBy(() -> lEx.setValue(new StandardEvaluationContext(), "flibble")) .satisfies(ex -> assertThat(ex.getExpressionString()).isEqualTo("somevalue")); - assertThatExceptionOfType(EvaluationException.class).isThrownBy(() -> - new LiteralExpression("somevalue").setValue(new Rooty(), "flibble")) + assertThatExceptionOfType(EvaluationException.class) + .isThrownBy(() -> lEx.setValue(new Rooty(), "flibble")) .satisfies(ex -> assertThat(ex.getExpressionString()).isEqualTo("somevalue")); - assertThatExceptionOfType(EvaluationException.class).isThrownBy(() -> - new LiteralExpression("somevalue").setValue(new StandardEvaluationContext(), new Rooty(), "flibble")) + assertThatExceptionOfType(EvaluationException.class) + .isThrownBy(() -> lEx.setValue(new StandardEvaluationContext(), new Rooty(), "flibble")) .satisfies(ex -> assertThat(ex.getExpressionString()).isEqualTo("somevalue")); } @Test - public void testGetValueType() throws Exception { - LiteralExpression lEx = new LiteralExpression("somevalue"); + void getValueType() throws Exception { assertThat(lEx.getValueType()).isEqualTo(String.class); assertThat(lEx.getValueType(new StandardEvaluationContext())).isEqualTo(String.class); assertThat(lEx.getValueType(new Rooty())).isEqualTo(String.class); assertThat(lEx.getValueType(new StandardEvaluationContext(), new Rooty())).isEqualTo(String.class); + } + + @Test + void getValueTypeDescriptor() throws Exception { assertThat(lEx.getValueTypeDescriptor().getType()).isEqualTo(String.class); assertThat(lEx.getValueTypeDescriptor(new StandardEvaluationContext()).getType()).isEqualTo(String.class); assertThat(lEx.getValueTypeDescriptor(new Rooty()).getType()).isEqualTo(String.class); assertThat(lEx.getValueTypeDescriptor(new StandardEvaluationContext(), new Rooty()).getType()).isEqualTo(String.class); } + + static class Rooty {} + } diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/OperatorTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/OperatorTests.java index baef9bd558c9..b7697f831ece 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/OperatorTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/OperatorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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,22 +21,26 @@ import org.junit.jupiter.api.Test; +import org.springframework.expression.Expression; import org.springframework.expression.spel.ast.Operator; import org.springframework.expression.spel.standard.SpelExpression; import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.expression.spel.SpelMessage.MAX_CONCATENATED_STRING_LENGTH_EXCEEDED; +import static org.springframework.expression.spel.SpelMessage.MAX_REPEATED_TEXT_SIZE_EXCEEDED; /** - * Tests the evaluation of expressions using relational operators. + * Tests the evaluation of expressions using various operators. * * @author Andy Clement * @author Juergen Hoeller * @author Giovanni Dall'Oglio Risso + * @author Sam Brannen */ class OperatorTests extends AbstractExpressionTests { @Test - void testEqual() { + void equal() { evaluate("3 == 5", false, Boolean.class); evaluate("5 == 3", false, Boolean.class); evaluate("6 == 6", true, Boolean.class); @@ -85,7 +89,7 @@ void testEqual() { } @Test - void testNotEqual() { + void notEqual() { evaluate("3 != 5", true, Boolean.class); evaluate("5 != 3", true, Boolean.class); evaluate("6 != 6", false, Boolean.class); @@ -134,7 +138,7 @@ void testNotEqual() { } @Test - void testLessThan() { + void lessThan() { evaluate("5 < 5", false, Boolean.class); evaluate("3 < 5", true, Boolean.class); evaluate("5 < 3", false, Boolean.class); @@ -176,7 +180,7 @@ void testLessThan() { } @Test - void testLessThanOrEqual() { + void lessThanOrEqual() { evaluate("3 <= 5", true, Boolean.class); evaluate("5 <= 3", false, Boolean.class); evaluate("6 <= 6", true, Boolean.class); @@ -225,7 +229,7 @@ void testLessThanOrEqual() { } @Test - void testGreaterThan() { + void greaterThan() { evaluate("3 > 5", false, Boolean.class); evaluate("5 > 3", true, Boolean.class); evaluate("3L > 5L", false, Boolean.class); @@ -266,7 +270,7 @@ void testGreaterThan() { } @Test - void testGreaterThanOrEqual() { + void greaterThanOrEqual() { evaluate("3 >= 5", false, Boolean.class); evaluate("5 >= 3", true, Boolean.class); evaluate("6 >= 6", true, Boolean.class); @@ -315,27 +319,22 @@ void testGreaterThanOrEqual() { } @Test - void testIntegerLiteral() { + void integerLiteral() { evaluate("3", 3, Integer.class); } @Test - void testRealLiteral() { + void realLiteral() { evaluate("3.5", 3.5d, Double.class); } @Test - void testMultiplyStringInt() { - evaluate("'a' * 5", "aaaaa", String.class); - } - - @Test - void testMultiplyDoubleDoubleGivesDouble() { + void multiplyDoubleDoubleGivesDouble() { evaluate("3.0d * 5.0d", 15.0d, Double.class); } @Test - void testMixedOperandsBigDecimal() { + void mixedOperandsBigDecimal() { evaluate("3 * new java.math.BigDecimal('5')", new BigDecimal("15"), BigDecimal.class); evaluate("3L * new java.math.BigDecimal('5')", new BigDecimal("15"), BigDecimal.class); evaluate("3.0d * new java.math.BigDecimal('5')", new BigDecimal("15.0"), BigDecimal.class); @@ -361,19 +360,19 @@ void testMixedOperandsBigDecimal() { } @Test - void testMathOperatorAdd02() { + void mathOperatorAdd02() { evaluate("'hello' + ' ' + 'world'", "hello world", String.class); } @Test - void testMathOperatorsInChains() { + void mathOperatorsInChains() { evaluate("1+2+3",6,Integer.class); evaluate("2*3*4",24,Integer.class); evaluate("12-1-2",9,Integer.class); } @Test - void testIntegerArithmetic() { + void integerArithmetic() { evaluate("2 + 4", "6", Integer.class); evaluate("5 - 4", "1", Integer.class); evaluate("3 * 5", 15, Integer.class); @@ -388,56 +387,47 @@ void testIntegerArithmetic() { } @Test - void testPlus() { + void plus() { evaluate("7 + 2", "9", Integer.class); evaluate("3.0f + 5.0f", 8.0f, Float.class); evaluate("3.0d + 5.0d", 8.0d, Double.class); evaluate("3 + new java.math.BigDecimal('5')", new BigDecimal("8"), BigDecimal.class); - - evaluate("'ab' + 2", "ab2", String.class); - evaluate("2 + 'a'", "2a", String.class); - evaluate("'ab' + null", "abnull", String.class); - evaluate("null + 'ab'", "nullab", String.class); + evaluate("5 + new Integer('37')", 42, Integer.class); // AST: - SpelExpression expr = (SpelExpression)parser.parseExpression("+3"); + SpelExpression expr = (SpelExpression) parser.parseExpression("+3"); assertThat(expr.toStringAST()).isEqualTo("+3"); - expr = (SpelExpression)parser.parseExpression("2+3"); + expr = (SpelExpression) parser.parseExpression("2+3"); assertThat(expr.toStringAST()).isEqualTo("(2 + 3)"); // use as a unary operator - evaluate("+5d",5d,Double.class); - evaluate("+5L",5L,Long.class); - evaluate("+5",5,Integer.class); - evaluate("+new java.math.BigDecimal('5')", new BigDecimal("5"),BigDecimal.class); - evaluateAndCheckError("+'abc'",SpelMessage.OPERATOR_NOT_SUPPORTED_BETWEEN_TYPES); - - // string concatenation - evaluate("'abc'+'def'","abcdef",String.class); - - evaluate("5 + new Integer('37')",42,Integer.class); + evaluate("+5d", 5d, Double.class); + evaluate("+5L", 5L, Long.class); + evaluate("+5", 5, Integer.class); + evaluate("+new java.math.BigDecimal('5')", new BigDecimal("5"), BigDecimal.class); + evaluateAndCheckError("+'abc'", SpelMessage.OPERATOR_NOT_SUPPORTED_BETWEEN_TYPES); } @Test - void testMinus() { + void minus() { evaluate("'c' - 2", "a", String.class); evaluate("3.0f - 5.0f", -2.0f, Float.class); evaluateAndCheckError("'ab' - 2", SpelMessage.OPERATOR_NOT_SUPPORTED_BETWEEN_TYPES); evaluateAndCheckError("2-'ab'", SpelMessage.OPERATOR_NOT_SUPPORTED_BETWEEN_TYPES); - SpelExpression expr = (SpelExpression)parser.parseExpression("-3"); + SpelExpression expr = (SpelExpression) parser.parseExpression("-3"); assertThat(expr.toStringAST()).isEqualTo("-3"); - expr = (SpelExpression)parser.parseExpression("2-3"); + expr = (SpelExpression) parser.parseExpression("2-3"); assertThat(expr.toStringAST()).isEqualTo("(2 - 3)"); - evaluate("-5d",-5d,Double.class); - evaluate("-5L",-5L,Long.class); + evaluate("-5d", -5d, Double.class); + evaluate("-5L", -5L, Long.class); evaluate("-5", -5, Integer.class); - evaluate("-new java.math.BigDecimal('5')", new BigDecimal("-5"),BigDecimal.class); + evaluate("-new java.math.BigDecimal('5')", new BigDecimal("-5"), BigDecimal.class); evaluateAndCheckError("-'abc'", SpelMessage.OPERATOR_NOT_SUPPORTED_BETWEEN_TYPES); } @Test - void testModulus() { + void modulus() { evaluate("3%2",1,Integer.class); evaluate("3L%2L",1L,Long.class); evaluate("3.0f%2.0f",1f,Float.class); @@ -448,7 +438,7 @@ void testModulus() { } @Test - void testDivide() { + void divide() { evaluate("3.0f / 5.0f", 0.6f, Float.class); evaluate("4L/2L",2L,Long.class); evaluate("3.0f div 5.0f", 0.6f, Float.class); @@ -461,17 +451,17 @@ void testDivide() { } @Test - void testMathOperatorDivide_ConvertToDouble() { + void mathOperatorDivide_ConvertToDouble() { evaluateAndAskForReturnType("8/4", 2.0, Double.class); } @Test - void testMathOperatorDivide04_ConvertToFloat() { + void mathOperatorDivide04_ConvertToFloat() { evaluateAndAskForReturnType("8/4", 2.0F, Float.class); } @Test - void testDoubles() { + void doubles() { evaluate("3.0d == 5.0d", false, Boolean.class); evaluate("3.0d == 3.0d", true, Boolean.class); evaluate("3.0d != 5.0d", true, Boolean.class); @@ -484,7 +474,7 @@ void testDoubles() { } @Test - void testBigDecimals() { + void bigDecimals() { evaluate("3 + new java.math.BigDecimal('5')", new BigDecimal("8"), BigDecimal.class); evaluate("3 - new java.math.BigDecimal('5')", new BigDecimal("-2"), BigDecimal.class); evaluate("3 * new java.math.BigDecimal('5')", new BigDecimal("15"), BigDecimal.class); @@ -495,7 +485,7 @@ void testBigDecimals() { } @Test - void testOperatorNames() { + void operatorNames() { Operator node = getOperatorNode((SpelExpression)parser.parseExpression("1==3")); assertThat(node.getOperatorName()).isEqualTo("=="); @@ -534,22 +524,22 @@ void testOperatorNames() { } @Test - void testOperatorOverloading() { + void operatorOverloading() { evaluateAndCheckError("'a' * '2'", SpelMessage.OPERATOR_NOT_SUPPORTED_BETWEEN_TYPES); evaluateAndCheckError("'a' ^ '2'", SpelMessage.OPERATOR_NOT_SUPPORTED_BETWEEN_TYPES); } @Test - void testPower() { - evaluate("3^2",9,Integer.class); - evaluate("3.0d^2.0d",9.0d,Double.class); - evaluate("3L^2L",9L,Long.class); + void power() { + evaluate("3^2", 9, Integer.class); + evaluate("3.0d^2.0d", 9.0d, Double.class); + evaluate("3L^2L", 9L, Long.class); evaluate("(2^32)^2", 9223372036854775807L, Long.class); evaluate("new java.math.BigDecimal('5') ^ 3", new BigDecimal("125"), BigDecimal.class); } @Test - void testMixedOperands_FloatsAndDoubles() { + void mixedOperands_FloatsAndDoubles() { evaluate("3.0d + 5.0f", 8.0d, Double.class); evaluate("3.0D - 5.0f", -2.0d, Double.class); evaluate("3.0f * 5.0d", 15.0d, Double.class); @@ -558,7 +548,7 @@ void testMixedOperands_FloatsAndDoubles() { } @Test - void testMixedOperands_DoublesAndInts() { + void mixedOperands_DoublesAndInts() { evaluate("3.0d + 5", 8.0d, Double.class); evaluate("3.0D - 5", -2.0d, Double.class); evaluate("3.0f * 5", 15.0f, Float.class); @@ -569,7 +559,7 @@ void testMixedOperands_DoublesAndInts() { } @Test - void testStrings() { + void strings() { evaluate("'abc' == 'abc'", true, Boolean.class); evaluate("'abc' == 'def'", false, Boolean.class); evaluate("'abc' != 'abc'", false, Boolean.class); @@ -577,7 +567,73 @@ void testStrings() { } @Test - void testLongs() { + void stringRepeat() { + evaluate("'abc' * 0", "", String.class); + evaluate("'abc' * 1", "abc", String.class); + evaluate("'abc' * 2", "abcabc", String.class); + + Expression expr = parser.parseExpression("'a' * 256"); + assertThat(expr.getValue(context, String.class)).hasSize(256); + + // 4 is the position of the '*' (repeat operator) + evaluateAndCheckError("'a' * 257", String.class, MAX_REPEATED_TEXT_SIZE_EXCEEDED, 4); + } + + @Test + void stringConcatenation() { + evaluate("'' + ''", "", String.class); + evaluate("'' + null", "null", String.class); + evaluate("null + ''", "null", String.class); + evaluate("'ab' + null", "abnull", String.class); + evaluate("null + 'ab'", "nullab", String.class); + evaluate("'ab' + 2", "ab2", String.class); + evaluate("2 + 'ab'", "2ab", String.class); + evaluate("'abc' + 'def'", "abcdef", String.class); + + // Text is big but not too big + final int maxSize = 100_000; + context.setVariable("text1", createString(maxSize)); + Expression expr = parser.parseExpression("#text1 + ''"); + assertThat(expr.getValue(context, String.class)).hasSize(maxSize); + + expr = parser.parseExpression("'' + #text1"); + assertThat(expr.getValue(context, String.class)).hasSize(maxSize); + + context.setVariable("text1", createString(maxSize / 2)); + expr = parser.parseExpression("#text1 + #text1"); + assertThat(expr.getValue(context, String.class)).hasSize(maxSize); + + // Text is too big + context.setVariable("text1", createString(maxSize + 1)); + evaluateAndCheckError("#text1 + ''", String.class, MAX_CONCATENATED_STRING_LENGTH_EXCEEDED, 7); + evaluateAndCheckError("#text1 + true", String.class, MAX_CONCATENATED_STRING_LENGTH_EXCEEDED, 7); + evaluateAndCheckError("'' + #text1", String.class, MAX_CONCATENATED_STRING_LENGTH_EXCEEDED, 3); + evaluateAndCheckError("true + #text1", String.class, MAX_CONCATENATED_STRING_LENGTH_EXCEEDED, 5); + + context.setVariable("text1", createString(maxSize / 2)); + context.setVariable("text2", createString((maxSize / 2) + 1)); + evaluateAndCheckError("#text1 + #text2", String.class, MAX_CONCATENATED_STRING_LENGTH_EXCEEDED, 7); + evaluateAndCheckError("#text1 + #text2 + true", String.class, MAX_CONCATENATED_STRING_LENGTH_EXCEEDED, 7); + evaluateAndCheckError("#text1 + true + #text2", String.class, MAX_CONCATENATED_STRING_LENGTH_EXCEEDED, 14); + evaluateAndCheckError("true + #text1 + #text2", String.class, MAX_CONCATENATED_STRING_LENGTH_EXCEEDED, 14); + + evaluateAndCheckError("#text2 + #text1", String.class, MAX_CONCATENATED_STRING_LENGTH_EXCEEDED, 7); + evaluateAndCheckError("#text2 + #text1 + true", String.class, MAX_CONCATENATED_STRING_LENGTH_EXCEEDED, 7); + evaluateAndCheckError("#text2 + true + #text1", String.class, MAX_CONCATENATED_STRING_LENGTH_EXCEEDED, 14); + evaluateAndCheckError("true + #text2 + #text1", String.class, MAX_CONCATENATED_STRING_LENGTH_EXCEEDED, 14); + + context.setVariable("text1", createString((maxSize / 3) + 1)); + evaluateAndCheckError("#text1 + #text1 + #text1", String.class, MAX_CONCATENATED_STRING_LENGTH_EXCEEDED, 16); + evaluateAndCheckError("(#text1 + #text1) + #text1", String.class, MAX_CONCATENATED_STRING_LENGTH_EXCEEDED, 18); + evaluateAndCheckError("#text1 + (#text1 + #text1)", String.class, MAX_CONCATENATED_STRING_LENGTH_EXCEEDED, 7); + } + + private static String createString(int size) { + return new String(new char[size]); + } + + @Test + void longs() { evaluate("3L == 4L", false, Boolean.class); evaluate("3L == 3L", true, Boolean.class); evaluate("3L != 4L", true, Boolean.class); @@ -588,7 +644,7 @@ void testLongs() { } @Test - void testBigIntegers() { + void bigIntegers() { evaluate("3 + new java.math.BigInteger('5')", new BigInteger("8"), BigInteger.class); evaluate("3 - new java.math.BigInteger('5')", new BigInteger("-2"), BigInteger.class); evaluate("3 * new java.math.BigInteger('5')", new BigInteger("15"), BigInteger.class); @@ -619,7 +675,7 @@ private Operator findOperator(SpelNode node) { } - public static class BaseComparable implements Comparable { + static class BaseComparable implements Comparable { @Override public int compareTo(BaseComparable other) { diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/ParsingTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/ParsingTests.java index 075688ea8449..613468ac3694 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/ParsingTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/ParsingTests.java @@ -16,6 +16,8 @@ package org.springframework.expression.spel; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.expression.spel.standard.SpelExpression; @@ -37,413 +39,517 @@ class ParsingTests { private final SpelExpressionParser parser = new SpelExpressionParser(); - // literals - @Test - void literalBoolean01() { - parseCheck("false"); + @Nested + class Miscellaneous { + + @Test + void literalNull() { + parseCheck("null"); + } + + @Test + void literalDate01() { + parseCheck("date('1974/08/24')"); + } + + @Test + void literalDate02() { + parseCheck("date('19740824T131030','yyyyMMddTHHmmss')"); + } + + @Test + void mixedOperators() { + parseCheck("true and 5>3", "(true and (5 > 3))"); + } + + @Test + void assignmentToVariables() { + parseCheck("#var1='value1'"); + } + + @Test + void collectionProcessorsCountStringArray() { + parseCheck("new String[] {'abc','def','xyz'}.count()"); + } + + @Test + void collectionProcessorsCountIntArray() { + parseCheck("new int[] {1,2,3}.count()"); + } + + @Test + void collectionProcessorsMax() { + parseCheck("new int[] {1,2,3}.max()"); + } + + @Test + void collectionProcessorsMin() { + parseCheck("new int[] {1,2,3}.min()"); + } + + @Test + void collectionProcessorsAverage() { + parseCheck("new int[] {1,2,3}.average()"); + } + + @Test + void collectionProcessorsSort() { + parseCheck("new int[] {3,2,1}.sort()"); + } + + @Test + void collectionProcessorsNonNull() { + parseCheck("{'a','b',null,'d',null}.nonNull()"); + } + + @Test + void collectionProcessorsDistinct() { + parseCheck("{'a','b','a','d','e'}.distinct()"); + } + + @Disabled("Unsupported syntax/feature") + @Test + void lambdaMax() { + parseCheck("(#max = {|x,y| $x > $y ? $x : $y }; #max(5,25))", + "(#max={|x,y| ($x > $y) ? $x : $y };#max(5,25))"); + } + + @Disabled("Unsupported syntax/feature") + @Test + void lambdaFactorial() { + parseCheck("(#fact = {|n| $n <= 1 ? 1 : $n * #fact($n-1) }; #fact(5))", + "(#fact={|n| ($n <= 1) ? 1 : ($n * #fact(($n - 1))) };#fact(5))"); + } + + @Disabled("Unsupported syntax/feature") + @Test + void projection() { + parseCheck("{1,2,3,4,5,6,7,8,9,10}.!{#isEven()}"); + } + + @Disabled("Unsupported syntax/feature") + @Test + void selection() { + parseCheck("{1,2,3,4,5,6,7,8,9,10}.?{#isEven(#this) == 'y'}", + "{1,2,3,4,5,6,7,8,9,10}.?{(#isEven(#this) == 'y')}"); + } + + @Disabled("Unsupported syntax/feature") + @Test + void selectionFirst() { + parseCheck("{1,2,3,4,5,6,7,8,9,10}.^{#isEven(#this) == 'y'}", + "{1,2,3,4,5,6,7,8,9,10}.^{(#isEven(#this) == 'y')}"); + } + + @Disabled("Unsupported syntax/feature") + @Test + void selectionLast() { + parseCheck("{1,2,3,4,5,6,7,8,9,10}.${#isEven(#this) == 'y'}", + "{1,2,3,4,5,6,7,8,9,10}.${(#isEven(#this) == 'y')}"); + } + } + + @Nested + class LiteralBooleans { + + @Test + void literalBooleanFalse() { + parseCheck("false"); + } + + @Test + void literalBooleanTrue() { + parseCheck("true"); + } + + @Test + void literalBooleanNotTrue() { + parseCheck("!true"); + parseCheck("not true", "!true"); + } + } + + @Nested + class LiteralNumbers { + + @Test + void literalLong() { + parseCheck("37L", "37"); + } + + @Test + void literalIntegers() { + parseCheck("1"); + parseCheck("1415"); + } + + @Test + void literalReal() { + parseCheck("6.0221415E+23", "6.0221415E23"); + } + + @Test + void literalHex() { + parseCheck("0x7FFFFFFF", "2147483647"); + } + } + + @Nested + class LiteralStrings { + + @Test + void insideSingleQuotes() { + parseCheck("'hello'"); + parseCheck("'hello world'"); + } + + @Test + void insideDoubleQuotes() { + parseCheck("\"hello\"", "'hello'"); + parseCheck("\"hello world\"", "'hello world'"); + } + + @Test + void singleQuotesInsideSingleQuotes() { + parseCheck("'Tony''s Pizza'"); + parseCheck("'big ''''pizza'''' parlor'"); + } + + @Test + void doubleQuotesInsideDoubleQuotes() { + parseCheck("\"big \"\"pizza\"\" parlor\"", "'big \"pizza\" parlor'"); + parseCheck("\"big \"\"\"\"pizza\"\"\"\" parlor\"", "'big \"\"pizza\"\" parlor'"); + } + + @Test + void singleQuotesInsideDoubleQuotes() { + parseCheck("\"Tony's Pizza\"", "'Tony''s Pizza'"); + parseCheck("\"big ''pizza'' parlor\"", "'big ''''pizza'''' parlor'"); + } + + @Test + void doubleQuotesInsideSingleQuotes() { + parseCheck("'big \"pizza\" parlor'"); + parseCheck("'two double \"\" quotes'"); + } + + @Test + void inCompoundExpressions() { + parseCheck("'123''4' == '123''4'", "('123''4' == '123''4')"); + parseCheck("('123''4'=='123''4')", "('123''4' == '123''4')"); + parseCheck("\"123\"\"4\" == \"123\"\"4\"", "('123\"4' == '123\"4')"); + } + } + + @Nested + class BooleanOperators { + + @Test + void booleanOperatorsOr01() { + parseCheck("false or false", "(false or false)"); + } + + @Test + void booleanOperatorsOr02() { + parseCheck("false or true", "(false or true)"); + } + + @Test + void booleanOperatorsOr03() { + parseCheck("true or false", "(true or false)"); + } + + @Test + void booleanOperatorsOr04() { + parseCheck("true or false", "(true or false)"); + } + + @Test + void booleanOperatorsMix() { + parseCheck("false or true and false", "(false or (true and false))"); + } + } + + @Nested + class RelationalOperators { + + @Test + void relOperatorsGT() { + parseCheck("3>6", "(3 > 6)"); + } + + @Test + void relOperatorsLT() { + parseCheck("3<6", "(3 < 6)"); + } + + @Test + void relOperatorsLE() { + parseCheck("3<=6", "(3 <= 6)"); + } + + @Test + void relOperatorsGE01() { + parseCheck("3>=6", "(3 >= 6)"); + } + + @Test + void relOperatorsGE02() { + parseCheck("3>=3", "(3 >= 3)"); + } + + @Disabled("Unsupported syntax/feature") + @Test + void relOperatorsIn() { + parseCheck("3 in {1,2,3,4,5}", "(3 in {1,2,3,4,5})"); + } + + @Test + void relOperatorsBetweenNumbers() { + parseCheck("1 between {1, 5}", "(1 between {1,5})"); + } + + @Test + void relOperatorsBetweenStrings() { + parseCheck("'efg' between {'abc', 'xyz'}", "('efg' between {'abc','xyz'})"); + } + + @Test + void relOperatorsInstanceOfInt() { + parseCheck("'xyz' instanceof int", "('xyz' instanceof int)"); + } + + @Test + void relOperatorsInstanceOfList() { + parseCheck("{1, 2, 3, 4, 5} instanceof List", "({1,2,3,4,5} instanceof List)"); + } + + @Test + void relOperatorsMatches() { + parseCheck("'5.0067' matches '^-?\\d+(\\.\\d{2})?$'", "('5.0067' matches '^-?\\d+(\\.\\d{2})?$')"); + parseCheck("'5.00' matches '^-?\\d+(\\.\\d{2})?$'", "('5.00' matches '^-?\\d+(\\.\\d{2})?$')"); + } + } + + @Nested + class MathematicalOperators { + + @Test + void mathOperatorsAddIntegers() { + parseCheck("2+4", "(2 + 4)"); + } + + @Test + void mathOperatorsAddStrings() { + parseCheck("'a'+'b'", "('a' + 'b')"); + } + + @Test + void mathOperatorsAddMultipleStrings() { + parseCheck("'hello'+' '+'world'", "(('hello' + ' ') + 'world')"); + } + + @Test + void mathOperatorsSubtract() { + parseCheck("5-4", "(5 - 4)"); + } + + @Test + void mathOperatorsMultiply() { + parseCheck("7*4", "(7 * 4)"); + } + + @Test + void mathOperatorsDivide() { + parseCheck("8/4", "(8 / 4)"); + } + + @Test + void mathOperatorModulus() { + parseCheck("7 % 4", "(7 % 4)"); + } + } + + @Nested + class References { + + @Test + void references() { + parseCheck("@foo"); + parseCheck("@'foo.bar'"); + parseCheck("@\"foo.bar.goo\"" , "@'foo.bar.goo'"); + parseCheck("@$$foo"); + } + } + + @Nested + class Properties { + + @Test + void propertiesSingle() { + parseCheck("name"); + } + + @Test + void propertiesDouble() { + parseCheck("placeofbirth.CitY"); + } + + @Test + void propertiesMultiple() { + parseCheck("a.b.c.d.e"); + } + } + + @Nested + class InlineCollections { + + @Test + void inlineListOfIntegers() { + parseCheck("{1,2,3,4}"); + parseCheck("{1, 2, 3, 4, 5}", "{1,2,3,4,5}"); + } + + @Test + void inlineListOfStrings() { + parseCheck("{'abc','xyz'}", "{'abc','xyz'}"); + parseCheck("{\"abc\", 'xyz'}", "{'abc','xyz'}"); + } + + @Test + void inlineMapStringToObject() { + parseCheck("{'key1':'Value 1','today':DateTime.Today}"); + } + + @Test + void inlineMapIntegerToString() { + parseCheck("{1:'January',2:'February',3:'March'}"); + } + } + + @Nested + class MethodsConstructorsAndArrays { + + @Test + void methods() { + parseCheck("echo()"); + parseCheck("echo(12)"); + parseCheck("echo(name)"); + parseCheck("echo('Jane')"); + parseCheck("echo('Jane',32)"); + parseCheck("echo('Jane', 32)", "echo('Jane',32)"); + parseCheck("age.doubleItAndAdd(12)"); + } + + @Test + void constructorWithNoArguments() { + parseCheck("new Foo()"); + parseCheck("new example.Foo()"); + } + + @Test + void constructorWithOneArgument() { + parseCheck("new String('hello')"); + parseCheck("new String( 'hello' )", "new String('hello')"); + parseCheck("new String(\"hello\" )", "new String('hello')"); + } + + @Test + void constructorWithMultipleArguments() { + parseCheck("new example.Person('Jane',32,true)"); + parseCheck("new example.Person('Jane', 32, true)", "new example.Person('Jane',32,true)"); + parseCheck("new example.Person('Jane', 2 * 16, true)", "new example.Person('Jane',(2 * 16),true)"); + } + + @Test + void arrayConstructionWithOneDimensionalReferenceType() { + parseCheck("new String[3]"); + } + + @Test + void arrayConstructionWithOneDimensionalFullyQualifiedReferenceType() { + parseCheck("new java.lang.String[3]"); + } + + @Test + void arrayConstructionWithOneDimensionalPrimitiveType() { + parseCheck("new int[3]"); + } + + @Test + void arrayConstructionWithMultiDimensionalReferenceType() { + parseCheck("new Float[3][4]"); + } + + @Test + void arrayConstructionWithMultiDimensionalPrimitiveType() { + parseCheck("new int[3][4]"); + } + + @Test + void arrayConstructionWithOneDimensionalReferenceTypeWithInitializer() { + parseCheck("new String[] {'abc','xyz'}"); + parseCheck("new String[] {'abc', 'xyz'}", "new String[] {'abc','xyz'}"); + } + + @Test + void arrayConstructionWithOneDimensionalPrimitiveTypeWithInitializer() { + parseCheck("new int[] {1,2,3,4,5}"); + parseCheck("new int[] {1, 2, 3, 4, 5}", "new int[] {1,2,3,4,5}"); + } + } + + @Nested + class VariablesAndFunctions { + + @Test + void variables() { + parseCheck("#foo"); + } + + @Test + void functions() { + parseCheck("#fn(1,2,3)"); + parseCheck("#fn('hello')"); + } + } + + @Nested + class ElvisAndTernaryOperators { + + @Test + void elvis() { + parseCheck("3?:1", "(3 ?: 1)"); + parseCheck("(2*3)?:1*10", "((2 * 3) ?: (1 * 10))"); + parseCheck("((2*3)?:1)*10", "(((2 * 3) ?: 1) * 10)"); + } + + @Test + void ternary() { + parseCheck("1>2?3:4", "((1 > 2) ? 3 : 4)"); + parseCheck("(a ? 1 : 0) * 10", "((a ? 1 : 0) * 10)"); + parseCheck("(a?1:0)*10", "((a ? 1 : 0) * 10)"); + parseCheck("(4 % 2 == 0 ? 1 : 0) * 10", "((((4 % 2) == 0) ? 1 : 0) * 10)"); + parseCheck("((4 % 2 == 0) ? 1 : 0) * 10", "((((4 % 2) == 0) ? 1 : 0) * 10)"); + parseCheck("{1}.#isEven(#this) == 'y'?'it is even':'it is odd'", + "(({1}.#isEven(#this) == 'y') ? 'it is even' : 'it is odd')"); + } + } + + @Nested + class TypeReferences { + + @Test + void typeReferences01() { + parseCheck("T(java.lang.String)"); + } + + @Test + void typeReferences02() { + parseCheck("T(String)"); + } } - @Test - void literalLong01() { - parseCheck("37L", "37"); - } - - @Test - void literalBoolean02() { - parseCheck("true"); - } - - @Test - void literalBoolean03() { - parseCheck("!true"); - } - - @Test - void literalInteger01() { - parseCheck("1"); - } - - @Test - void literalInteger02() { - parseCheck("1415"); - } - - @Test - void literalString01() { - parseCheck("'hello'"); - } - - @Test - void literalString02() { - parseCheck("'joe bloggs'"); - } - - @Test - void literalString03() { - parseCheck("'Tony''s Pizza'", "'Tony's Pizza'"); - } - - @Test - void literalReal01() { - parseCheck("6.0221415E+23", "6.0221415E23"); - } - - @Test - void literalHex01() { - parseCheck("0x7FFFFFFF", "2147483647"); - } - - @Test - void literalDate01() { - parseCheck("date('1974/08/24')"); - } - - @Test - void literalDate02() { - parseCheck("date('19740824T131030','yyyyMMddTHHmmss')"); - } - - @Test - void literalNull01() { - parseCheck("null"); - } - - // boolean operators - @Test - void booleanOperatorsOr01() { - parseCheck("false or false", "(false or false)"); - } - - @Test - void booleanOperatorsOr02() { - parseCheck("false or true", "(false or true)"); - } - - @Test - void booleanOperatorsOr03() { - parseCheck("true or false", "(true or false)"); - } - - @Test - void booleanOperatorsOr04() { - parseCheck("true or false", "(true or false)"); - } - - @Test - void booleanOperatorsMix01() { - parseCheck("false or true and false", "(false or (true and false))"); - } - - // relational operators - @Test - void relOperatorsGT01() { - parseCheck("3>6", "(3 > 6)"); - } - - @Test - void relOperatorsLT01() { - parseCheck("3<6", "(3 < 6)"); - } - - @Test - void relOperatorsLE01() { - parseCheck("3<=6", "(3 <= 6)"); - } - - @Test - void relOperatorsGE01() { - parseCheck("3>=6", "(3 >= 6)"); - } - - @Test - void relOperatorsGE02() { - parseCheck("3>=3", "(3 >= 3)"); - } - - @Test - void elvis() { - parseCheck("3?:1", "(3 ?: 1)"); - parseCheck("(2*3)?:1*10", "((2 * 3) ?: (1 * 10))"); - parseCheck("((2*3)?:1)*10", "(((2 * 3) ?: 1) * 10)"); - } - - // void relOperatorsIn01() { - // parseCheck("3 in {1,2,3,4,5}", "(3 in {1,2,3,4,5})"); - // } - // - // void relOperatorsBetween01() { - // parseCheck("1 between {1, 5}", "(1 between {1,5})"); - // } - - // void relOperatorsBetween02() { - // parseCheck("'efg' between {'abc', 'xyz'}", "('efg' between {'abc','xyz'})"); - // }// true - - @Test - void relOperatorsIs01() { - parseCheck("'xyz' instanceof int", "('xyz' instanceof int)"); - }// false - - // void relOperatorsIs02() { - // parseCheck("{1, 2, 3, 4, 5} instanceof List", "({1,2,3,4,5} instanceof List)"); - // }// true - - @Test - void relOperatorsMatches01() { - parseCheck("'5.0067' matches '^-?\\d+(\\.\\d{2})?$'", "('5.0067' matches '^-?\\d+(\\.\\d{2})?$')"); - }// false - - @Test - void relOperatorsMatches02() { - parseCheck("'5.00' matches '^-?\\d+(\\.\\d{2})?$'", "('5.00' matches '^-?\\d+(\\.\\d{2})?$')"); - }// true - - // mathematical operators - @Test - void mathOperatorsAdd01() { - parseCheck("2+4", "(2 + 4)"); - } - - @Test - void mathOperatorsAdd02() { - parseCheck("'a'+'b'", "('a' + 'b')"); - } - - @Test - void mathOperatorsAdd03() { - parseCheck("'hello'+' '+'world'", "(('hello' + ' ') + 'world')"); - } - - @Test - void mathOperatorsSubtract01() { - parseCheck("5-4", "(5 - 4)"); - } - - @Test - void mathOperatorsMultiply01() { - parseCheck("7*4", "(7 * 4)"); - } - - @Test - void mathOperatorsDivide01() { - parseCheck("8/4", "(8 / 4)"); - } - - @Test - void mathOperatorModulus01() { - parseCheck("7 % 4", "(7 % 4)"); - } - - // mixed operators - @Test - void mixedOperators01() { - parseCheck("true and 5>3", "(true and (5 > 3))"); - } - - // collection processors - // void collectionProcessorsCount01() { - // parseCheck("new String[] {'abc','def','xyz'}.count()"); - // } - - // void collectionProcessorsCount02() { - // parseCheck("new int[] {1,2,3}.count()"); - // } - // - // void collectionProcessorsMax01() { - // parseCheck("new int[] {1,2,3}.max()"); - // } - // - // void collectionProcessorsMin01() { - // parseCheck("new int[] {1,2,3}.min()"); - // } - // - // void collectionProcessorsAverage01() { - // parseCheck("new int[] {1,2,3}.average()"); - // } - // - // void collectionProcessorsSort01() { - // parseCheck("new int[] {3,2,1}.sort()"); - // } - // - // void collectionProcessorsNonNull01() { - // parseCheck("{'a','b',null,'d',null}.nonNull()"); - // } - // - // void collectionProcessorsDistinct01() { - // parseCheck("{'a','b','a','d','e'}.distinct()"); - // } - - // references - @Test - void references01() { - parseCheck("@foo"); - parseCheck("@'foo.bar'"); - parseCheck("@\"foo.bar.goo\"" , "@'foo.bar.goo'"); - } - - @Test - void references03() { - parseCheck("@$$foo"); - } - - // properties - @Test - void properties01() { - parseCheck("name"); - } - - @Test - void properties02() { - parseCheck("placeofbirth.CitY"); - } - - @Test - void properties03() { - parseCheck("a.b.c.d.e"); - } - - // inline list creation - @Test - void inlineListCreation01() { - parseCheck("{1, 2, 3, 4, 5}", "{1,2,3,4,5}"); - } - - @Test - void inlineListCreation02() { - parseCheck("{'abc','xyz'}", "{'abc','xyz'}"); - } - - // inline map creation - @Test - void inlineMapCreation01() { - parseCheck("{'key1':'Value 1','today':DateTime.Today}"); - } - - @Test - void inlineMapCreation02() { - parseCheck("{1:'January',2:'February',3:'March'}"); - } - - // methods - @Test - void methods01() { - parseCheck("echo(12)"); - } - - @Test - void methods02() { - parseCheck("echo(name)"); - } - - @Test - void methods03() { - parseCheck("age.doubleItAndAdd(12)"); - } - - // constructors - @Test - void constructors01() { - parseCheck("new String('hello')"); - } - - // void constructors02() { - // parseCheck("new String[3]"); - // } - - // array construction - // void arrayConstruction01() { - // parseCheck("new int[] {1, 2, 3, 4, 5}", "new int[] {1,2,3,4,5}"); - // } - // - // void arrayConstruction02() { - // parseCheck("new String[] {'abc','xyz'}", "new String[] {'abc','xyz'}"); - // } - - // variables and functions - @Test - void variables01() { - parseCheck("#foo"); - } - - @Test - void functions01() { - parseCheck("#fn(1,2,3)"); - } - - @Test - void functions02() { - parseCheck("#fn('hello')"); - } - - // projections and selections - // void projections01() { - // parseCheck("{1,2,3,4,5,6,7,8,9,10}.!{#isEven()}"); - // } - - // void selections01() { - // parseCheck("{1,2,3,4,5,6,7,8,9,10}.?{#isEven(#this) == 'y'}", - // "{1,2,3,4,5,6,7,8,9,10}.?{(#isEven(#this) == 'y')}"); - // } - - // void selectionsFirst01() { - // parseCheck("{1,2,3,4,5,6,7,8,9,10}.^{#isEven(#this) == 'y'}", - // "{1,2,3,4,5,6,7,8,9,10}.^{(#isEven(#this) == 'y')}"); - // } - - // void selectionsLast01() { - // parseCheck("{1,2,3,4,5,6,7,8,9,10}.${#isEven(#this) == 'y'}", - // "{1,2,3,4,5,6,7,8,9,10}.${(#isEven(#this) == 'y')}"); - // } - - // assignment - @Test - void assignmentToVariables01() { - parseCheck("#var1='value1'"); - } - - - // ternary operator - - @Test - void ternaryOperator01() { - parseCheck("1>2?3:4", "((1 > 2) ? 3 : 4)"); - parseCheck("(a ? 1 : 0) * 10", "((a ? 1 : 0) * 10)"); - parseCheck("(a?1:0)*10", "((a ? 1 : 0) * 10)"); - parseCheck("(4 % 2 == 0 ? 1 : 0) * 10", "((((4 % 2) == 0) ? 1 : 0) * 10)"); - parseCheck("((4 % 2 == 0) ? 1 : 0) * 10", "((((4 % 2) == 0) ? 1 : 0) * 10)"); - } - - @Test - void ternaryOperator02() { - parseCheck("{1}.#isEven(#this) == 'y'?'it is even':'it is odd'", - "(({1}.#isEven(#this) == 'y') ? 'it is even' : 'it is odd')"); - } - - // - // void lambdaMax() { - // parseCheck("(#max = {|x,y| $x > $y ? $x : $y }; #max(5,25))", "(#max={|x,y| ($x > $y) ? $x : $y };#max(5,25))"); - // } - // - // void lambdaFactorial() { - // parseCheck("(#fact = {|n| $n <= 1 ? 1 : $n * #fact($n-1) }; #fact(5))", - // "(#fact={|n| ($n <= 1) ? 1 : ($n * #fact(($n - 1))) };#fact(5))"); - // } // 120 - - // Type references - @Test - void typeReferences01() { - parseCheck("T(java.lang.String)"); - } - - @Test - void typeReferences02() { - parseCheck("T(String)"); - } - - @Test - void inlineList1() { - parseCheck("{1,2,3,4}"); - } /** * Parse the supplied expression and then create a string representation of the resultant AST, it should be the same diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/PropertyAccessTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/PropertyAccessTests.java index 6d7d669217db..680c0dd63c8c 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/PropertyAccessTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/PropertyAccessTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-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,6 +21,7 @@ import java.util.List; import java.util.Map; +import org.assertj.core.api.ThrowableTypeAssert; import org.junit.jupiter.api.Test; import org.springframework.core.convert.TypeDescriptor; @@ -83,11 +84,11 @@ void nonExistentPropertiesAndMethods() { void accessingOnNullObject() { SpelExpression expr = (SpelExpression) parser.parseExpression("madeup"); EvaluationContext context = new StandardEvaluationContext(null); - assertThatExceptionOfType(SpelEvaluationException.class) + assertThatSpelEvaluationException() .isThrownBy(() -> expr.getValue(context)) .extracting(SpelEvaluationException::getMessageCode).isEqualTo(SpelMessage.PROPERTY_OR_FIELD_NOT_READABLE_ON_NULL); assertThat(expr.isWritable(context)).isFalse(); - assertThatExceptionOfType(SpelEvaluationException.class) + assertThatSpelEvaluationException() .isThrownBy(() -> expr.setValue(context, "abc")) .extracting(SpelEvaluationException::getMessageCode).isEqualTo(SpelMessage.PROPERTY_OR_FIELD_NOT_WRITABLE_ON_NULL); } @@ -117,8 +118,7 @@ void addingSpecificPropertyAccessor() { assertThat((int) i).isEqualTo(99); // Cannot set it to a string value - assertThatExceptionOfType(EvaluationException.class).isThrownBy(() -> - flibbleexpr.setValue(ctx, "not allowed")); + assertThatSpelEvaluationException().isThrownBy(() -> flibbleexpr.setValue(ctx, "not allowed")); // message will be: EL1063E:(pos 20): A problem occurred whilst attempting to set the property // 'flibbles': 'Cannot set flibbles to an object of type 'class java.lang.String'' // System.out.println(e.getMessage()); @@ -173,8 +173,7 @@ void standardGetClassAccess() { @Test void noGetClassAccess() { EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build(); - assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> - parser.parseExpression("'a'.class.name").getValue(context)); + assertThatSpelEvaluationException().isThrownBy(() -> parser.parseExpression("'a'.class.name").getValue(context)); } @Test @@ -187,8 +186,13 @@ void propertyReadOnly() { target.setName("p2"); assertThat(expr.getValue(context, target)).isEqualTo("p2"); - assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> - parser.parseExpression("name='p3'").getValue(context, target)); + assertThatSpelEvaluationException() + .isThrownBy(() -> parser.parseExpression("name='p3'").getValue(context, target)) + .extracting(SpelEvaluationException::getMessageCode).isEqualTo(SpelMessage.NOT_ASSIGNABLE); + + assertThatSpelEvaluationException() + .isThrownBy(() -> parser.parseExpression("['name']='p4'").getValue(context, target)) + .extracting(SpelEvaluationException::getMessageCode).isEqualTo(SpelMessage.NOT_ASSIGNABLE); } @Test @@ -201,8 +205,9 @@ void propertyReadOnlyWithRecordStyle() { RecordPerson target2 = new RecordPerson("p2"); assertThat(expr.getValue(context, target2)).isEqualTo("p2"); - assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> - parser.parseExpression("name='p3'").getValue(context, target2)); + assertThatSpelEvaluationException() + .isThrownBy(() -> parser.parseExpression("name='p3'").getValue(context, target2)) + .extracting(SpelEvaluationException::getMessageCode).isEqualTo(SpelMessage.NOT_ASSIGNABLE); } @Test @@ -248,7 +253,7 @@ void propertyReadWriteWithRootObject() { void propertyAccessWithoutMethodResolver() { EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build(); Person target = new Person("p1"); - assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> + assertThatSpelEvaluationException().isThrownBy(() -> parser.parseExpression("name.substring(1)").getValue(context, target)); } @@ -274,12 +279,17 @@ void propertyAccessWithInstanceMethodResolverAndTypedRootObject() { void propertyAccessWithArrayIndexOutOfBounds() { EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build(); Expression expression = parser.parseExpression("stringArrayOfThreeItems[3]"); - assertThatExceptionOfType(SpelEvaluationException.class) + assertThatSpelEvaluationException() .isThrownBy(() -> expression.getValue(context, new Inventor())) .extracting(SpelEvaluationException::getMessageCode).isEqualTo(SpelMessage.ARRAY_INDEX_OUT_OF_BOUNDS); } + private ThrowableTypeAssert assertThatSpelEvaluationException() { + return assertThatExceptionOfType(SpelEvaluationException.class); + } + + // This can resolve the property 'flibbles' on any String (very useful...) private static class StringyPropertyAccessor implements PropertyAccessor { diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/ScenariosForSpringSecurityExpressionTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/ScenariosForSpringSecurityExpressionTests.java index abe0bd248d17..a8d3c4ca84b1 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/ScenariosForSpringSecurityExpressionTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/ScenariosForSpringSecurityExpressionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2022 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-expression/src/test/java/org/springframework/expression/spel/SpelCompilationCoverageTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/SpelCompilationCoverageTests.java index b55e9d1c61de..de5c71d268d0 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/SpelCompilationCoverageTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/SpelCompilationCoverageTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-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,8 @@ import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -27,7 +29,10 @@ import java.util.Map; import java.util.Set; import java.util.StringTokenizer; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.asm.MethodVisitor; @@ -55,6 +60,7 @@ * Checks SpelCompiler behavior. This should cover compilation all compiled node types. * * @author Andy Clement + * @author Sam Brannen * @since 4.1 */ public class SpelCompilationCoverageTests extends AbstractExpressionTests { @@ -129,6 +135,488 @@ public class SpelCompilationCoverageTests extends AbstractExpressionTests { private SpelNodeImpl ast; + @Nested + class VariableReferenceTests { + + @Test + void userDefinedVariable() { + EvaluationContext ctx = new StandardEvaluationContext(); + ctx.setVariable("target", "abc"); + expression = parser.parseExpression("#target"); + assertThat(expression.getValue(ctx)).isEqualTo("abc"); + assertCanCompile(expression); + assertThat(expression.getValue(ctx)).isEqualTo("abc"); + ctx.setVariable("target", "123"); + assertThat(expression.getValue(ctx)).isEqualTo("123"); + + // Changing the variable type from String to Integer results in a + // ClassCastException in the compiled code. + ctx.setVariable("target", 42); + assertThatExceptionOfType(SpelEvaluationException.class) + .isThrownBy(() -> expression.getValue(ctx)) + .withCauseInstanceOf(ClassCastException.class); + + ctx.setVariable("target", "abc"); + expression = parser.parseExpression("#target.charAt(0)"); + assertThat(expression.getValue(ctx)).isEqualTo('a'); + assertCanCompile(expression); + assertThat(expression.getValue(ctx)).isEqualTo('a'); + ctx.setVariable("target", "1"); + assertThat(expression.getValue(ctx)).isEqualTo('1'); + + // Changing the variable type from String to Integer results in a + // ClassCastException in the compiled code. + ctx.setVariable("target", 42); + assertThatExceptionOfType(SpelEvaluationException.class) + .isThrownBy(() -> expression.getValue(ctx)) + .withCauseInstanceOf(ClassCastException.class); + } + + } + + @Nested + class IndexingTests { + + @Test + void indexIntoPrimitiveShortArray() { + short[] shorts = { (short) 33, (short) 44, (short) 55 }; + + expression = parser.parseExpression("[2]"); + + assertThat(expression.getValue(shorts)).isEqualTo((short) 55); + assertCanCompile(expression); + assertThat(expression.getValue(shorts)).isEqualTo((short) 55); + assertThat(getAst().getExitDescriptor()).isEqualTo("S"); + } + + @Test + void indexIntoPrimitiveByteArray() { + byte[] bytes = { (byte) 2, (byte) 3, (byte) 4 }; + + expression = parser.parseExpression("[2]"); + + assertThat(expression.getValue(bytes)).isEqualTo((byte) 4); + assertCanCompile(expression); + assertThat(expression.getValue(bytes)).isEqualTo((byte) 4); + assertThat(getAst().getExitDescriptor()).isEqualTo("B"); + } + + @Test + void indexIntoPrimitiveIntArray() { + int[] ints = { 8, 9, 10 }; + + expression = parser.parseExpression("[2]"); + + assertThat(expression.getValue(ints)).isEqualTo(10); + assertCanCompile(expression); + assertThat(expression.getValue(ints)).isEqualTo(10); + assertThat(getAst().getExitDescriptor()).isEqualTo("I"); + } + + @Test + void indexIntoPrimitiveLongArray() { + long[] longs = { 2L, 3L, 4L }; + + expression = parser.parseExpression("[0]"); + + assertThat(expression.getValue(longs)).isEqualTo(2L); + assertCanCompile(expression); + assertThat(expression.getValue(longs)).isEqualTo(2L); + assertThat(getAst().getExitDescriptor()).isEqualTo("J"); + } + + @Test + void indexIntoPrimitiveFloatArray() { + float[] floats = { 6.0f, 7.0f, 8.0f }; + + expression = parser.parseExpression("[0]"); + + assertThat(expression.getValue(floats)).isEqualTo(6.0f); + assertCanCompile(expression); + assertThat(expression.getValue(floats)).isEqualTo(6.0f); + assertThat(getAst().getExitDescriptor()).isEqualTo("F"); + } + + @Test + void indexIntoPrimitiveDoubleArray() { + double[] doubles = { 3.0d, 4.0d, 5.0d }; + + expression = parser.parseExpression("[1]"); + + assertThat(expression.getValue(doubles)).isEqualTo(4.0d); + assertCanCompile(expression); + assertThat(expression.getValue(doubles)).isEqualTo(4.0d); + assertThat(getAst().getExitDescriptor()).isEqualTo("D"); + } + + @Test + void indexIntoPrimitiveCharArray() { + char[] chars = { 'a', 'b', 'c' }; + + expression = parser.parseExpression("[1]"); + + assertThat(expression.getValue(chars)).isEqualTo('b'); + assertCanCompile(expression); + assertThat(expression.getValue(chars)).isEqualTo('b'); + assertThat(getAst().getExitDescriptor()).isEqualTo("C"); + } + + @Test + void indexIntoStringArray() { + String[] strings = { "a", "b", "c" }; + + expression = parser.parseExpression("[0]"); + + assertThat(expression.getValue(strings)).isEqualTo("a"); + assertCanCompile(expression); + assertThat(expression.getValue(strings)).isEqualTo("a"); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/String"); + } + + @Test + void indexIntoNumberArray() { + Number[] numbers = { 2, 8, 9 }; + + expression = parser.parseExpression("[1]"); + + assertThat(expression.getValue(numbers)).isEqualTo(8); + assertCanCompile(expression); + assertThat(expression.getValue(numbers)).isEqualTo(8); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Number"); + } + + @Test + void indexInto2DPrimitiveIntArray() { + int[][] array = new int[][] { + { 1, 2, 3 }, + { 4, 5, 6 } + }; + + expression = parser.parseExpression("[1]"); + + assertThat(stringify(expression.getValue(array))).isEqualTo("4 5 6"); + assertCanCompile(expression); + assertThat(stringify(expression.getValue(array))).isEqualTo("4 5 6"); + assertThat(getAst().getExitDescriptor()).isEqualTo("[I"); + + expression = parser.parseExpression("[1][2]"); + + assertThat(stringify(expression.getValue(array))).isEqualTo("6"); + assertCanCompile(expression); + assertThat(stringify(expression.getValue(array))).isEqualTo("6"); + assertThat(getAst().getExitDescriptor()).isEqualTo("I"); + } + + @Test + void indexInto2DStringArray() { + String[][] array = new String[][] { + { "a", "b", "c" }, + { "d", "e", "f" } + }; + + expression = parser.parseExpression("[1]"); + + assertThat(stringify(expression.getValue(array))).isEqualTo("d e f"); + assertCanCompile(expression); + assertThat(getAst().getExitDescriptor()).isEqualTo("[Ljava/lang/String"); + assertThat(stringify(expression.getValue(array))).isEqualTo("d e f"); + assertThat(getAst().getExitDescriptor()).isEqualTo("[Ljava/lang/String"); + + expression = parser.parseExpression("[1][2]"); + + assertThat(stringify(expression.getValue(array))).isEqualTo("f"); + assertCanCompile(expression); + assertThat(stringify(expression.getValue(array))).isEqualTo("f"); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/String"); + } + + @Test + @SuppressWarnings("unchecked") + void indexIntoArrayOfListOfString() { + List[] array = new List[] { + Arrays.asList("a", "b", "c"), + Arrays.asList("d", "e", "f") + }; + + expression = parser.parseExpression("[1]"); + + assertThat(stringify(expression.getValue(array))).isEqualTo("d e f"); + assertCanCompile(expression); + assertThat(stringify(expression.getValue(array))).isEqualTo("d e f"); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/util/List"); + + expression = parser.parseExpression("[1][2]"); + + assertThat(stringify(expression.getValue(array))).isEqualTo("f"); + assertCanCompile(expression); + assertThat(stringify(expression.getValue(array))).isEqualTo("f"); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); + } + + @Test + @SuppressWarnings("unchecked") + void indexIntoArrayOfMap() { + Map[] array = new Map[] { Collections.singletonMap("key", "value1") }; + + expression = parser.parseExpression("[0]"); + + assertThat(stringify(expression.getValue(array))).isEqualTo("{key=value1}"); + assertCanCompile(expression); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/util/Map"); + assertThat(stringify(expression.getValue(array))).isEqualTo("{key=value1}"); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/util/Map"); + + expression = parser.parseExpression("[0]['key']"); + + assertThat(stringify(expression.getValue(array))).isEqualTo("value1"); + assertCanCompile(expression); + assertThat(stringify(expression.getValue(array))).isEqualTo("value1"); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); + } + + @Test + void indexIntoListOfString() { + List list = Arrays.asList("aaa", "bbb", "ccc"); + + expression = parser.parseExpression("[1]"); + + assertThat(expression.getValue(list)).isEqualTo("bbb"); + assertCanCompile(expression); + assertThat(expression.getValue(list)).isEqualTo("bbb"); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); + } + + @Test + void indexIntoListOfInteger() { + List list = Arrays.asList(123, 456, 789); + + expression = parser.parseExpression("[2]"); + + assertThat(expression.getValue(list)).isEqualTo(789); + assertCanCompile(expression); + assertThat(expression.getValue(list)).isEqualTo(789); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); + } + + @Test + void indexIntoListOfStringArray() { + List list = Arrays.asList( + new String[] { "a", "b", "c" }, + new String[] { "d", "e", "f" } + ); + + expression = parser.parseExpression("[1]"); + + assertThat(stringify(expression.getValue(list))).isEqualTo("d e f"); + assertCanCompile(expression); + assertThat(stringify(expression.getValue(list))).isEqualTo("d e f"); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); + + expression = parser.parseExpression("[1][0]"); + + assertThat(stringify(expression.getValue(list))).isEqualTo("d"); + assertCanCompile(expression); + assertThat(stringify(expression.getValue(list))).isEqualTo("d"); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/String"); + } + + @Test + void indexIntoListOfIntegerArray() { + List list = Arrays.asList( + new Integer[] { 1, 2, 3 }, + new Integer[] { 4, 5, 6 } + ); + + expression = parser.parseExpression("[0]"); + + assertThat(stringify(expression.getValue(list))).isEqualTo("1 2 3"); + assertCanCompile(expression); + assertThat(stringify(expression.getValue(list))).isEqualTo("1 2 3"); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); + + expression = parser.parseExpression("[0][1]"); + + assertThat(expression.getValue(list)).isEqualTo(2); + assertCanCompile(expression); + assertThat(expression.getValue(list)).isEqualTo(2); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Integer"); + } + + @Test + void indexIntoListOfListOfString() { + List> list = Arrays.asList( + Arrays.asList("a", "b", "c"), + Arrays.asList("d", "e", "f") + ); + + expression = parser.parseExpression("[1]"); + + assertThat(stringify(expression.getValue(list))).isEqualTo("d e f"); + assertCanCompile(expression); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); + assertThat(stringify(expression.getValue(list))).isEqualTo("d e f"); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); + + expression = parser.parseExpression("[1][2]"); + + assertThat(stringify(expression.getValue(list))).isEqualTo("f"); + assertCanCompile(expression); + assertThat(stringify(expression.getValue(list))).isEqualTo("f"); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); + } + + @Test + void indexIntoMap() { + Map map = Collections.singletonMap("aaa", 111); + + expression = parser.parseExpression("['aaa']"); + + assertThat(expression.getValue(map)).isEqualTo(111); + assertCanCompile(expression); + assertThat(expression.getValue(map)).isEqualTo(111); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); + } + + @Test + void indexIntoMapOfListOfString() { + Map> map = Collections.singletonMap("foo", Arrays.asList("a", "b", "c")); + + expression = parser.parseExpression("['foo']"); + + assertThat(stringify(expression.getValue(map))).isEqualTo("a b c"); + assertCanCompile(expression); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); + assertThat(stringify(expression.getValue(map))).isEqualTo("a b c"); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); + + expression = parser.parseExpression("['foo'][2]"); + + assertThat(stringify(expression.getValue(map))).isEqualTo("c"); + assertCanCompile(expression); + assertThat(stringify(expression.getValue(map))).isEqualTo("c"); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); + } + + @Test + void indexIntoObject() { + TestClass6 tc = new TestClass6(); + + // field access + expression = parser.parseExpression("['orange']"); + + assertThat(expression.getValue(tc)).isEqualTo("value1"); + assertCanCompile(expression); + assertThat(expression.getValue(tc)).isEqualTo("value1"); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/String"); + + // field access + expression = parser.parseExpression("['peach']"); + + assertThat(expression.getValue(tc)).isEqualTo(34L); + assertCanCompile(expression); + assertThat(expression.getValue(tc)).isEqualTo(34L); + assertThat(getAst().getExitDescriptor()).isEqualTo("J"); + + // property access (getter) + expression = parser.parseExpression("['banana']"); + + assertThat(expression.getValue(tc)).isEqualTo("value3"); + assertCanCompile(expression); + assertThat(expression.getValue(tc)).isEqualTo("value3"); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/String"); + } + + @Test // gh-32694, gh-32908 + void indexIntoArrayUsingIntegerWrapper() { + context.setVariable("array", new int[] {1, 2, 3, 4}); + context.setVariable("index", 2); + + expression = parser.parseExpression("#array[#index]"); + + assertThat(expression.getValue(context)).isEqualTo(3); + assertCanCompile(expression); + assertThat(expression.getValue(context)).isEqualTo(3); + assertThat(getAst().getExitDescriptor()).isEqualTo("I"); + } + + @Test // gh-32694, gh-32908 + void indexIntoListUsingIntegerWrapper() { + context.setVariable("list", Arrays.asList(1, 2, 3, 4)); + context.setVariable("index", 2); + + expression = parser.parseExpression("#list[#index]"); + + assertThat(expression.getValue(context)).isEqualTo(3); + assertCanCompile(expression); + assertThat(expression.getValue(context)).isEqualTo(3); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); + } + + @Test // gh-32903 + void indexIntoMapUsingPrimitiveLiteral() { + Map map = new HashMap<>(); + map.put(false, "0"); // BooleanLiteral + map.put(1, "ABC"); // IntLiteral + map.put(2L, "XYZ"); // LongLiteral + map.put(9.99F, "~10"); // FloatLiteral + map.put(3.14159, "PI"); // RealLiteral + context.setVariable("map", map); + + // BooleanLiteral + expression = parser.parseExpression("#map[false]"); + assertThat(expression.getValue(context)).isEqualTo("0"); + assertCanCompile(expression); + assertThat(expression.getValue(context)).isEqualTo("0"); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); + + // IntLiteral + expression = parser.parseExpression("#map[1]"); + assertThat(expression.getValue(context)).isEqualTo("ABC"); + assertCanCompile(expression); + assertThat(expression.getValue(context)).isEqualTo("ABC"); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); + + // LongLiteral + expression = parser.parseExpression("#map[2L]"); + assertThat(expression.getValue(context)).isEqualTo("XYZ"); + assertCanCompile(expression); + assertThat(expression.getValue(context)).isEqualTo("XYZ"); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); + + // FloatLiteral + expression = parser.parseExpression("#map[9.99F]"); + assertThat(expression.getValue(context)).isEqualTo("~10"); + assertCanCompile(expression); + assertThat(expression.getValue(context)).isEqualTo("~10"); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); + + // RealLiteral + expression = parser.parseExpression("#map[3.14159]"); + assertThat(expression.getValue(context)).isEqualTo("PI"); + assertCanCompile(expression); + assertThat(expression.getValue(context)).isEqualTo("PI"); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); + } + + private String stringify(Object object) { + Stream stream; + if (object instanceof Collection) { + stream = ((Collection) object).stream(); + } + else if (object instanceof Object[]) { + stream = Arrays.stream((Object[]) object); + } + else if (object instanceof int[]) { + stream = Arrays.stream((int[]) object).mapToObj(Integer::valueOf); + } + else { + return String.valueOf(object); + } + return stream.map(Object::toString).collect(Collectors.joining(" ")); + } + + } + @Test void typeReference() { expression = parse("T(String)"); @@ -5308,84 +5796,6 @@ public void generateCode(String propertyName, MethodVisitor mv, CodeFlow cf) { } - static class CompilableMapAccessor implements CompilablePropertyAccessor { - - @Override - public boolean canRead(EvaluationContext context, Object target, String name) throws AccessException { - Map map = (Map) target; - return map.containsKey(name); - } - - @Override - public TypedValue read(EvaluationContext context, Object target, String name) throws AccessException { - Map map = (Map) target; - Object value = map.get(name); - if (value == null && !map.containsKey(name)) { - throw new MapAccessException(name); - } - return new TypedValue(value); - } - - @Override - public boolean canWrite(EvaluationContext context, Object target, String name) throws AccessException { - return true; - } - - @Override - @SuppressWarnings("unchecked") - public void write(EvaluationContext context, Object target, String name, Object newValue) throws AccessException { - Map map = (Map) target; - map.put(name, newValue); - } - - @Override - public Class[] getSpecificTargetClasses() { - return new Class[] {Map.class}; - } - - @Override - public boolean isCompilable() { - return true; - } - - @Override - public Class getPropertyType() { - return Object.class; - } - - @Override - public void generateCode(String propertyName, MethodVisitor mv, CodeFlow cf) { - String descriptor = cf.lastDescriptor(); - if (descriptor == null) { - cf.loadTarget(mv); - } - mv.visitLdcInsn(propertyName); - mv.visitMethodInsn(INVOKEINTERFACE, "java/util/Map", "get","(Ljava/lang/Object;)Ljava/lang/Object;",true); - } - } - - - /** - * Exception thrown from {@code read} in order to reset a cached - * PropertyAccessor, allowing other accessors to have a try. - */ - @SuppressWarnings("serial") - private static class MapAccessException extends AccessException { - - private final String key; - - public MapAccessException(String key) { - super(null); - this.key = key; - } - - @Override - public String getMessage() { - return "Map does not contain a value for key '" + this.key + "'"; - } - } - - public static class Greeter { public String getWorld() { diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/SpelReproTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/SpelReproTests.java index 508358f445bf..94aa7468cfa6 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/SpelReproTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/SpelReproTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -209,7 +209,7 @@ void NPE_SPR5673() { checkTemplateParsingError("abc${ } }", "No expression defined within delimiter '${}' at character 3"); checkTemplateParsingError("abc$[ } ]", DOLLARSQUARE_TEMPLATE_PARSER_CONTEXT, "Found closing '}' at position 6 without an opening '{'"); - checkTemplateParsing("abc ${\"def''g}hi\"} jkl", "abc def'g}hi jkl"); + checkTemplateParsing("abc ${\"def''g}hi\"} jkl", "abc def''g}hi jkl"); checkTemplateParsing("abc ${'def''g}hi'} jkl", "abc def'g}hi jkl"); checkTemplateParsing("}", "}"); checkTemplateParsing("${'hello'} world", "hello world"); @@ -1817,7 +1817,6 @@ else if (beanName.equals("goo")) { static class CCC { public boolean method(Object o) { - System.out.println(o); return false; } } @@ -1889,7 +1888,6 @@ static class Foo { static class Foo2 { public void execute(String str) { - System.out.println("Value: " + str); } } @@ -1964,7 +1962,6 @@ public String process(String... args) { public static class ReflectionUtil { public Object methodToCall(T param) { - System.out.println(param + " " + param.getClass()); return "Object methodToCall(T param)"; } diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/SpringExpressionTestSuite.java b/spring-expression/src/test/java/org/springframework/expression/spel/SpringExpressionTestSuite.java new file mode 100644 index 000000000000..eca319fecbac --- /dev/null +++ b/spring-expression/src/test/java/org/springframework/expression/spel/SpringExpressionTestSuite.java @@ -0,0 +1,34 @@ +/* + * Copyright 2002-2022 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.expression.spel; + +import org.junit.platform.suite.api.IncludeClassNamePatterns; +import org.junit.platform.suite.api.SelectPackages; +import org.junit.platform.suite.api.Suite; + +/** + * JUnit Platform based test suite for tests in the spring-expression module. + * + *

    This suite is only intended to be used manually within an IDE. + * + * @author Sam Brannen + */ +@Suite +@SelectPackages("org.springframework.expression.spel") +@IncludeClassNamePatterns(".*Tests?$") +class SpringExpressionTestSuite { +} diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/TemplateExpressionParsingTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/TemplateExpressionParsingTests.java index 49face8bf7d8..33ba6b6c1837 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/TemplateExpressionParsingTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/TemplateExpressionParsingTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 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,14 +30,16 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; /** * @author Andy Clement * @author Juergen Hoeller + * @author Sam Brannen */ -public class TemplateExpressionParsingTests extends AbstractExpressionTests { +class TemplateExpressionParsingTests extends AbstractExpressionTests { - public static final ParserContext DEFAULT_TEMPLATE_PARSER_CONTEXT = new ParserContext() { + static final ParserContext DEFAULT_TEMPLATE_PARSER_CONTEXT = new ParserContext() { @Override public String getExpressionPrefix() { return "${"; @@ -52,7 +54,7 @@ public boolean isTemplate() { } }; - public static final ParserContext HASH_DELIMITED_PARSER_CONTEXT = new ParserContext() { + static final ParserContext HASH_DELIMITED_PARSER_CONTEXT = new ParserContext() { @Override public String getExpressionPrefix() { return "#{"; @@ -68,25 +70,32 @@ public boolean isTemplate() { }; + private final SpelExpressionParser parser = new SpelExpressionParser(); + + + @Test + void nullTemplateExpressionIsRejected() { + assertThatIllegalArgumentException() + .isThrownBy(() -> parser.parseExpression(null, DEFAULT_TEMPLATE_PARSER_CONTEXT)) + .withMessage("'expressionString' must not be null"); + } + @Test - public void testParsingSimpleTemplateExpression01() throws Exception { - SpelExpressionParser parser = new SpelExpressionParser(); + void parsingSimpleTemplateExpression01() { Expression expr = parser.parseExpression("hello ${'world'}", DEFAULT_TEMPLATE_PARSER_CONTEXT); Object o = expr.getValue(); assertThat(o.toString()).isEqualTo("hello world"); } @Test - public void testParsingSimpleTemplateExpression02() throws Exception { - SpelExpressionParser parser = new SpelExpressionParser(); + void parsingSimpleTemplateExpression02() { Expression expr = parser.parseExpression("hello ${'to'} you", DEFAULT_TEMPLATE_PARSER_CONTEXT); Object o = expr.getValue(); assertThat(o.toString()).isEqualTo("hello to you"); } @Test - public void testParsingSimpleTemplateExpression03() throws Exception { - SpelExpressionParser parser = new SpelExpressionParser(); + void parsingSimpleTemplateExpression03() { Expression expr = parser.parseExpression("The quick ${'brown'} fox jumped over the ${'lazy'} dog", DEFAULT_TEMPLATE_PARSER_CONTEXT); Object o = expr.getValue(); @@ -94,8 +103,7 @@ public void testParsingSimpleTemplateExpression03() throws Exception { } @Test - public void testParsingSimpleTemplateExpression04() throws Exception { - SpelExpressionParser parser = new SpelExpressionParser(); + void parsingSimpleTemplateExpression04() { Expression expr = parser.parseExpression("${'hello'} world", DEFAULT_TEMPLATE_PARSER_CONTEXT); Object o = expr.getValue(); assertThat(o.toString()).isEqualTo("hello world"); @@ -114,8 +122,7 @@ public void testParsingSimpleTemplateExpression04() throws Exception { } @Test - public void testCompositeStringExpression() throws Exception { - SpelExpressionParser parser = new SpelExpressionParser(); + void compositeStringExpression() { Expression ex = parser.parseExpression("hello ${'world'}", DEFAULT_TEMPLATE_PARSER_CONTEXT); assertThat(ex.getValue()).isInstanceOf(String.class).isEqualTo("hello world"); assertThat(ex.getValue(String.class)).isInstanceOf(String.class).isEqualTo("hello world"); @@ -154,8 +161,7 @@ public void testCompositeStringExpression() throws Exception { static class Rooty {} @Test - public void testNestedExpressions() throws Exception { - SpelExpressionParser parser = new SpelExpressionParser(); + void nestedExpressions() { // treat the nested ${..} as a part of the expression Expression ex = parser.parseExpression("hello ${listOfNumbersUpToTen.$[#this<5]} world",DEFAULT_TEMPLATE_PARSER_CONTEXT); String s = ex.getValue(TestScenarioCreator.getTestEvaluationContext(),String.class); @@ -185,8 +191,7 @@ public void testNestedExpressions() throws Exception { } @Test - - public void testClashingWithSuffixes() throws Exception { + void clashingWithSuffixes() { // Just wanting to use the prefix or suffix within the template: Expression ex = parser.parseExpression("hello ${3+4} world",DEFAULT_TEMPLATE_PARSER_CONTEXT); String s = ex.getValue(TestScenarioCreator.getTestEvaluationContext(),String.class); @@ -202,13 +207,13 @@ public void testClashingWithSuffixes() throws Exception { } @Test - public void testParsingNormalExpressionThroughTemplateParser() throws Exception { + void parsingNormalExpressionThroughTemplateParser() { Expression expr = parser.parseExpression("1+2+3"); assertThat(expr.getValue()).isEqualTo(6); } @Test - public void testErrorCases() throws Exception { + void errorCases() { assertThatExceptionOfType(ParseException.class).isThrownBy(() -> parser.parseExpression("hello ${'world'", DEFAULT_TEMPLATE_PARSER_CONTEXT)) .satisfies(pex -> { @@ -224,7 +229,7 @@ public void testErrorCases() throws Exception { } @Test - public void testTemplateParserContext() { + void testTemplateParserContext() { TemplateParserContext tpc = new TemplateParserContext("abc","def"); assertThat(tpc.getExpressionPrefix()).isEqualTo("abc"); assertThat(tpc.getExpressionSuffix()).isEqualTo("def"); diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/standard/SpelParserTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/standard/SpelParserTests.java index 9133b7baf003..bccc0a5b8c2b 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/standard/SpelParserTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/standard/SpelParserTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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,8 @@ import java.util.function.Consumer; +import org.assertj.core.api.ThrowableAssert.ThrowingCallable; +import org.assertj.core.api.ThrowableAssertAlternative; import org.junit.jupiter.api.Test; import org.springframework.expression.EvaluationContext; @@ -31,16 +33,57 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.springframework.expression.spel.SpelMessage.MISSING_CONSTRUCTOR_ARGS; +import static org.springframework.expression.spel.SpelMessage.NON_TERMINATING_DOUBLE_QUOTED_STRING; +import static org.springframework.expression.spel.SpelMessage.NON_TERMINATING_QUOTED_STRING; +import static org.springframework.expression.spel.SpelMessage.NOT_AN_INTEGER; +import static org.springframework.expression.spel.SpelMessage.NOT_A_LONG; +import static org.springframework.expression.spel.SpelMessage.OOD; +import static org.springframework.expression.spel.SpelMessage.REAL_CANNOT_BE_LONG; +import static org.springframework.expression.spel.SpelMessage.RIGHT_OPERAND_PROBLEM; +import static org.springframework.expression.spel.SpelMessage.RUN_OUT_OF_ARGUMENTS; +import static org.springframework.expression.spel.SpelMessage.UNEXPECTED_DATA_AFTER_DOT; +import static org.springframework.expression.spel.SpelMessage.UNEXPECTED_ESCAPE_CHAR; /** * @author Andy Clement * @author Juergen Hoeller + * @author Sam Brannen */ class SpelParserTests { + private final SpelExpressionParser parser = new SpelExpressionParser(); + + + @Test + void nullExpressionIsRejected() { + assertNullOrEmptyExpressionIsRejected(() -> parser.parseExpression(null)); + assertNullOrEmptyExpressionIsRejected(() -> parser.parseRaw(null)); + } + + @Test + void emptyExpressionIsRejected() { + assertNullOrEmptyExpressionIsRejected(() -> parser.parseExpression("")); + assertNullOrEmptyExpressionIsRejected(() -> parser.parseRaw("")); + } + + @Test + void blankExpressionIsRejected() { + assertNullOrEmptyExpressionIsRejected(() -> parser.parseExpression(" ")); + assertNullOrEmptyExpressionIsRejected(() -> parser.parseExpression("\t\n")); + assertNullOrEmptyExpressionIsRejected(() -> parser.parseRaw(" ")); + assertNullOrEmptyExpressionIsRejected(() -> parser.parseRaw("\t\n")); + } + + private static void assertNullOrEmptyExpressionIsRejected(ThrowingCallable throwingCallable) { + assertThatIllegalArgumentException() + .isThrownBy(throwingCallable) + .withMessage("'expressionString' must not be null or blank"); + } + @Test void theMostBasic() { - SpelExpressionParser parser = new SpelExpressionParser(); SpelExpression expr = parser.parseRaw("2"); assertThat(expr).isNotNull(); assertThat(expr.getAST()).isNotNull(); @@ -51,7 +94,6 @@ void theMostBasic() { @Test void valueType() { - SpelExpressionParser parser = new SpelExpressionParser(); EvaluationContext ctx = new StandardEvaluationContext(); Class c = parser.parseRaw("2").getValueType(); assertThat(c).isEqualTo(Integer.class); @@ -67,7 +109,6 @@ void valueType() { @Test void whitespace() { - SpelExpressionParser parser = new SpelExpressionParser(); SpelExpression expr = parser.parseRaw("2 + 3"); assertThat(expr.getValue()).isEqualTo(5); expr = parser.parseRaw("2 + 3"); @@ -80,7 +121,6 @@ void whitespace() { @Test void arithmeticPlus1() { - SpelExpressionParser parser = new SpelExpressionParser(); SpelExpression expr = parser.parseRaw("2+2"); assertThat(expr).isNotNull(); assertThat(expr.getAST()).isNotNull(); @@ -89,14 +129,12 @@ void arithmeticPlus1() { @Test void arithmeticPlus2() { - SpelExpressionParser parser = new SpelExpressionParser(); SpelExpression expr = parser.parseRaw("37+41"); assertThat(expr.getValue()).isEqualTo(78); } @Test void arithmeticMultiply1() { - SpelExpressionParser parser = new SpelExpressionParser(); SpelExpression expr = parser.parseRaw("2*3"); assertThat(expr).isNotNull(); assertThat(expr.getAST()).isNotNull(); @@ -105,162 +143,126 @@ void arithmeticMultiply1() { @Test void arithmeticPrecedence1() { - SpelExpressionParser parser = new SpelExpressionParser(); SpelExpression expr = parser.parseRaw("2*3+5"); assertThat(expr.getValue()).isEqualTo(11); } @Test - void generalExpressions() { - assertThatExceptionOfType(SpelParseException.class).isThrownBy(() -> { - SpelExpressionParser parser = new SpelExpressionParser(); - parser.parseRaw("new String"); - }) - .satisfies(parseExceptionRequirements(SpelMessage.MISSING_CONSTRUCTOR_ARGS, 10)); - - assertThatExceptionOfType(SpelParseException.class).isThrownBy(() -> { - SpelExpressionParser parser = new SpelExpressionParser(); - parser.parseRaw("new String(3,"); - }) - .satisfies(parseExceptionRequirements(SpelMessage.RUN_OUT_OF_ARGUMENTS, 10)); - - assertThatExceptionOfType(SpelParseException.class).isThrownBy(() -> { - SpelExpressionParser parser = new SpelExpressionParser(); - parser.parseRaw("new String(3"); - }) - .satisfies(parseExceptionRequirements(SpelMessage.RUN_OUT_OF_ARGUMENTS, 10)); - - assertThatExceptionOfType(SpelParseException.class).isThrownBy(() -> { - SpelExpressionParser parser = new SpelExpressionParser(); - parser.parseRaw("new String("); - }) - .satisfies(parseExceptionRequirements(SpelMessage.RUN_OUT_OF_ARGUMENTS, 10)); - - assertThatExceptionOfType(SpelParseException.class).isThrownBy(() -> { - SpelExpressionParser parser = new SpelExpressionParser(); - parser.parseRaw("\"abc"); - }) - .satisfies(parseExceptionRequirements(SpelMessage.NON_TERMINATING_DOUBLE_QUOTED_STRING, 0)); - - assertThatExceptionOfType(SpelParseException.class).isThrownBy(() -> { - SpelExpressionParser parser = new SpelExpressionParser(); - parser.parseRaw("'abc"); - }) - .satisfies(parseExceptionRequirements(SpelMessage.NON_TERMINATING_QUOTED_STRING, 0)); - - } - - private Consumer parseExceptionRequirements( - SpelMessage expectedMessage, int expectedPosition) { - return ex -> { - assertThat(ex.getMessageCode()).isEqualTo(expectedMessage); - assertThat(ex.getPosition()).isEqualTo(expectedPosition); - assertThat(ex.getMessage()).contains(ex.getExpressionString()); - }; + void parseExceptions() { + assertParseException(() -> parser.parseRaw("new String"), MISSING_CONSTRUCTOR_ARGS, 10); + assertParseException(() -> parser.parseRaw("new String(3,"), RUN_OUT_OF_ARGUMENTS, 10); + assertParseException(() -> parser.parseRaw("new String(3"), RUN_OUT_OF_ARGUMENTS, 10); + assertParseException(() -> parser.parseRaw("new String("), RUN_OUT_OF_ARGUMENTS, 10); + assertParseException(() -> parser.parseRaw("\"abc"), NON_TERMINATING_DOUBLE_QUOTED_STRING, 0); + assertParseException(() -> parser.parseRaw("abc\""), NON_TERMINATING_DOUBLE_QUOTED_STRING, 3); + assertParseException(() -> parser.parseRaw("'abc"), NON_TERMINATING_QUOTED_STRING, 0); + assertParseException(() -> parser.parseRaw("abc'"), NON_TERMINATING_QUOTED_STRING, 3); + assertParseException(() -> parser.parseRaw("("), OOD, 0); + assertParseException(() -> parser.parseRaw(")"), OOD, 0); + assertParseException(() -> parser.parseRaw("+"), OOD, 0); + assertParseException(() -> parser.parseRaw("1+"), RIGHT_OPERAND_PROBLEM, 1); } @Test void arithmeticPrecedence2() { - SpelExpressionParser parser = new SpelExpressionParser(); SpelExpression expr = parser.parseRaw("2+3*5"); assertThat(expr.getValue()).isEqualTo(17); } @Test void arithmeticPrecedence3() { - SpelExpression expr = new SpelExpressionParser().parseRaw("3+10/2"); + SpelExpression expr = parser.parseRaw("3+10/2"); assertThat(expr.getValue()).isEqualTo(8); } @Test void arithmeticPrecedence4() { - SpelExpression expr = new SpelExpressionParser().parseRaw("10/2+3"); + SpelExpression expr = parser.parseRaw("10/2+3"); assertThat(expr.getValue()).isEqualTo(8); } @Test void arithmeticPrecedence5() { - SpelExpression expr = new SpelExpressionParser().parseRaw("(4+10)/2"); + SpelExpression expr = parser.parseRaw("(4+10)/2"); assertThat(expr.getValue()).isEqualTo(7); } @Test void arithmeticPrecedence6() { - SpelExpression expr = new SpelExpressionParser().parseRaw("(3+2)*2"); + SpelExpression expr = parser.parseRaw("(3+2)*2"); assertThat(expr.getValue()).isEqualTo(10); } @Test void booleanOperators() { - SpelExpression expr = new SpelExpressionParser().parseRaw("true"); - assertThat(expr.getValue(Boolean.class)).isEqualTo(Boolean.TRUE); - expr = new SpelExpressionParser().parseRaw("false"); - assertThat(expr.getValue(Boolean.class)).isEqualTo(Boolean.FALSE); - expr = new SpelExpressionParser().parseRaw("false and false"); - assertThat(expr.getValue(Boolean.class)).isEqualTo(Boolean.FALSE); - expr = new SpelExpressionParser().parseRaw("true and (true or false)"); - assertThat(expr.getValue(Boolean.class)).isEqualTo(Boolean.TRUE); - expr = new SpelExpressionParser().parseRaw("true and true or false"); - assertThat(expr.getValue(Boolean.class)).isEqualTo(Boolean.TRUE); - expr = new SpelExpressionParser().parseRaw("!true"); - assertThat(expr.getValue(Boolean.class)).isEqualTo(Boolean.FALSE); - expr = new SpelExpressionParser().parseRaw("!(false or true)"); - assertThat(expr.getValue(Boolean.class)).isEqualTo(Boolean.FALSE); + SpelExpression expr = parser.parseRaw("true"); + assertThat(expr.getValue(Boolean.class)).isTrue(); + expr = parser.parseRaw("false"); + assertThat(expr.getValue(Boolean.class)).isFalse(); + expr = parser.parseRaw("false and false"); + assertThat(expr.getValue(Boolean.class)).isFalse(); + expr = parser.parseRaw("true and (true or false)"); + assertThat(expr.getValue(Boolean.class)).isTrue(); + expr = parser.parseRaw("true and true or false"); + assertThat(expr.getValue(Boolean.class)).isTrue(); + expr = parser.parseRaw("!true"); + assertThat(expr.getValue(Boolean.class)).isFalse(); + expr = parser.parseRaw("!(false or true)"); + assertThat(expr.getValue(Boolean.class)).isFalse(); } @Test void booleanOperators_symbolic_spr9614() { - SpelExpression expr = new SpelExpressionParser().parseRaw("true"); - assertThat(expr.getValue(Boolean.class)).isEqualTo(Boolean.TRUE); - expr = new SpelExpressionParser().parseRaw("false"); - assertThat(expr.getValue(Boolean.class)).isEqualTo(Boolean.FALSE); - expr = new SpelExpressionParser().parseRaw("false && false"); - assertThat(expr.getValue(Boolean.class)).isEqualTo(Boolean.FALSE); - expr = new SpelExpressionParser().parseRaw("true && (true || false)"); - assertThat(expr.getValue(Boolean.class)).isEqualTo(Boolean.TRUE); - expr = new SpelExpressionParser().parseRaw("true && true || false"); - assertThat(expr.getValue(Boolean.class)).isEqualTo(Boolean.TRUE); - expr = new SpelExpressionParser().parseRaw("!true"); - assertThat(expr.getValue(Boolean.class)).isEqualTo(Boolean.FALSE); - expr = new SpelExpressionParser().parseRaw("!(false || true)"); - assertThat(expr.getValue(Boolean.class)).isEqualTo(Boolean.FALSE); + SpelExpression expr = parser.parseRaw("true"); + assertThat(expr.getValue(Boolean.class)).isTrue(); + expr = parser.parseRaw("false"); + assertThat(expr.getValue(Boolean.class)).isFalse(); + expr = parser.parseRaw("false && false"); + assertThat(expr.getValue(Boolean.class)).isFalse(); + expr = parser.parseRaw("true && (true || false)"); + assertThat(expr.getValue(Boolean.class)).isTrue(); + expr = parser.parseRaw("true && true || false"); + assertThat(expr.getValue(Boolean.class)).isTrue(); + expr = parser.parseRaw("!true"); + assertThat(expr.getValue(Boolean.class)).isFalse(); + expr = parser.parseRaw("!(false || true)"); + assertThat(expr.getValue(Boolean.class)).isFalse(); } @Test void stringLiterals() { - SpelExpression expr = new SpelExpressionParser().parseRaw("'howdy'"); + SpelExpression expr = parser.parseRaw("'howdy'"); assertThat(expr.getValue()).isEqualTo("howdy"); - expr = new SpelExpressionParser().parseRaw("'hello '' world'"); + expr = parser.parseRaw("'hello '' world'"); assertThat(expr.getValue()).isEqualTo("hello ' world"); } @Test void stringLiterals2() { - SpelExpression expr = new SpelExpressionParser().parseRaw("'howdy'.substring(0,2)"); + SpelExpression expr = parser.parseRaw("'howdy'.substring(0,2)"); assertThat(expr.getValue()).isEqualTo("ho"); } @Test void testStringLiterals_DoubleQuotes_spr9620() { - SpelExpression expr = new SpelExpressionParser().parseRaw("\"double quote: \"\".\""); + SpelExpression expr = parser.parseRaw("\"double quote: \"\".\""); assertThat(expr.getValue()).isEqualTo("double quote: \"."); - expr = new SpelExpressionParser().parseRaw("\"hello \"\" world\""); + expr = parser.parseRaw("\"hello \"\" world\""); assertThat(expr.getValue()).isEqualTo("hello \" world"); } @Test void testStringLiterals_DoubleQuotes_spr9620_2() { - assertThatExceptionOfType(SpelParseException.class).isThrownBy(() -> - new SpelExpressionParser().parseRaw("\"double quote: \\\"\\\".\"")) + assertParseExceptionThrownBy(() -> parser.parseRaw("\"double quote: \\\"\\\".\"")) .satisfies(ex -> { assertThat(ex.getPosition()).isEqualTo(17); - assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.UNEXPECTED_ESCAPE_CHAR); + assertThat(ex.getMessageCode()).isEqualTo(UNEXPECTED_ESCAPE_CHAR); }); } @Test void positionalInformation() { - SpelExpression expr = new SpelExpressionParser().parseRaw("true and true or false"); + SpelExpression expr = parser.parseRaw("true and true or false"); SpelNode rootAst = expr.getAST(); OpOr operatorOr = (OpOr) rootAst; OpAnd operatorAnd = (OpAnd) operatorOr.getLeftOperand(); @@ -355,10 +357,10 @@ void numerics() { checkNumber("0xa", 10, Integer.class); checkNumber("0xAL", 10L, Long.class); - checkNumberError("0x", SpelMessage.NOT_AN_INTEGER); - checkNumberError("0xL", SpelMessage.NOT_A_LONG); - checkNumberError(".324", SpelMessage.UNEXPECTED_DATA_AFTER_DOT); - checkNumberError("3.4L", SpelMessage.REAL_CANNOT_BE_LONG); + checkNumberError("0x", NOT_AN_INTEGER); + checkNumberError("0xL", NOT_A_LONG); + checkNumberError(".324", UNEXPECTED_DATA_AFTER_DOT); + checkNumberError("3.4L", REAL_CANNOT_BE_LONG); checkNumber("3.5f", 3.5f, Float.class); checkNumber("1.2e3", 1.2e3d, Double.class); @@ -371,7 +373,6 @@ void numerics() { private void checkNumber(String expression, Object value, Class type) { try { - SpelExpressionParser parser = new SpelExpressionParser(); SpelExpression expr = parser.parseRaw(expression); Object exprVal = expr.getValue(); assertThat(exprVal).isEqualTo(value); @@ -383,9 +384,29 @@ private void checkNumber(String expression, Object value, Class type) { } private void checkNumberError(String expression, SpelMessage expectedMessage) { - SpelExpressionParser parser = new SpelExpressionParser(); - assertThatExceptionOfType(SpelParseException.class).isThrownBy(() -> parser.parseRaw(expression)) - .satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(expectedMessage)); + assertParseExceptionThrownBy(() -> parser.parseRaw(expression)) + .satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(expectedMessage)); + } + + private static ThrowableAssertAlternative assertParseExceptionThrownBy(ThrowingCallable throwingCallable) { + return assertThatExceptionOfType(SpelParseException.class).isThrownBy(throwingCallable); + } + + private static void assertParseException(ThrowingCallable throwingCallable, SpelMessage expectedMessage, int expectedPosition) { + assertParseExceptionThrownBy(throwingCallable) + .satisfies(parseExceptionRequirements(expectedMessage, expectedPosition)); + } + + private static Consumer parseExceptionRequirements( + SpelMessage expectedMessage, int expectedPosition) { + + return ex -> { + assertThat(ex.getMessageCode()).isEqualTo(expectedMessage); + assertThat(ex.getPosition()).isEqualTo(expectedPosition); + if (ex.getExpressionString() != null) { + assertThat(ex.getMessage()).contains(ex.getExpressionString()); + } + }; } } diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/support/ReflectionHelperTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/support/ReflectionHelperTests.java index c651d41d47b0..5d08146dd84e 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/support/ReflectionHelperTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/support/ReflectionHelperTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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,13 +19,17 @@ import java.io.ByteArrayOutputStream; import java.io.PrintStream; import java.lang.reflect.Method; +import java.lang.reflect.Proxy; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import org.junit.jupiter.api.Test; import org.springframework.core.convert.TypeDescriptor; import org.springframework.expression.EvaluationContext; +import org.springframework.expression.MethodExecutor; +import org.springframework.expression.MethodResolver; import org.springframework.expression.ParseException; import org.springframework.expression.PropertyAccessor; import org.springframework.expression.TypedValue; @@ -364,6 +368,20 @@ public void testOptimalReflectivePropertyAccessor() throws Exception { field.write(ctx, tester, "field", null)); } + @Test + void reflectiveMethodResolverForJdkProxies() throws Exception { + Object proxy = Proxy.newProxyInstance(getClass().getClassLoader(), new Class[] { Runnable.class }, (p, m, args) -> null); + + MethodResolver resolver = new ReflectiveMethodResolver(); + StandardEvaluationContext evaluationContext = new StandardEvaluationContext(); + + MethodExecutor bogus = resolver.resolve(evaluationContext, proxy, "bogus", Collections.emptyList()); + assertThat(bogus).as("MethodExecutor for bogus()").isNull(); + MethodExecutor toString = resolver.resolve(evaluationContext, proxy, "toString", Collections.emptyList()); + assertThat(toString).as("MethodExecutor for toString()").isNotNull(); + MethodExecutor hashCode = resolver.resolve(evaluationContext, proxy, "hashCode", Collections.emptyList()); + assertThat(hashCode).as("MethodExecutor for hashCode()").isNotNull(); + } /** * Used to validate the match returned from a compareArguments call. diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/support/SimpleEvaluationContextTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/support/SimpleEvaluationContextTests.java new file mode 100644 index 000000000000..e2106ac25a35 --- /dev/null +++ b/spring-expression/src/test/java/org/springframework/expression/spel/support/SimpleEvaluationContextTests.java @@ -0,0 +1,530 @@ +/* + * Copyright 2002-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.expression.spel.support; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.assertj.core.api.ThrowableTypeAssert; +import org.junit.jupiter.api.Test; + +import org.springframework.expression.Expression; +import org.springframework.expression.spel.CompilableMapAccessor; +import org.springframework.expression.spel.SpelEvaluationException; +import org.springframework.expression.spel.SpelMessage; +import org.springframework.expression.spel.standard.SpelExpressionParser; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.entry; + +/** + * Tests for {@link SimpleEvaluationContext}. + * + *

    Some of the use cases in this test class are duplicated elsewhere within the test + * suite; however, we include them here to consistently focus on related features in this + * test class. + * + * @author Sam Brannen + */ +class SimpleEvaluationContextTests { + + private final SpelExpressionParser parser = new SpelExpressionParser(); + + private final Model model = new Model(); + + + @Test + void forReadWriteDataBinding() { + SimpleEvaluationContext context = SimpleEvaluationContext.forReadWriteDataBinding().build(); + + assertReadWriteMode(context); + } + + @Test + void forReadOnlyDataBinding() { + SimpleEvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build(); + + assertCommonReadOnlyModeBehavior(context); + + // WRITE -- via assignment operator + + // Variable + assertAssignmentDisabled(context, "#myVar = 'rejected'"); + + // Property + assertAssignmentDisabled(context, "name = 'rejected'"); + assertIncrementDisabled(context, "count++"); + assertIncrementDisabled(context, "++count"); + assertDecrementDisabled(context, "count--"); + assertDecrementDisabled(context, "--count"); + + // Array Index + assertAssignmentDisabled(context, "array[0] = 'rejected'"); + assertIncrementDisabled(context, "numbers[0]++"); + assertIncrementDisabled(context, "++numbers[0]"); + assertDecrementDisabled(context, "numbers[0]--"); + assertDecrementDisabled(context, "--numbers[0]"); + + // List Index + assertAssignmentDisabled(context, "list[0] = 'rejected'"); + + // Map Index -- key as String + assertAssignmentDisabled(context, "map['red'] = 'rejected'"); + + // Map Index -- key as pseudo property name + assertAssignmentDisabled(context, "map[yellow] = 'rejected'"); + + // String Index + assertAssignmentDisabled(context, "name[0] = 'rejected'"); + + // Object Index + assertAssignmentDisabled(context, "['name'] = 'rejected'"); + } + + @Test + void forPropertyAccessorsInReadWriteMode() { + SimpleEvaluationContext context = SimpleEvaluationContext + .forPropertyAccessors(new CompilableMapAccessor(), DataBindingPropertyAccessor.forReadWriteAccess()) + .build(); + + assertReadWriteMode(context); + + // Map -- with key as property name supported by CompilableMapAccessor + + Expression expression; + expression = parser.parseExpression("map.yellow"); + expression.setValue(context, model, "pineapple"); + assertThat(expression.getValue(context, model, String.class)).isEqualTo("pineapple"); + + expression = parser.parseExpression("map.yellow = 'banana'"); + assertThat(expression.getValue(context, model, String.class)).isEqualTo("banana"); + expression = parser.parseExpression("map.yellow"); + assertThat(expression.getValue(context, model, String.class)).isEqualTo("banana"); + } + + /** + * We call this "mixed" read-only mode, because write access via PropertyAccessors is + * disabled, but write access via the Indexer is not disabled. + */ + @Test + void forPropertyAccessorsInMixedReadOnlyMode() { + SimpleEvaluationContext context = SimpleEvaluationContext + .forPropertyAccessors(new CompilableMapAccessor(), DataBindingPropertyAccessor.forReadOnlyAccess()) + .build(); + + assertCommonReadOnlyModeBehavior(context); + + // Map -- with key as property name supported by CompilableMapAccessor + + Expression expression; + expression = parser.parseExpression("map.yellow"); + expression.setValue(context, model, "pineapple"); + assertThat(expression.getValue(context, model, String.class)).isEqualTo("pineapple"); + + expression = parser.parseExpression("map.yellow = 'banana'"); + assertThat(expression.getValue(context, model, String.class)).isEqualTo("banana"); + expression = parser.parseExpression("map.yellow"); + assertThat(expression.getValue(context, model, String.class)).isEqualTo("banana"); + + // WRITE -- via assignment operator + + // Variable + assertThatSpelEvaluationException() + .isThrownBy(() -> parser.parseExpression("#myVar = 'rejected'").getValue(context, model)) + .satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.VARIABLE_ASSIGNMENT_NOT_SUPPORTED)); + + // Property + assertThatSpelEvaluationException() + .isThrownBy(() -> parser.parseExpression("name = 'rejected'").getValue(context, model)) + .satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.PROPERTY_OR_FIELD_NOT_WRITABLE)); + + // Array Index + parser.parseExpression("array[0]").setValue(context, model, "foo"); + assertThat(model.array).containsExactly("foo"); + + // List Index + parser.parseExpression("list[0]").setValue(context, model, "cat"); + assertThat(model.list).containsExactly("cat"); + + // Map Index -- key as String + parser.parseExpression("map['red']").setValue(context, model, "cherry"); + assertThat(model.map).containsOnly(entry("red", "cherry"), entry("yellow", "banana")); + + // Map Index -- key as pseudo property name + parser.parseExpression("map[yellow]").setValue(context, model, "lemon"); + assertThat(model.map).containsOnly(entry("red", "cherry"), entry("yellow", "lemon")); + + // String Index + // The Indexer does not support writes when indexing into a String. + assertThatSpelEvaluationException() + .isThrownBy(() -> parser.parseExpression("name[0] = 'rejected'").getValue(context, model)) + .satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.INDEXING_NOT_SUPPORTED_FOR_TYPE)); + + // Object Index + assertThatSpelEvaluationException() + .isThrownBy(() -> parser.parseExpression("['name'] = 'rejected'").getValue(context, model)) + .satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.INDEXING_NOT_SUPPORTED_FOR_TYPE)); + + // WRITE -- via increment and decrement operators + + assertIncrementAndDecrementWritesForIndexedStructures(context); + } + + @Test + void forPropertyAccessorsWithAssignmentDisabled() { + SimpleEvaluationContext context = SimpleEvaluationContext + .forPropertyAccessors(new CompilableMapAccessor(), DataBindingPropertyAccessor.forReadOnlyAccess()) + .withAssignmentDisabled() + .build(); + + assertCommonReadOnlyModeBehavior(context); + + // Map -- with key as property name supported by CompilableMapAccessor + + Expression expression; + expression = parser.parseExpression("map.yellow"); + // setValue() is supported even though assignment is not. + expression.setValue(context, model, "pineapple"); + assertThat(expression.getValue(context, model, String.class)).isEqualTo("pineapple"); + + // WRITE -- via assignment operator + + // Variable + assertAssignmentDisabled(context, "#myVar = 'rejected'"); + + // Property + assertAssignmentDisabled(context, "name = 'rejected'"); + assertAssignmentDisabled(context, "map.yellow = 'rejected'"); + assertIncrementDisabled(context, "count++"); + assertIncrementDisabled(context, "++count"); + assertDecrementDisabled(context, "count--"); + assertDecrementDisabled(context, "--count"); + + // Array Index + assertAssignmentDisabled(context, "array[0] = 'rejected'"); + assertIncrementDisabled(context, "numbers[0]++"); + assertIncrementDisabled(context, "++numbers[0]"); + assertDecrementDisabled(context, "numbers[0]--"); + assertDecrementDisabled(context, "--numbers[0]"); + + // List Index + assertAssignmentDisabled(context, "list[0] = 'rejected'"); + + // Map Index -- key as String + assertAssignmentDisabled(context, "map['red'] = 'rejected'"); + + // Map Index -- key as pseudo property name + assertAssignmentDisabled(context, "map[yellow] = 'rejected'"); + + // String Index + assertAssignmentDisabled(context, "name[0] = 'rejected'"); + + // Object Index + assertAssignmentDisabled(context, "['name'] = 'rejected'"); + } + + + private void assertReadWriteMode(SimpleEvaluationContext context) { + // Variables can always be set programmatically within an EvaluationContext. + context.setVariable("myVar", "enigma"); + + // WRITE -- via setValue() + + // Property + parser.parseExpression("name").setValue(context, model, "test"); + assertThat(model.name).isEqualTo("test"); + parser.parseExpression("count").setValue(context, model, 42); + assertThat(model.count).isEqualTo(42); + + // Array Index + parser.parseExpression("array[0]").setValue(context, model, "foo"); + assertThat(model.array).containsExactly("foo"); + + // List Index + parser.parseExpression("list[0]").setValue(context, model, "cat"); + assertThat(model.list).containsExactly("cat"); + + // Map Index -- key as String + parser.parseExpression("map['red']").setValue(context, model, "cherry"); + assertThat(model.map).containsOnly(entry("red", "cherry"), entry("yellow", "replace me")); + + // Map Index -- key as pseudo property name + parser.parseExpression("map[yellow]").setValue(context, model, "lemon"); + assertThat(model.map).containsOnly(entry("red", "cherry"), entry("yellow", "lemon")); + + // READ + assertReadAccess(context); + + // WRITE -- via assignment operator + + // Variable assignment is always disabled in a SimpleEvaluationContext. + assertThatSpelEvaluationException() + .isThrownBy(() -> parser.parseExpression("#myVar = 'rejected'").getValue(context, model)) + .satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.VARIABLE_ASSIGNMENT_NOT_SUPPORTED)); + + Expression expression; + + // Property + expression = parser.parseExpression("name = 'changed'"); + assertThat(expression.getValue(context, model, String.class)).isEqualTo("changed"); + expression = parser.parseExpression("name"); + assertThat(expression.getValue(context, model, String.class)).isEqualTo("changed"); + + // Array Index + expression = parser.parseExpression("array[0] = 'bar'"); + assertThat(expression.getValue(context, model, String.class)).isEqualTo("bar"); + expression = parser.parseExpression("array[0]"); + assertThat(expression.getValue(context, model, String.class)).isEqualTo("bar"); + + // List Index + expression = parser.parseExpression("list[0] = 'dog'"); + assertThat(expression.getValue(context, model, String.class)).isEqualTo("dog"); + expression = parser.parseExpression("list[0]"); + assertThat(expression.getValue(context, model, String.class)).isEqualTo("dog"); + + // Map Index -- key as String + expression = parser.parseExpression("map['red'] = 'strawberry'"); + assertThat(expression.getValue(context, model, String.class)).isEqualTo("strawberry"); + expression = parser.parseExpression("map['red']"); + assertThat(expression.getValue(context, model, String.class)).isEqualTo("strawberry"); + + // Map Index -- key as pseudo property name + expression = parser.parseExpression("map[yellow] = 'banana'"); + assertThat(expression.getValue(context, model, String.class)).isEqualTo("banana"); + expression = parser.parseExpression("map[yellow]"); + assertThat(expression.getValue(context, model, String.class)).isEqualTo("banana"); + + // String Index + // The Indexer does not support writes when indexing into a String. + assertThatSpelEvaluationException() + .isThrownBy(() -> parser.parseExpression("name[0] = 'rejected'").getValue(context, model)) + .satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.INDEXING_NOT_SUPPORTED_FOR_TYPE)); + + // Object Index + expression = parser.parseExpression("['name'] = 'new name'"); + assertThat(expression.getValue(context, model, String.class)).isEqualTo("new name"); + expression = parser.parseExpression("['name']"); + assertThat(expression.getValue(context, model, String.class)).isEqualTo("new name"); + + // WRITE -- via increment and decrement operators + + assertIncrementAndDecrementWritesForProperties(context); + assertIncrementAndDecrementWritesForIndexedStructures(context); + } + + private void assertCommonReadOnlyModeBehavior(SimpleEvaluationContext context) { + // Variables can always be set programmatically within an EvaluationContext. + context.setVariable("myVar", "enigma"); + + // WRITE -- via setValue() + + // Note: forReadOnlyDataBinding() disables programmatic writes via setValue() for + // properties but allows programmatic writes via setValue() for indexed structures. + + // Property + assertThatSpelEvaluationException() + .isThrownBy(() -> parser.parseExpression("name").setValue(context, model, "test")) + .satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.PROPERTY_OR_FIELD_NOT_WRITABLE)); + assertThatSpelEvaluationException() + .isThrownBy(() -> parser.parseExpression("count").setValue(context, model, 42)) + .satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.PROPERTY_OR_FIELD_NOT_WRITABLE)); + + // Array Index + parser.parseExpression("array[0]").setValue(context, model, "foo"); + assertThat(model.array).containsExactly("foo"); + + // List Index + parser.parseExpression("list[0]").setValue(context, model, "cat"); + assertThat(model.list).containsExactly("cat"); + + // Map Index -- key as String + parser.parseExpression("map['red']").setValue(context, model, "cherry"); + assertThat(model.map).containsOnly(entry("red", "cherry"), entry("yellow", "replace me")); + + // Map Index -- key as pseudo property name + parser.parseExpression("map[yellow]").setValue(context, model, "lemon"); + assertThat(model.map).containsOnly(entry("red", "cherry"), entry("yellow", "lemon")); + + // Since the setValue() attempts for "name" and "count" failed above, we have to set + // them directly for assertReadAccess(). + model.name = "test"; + model.count = 42; + + // READ + assertReadAccess(context); + } + + private void assertReadAccess(SimpleEvaluationContext context) { + Expression expression; + + // Variable + expression = parser.parseExpression("#myVar"); + assertThat(expression.getValue(context, model, String.class)).isEqualTo("enigma"); + + // Property + expression = parser.parseExpression("name"); + assertThat(expression.getValue(context, model, String.class)).isEqualTo("test"); + expression = parser.parseExpression("count"); + assertThat(expression.getValue(context, model, Integer.class)).isEqualTo(42); + + // Array Index + expression = parser.parseExpression("array[0]"); + assertThat(expression.getValue(context, model, String.class)).isEqualTo("foo"); + + // List Index + expression = parser.parseExpression("list[0]"); + assertThat(expression.getValue(context, model, String.class)).isEqualTo("cat"); + + // Map Index -- key as String + expression = parser.parseExpression("map['red']"); + assertThat(expression.getValue(context, model, String.class)).isEqualTo("cherry"); + + // Map Index -- key as pseudo property name + expression = parser.parseExpression("map[yellow]"); + assertThat(expression.getValue(context, model, String.class)).isEqualTo("lemon"); + + // String Index + expression = parser.parseExpression("name[0]"); + assertThat(expression.getValue(context, model, String.class)).isEqualTo("t"); + + // Object Index + expression = parser.parseExpression("['name']"); + assertThat(expression.getValue(context, model, String.class)).isEqualTo("test"); + } + + private void assertIncrementAndDecrementWritesForProperties(SimpleEvaluationContext context) { + Expression expression; + expression = parser.parseExpression("count++"); + assertThat(expression.getValue(context, model, Integer.class)).isEqualTo(42); + expression = parser.parseExpression("count"); + assertThat(expression.getValue(context, model, Integer.class)).isEqualTo(43); + + expression = parser.parseExpression("++count"); + assertThat(expression.getValue(context, model, Integer.class)).isEqualTo(44); + expression = parser.parseExpression("count"); + assertThat(expression.getValue(context, model, Integer.class)).isEqualTo(44); + + expression = parser.parseExpression("count--"); + assertThat(expression.getValue(context, model, Integer.class)).isEqualTo(44); + expression = parser.parseExpression("count"); + assertThat(expression.getValue(context, model, Integer.class)).isEqualTo(43); + + expression = parser.parseExpression("--count"); + assertThat(expression.getValue(context, model, Integer.class)).isEqualTo(42); + expression = parser.parseExpression("count"); + assertThat(expression.getValue(context, model, Integer.class)).isEqualTo(42); + } + + private void assertIncrementAndDecrementWritesForIndexedStructures(SimpleEvaluationContext context) { + Expression expression; + expression = parser.parseExpression("numbers[0]++"); + assertThat(expression.getValue(context, model, Integer.class)).isEqualTo(99); + expression = parser.parseExpression("numbers[0]"); + assertThat(expression.getValue(context, model, Integer.class)).isEqualTo(100); + + expression = parser.parseExpression("++numbers[0]"); + assertThat(expression.getValue(context, model, Integer.class)).isEqualTo(101); + expression = parser.parseExpression("numbers[0]"); + assertThat(expression.getValue(context, model, Integer.class)).isEqualTo(101); + + expression = parser.parseExpression("numbers[0]--"); + assertThat(expression.getValue(context, model, Integer.class)).isEqualTo(101); + expression = parser.parseExpression("numbers[0]"); + assertThat(expression.getValue(context, model, Integer.class)).isEqualTo(100); + + expression = parser.parseExpression("--numbers[0]"); + assertThat(expression.getValue(context, model, Integer.class)).isEqualTo(99); + expression = parser.parseExpression("numbers[0]"); + assertThat(expression.getValue(context, model, Integer.class)).isEqualTo(99); + } + + private ThrowableTypeAssert assertThatSpelEvaluationException() { + return assertThatExceptionOfType(SpelEvaluationException.class); + } + + private void assertAssignmentDisabled(SimpleEvaluationContext context, String expression) { + assertEvaluationException(context, expression, SpelMessage.NOT_ASSIGNABLE); + } + + private void assertIncrementDisabled(SimpleEvaluationContext context, String expression) { + assertEvaluationException(context, expression, SpelMessage.OPERAND_NOT_INCREMENTABLE); + } + + private void assertDecrementDisabled(SimpleEvaluationContext context, String expression) { + assertEvaluationException(context, expression, SpelMessage.OPERAND_NOT_DECREMENTABLE); + } + + private void assertEvaluationException(SimpleEvaluationContext context, String expression, SpelMessage spelMessage) { + assertThatSpelEvaluationException() + .isThrownBy(() -> parser.parseExpression(expression).getValue(context, model)) + .satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(spelMessage)); + } + + + static class Model { + + private String name = "replace me"; + private int count = 0; + private final String[] array = {"replace me"}; + private final int[] numbers = {99}; + private final List list = new ArrayList<>(); + private final Map map = new HashMap<>(); + + Model() { + this.list.add("replace me"); + this.map.put("red", "replace me"); + this.map.put("yellow", "replace me"); + } + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + public int getCount() { + return this.count; + } + + public void setCount(int count) { + this.count = count; + } + + public String[] getArray() { + return this.array; + } + + public int[] getNumbers() { + return this.numbers; + } + + public List getList() { + return this.list; + } + + public Map getMap() { + return this.map; + } + + } + +} diff --git a/spring-jcl/src/main/java/org/apache/commons/logging/LogAdapter.java b/spring-jcl/src/main/java/org/apache/commons/logging/LogAdapter.java index cc5bed06ea93..0559f53ca078 100644 --- a/spring-jcl/src/main/java/org/apache/commons/logging/LogAdapter.java +++ b/spring-jcl/src/main/java/org/apache/commons/logging/LogAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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. @@ -154,9 +154,12 @@ private static class Log4jLog implements Log, Serializable { private static final LoggerContext loggerContext = LogManager.getContext(Log4jLog.class.getClassLoader(), false); - private final ExtendedLogger logger; + private final String name; + + private final transient ExtendedLogger logger; public Log4jLog(String name) { + this.name = name; LoggerContext context = loggerContext; if (context == null) { // Circular call in early-init scenario -> static field not initialized yet @@ -270,6 +273,10 @@ private void log(Level level, Object message, Throwable exception) { this.logger.logIfEnabled(FQCN, level, null, message, exception); } } + + protected Object readResolve() { + return new Log4jLog(this.name); + } } diff --git a/spring-jcl/src/main/java/org/apache/commons/logging/LogFactory.java b/spring-jcl/src/main/java/org/apache/commons/logging/LogFactory.java index 0600afc8aa2c..34426cc33f48 100644 --- a/spring-jcl/src/main/java/org/apache/commons/logging/LogFactory.java +++ b/spring-jcl/src/main/java/org/apache/commons/logging/LogFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 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. @@ -77,7 +77,25 @@ public static Log getLog(String name) { */ @Deprecated public static LogFactory getFactory() { - return new LogFactory() {}; + return new LogFactory() { + @Override + public Object getAttribute(String name) { + return null; + } + @Override + public String[] getAttributeNames() { + return new String[0]; + } + @Override + public void removeAttribute(String name) { + } + @Override + public void setAttribute(String name, Object value) { + } + @Override + public void release() { + } + }; } /** @@ -102,4 +120,37 @@ public Log getInstance(String name) { return getLog(name); } + + // Just in case some code happens to call uncommon Commons Logging methods... + + @Deprecated + public abstract Object getAttribute(String name); + + @Deprecated + public abstract String[] getAttributeNames(); + + @Deprecated + public abstract void removeAttribute(String name); + + @Deprecated + public abstract void setAttribute(String name, Object value); + + @Deprecated + public abstract void release(); + + @Deprecated + public static void release(ClassLoader classLoader) { + // do nothing + } + + @Deprecated + public static void releaseAll() { + // do nothing + } + + @Deprecated + public static String objectId(Object o) { + return (o == null ? "null" : o.getClass().getName() + "@" + System.identityHashCode(o)); + } + } diff --git a/spring-jcl/src/main/java/org/apache/commons/logging/LogFactoryService.java b/spring-jcl/src/main/java/org/apache/commons/logging/LogFactoryService.java index ef82eb90c777..c40a928a0272 100644 --- a/spring-jcl/src/main/java/org/apache/commons/logging/LogFactoryService.java +++ b/spring-jcl/src/main/java/org/apache/commons/logging/LogFactoryService.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2023 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. @@ -47,8 +47,9 @@ public Log getInstance(String name) { } - // Just in case some code happens to call uncommon Commons Logging methods... + // Just in case some code happens to rely on Commons Logging attributes... + @Override public void setAttribute(String name, Object value) { if (value != null) { this.attributes.put(name, value); @@ -58,18 +59,22 @@ public void setAttribute(String name, Object value) { } } + @Override public void removeAttribute(String name) { this.attributes.remove(name); } + @Override public Object getAttribute(String name) { return this.attributes.get(name); } + @Override public String[] getAttributeNames() { return this.attributes.keySet().toArray(new String[0]); } + @Override public void release() { } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/IncorrectResultSetColumnCountException.java b/spring-jdbc/src/main/java/org/springframework/jdbc/IncorrectResultSetColumnCountException.java index 3cc779b19766..562140faf025 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/IncorrectResultSetColumnCountException.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/IncorrectResultSetColumnCountException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-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,7 +20,7 @@ /** * Data access exception thrown when a result set did not have the correct column count, - * for example when expecting a single column but getting 0 or more than 1 columns. + * for example when expecting a single column but getting 0 or more than 1 column. * * @author Juergen Hoeller * @since 2.0 diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/JdbcUpdateAffectedIncorrectNumberOfRowsException.java b/spring-jdbc/src/main/java/org/springframework/jdbc/JdbcUpdateAffectedIncorrectNumberOfRowsException.java index 4b88b4159fa8..d7549b0f5cfd 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/JdbcUpdateAffectedIncorrectNumberOfRowsException.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/JdbcUpdateAffectedIncorrectNumberOfRowsException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-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,7 +20,7 @@ /** * Exception thrown when a JDBC update affects an unexpected number of rows. - * Typically we expect an update to affect a single row, meaning it's an + * Typically, we expect an update to affect a single row, meaning it is an * error if it affects multiple rows. * * @author Rod Johnson diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/SQLWarningException.java b/spring-jdbc/src/main/java/org/springframework/jdbc/SQLWarningException.java index 1256e718ca08..2394eb2de330 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/SQLWarningException.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/SQLWarningException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2023 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. @@ -44,11 +44,22 @@ public SQLWarningException(String msg, SQLWarning ex) { super(msg, ex); } + /** - * Return the underlying SQLWarning. + * Return the underlying {@link SQLWarning}. + * @since 5.3.29 */ - public SQLWarning SQLWarning() { + public SQLWarning getSQLWarning() { return (SQLWarning) getCause(); } + /** + * Return the underlying {@link SQLWarning}. + * @deprecated as of 5.3.29, in favor of {@link #getSQLWarning()} + */ + @Deprecated + public SQLWarning SQLWarning() { + return getSQLWarning(); + } + } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/BeanPropertyRowMapper.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/BeanPropertyRowMapper.java index 5c2ae3b8c903..6e74af59aa18 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/BeanPropertyRowMapper.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/BeanPropertyRowMapper.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -48,31 +48,42 @@ /** * {@link RowMapper} implementation that converts a row into a new instance * of the specified mapped target class. The mapped target class must be a - * top-level class and it must have a default or no-arg constructor. + * top-level class or {@code static} nested class, and it must have a default or + * no-arg constructor. * - *

    Column values are mapped based on matching the column name as obtained from result set - * meta-data to public setters for the corresponding properties. The names are matched either - * directly or by transforming a name separating the parts with underscores to the same name - * using "camel" case. + *

    Column values are mapped based on matching the column name (as obtained from + * result set meta-data) to public setters in the target class for the corresponding + * properties. The names are matched either directly or by transforming a name + * separating the parts with underscores to the same name using "camel" case. * - *

    Mapping is provided for fields in the target class for many common types, e.g.: - * String, boolean, Boolean, byte, Byte, short, Short, int, Integer, long, Long, - * float, Float, double, Double, BigDecimal, {@code java.util.Date}, etc. + *

    Mapping is provided for properties in the target class for many common types — + * for example: String, boolean, Boolean, byte, Byte, short, Short, int, Integer, + * long, Long, float, Float, double, Double, BigDecimal, {@code java.util.Date}, etc. * - *

    To facilitate mapping between columns and fields that don't have matching names, - * try using column aliases in the SQL statement like "select fname as first_name from customer". + *

    To facilitate mapping between columns and properties that don't have matching + * names, try using column aliases in the SQL statement like + * {@code "select fname as first_name from customer"}, where {@code first_name} + * can be mapped to a {@code setFirstName(String)} method in the target class. * - *

    For 'null' values read from the database, we will attempt to call the setter, but in the case of - * Java primitives, this causes a TypeMismatchException. This class can be configured (using the - * primitivesDefaultedForNullValue property) to trap this exception and use the primitives default value. - * Be aware that if you use the values from the generated bean to update the database the primitive value - * will have been set to the primitive's default value instead of null. + *

    For a {@code NULL} value read from the database, an attempt will be made to + * call the corresponding setter method with {@code null}, but in the case of + * Java primitives this will result in a {@link TypeMismatchException} by default. + * To ignore {@code NULL} database values for all primitive properties in the + * target class, set the {@code primitivesDefaultedForNullValue} flag to + * {@code true}. See {@link #setPrimitivesDefaultedForNullValue(boolean)} for + * details. * - *

    Please note that this class is designed to provide convenience rather than high performance. - * For best performance, consider using a custom {@link RowMapper} implementation. + *

    If you need to map to a target class which has a data class constructor + * — for example, a Java {@code record} or a Kotlin {@code data} class — + * use {@link DataClassRowMapper} instead. + * + *

    Please note that this class is designed to provide convenience rather than + * high performance. For best performance, consider using a custom {@code RowMapper} + * implementation. * * @author Thomas Risberg * @author Juergen Hoeller + * @author Sam Brannen * @since 2.5 * @param the result type * @see DataClassRowMapper @@ -89,20 +100,24 @@ public class BeanPropertyRowMapper implements RowMapper { /** Whether we're strictly validating. */ private boolean checkFullyPopulated = false; - /** Whether we're defaulting primitives when mapping a null value. */ + /** + * Whether {@code NULL} database values should be ignored for primitive + * properties in the target class. + * @see #setPrimitivesDefaultedForNullValue(boolean) + */ private boolean primitivesDefaultedForNullValue = false; /** ConversionService for binding JDBC values to bean properties. */ @Nullable private ConversionService conversionService = DefaultConversionService.getSharedInstance(); - /** Map of the fields we provide mapping for. */ + /** Map of the properties we provide mapping for. */ @Nullable - private Map mappedFields; + private Map mappedProperties; - /** Set of bean properties we provide mapping for. */ + /** Set of bean property names we provide mapping for. */ @Nullable - private Set mappedProperties; + private Set mappedPropertyNames; /** @@ -126,7 +141,7 @@ public BeanPropertyRowMapper(Class mappedClass) { * Create a new {@code BeanPropertyRowMapper}. * @param mappedClass the class that each row should be mapped to * @param checkFullyPopulated whether we're strictly validating that - * all bean properties have been mapped from corresponding database fields + * all bean properties have been mapped from corresponding database columns */ public BeanPropertyRowMapper(Class mappedClass, boolean checkFullyPopulated) { initialize(mappedClass); @@ -159,7 +174,7 @@ public final Class getMappedClass() { /** * Set whether we're strictly validating that all bean properties have been mapped - * from corresponding database fields. + * from corresponding database columns. *

    Default is {@code false}, accepting unpopulated properties in the target bean. */ public void setCheckFullyPopulated(boolean checkFullyPopulated) { @@ -168,24 +183,33 @@ public void setCheckFullyPopulated(boolean checkFullyPopulated) { /** * Return whether we're strictly validating that all bean properties have been - * mapped from corresponding database fields. + * mapped from corresponding database columns. */ public boolean isCheckFullyPopulated() { return this.checkFullyPopulated; } /** - * Set whether we're defaulting Java primitives in the case of mapping a null value - * from corresponding database fields. - *

    Default is {@code false}, throwing an exception when nulls are mapped to Java primitives. + * Set whether a {@code NULL} database column value should be ignored when + * mapping to a corresponding primitive property in the target class. + *

    Default is {@code false}, throwing an exception when nulls are mapped + * to Java primitives. + *

    If this flag is set to {@code true} and you use an ignored + * primitive property value from the mapped bean to update the database, the + * value in the database will be changed from {@code NULL} to the current value + * of that primitive property. That value may be the property's initial value + * (potentially Java's default value for the respective primitive type), or + * it may be some other value set for the property in the default constructor + * (or initialization block) or as a side effect of setting some other property + * in the mapped bean. */ public void setPrimitivesDefaultedForNullValue(boolean primitivesDefaultedForNullValue) { this.primitivesDefaultedForNullValue = primitivesDefaultedForNullValue; } /** - * Return whether we're defaulting Java primitives in the case of mapping a null value - * from corresponding database fields. + * Get the value of the {@code primitivesDefaultedForNullValue} flag. + * @see #setPrimitivesDefaultedForNullValue(boolean) */ public boolean isPrimitivesDefaultedForNullValue() { return this.primitivesDefaultedForNullValue; @@ -220,31 +244,31 @@ public ConversionService getConversionService() { */ protected void initialize(Class mappedClass) { this.mappedClass = mappedClass; - this.mappedFields = new HashMap<>(); - this.mappedProperties = new HashSet<>(); + this.mappedProperties = new HashMap<>(); + this.mappedPropertyNames = new HashSet<>(); for (PropertyDescriptor pd : BeanUtils.getPropertyDescriptors(mappedClass)) { if (pd.getWriteMethod() != null) { String lowerCaseName = lowerCaseName(pd.getName()); - this.mappedFields.put(lowerCaseName, pd); + this.mappedProperties.put(lowerCaseName, pd); String underscoreName = underscoreName(pd.getName()); if (!lowerCaseName.equals(underscoreName)) { - this.mappedFields.put(underscoreName, pd); + this.mappedProperties.put(underscoreName, pd); } - this.mappedProperties.add(pd.getName()); + this.mappedPropertyNames.add(pd.getName()); } } } /** - * Remove the specified property from the mapped fields. + * Remove the specified property from the mapped properties. * @param propertyName the property name (as used by property descriptors) * @since 5.3.9 */ protected void suppressProperty(String propertyName) { - if (this.mappedFields != null) { - this.mappedFields.remove(lowerCaseName(propertyName)); - this.mappedFields.remove(underscoreName(propertyName)); + if (this.mappedProperties != null) { + this.mappedProperties.remove(lowerCaseName(propertyName)); + this.mappedProperties.remove(underscoreName(propertyName)); } } @@ -306,8 +330,8 @@ public T mapRow(ResultSet rs, int rowNumber) throws SQLException { for (int index = 1; index <= columnCount; index++) { String column = JdbcUtils.lookupColumnName(rsmd, index); - String field = lowerCaseName(StringUtils.delete(column, " ")); - PropertyDescriptor pd = (this.mappedFields != null ? this.mappedFields.get(field) : null); + String property = lowerCaseName(StringUtils.delete(column, " ")); + PropertyDescriptor pd = (this.mappedProperties != null ? this.mappedProperties.get(property) : null); if (pd != null) { try { Object value = getColumnValue(rs, index, pd); @@ -319,13 +343,13 @@ public T mapRow(ResultSet rs, int rowNumber) throws SQLException { bw.setPropertyValue(pd.getName(), value); } catch (TypeMismatchException ex) { - if (value == null && this.primitivesDefaultedForNullValue) { + if (value == null && isPrimitivesDefaultedForNullValue()) { if (logger.isDebugEnabled()) { - logger.debug("Intercepted TypeMismatchException for row " + rowNumber + - " and column '" + column + "' with null value when setting property '" + - pd.getName() + "' of type '" + - ClassUtils.getQualifiedName(pd.getPropertyType()) + - "' on object: " + mappedObject, ex); + String propertyType = ClassUtils.getQualifiedName(pd.getPropertyType()); + logger.debug(String.format( + "Ignoring intercepted TypeMismatchException for row %d and column '%s' " + + "with null value when setting property '%s' of type '%s' on object: %s", + rowNumber, column, pd.getName(), propertyType, mappedObject), ex); } } else { @@ -343,9 +367,9 @@ public T mapRow(ResultSet rs, int rowNumber) throws SQLException { } } - if (populatedProperties != null && !populatedProperties.equals(this.mappedProperties)) { - throw new InvalidDataAccessApiUsageException("Given ResultSet does not contain all fields " + - "necessary to populate object of " + this.mappedClass + ": " + this.mappedProperties); + if (populatedProperties != null && !populatedProperties.equals(this.mappedPropertyNames)) { + throw new InvalidDataAccessApiUsageException("Given ResultSet does not contain all properties " + + "necessary to populate object of " + this.mappedClass + ": " + this.mappedPropertyNames); } return mappedObject; @@ -382,8 +406,11 @@ protected void initBeanWrapper(BeanWrapper bw) { /** * Retrieve a JDBC object value for the specified column. - *

    The default implementation delegates to - * {@link #getColumnValue(ResultSet, int, Class)}. + *

    The default implementation calls + * {@link JdbcUtils#getResultSetValue(java.sql.ResultSet, int, Class)} + * using the type of the specified {@link PropertyDescriptor}. + *

    Subclasses may override this to check specific value types upfront, + * or to post-process values returned from {@code getResultSetValue}. * @param rs is the ResultSet holding the data * @param index is the column index * @param pd the bean property that each result object is expected to match @@ -400,8 +427,8 @@ protected Object getColumnValue(ResultSet rs, int index, PropertyDescriptor pd) * Retrieve a JDBC object value for the specified column. *

    The default implementation calls * {@link JdbcUtils#getResultSetValue(java.sql.ResultSet, int, Class)}. - * Subclasses may override this to check specific value types upfront, - * or to post-process values return from {@code getResultSetValue}. + *

    Subclasses may override this to check specific value types upfront, + * or to post-process values returned from {@code getResultSetValue}. * @param rs is the ResultSet holding the data * @param index is the column index * @param paramType the target parameter type diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/DataClassRowMapper.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/DataClassRowMapper.java index 274bfdfa0cbf..eba4e83aca1a 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/DataClassRowMapper.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/DataClassRowMapper.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -31,15 +31,30 @@ /** * {@link RowMapper} implementation that converts a row into a new instance * of the specified mapped target class. The mapped target class must be a - * top-level class and may either expose a data class constructor with named - * parameters corresponding to column names or classic bean property setters - * (or even a combination of both). + * top-level class or {@code static} nested class, and it may expose either a + * data class constructor with named parameters corresponding to column + * names or classic bean property setter methods with property names corresponding + * to column names (or even a combination of both). + * + *

    The term "data class" applies to Java records, Kotlin data + * classes, and any class which has a constructor with named parameters + * that are intended to be mapped to corresponding column names. + * + *

    When combining a data class constructor with setter methods, any property + * mapped successfully via a constructor argument will not be mapped additionally + * via a corresponding setter method. This means that constructor arguments take + * precedence over property setter methods. * *

    Note that this class extends {@link BeanPropertyRowMapper} and can * therefore serve as a common choice for any mapped target class, flexibly * adapting to constructor style versus setter methods in the mapped class. * + *

    Please note that this class is designed to provide convenience rather than + * high performance. For best performance, consider using a custom {@code RowMapper} + * implementation. + * * @author Juergen Hoeller + * @author Sam Brannen * @since 5.3 * @param the result type */ diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/InterruptibleBatchPreparedStatementSetter.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/InterruptibleBatchPreparedStatementSetter.java index 9de0357d6415..9a304e1cf115 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/InterruptibleBatchPreparedStatementSetter.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/InterruptibleBatchPreparedStatementSetter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-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,7 +22,7 @@ * *

    This interface allows you to signal the end of a batch rather than * having to determine the exact batch size upfront. Batch size is still - * being honored but it is now the maximum size of the batch. + * being honored, but it is now the maximum size of the batch. * *

    The {@link #isBatchExhausted} method is called after each call to * {@link #setValues} to determine whether there were some values added, diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/JdbcOperations.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/JdbcOperations.java index 75937f348bbf..7af681f52b4d 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/JdbcOperations.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/JdbcOperations.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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. @@ -35,12 +35,13 @@ *

    Alternatively, the standard JDBC infrastructure can be mocked. * However, mocking this interface constitutes significantly less work. * As an alternative to a mock objects approach to testing data access code, - * consider the powerful integration testing support provided via the Spring - * TestContext Framework, in the {@code spring-test} artifact. + * consider the powerful integration testing support provided via the + * Spring TestContext Framework, in the {@code spring-test} artifact. * * @author Rod Johnson * @author Juergen Hoeller * @see JdbcTemplate + * @see org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations */ public interface JdbcOperations { diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java index 4cba2e4d65a4..cb05218656a1 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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. @@ -60,13 +60,12 @@ import org.springframework.util.StringUtils; /** - * This is the central class in the JDBC core package. + * This is the central delegate in the JDBC core package. * It simplifies the use of JDBC and helps to avoid common errors. * It executes core JDBC workflow, leaving application code to provide SQL * and extract results. This class executes SQL queries or updates, initiating * iteration over ResultSets and catching JDBC exceptions and translating - * them to the generic, more informative exception hierarchy defined in the - * {@code org.springframework.dao} package. + * them to the common {@code org.springframework.dao} exception hierarchy. * *

    Code using this class need only implement callback interfaces, giving * them a clearly defined contract. The {@link PreparedStatementCreator} callback @@ -75,7 +74,8 @@ * values from a ResultSet. See also {@link PreparedStatementSetter} and * {@link RowMapper} for two popular alternative callback interfaces. * - *

    Can be used within a service implementation via direct instantiation + *

    An instance of this template class is thread-safe once configured. + * Can be used within a service implementation via direct instantiation * with a DataSource reference, or get prepared in an application context * and given to services as bean reference. Note: The DataSource should * always be configured as a bean in the application context, in the first case @@ -88,12 +88,11 @@ *

    All SQL operations performed by this class are logged at debug level, * using "org.springframework.jdbc.core.JdbcTemplate" as log category. * - *

    NOTE: An instance of this class is thread-safe once configured. - * * @author Rod Johnson * @author Juergen Hoeller * @author Thomas Risberg * @since May 3, 2001 + * @see JdbcOperations * @see PreparedStatementCreator * @see PreparedStatementSetter * @see CallableStatementCreator @@ -103,6 +102,7 @@ * @see RowCallbackHandler * @see RowMapper * @see org.springframework.jdbc.support.SQLExceptionTranslator + * @see org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate */ public class JdbcTemplate extends JdbcAccessor implements JdbcOperations { @@ -187,12 +187,14 @@ public JdbcTemplate(DataSource dataSource, boolean lazyInit) { /** - * Set whether we want to ignore SQLWarnings. - *

    Default is "true", swallowing and logging all warnings. Switch this flag - * to "false" to make the JdbcTemplate throw an SQLWarningException instead. + * Set whether we want to ignore JDBC statement warnings ({@link SQLWarning}). + *

    Default is {@code true}, swallowing and logging all warnings. Switch this flag to + * {@code false} to make this JdbcTemplate throw a {@link SQLWarningException} instead + * (or chain the {@link SQLWarning} into the primary {@link SQLException}, if any). + * @see Statement#getWarnings() * @see java.sql.SQLWarning * @see org.springframework.jdbc.SQLWarningException - * @see #handleWarnings + * @see #handleWarnings(Statement) */ public void setIgnoreWarnings(boolean ignoreWarnings) { this.ignoreWarnings = ignoreWarnings; @@ -385,6 +387,9 @@ private T execute(StatementCallback action, boolean closeResources) throw catch (SQLException ex) { // Release Connection early, to avoid potential connection pool deadlock // in the case when the exception translator hasn't been initialized yet. + if (stmt != null) { + handleWarnings(stmt, ex); + } String sql = getSql(action); JdbcUtils.closeStatement(stmt); stmt = null; @@ -412,9 +417,7 @@ public void execute(final String sql) throws DataAccessException { logger.debug("Executing SQL statement [" + sql + "]"); } - /** - * Callback to execute the statement. - */ + // Callback to execute the statement. class ExecuteStatementCallback implements StatementCallback, SqlProvider { @Override @Nullable @@ -440,9 +443,7 @@ public T query(final String sql, final ResultSetExtractor rse) throws Dat logger.debug("Executing SQL query [" + sql + "]"); } - /** - * Callback to execute the query. - */ + // Callback to execute the query. class QueryStatementCallback implements StatementCallback, SqlProvider { @Override @Nullable @@ -537,9 +538,7 @@ public int update(final String sql) throws DataAccessException { logger.debug("Executing SQL update [" + sql + "]"); } - /** - * Callback to execute the update statement. - */ + // Callback to execute the update statement. class UpdateStatementCallback implements StatementCallback, SqlProvider { @Override public Integer doInStatement(Statement stmt) throws SQLException { @@ -565,9 +564,7 @@ public int[] batchUpdate(final String... sql) throws DataAccessException { logger.debug("Executing SQL batch update of " + sql.length + " statements"); } - /** - * Callback to execute the batch update. - */ + // Callback to execute the batch update. class BatchUpdateStatementCallback implements StatementCallback, SqlProvider { @Nullable @@ -658,6 +655,9 @@ private T execute(PreparedStatementCreator psc, PreparedStatementCallback if (psc instanceof ParameterDisposer) { ((ParameterDisposer) psc).cleanupParameters(); } + if (ps != null) { + handleWarnings(ps, ex); + } String sql = getSql(psc); psc = null; JdbcUtils.closeStatement(ps); @@ -1197,6 +1197,9 @@ public T execute(CallableStatementCreator csc, CallableStatementCallback if (csc instanceof ParameterDisposer) { ((ParameterDisposer) csc).cleanupParameters(); } + if (cs != null) { + handleWarnings(cs, ex); + } String sql = getSql(csc); csc = null; JdbcUtils.closeStatement(cs); @@ -1365,7 +1368,7 @@ protected Map extractOutputParameters(CallableStatement cs, List } } } - if (!(param.isResultsParameter())) { + if (!param.isResultsParameter()) { sqlColIndex++; } } @@ -1494,13 +1497,44 @@ protected PreparedStatementSetter newArgTypePreparedStatementSetter(Object[] arg } /** - * Throw an SQLWarningException if we're not ignoring warnings, - * otherwise log the warnings at debug level. + * Handle warnings before propagating a primary {@code SQLException} + * from executing the given statement. + *

    Calls regular {@link #handleWarnings(Statement)} but catches + * {@link SQLWarningException} in order to chain the {@link SQLWarning} + * into the primary exception instead. * @param stmt the current JDBC statement - * @throws SQLWarningException if not ignoring warnings - * @see org.springframework.jdbc.SQLWarningException + * @param ex the primary exception after failed statement execution + * @since 5.3.29 + * @see #handleWarnings(Statement) + * @see SQLException#setNextException + */ + protected void handleWarnings(Statement stmt, SQLException ex) { + try { + handleWarnings(stmt); + } + catch (SQLWarningException nonIgnoredWarning) { + ex.setNextException(nonIgnoredWarning.getSQLWarning()); + } + catch (SQLException warningsEx) { + logger.debug("Failed to retrieve warnings", warningsEx); + } + catch (Throwable warningsEx) { + logger.debug("Failed to process warnings", warningsEx); + } + } + + /** + * Handle the warnings for the given JDBC statement, if any. + *

    Throws a {@link SQLWarningException} if we're not ignoring warnings, + * otherwise logs the warnings at debug level. + * @param stmt the current JDBC statement + * @throws SQLException in case of warnings retrieval failure + * @throws SQLWarningException for a concrete warning to raise + * (when not ignoring warnings) + * @see #setIgnoreWarnings + * @see #handleWarnings(SQLWarning) */ - protected void handleWarnings(Statement stmt) throws SQLException { + protected void handleWarnings(Statement stmt) throws SQLException, SQLWarningException { if (isIgnoreWarnings()) { if (logger.isDebugEnabled()) { SQLWarning warningToLog = stmt.getWarnings(); @@ -1517,7 +1551,7 @@ protected void handleWarnings(Statement stmt) throws SQLException { } /** - * Throw an SQLWarningException if encountering an actual warning. + * Throw a {@link SQLWarningException} if encountering an actual warning. * @param warning the warnings object from the current statement. * May be {@code null}, in which case this method does nothing. * @throws SQLWarningException in case of an actual warning to be raised diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/RowMapper.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/RowMapper.java index a520b67e8e8d..291d3f2ac1a4 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/RowMapper.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/RowMapper.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2023 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,21 +24,21 @@ /** * An interface used by {@link JdbcTemplate} for mapping rows of a * {@link java.sql.ResultSet} on a per-row basis. Implementations of this - * interface perform the actual work of mapping each row to a result object, + * interface perform the actual work of mapping each row to a result object * but don't need to worry about exception handling. * {@link java.sql.SQLException SQLExceptions} will be caught and handled - * by the calling JdbcTemplate. + * by the calling {@code JdbcTemplate}. * - *

    Typically used either for {@link JdbcTemplate}'s query methods - * or for out parameters of stored procedures. RowMapper objects are + *

    Typically used either for {@code JdbcTemplate}'s query methods or for + * {@code out} parameters of stored procedures. {@code RowMapper} objects are * typically stateless and thus reusable; they are an ideal choice for * implementing row-mapping logic in a single place. * *

    Alternatively, consider subclassing * {@link org.springframework.jdbc.object.MappingSqlQuery} from the - * {@code jdbc.object} package: Instead of working with separate - * JdbcTemplate and RowMapper objects, you can build executable query - * objects (containing row-mapping logic) in that style. + * {@code jdbc.object} package: instead of working with separate + * {@code JdbcTemplate} and {@code RowMapper} objects, you can build executable + * query objects (containing row-mapping logic) in that style. * * @author Thomas Risberg * @author Juergen Hoeller @@ -52,13 +52,13 @@ public interface RowMapper { /** - * Implementations must implement this method to map each row of data - * in the ResultSet. This method should not call {@code next()} on - * the ResultSet; it is only supposed to map values of the current row. - * @param rs the ResultSet to map (pre-initialized for the current row) + * Implementations must implement this method to map each row of data in the + * {@code ResultSet}. This method should not call {@code next()} on the + * {@code ResultSet}; it is only supposed to map values of the current row. + * @param rs the {@code ResultSet} to map (pre-initialized for the current row) * @param rowNum the number of the current row * @return the result object for the current row (may be {@code null}) - * @throws SQLException if an SQLException is encountered getting + * @throws SQLException if an SQLException is encountered while getting * column values (that is, there's no need to catch SQLException) */ @Nullable diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/CallMetaDataContext.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/CallMetaDataContext.java index e55ee9621da0..2ea0c544ae0e 100755 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/CallMetaDataContext.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/CallMetaDataContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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.jdbc.core.metadata; -import java.sql.DatabaseMetaData; import java.util.ArrayList; import java.util.HashSet; import java.util.LinkedHashMap; @@ -115,28 +114,28 @@ public String getFunctionReturnName() { } /** - * Specify a limited set of in parameters to be used. + * Specify a limited set of the {@code in} parameters to be used. */ public void setLimitedInParameterNames(Set limitedInParameterNames) { this.limitedInParameterNames = limitedInParameterNames; } /** - * Get a limited set of in parameters to be used. + * Get the limited set of the {@code in} parameters to be used. */ public Set getLimitedInParameterNames() { return this.limitedInParameterNames; } /** - * Specify the names of the out parameters. + * Specify the names of the {@code out} parameters. */ public void setOutParameterNames(List outParameterNames) { this.outParameterNames = outParameterNames; } /** - * Get a list of the out parameter names. + * Get the list of the {@code out} parameter names. */ public List getOutParameterNames() { return this.outParameterNames; @@ -434,14 +433,14 @@ protected List reconcileParameters(List parameters) if (paramNameToUse == null) { paramNameToUse = ""; } - if (meta.getParameterType() == DatabaseMetaData.procedureColumnOut) { + if (meta.isOutParameter()) { workParams.add(provider.createDefaultOutParameter(paramNameToUse, meta)); outParamNames.add(paramNameToUse); if (logger.isDebugEnabled()) { logger.debug("Added meta-data out parameter for '" + paramNameToUse + "'"); } } - else if (meta.getParameterType() == DatabaseMetaData.procedureColumnInOut) { + else if (meta.isInOutParameter()) { workParams.add(provider.createDefaultInOutParameter(paramNameToUse, meta)); outParamNames.add(paramNameToUse); if (logger.isDebugEnabled()) { @@ -554,7 +553,7 @@ else if (logger.isInfoEnabled()) { Map callParameterNames = CollectionUtils.newHashMap(this.callParameters.size()); for (SqlParameter parameter : this.callParameters) { if (parameter.isInputValueProvided()) { - String parameterName = parameter.getName(); + String parameterName = parameter.getName(); String parameterNameToMatch = provider.parameterNameToUse(parameterName); if (parameterNameToMatch != null) { callParameterNames.put(parameterNameToMatch.toLowerCase(), parameterName); @@ -606,7 +605,7 @@ else if (logger.isInfoEnabled()) { int i = 0; for (SqlParameter parameter : this.callParameters) { if (parameter.isInputValueProvided()) { - String parameterName = parameter.getName(); + String parameterName = parameter.getName(); matchedParameters.put(parameterName, parameterValues[i++]); } } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/CallMetaDataProvider.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/CallMetaDataProvider.java index 068a1c457813..1666daf420ae 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/CallMetaDataProvider.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/CallMetaDataProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2023 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. @@ -43,7 +43,7 @@ public interface CallMetaDataProvider { /** * Initialize the database specific management of procedure column meta-data. - * This is only called for databases that are supported. This initialization + *

    This is only called for databases that are supported. This initialization * can be turned off by specifying that column meta-data should not be used. * @param databaseMetaData used to retrieve database specific information * @param catalogName name of catalog to use (or {@code null} if none) @@ -55,30 +55,36 @@ public interface CallMetaDataProvider { void initializeWithProcedureColumnMetaData(DatabaseMetaData databaseMetaData, @Nullable String catalogName, @Nullable String schemaName, @Nullable String procedureName) throws SQLException; + /** + * Get the call parameter meta-data that is currently used. + * @return a List of {@link CallParameterMetaData} + */ + List getCallParameterMetaData(); + /** * Provide any modification of the procedure name passed in to match the meta-data currently used. - * This could include altering the case. + *

    This could include altering the case. */ @Nullable String procedureNameToUse(@Nullable String procedureName); /** * Provide any modification of the catalog name passed in to match the meta-data currently used. - * This could include altering the case. + *

    This could include altering the case. */ @Nullable String catalogNameToUse(@Nullable String catalogName); /** * Provide any modification of the schema name passed in to match the meta-data currently used. - * This could include altering the case. + *

    This could include altering the case. */ @Nullable String schemaNameToUse(@Nullable String schemaName); /** * Provide any modification of the catalog name passed in to match the meta-data currently used. - * The returned value will be used for meta-data lookups. This could include altering the case + *

    The returned value will be used for meta-data lookups. This could include altering the case * used or providing a base catalog if none is provided. */ @Nullable @@ -86,7 +92,7 @@ void initializeWithProcedureColumnMetaData(DatabaseMetaData databaseMetaData, @N /** * Provide any modification of the schema name passed in to match the meta-data currently used. - * The returned value will be used for meta-data lookups. This could include altering the case + *

    The returned value will be used for meta-data lookups. This could include altering the case * used or providing a base schema if none is provided. */ @Nullable @@ -94,7 +100,7 @@ void initializeWithProcedureColumnMetaData(DatabaseMetaData databaseMetaData, @N /** * Provide any modification of the column name passed in to match the meta-data currently used. - * This could include altering the case. + *

    This could include altering the case. * @param parameterName name of the parameter of column */ @Nullable @@ -102,7 +108,7 @@ void initializeWithProcedureColumnMetaData(DatabaseMetaData databaseMetaData, @N /** * Create a default out parameter based on the provided meta-data. - * This is used when no explicit parameter declaration has been made. + *

    This is used when no explicit parameter declaration has been made. * @param parameterName the name of the parameter * @param meta meta-data used for this call * @return the configured SqlOutParameter @@ -111,7 +117,7 @@ void initializeWithProcedureColumnMetaData(DatabaseMetaData databaseMetaData, @N /** * Create a default in/out parameter based on the provided meta-data. - * This is used when no explicit parameter declaration has been made. + *

    This is used when no explicit parameter declaration has been made. * @param parameterName the name of the parameter * @param meta meta-data used for this call * @return the configured SqlInOutParameter @@ -120,7 +126,7 @@ void initializeWithProcedureColumnMetaData(DatabaseMetaData databaseMetaData, @N /** * Create a default in parameter based on the provided meta-data. - * This is used when no explicit parameter declaration has been made. + *

    This is used when no explicit parameter declaration has been made. * @param parameterName the name of the parameter * @param meta meta-data used for this call * @return the configured SqlParameter @@ -142,7 +148,7 @@ void initializeWithProcedureColumnMetaData(DatabaseMetaData databaseMetaData, @N /** * Does this database support returning ResultSets as ref cursors to be retrieved with - * {@link java.sql.CallableStatement#getObject(int)} for the specified column. + * {@link java.sql.CallableStatement#getObject(int)} for the specified column? */ boolean isRefCursorSupported(); @@ -158,18 +164,12 @@ void initializeWithProcedureColumnMetaData(DatabaseMetaData databaseMetaData, @N boolean isProcedureColumnMetaDataUsed(); /** - * Should we bypass the return parameter with the specified name. - * This allows the database specific implementation to skip the processing + * Should we bypass the return parameter with the specified name? + *

    This allows the database specific implementation to skip the processing * for specific results returned by the database call. */ boolean byPassReturnParameter(String parameterName); - /** - * Get the call parameter meta-data that is currently used. - * @return a List of {@link CallParameterMetaData} - */ - List getCallParameterMetaData(); - /** * Does the database support the use of catalog name in procedure calls? */ diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/CallParameterMetaData.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/CallParameterMetaData.java index 76d9cec68f4e..99d529f343f5 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/CallParameterMetaData.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/CallParameterMetaData.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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. @@ -107,6 +107,28 @@ public boolean isReturnParameter() { this.parameterType == DatabaseMetaData.procedureColumnResult)); } + /** + * Determine whether the declared parameter qualifies as an 'out' parameter + * for our purposes: type {@link DatabaseMetaData#procedureColumnOut}, + * or in case of a function, {@link DatabaseMetaData#functionColumnOut}. + * @since 5.3.31 + */ + public boolean isOutParameter() { + return (this.function ? this.parameterType == DatabaseMetaData.functionColumnOut : + this.parameterType == DatabaseMetaData.procedureColumnOut); + } + + /** + * Determine whether the declared parameter qualifies as an 'in-out' parameter + * for our purposes: type {@link DatabaseMetaData#procedureColumnInOut}, + * or in case of a function, {@link DatabaseMetaData#functionColumnInOut}. + * @since 5.3.31 + */ + public boolean isInOutParameter() { + return (this.function ? this.parameterType == DatabaseMetaData.functionColumnInOut : + this.parameterType == DatabaseMetaData.procedureColumnInOut); + } + /** * Return the parameter SQL type. */ diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/GenericCallMetaDataProvider.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/GenericCallMetaDataProvider.java index 8a9b2d0a3fb4..789351ff5eee 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/GenericCallMetaDataProvider.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/GenericCallMetaDataProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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. @@ -35,7 +35,8 @@ /** * A generic implementation of the {@link CallMetaDataProvider} interface. - * This class can be extended to provide database specific behavior. + * + *

    This class can be extended to provide database specific behavior. * * @author Thomas Risberg * @author Juergen Hoeller @@ -113,7 +114,7 @@ public void initializeWithProcedureColumnMetaData(DatabaseMetaData databaseMetaD @Nullable String schemaName, @Nullable String procedureName) throws SQLException { this.procedureColumnMetaDataUsed = true; - processProcedureColumns(databaseMetaData, catalogName, schemaName, procedureName); + processProcedureColumns(databaseMetaData, catalogName, schemaName, procedureName); } @Override @@ -124,52 +125,19 @@ public List getCallParameterMetaData() { @Override @Nullable public String procedureNameToUse(@Nullable String procedureName) { - if (procedureName == null) { - return null; - } - else if (isStoresUpperCaseIdentifiers()) { - return procedureName.toUpperCase(); - } - else if (isStoresLowerCaseIdentifiers()) { - return procedureName.toLowerCase(); - } - else { - return procedureName; - } + return identifierNameToUse(procedureName); } @Override @Nullable public String catalogNameToUse(@Nullable String catalogName) { - if (catalogName == null) { - return null; - } - else if (isStoresUpperCaseIdentifiers()) { - return catalogName.toUpperCase(); - } - else if (isStoresLowerCaseIdentifiers()) { - return catalogName.toLowerCase(); - } - else { - return catalogName; - } + return identifierNameToUse(catalogName); } @Override @Nullable public String schemaNameToUse(@Nullable String schemaName) { - if (schemaName == null) { - return null; - } - else if (isStoresUpperCaseIdentifiers()) { - return schemaName.toUpperCase(); - } - else if (isStoresLowerCaseIdentifiers()) { - return schemaName.toLowerCase(); - } - else { - return schemaName; - } + return identifierNameToUse(schemaName); } @Override @@ -197,23 +165,7 @@ public String metaDataSchemaNameToUse(@Nullable String schemaName) { @Override @Nullable public String parameterNameToUse(@Nullable String parameterName) { - if (parameterName == null) { - return null; - } - else if (isStoresUpperCaseIdentifiers()) { - return parameterName.toUpperCase(); - } - else if (isStoresLowerCaseIdentifiers()) { - return parameterName.toLowerCase(); - } - else { - return parameterName; - } - } - - @Override - public boolean byPassReturnParameter(String parameterName) { - return false; + return identifierNameToUse(parameterName); } @Override @@ -256,6 +208,11 @@ public boolean isProcedureColumnMetaDataUsed() { return this.procedureColumnMetaDataUsed; } + @Override + public boolean byPassReturnParameter(String parameterName) { + return false; + } + /** * Specify whether the database supports the use of catalog name in procedure calls. @@ -316,6 +273,22 @@ protected boolean isStoresLowerCaseIdentifiers() { } + @Nullable + private String identifierNameToUse(@Nullable String identifierName) { + if (identifierName == null) { + return null; + } + else if (isStoresUpperCaseIdentifiers()) { + return identifierName.toUpperCase(); + } + else if (isStoresLowerCaseIdentifiers()) { + return identifierName.toLowerCase(); + } + else { + return identifierName; + } + } + /** * Process the procedure column meta-data. */ diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/BeanPropertySqlParameterSource.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/BeanPropertySqlParameterSource.java index 83b8edb24b90..be0233d47ce8 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/BeanPropertySqlParameterSource.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/BeanPropertySqlParameterSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 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. @@ -32,9 +32,10 @@ /** * {@link SqlParameterSource} implementation that obtains parameter values * from bean properties of a given JavaBean object. The names of the bean - * properties have to match the parameter names. + * properties have to match the parameter names. Supports components of + * record classes as well, with accessor methods matching parameter names. * - *

    Uses a Spring BeanWrapper for bean property access underneath. + *

    Uses a Spring {@link BeanWrapper} for bean property access underneath. * * @author Thomas Risberg * @author Juergen Hoeller diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterJdbcOperations.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterJdbcOperations.java index b308e06f735b..f56df5617d8e 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterJdbcOperations.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterJdbcOperations.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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. @@ -527,26 +527,26 @@ int update(String sql, SqlParameterSource paramSource, KeyHolder generatedKeyHol throws DataAccessException; /** - * Executes a batch using the supplied SQL statement with the batch of supplied arguments. + * Execute a batch using the supplied SQL statement with the batch of supplied arguments. * @param sql the SQL statement to execute - * @param batchValues the array of Maps containing the batch of arguments for the query + * @param batchArgs the array of {@link SqlParameterSource} containing the batch of + * arguments for the query * @return an array containing the numbers of rows affected by each update in the batch * (may also contain special JDBC-defined negative values for affected rows such as * {@link java.sql.Statement#SUCCESS_NO_INFO}/{@link java.sql.Statement#EXECUTE_FAILED}) * @throws DataAccessException if there is any problem issuing the update */ - int[] batchUpdate(String sql, Map[] batchValues); + int[] batchUpdate(String sql, SqlParameterSource[] batchArgs); /** - * Execute a batch using the supplied SQL statement with the batch of supplied arguments. + * Executes a batch using the supplied SQL statement with the batch of supplied arguments. * @param sql the SQL statement to execute - * @param batchArgs the array of {@link SqlParameterSource} containing the batch of - * arguments for the query + * @param batchValues the array of Maps containing the batch of arguments for the query * @return an array containing the numbers of rows affected by each update in the batch * (may also contain special JDBC-defined negative values for affected rows such as * {@link java.sql.Statement#SUCCESS_NO_INFO}/{@link java.sql.Statement#EXECUTE_FAILED}) * @throws DataAccessException if there is any problem issuing the update */ - int[] batchUpdate(String sql, SqlParameterSource[] batchArgs); + int[] batchUpdate(String sql, Map[] batchValues); } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterJdbcTemplate.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterJdbcTemplate.java index 3d3752b5bf59..38069d8f3e44 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterJdbcTemplate.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterJdbcTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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. @@ -55,16 +55,19 @@ * done at execution time. It also allows for expanding a {@link java.util.List} * of values to the appropriate number of placeholders. * - *

    The underlying {@link org.springframework.jdbc.core.JdbcTemplate} is + *

    An instance of this template class is thread-safe once configured. + * The underlying {@link org.springframework.jdbc.core.JdbcTemplate} is * exposed to allow for convenient access to the traditional * {@link org.springframework.jdbc.core.JdbcTemplate} methods. * - *

    NOTE: An instance of this class is thread-safe once configured. - * * @author Thomas Risberg * @author Juergen Hoeller * @since 2.0 * @see NamedParameterJdbcOperations + * @see SqlParameterSource + * @see ResultSetExtractor + * @see RowCallbackHandler + * @see RowMapper * @see org.springframework.jdbc.core.JdbcTemplate */ public class NamedParameterJdbcTemplate implements NamedParameterJdbcOperations { @@ -356,11 +359,6 @@ public int update( return getJdbcOperations().update(psc, generatedKeyHolder); } - @Override - public int[] batchUpdate(String sql, Map[] batchValues) { - return batchUpdate(sql, SqlParameterSourceUtils.createBatch(batchValues)); - } - @Override public int[] batchUpdate(String sql, SqlParameterSource[] batchArgs) { if (batchArgs.length == 0) { @@ -385,6 +383,11 @@ public int getBatchSize() { }); } + @Override + public int[] batchUpdate(String sql, Map[] batchValues) { + return batchUpdate(sql, SqlParameterSourceUtils.createBatch(batchValues)); + } + /** * Build a {@link PreparedStatementCreator} based on the given SQL and named parameters. @@ -433,6 +436,7 @@ protected PreparedStatementCreator getPreparedStatementCreator(String sql, SqlPa * @return a representation of the parsed SQL statement */ protected ParsedSql getParsedSql(String sql) { + Assert.notNull(sql, "SQL must not be null"); return this.parsedSqlCache.get(sql); } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/AbstractJdbcCall.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/AbstractJdbcCall.java index 82a2ad19c13d..0121db51c0b2 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/AbstractJdbcCall.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/AbstractJdbcCall.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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. @@ -43,7 +43,7 @@ * Abstract class to provide base functionality for easy stored procedure calls * based on configuration options and database meta-data. * - *

    This class provides the base SPI for {@link SimpleJdbcCall}. + *

    This class provides the processing arrangement for {@link SimpleJdbcCall}. * * @author Thomas Risberg * @author Juergen Hoeller @@ -453,7 +453,7 @@ protected Map matchInParameterValuesWithCallParameters(SqlParame /** * Match the provided in parameter values with registered parameters and * parameters defined via meta-data processing. - * @param args the parameter values provided in a Map + * @param args the parameter values provided as a Map * @return a Map with parameter names and values */ protected Map matchInParameterValuesWithCallParameters(Map args) { diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/AbstractJdbcInsert.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/AbstractJdbcInsert.java index 5d6d83c142d4..504ee7013b7e 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/AbstractJdbcInsert.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/AbstractJdbcInsert.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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,10 +50,10 @@ import org.springframework.util.Assert; /** - * Abstract class to provide base functionality for easy inserts + * Abstract class to provide base functionality for easy (batch) inserts * based on configuration options and database meta-data. * - *

    This class provides the base SPI for {@link SimpleJdbcInsert}. + *

    This class provides the processing arrangement for {@link SimpleJdbcInsert}. * * @author Thomas Risberg * @author Juergen Hoeller @@ -409,7 +409,7 @@ protected KeyHolder doExecuteAndReturnKeyHolder(SqlParameterSource parameterSour /** * Delegate method to execute the insert, generating a single key. */ - private Number executeInsertAndReturnKeyInternal(final List values) { + private Number executeInsertAndReturnKeyInternal(List values) { KeyHolder kh = executeInsertAndReturnKeyHolderInternal(values); if (kh.getKey() != null) { return kh.getKey(); @@ -423,11 +423,11 @@ private Number executeInsertAndReturnKeyInternal(final List values) { /** * Delegate method to execute the insert, generating any number of keys. */ - private KeyHolder executeInsertAndReturnKeyHolderInternal(final List values) { + private KeyHolder executeInsertAndReturnKeyHolderInternal(List values) { if (logger.isDebugEnabled()) { logger.debug("The following parameters are used for call " + getInsertString() + " with: " + values); } - final KeyHolder keyHolder = new GeneratedKeyHolder(); + KeyHolder keyHolder = new GeneratedKeyHolder(); if (this.tableMetaDataContext.isGetGeneratedKeysSupported()) { getJdbcTemplate().update( @@ -455,7 +455,7 @@ private KeyHolder executeInsertAndReturnKeyHolderInternal(final List values) } Assert.state(getTableName() != null, "No table name set"); - final String keyQuery = this.tableMetaDataContext.getSimpleQueryForGetGeneratedKey( + String keyQuery = this.tableMetaDataContext.getSimpleQueryForGetGeneratedKey( getTableName(), getGeneratedKeyNames()[0]); Assert.state(keyQuery != null, "Query for simulating get generated keys must not be null"); @@ -535,8 +535,8 @@ private PreparedStatement prepareStatementForGeneratedKeys(Connection con) throw /** * Delegate method that executes a batch insert using the passed-in Maps of parameters. - * @param batch array of Maps with parameter names and values to be used in batch insert - * @return array of number of rows affected + * @param batch maps with parameter names and values to be used in the batch insert + * @return an array of number of rows affected */ @SuppressWarnings("unchecked") protected int[] doExecuteBatch(Map... batch) { @@ -549,9 +549,10 @@ protected int[] doExecuteBatch(Map... batch) { } /** - * Delegate method that executes a batch insert using the passed-in {@link SqlParameterSource SqlParameterSources}. - * @param batch array of SqlParameterSource with parameter names and values to be used in insert - * @return array of number of rows affected + * Delegate method that executes a batch insert using the passed-in + * {@link SqlParameterSource SqlParameterSources}. + * @param batch parameter sources with names and values to be used in the batch insert + * @return an array of number of rows affected */ protected int[] doExecuteBatch(SqlParameterSource... batch) { checkCompiled(); @@ -606,7 +607,7 @@ private void setParameterValues(PreparedStatement preparedStatement, List val * Match the provided in parameter values with registered parameters and parameters * defined via meta-data processing. * @param parameterSource the parameter values provided as a {@link SqlParameterSource} - * @return a Map with parameter names and values + * @return a List of values */ protected List matchInParameterValuesWithInsertColumns(SqlParameterSource parameterSource) { return this.tableMetaDataContext.matchInParameterValuesWithInsertColumns(parameterSource); @@ -615,8 +616,8 @@ protected List matchInParameterValuesWithInsertColumns(SqlParameterSourc /** * Match the provided in parameter values with registered parameters and parameters * defined via meta-data processing. - * @param args the parameter values provided in a Map - * @return a Map with parameter names and values + * @param args the parameter values provided as a Map + * @return a List of values */ protected List matchInParameterValuesWithInsertColumns(Map args) { return this.tableMetaDataContext.matchInParameterValuesWithInsertColumns(args); diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/SimpleJdbcCall.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/SimpleJdbcCall.java index f26ea8aee3df..c5d946fad431 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/SimpleJdbcCall.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/SimpleJdbcCall.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2023 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. @@ -47,7 +47,7 @@ * any meta-data processing if you want to use parameter names that do not * match what is declared during the stored procedure compilation. * - *

    The actual insert is being handled using Spring's {@link JdbcTemplate}. + *

    The actual call is being handled using Spring's {@link JdbcTemplate}. * *

    Many of the configuration methods return the current instance of the * SimpleJdbcCall in order to provide the ability to chain multiple ones diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/SimpleJdbcInsert.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/SimpleJdbcInsert.java index ce210b39b43b..f48fb62d14d7 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/SimpleJdbcInsert.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/SimpleJdbcInsert.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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,17 +26,17 @@ import org.springframework.jdbc.support.KeyHolder; /** - * A SimpleJdbcInsert is a multithreaded, reusable object providing easy insert + * A SimpleJdbcInsert is a multithreaded, reusable object providing easy (batch) insert * capabilities for a table. It provides meta-data processing to simplify the code - * needed to construct a basic insert statement. All you need to provide is the - * name of the table and a Map containing the column names and the column values. + * needed to construct a basic insert statement. All you need to provide is the name + * of the table and a Map containing the column names and the column values. * *

    The meta-data processing is based on the DatabaseMetaData provided by the * JDBC driver. As long as the JDBC driver can provide the names of the columns * for a specified table then we can rely on this auto-detection feature. If that * is not the case, then the column names must be specified explicitly. * - *

    The actual insert is handled using Spring's {@link JdbcTemplate}. + *

    The actual (batch) insert is handled using Spring's {@link JdbcTemplate}. * *

    Many of the configuration methods return the current instance of the * SimpleJdbcInsert to provide the ability to chain multiple ones together @@ -142,8 +142,8 @@ public KeyHolder executeAndReturnKeyHolder(SqlParameterSource parameterSource) { return doExecuteAndReturnKeyHolder(parameterSource); } - @Override @SuppressWarnings("unchecked") + @Override public int[] executeBatch(Map... batch) { return doExecuteBatch(batch); } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/AbstractLobCreatingPreparedStatementCallback.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/AbstractLobCreatingPreparedStatementCallback.java index 42f415cba557..99135870c6c9 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/AbstractLobCreatingPreparedStatementCallback.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/AbstractLobCreatingPreparedStatementCallback.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -45,8 +45,8 @@ * lobCreator.setBlobAsBinaryStream(ps, 2, contentStream, contentLength); * lobCreator.setClobAsString(ps, 3, description); * } - * } - * ); + * }); + * * * @author Juergen Hoeller * @since 1.0.2 diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/AbstractLobStreamingResultSetExtractor.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/AbstractLobStreamingResultSetExtractor.java index 5427df20526c..7b6de1078e88 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/AbstractLobStreamingResultSetExtractor.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/AbstractLobStreamingResultSetExtractor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -41,13 +41,13 @@ * final LobHandler lobHandler = new DefaultLobHandler(); // reusable object * * jdbcTemplate.query( - * "SELECT content FROM imagedb WHERE image_name=?", new Object[] {name}, - * new AbstractLobStreamingResultSetExtractor() { - * public void streamData(ResultSet rs) throws SQLException, IOException { - * FileCopyUtils.copy(lobHandler.getBlobAsBinaryStream(rs, 1), contentStream); - * } - * } - * ); + * "SELECT content FROM imagedb WHERE image_name=?", new Object[] {name}, + * new AbstractLobStreamingResultSetExtractor() { + * public void streamData(ResultSet rs) throws SQLException, IOException { + * FileCopyUtils.copy(lobHandler.getBlobAsBinaryStream(rs, 1), contentStream); + * } + * }); + * * * @author Juergen Hoeller * @since 1.0.2 diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/JdbcDaoSupport.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/JdbcDaoSupport.java index 522b96d733cc..f9a5ceb934a4 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/JdbcDaoSupport.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/JdbcDaoSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2022 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-jdbc/src/main/java/org/springframework/jdbc/core/support/SqlLobValue.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/SqlLobValue.java index 44f027d2770e..43bdff36c539 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/SqlLobValue.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/SqlLobValue.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-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,15 @@ /** * Object to represent an SQL BLOB/CLOB value parameter. BLOBs can either be an - * InputStream or a byte array. CLOBs can be in the form of a Reader, InputStream + * InputStream or a byte array. CLOBs can be in the form of a Reader, InputStream, * or String. Each CLOB/BLOB value will be stored together with its length. - * The type is based on which constructor is used. Objects of this class are - * immutable except for the LobCreator reference. Use them and discard them. + * The type is based on which constructor is used. Instances of this class are + * stateful and immutable: use them and discard them. * - *

    This class holds a reference to a LocCreator that must be closed after the - * update has completed. This is done via a call to the closeLobCreator method. - * All handling of the LobCreator is done by the framework classes that use it - - * no need to set or close the LobCreator for end users of this class. + *

    This class holds a reference to a {@link LobCreator} that must be closed after + * the update has completed. This is done via a call to the {@link #cleanup()} method. + * All handling of the {@code LobCreator} is done by the framework classes that use it - + * no need to set or close the {@code LobCreator} for end users of this class. * *

    A usage example: * @@ -72,8 +72,7 @@ public class SqlLobValue implements DisposableSqlTypeValue { private final int length; /** - * This contains a reference to the LobCreator - so we can close it - * once the update is done. + * Reference to the LobCreator - so we can close it once the update is done. */ private final LobCreator lobCreator; @@ -210,7 +209,7 @@ else if (this.content instanceof Reader) { } /** - * Close the LobCreator, if any. + * Close the LobCreator. */ @Override public void cleanup() { diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/AbstractDataSource.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/AbstractDataSource.java index 2bb62d1c3d66..33e6c31d94e6 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/AbstractDataSource.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/AbstractDataSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2023 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. @@ -75,10 +75,10 @@ public void setLogWriter(PrintWriter pw) throws SQLException { throw new UnsupportedOperationException("setLogWriter"); } - - //--------------------------------------------------------------------- - // Implementation of JDBC 4.0's Wrapper interface - //--------------------------------------------------------------------- + @Override + public Logger getParentLogger() { + return Logger.getLogger(Logger.GLOBAL_LOGGER_NAME); + } @Override @SuppressWarnings("unchecked") @@ -95,14 +95,4 @@ public boolean isWrapperFor(Class iface) throws SQLException { return iface.isInstance(this); } - - //--------------------------------------------------------------------- - // Implementation of JDBC 4.1's getParentLogger method - //--------------------------------------------------------------------- - - @Override - public Logger getParentLogger() { - return Logger.getLogger(Logger.GLOBAL_LOGGER_NAME); - } - } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceTransactionManager.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceTransactionManager.java index 0de9c2c4ac4c..c9ecbcb94fb5 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceTransactionManager.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceTransactionManager.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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. @@ -35,49 +35,52 @@ import org.springframework.util.Assert; /** - * {@link org.springframework.transaction.PlatformTransactionManager} - * implementation for a single JDBC {@link javax.sql.DataSource}. This class is - * capable of working in any environment with any JDBC driver, as long as the setup - * uses a {@code javax.sql.DataSource} as its {@code Connection} factory mechanism. - * Binds a JDBC Connection from the specified DataSource to the current thread, - * potentially allowing for one thread-bound Connection per DataSource. + * {@link org.springframework.transaction.PlatformTransactionManager} implementation + * for a single JDBC {@link javax.sql.DataSource}. This class is capable of working + * in any environment with any JDBC driver, as long as the setup uses a + * {@code javax.sql.DataSource} as its {@code Connection} factory mechanism. + * Binds a JDBC {@code Connection} from the specified {@code DataSource} to the + * current thread, potentially allowing for one thread-bound {@code Connection} + * per {@code DataSource}. * - *

    Note: The DataSource that this transaction manager operates on needs - * to return independent Connections. The Connections may come from a pool - * (the typical case), but the DataSource must not return thread-scoped / - * request-scoped Connections or the like. This transaction manager will - * associate Connections with thread-bound transactions itself, according - * to the specified propagation behavior. It assumes that a separate, - * independent Connection can be obtained even during an ongoing transaction. + *

    Note: The {@code DataSource} that this transaction manager operates on + * needs to return independent {@code Connection}s. The {@code Connection}s + * typically come from a connection pool but the {@code DataSource} must not return + * specifically scoped or constrained {@code Connection}s. This transaction manager + * will associate {@code Connection}s with thread-bound transactions, according + * to the specified propagation behavior. It assumes that a separate, independent + * {@code Connection} can be obtained even during an ongoing transaction. * - *

    Application code is required to retrieve the JDBC Connection via + *

    Application code is required to retrieve the JDBC {@code Connection} via * {@link DataSourceUtils#getConnection(DataSource)} instead of a standard - * Java EE-style {@link DataSource#getConnection()} call. Spring classes such as + * EE-style {@link DataSource#getConnection()} call. Spring classes such as * {@link org.springframework.jdbc.core.JdbcTemplate} use this strategy implicitly. * If not used in combination with this transaction manager, the * {@link DataSourceUtils} lookup strategy behaves exactly like the native - * DataSource lookup; it can thus be used in a portable fashion. + * {@code DataSource} lookup; it can thus be used in a portable fashion. * *

    Alternatively, you can allow application code to work with the standard - * Java EE-style lookup pattern {@link DataSource#getConnection()}, for example for - * legacy code that is not aware of Spring at all. In that case, define a - * {@link TransactionAwareDataSourceProxy} for your target DataSource, and pass - * that proxy DataSource to your DAOs, which will automatically participate in - * Spring-managed transactions when accessing it. + * EE-style lookup pattern {@link DataSource#getConnection()}, for example + * for legacy code that is not aware of Spring at all. In that case, define a + * {@link TransactionAwareDataSourceProxy} for your target {@code DataSource}, + * and pass that proxy {@code DataSource} to your DAOs which will automatically + * participate in Spring-managed transactions when accessing it. * *

    Supports custom isolation levels, and timeouts which get applied as * appropriate JDBC statement timeouts. To support the latter, application code * must either use {@link org.springframework.jdbc.core.JdbcTemplate}, call - * {@link DataSourceUtils#applyTransactionTimeout} for each created JDBC Statement, - * or go through a {@link TransactionAwareDataSourceProxy} which will create - * timeout-aware JDBC Connections and Statements automatically. + * {@link DataSourceUtils#applyTransactionTimeout} for each created JDBC + * {@code Statement}, or go through a {@link TransactionAwareDataSourceProxy} + * which will create timeout-aware JDBC {@code Connection}s and {@code Statement}s + * automatically. * *

    Consider defining a {@link LazyConnectionDataSourceProxy} for your target - * DataSource, pointing both this transaction manager and your DAOs to it. + * {@code DataSource}, pointing both this transaction manager and your DAOs to it. * This will lead to optimized handling of "empty" transactions, i.e. of transactions - * without any JDBC statements executed. A LazyConnectionDataSourceProxy will not fetch - * an actual JDBC Connection from the target DataSource until a Statement gets executed, - * lazily applying the specified transaction settings to the target Connection. + * without any JDBC statements executed. A {@code LazyConnectionDataSourceProxy} will + * not fetch an actual JDBC {@code Connection} from the target {@code DataSource} + * until a {@code Statement} gets executed, lazily applying the specified transaction + * settings to the target {@code Connection}. * *

    This transaction manager supports nested transactions via the JDBC 3.0 * {@link java.sql.Savepoint} mechanism. The @@ -88,9 +91,9 @@ *

    This transaction manager can be used as a replacement for the * {@link org.springframework.transaction.jta.JtaTransactionManager} in the single * resource case, as it does not require a container that supports JTA, typically - * in combination with a locally defined JDBC DataSource (e.g. an Apache Commons - * DBCP connection pool). Switching between this local strategy and a JTA - * environment is just a matter of configuration! + * in combination with a locally defined JDBC {@code DataSource} (e.g. a Hikari + * connection pool). Switching between this local strategy and a JTA environment + * is just a matter of configuration! * *

    As of 4.3.4, this transaction manager triggers flush callbacks on registered * transaction synchronizations (if synchronization is generally active), assuming @@ -112,6 +115,7 @@ * @see TransactionAwareDataSourceProxy * @see LazyConnectionDataSourceProxy * @see org.springframework.jdbc.core.JdbcTemplate + * @see org.springframework.jdbc.support.JdbcTransactionManager */ @SuppressWarnings("serial") public class DataSourceTransactionManager extends AbstractPlatformTransactionManager @@ -124,8 +128,8 @@ public class DataSourceTransactionManager extends AbstractPlatformTransactionMan /** - * Create a new DataSourceTransactionManager instance. - * A DataSource has to be set to be able to use it. + * Create a new {@code DataSourceTransactionManager} instance. + * A {@code DataSource} has to be set to be able to use it. * @see #setDataSource */ public DataSourceTransactionManager() { @@ -133,7 +137,7 @@ public DataSourceTransactionManager() { } /** - * Create a new DataSourceTransactionManager instance. + * Create a new {@code DataSourceTransactionManager} instance. * @param dataSource the JDBC DataSource to manage transactions for */ public DataSourceTransactionManager(DataSource dataSource) { @@ -144,22 +148,22 @@ public DataSourceTransactionManager(DataSource dataSource) { /** - * Set the JDBC DataSource that this instance should manage transactions for. - *

    This will typically be a locally defined DataSource, for example an - * Apache Commons DBCP connection pool. Alternatively, you can also drive - * transactions for a non-XA J2EE DataSource fetched from JNDI. For an XA - * DataSource, use JtaTransactionManager. - *

    The DataSource specified here should be the target DataSource to manage - * transactions for, not a TransactionAwareDataSourceProxy. Only data access - * code may work with TransactionAwareDataSourceProxy, while the transaction - * manager needs to work on the underlying target DataSource. If there's - * nevertheless a TransactionAwareDataSourceProxy passed in, it will be - * unwrapped to extract its target DataSource. - *

    The DataSource passed in here needs to return independent Connections. - * The Connections may come from a pool (the typical case), but the DataSource - * must not return thread-scoped / request-scoped Connections or the like. - * @see TransactionAwareDataSourceProxy - * @see org.springframework.transaction.jta.JtaTransactionManager + * Set the JDBC {@code DataSource} that this instance should manage transactions for. + *

    This will typically be a locally defined {@code DataSource}, for example a + * Hikari connection pool. Alternatively, you can also manage transactions for a + * non-XA {@code DataSource} fetched from JNDI. For an XA {@code DataSource}, + * use {@link org.springframework.transaction.jta.JtaTransactionManager} instead. + *

    The {@code DataSource} specified here should be the target {@code DataSource} + * to manage transactions for, not a {@link TransactionAwareDataSourceProxy}. + * Only data access code may work with {@code TransactionAwareDataSourceProxy} while + * the transaction manager needs to work on the underlying target {@code DataSource}. + * If there is nevertheless a {@code TransactionAwareDataSourceProxy} passed in, + * it will be unwrapped to extract its target {@code DataSource}. + *

    The {@code DataSource} passed in here needs to return independent + * {@code Connection}s. The {@code Connection}s typically come from a + * connection pool but the {@code DataSource} must not return specifically + * scoped or constrained {@code Connection}s, just possibly lazily fetched. + * @see LazyConnectionDataSourceProxy */ public void setDataSource(@Nullable DataSource dataSource) { if (dataSource instanceof TransactionAwareDataSourceProxy) { @@ -174,7 +178,7 @@ public void setDataSource(@Nullable DataSource dataSource) { } /** - * Return the JDBC DataSource that this instance manages transactions for. + * Return the JDBC {@code DataSource} that this instance manages transactions for. */ @Nullable public DataSource getDataSource() { @@ -182,7 +186,7 @@ public DataSource getDataSource() { } /** - * Obtain the DataSource for actual use. + * Obtain the {@code DataSource} for actual use. * @return the DataSource (never {@code null}) * @throws IllegalStateException in case of no DataSource set * @since 5.0 diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceUtils.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceUtils.java index 8452c89fc7d7..9cb78a82ef14 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceUtils.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -33,9 +33,9 @@ import org.springframework.util.Assert; /** - * Helper class that provides static methods for obtaining JDBC Connections from - * a {@link javax.sql.DataSource}. Includes special support for Spring-managed - * transactional Connections, e.g. managed by {@link DataSourceTransactionManager} + * Helper class that provides static methods for obtaining JDBC {@code Connection}s + * from a {@link javax.sql.DataSource}. Includes special support for Spring-managed + * transactional {@code Connection}s, e.g. managed by {@link DataSourceTransactionManager} * or {@link org.springframework.transaction.jta.JtaTransactionManager}. * *

    Used internally by Spring's {@link org.springframework.jdbc.core.JdbcTemplate}, @@ -46,7 +46,8 @@ * @author Juergen Hoeller * @see #getConnection * @see #releaseConnection - * @see DataSourceTransactionManager + * @see org.springframework.jdbc.core.JdbcTemplate + * @see org.springframework.jdbc.support.JdbcTransactionManager * @see org.springframework.transaction.jta.JtaTransactionManager * @see org.springframework.transaction.support.TransactionSynchronizationManager */ @@ -474,7 +475,7 @@ private static class ConnectionSynchronization implements TransactionSynchroniza private final DataSource dataSource; - private int order; + private final int order; private boolean holderActive = true; diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/DelegatingDataSource.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/DelegatingDataSource.java index 720439782053..a2a39f3aa180 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/DelegatingDataSource.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/DelegatingDataSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2023 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. @@ -105,29 +105,29 @@ public Connection getConnection(String username, String password) throws SQLExce } @Override - public PrintWriter getLogWriter() throws SQLException { - return obtainTargetDataSource().getLogWriter(); + public int getLoginTimeout() throws SQLException { + return obtainTargetDataSource().getLoginTimeout(); } @Override - public void setLogWriter(PrintWriter out) throws SQLException { - obtainTargetDataSource().setLogWriter(out); + public void setLoginTimeout(int seconds) throws SQLException { + obtainTargetDataSource().setLoginTimeout(seconds); } @Override - public int getLoginTimeout() throws SQLException { - return obtainTargetDataSource().getLoginTimeout(); + public PrintWriter getLogWriter() throws SQLException { + return obtainTargetDataSource().getLogWriter(); } @Override - public void setLoginTimeout(int seconds) throws SQLException { - obtainTargetDataSource().setLoginTimeout(seconds); + public void setLogWriter(PrintWriter out) throws SQLException { + obtainTargetDataSource().setLogWriter(out); } - - //--------------------------------------------------------------------- - // Implementation of JDBC 4.0's Wrapper interface - //--------------------------------------------------------------------- + @Override + public Logger getParentLogger() { + return Logger.getLogger(Logger.GLOBAL_LOGGER_NAME); + } @Override @SuppressWarnings("unchecked") @@ -143,14 +143,4 @@ public boolean isWrapperFor(Class iface) throws SQLException { return (iface.isInstance(this) || obtainTargetDataSource().isWrapperFor(iface)); } - - //--------------------------------------------------------------------- - // Implementation of JDBC 4.1's getParentLogger method - //--------------------------------------------------------------------- - - @Override - public Logger getParentLogger() { - return Logger.getLogger(Logger.GLOBAL_LOGGER_NAME); - } - } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/TransactionAwareDataSourceProxy.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/TransactionAwareDataSourceProxy.java index aed3ddb54b4c..c053666cc73e 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/TransactionAwareDataSourceProxy.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/TransactionAwareDataSourceProxy.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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. @@ -186,7 +186,7 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl // Allow for differentiating between the proxy and the raw Connection. StringBuilder sb = new StringBuilder("Transaction-aware proxy for target Connection "); if (this.target != null) { - sb.append('[').append(this.target.toString()).append(']'); + sb.append('[').append(this.target).append(']'); } else { sb.append(" from DataSource [").append(this.targetDataSource).append(']'); diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseType.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseType.java index f9ac77a9cd9d..a6d0d1ace6ef 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseType.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseType.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2022 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-jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/AbstractRoutingDataSource.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/AbstractRoutingDataSource.java index ec33aba10199..84560a884d08 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/AbstractRoutingDataSource.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/AbstractRoutingDataSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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. @@ -61,7 +61,7 @@ public abstract class AbstractRoutingDataSource extends AbstractDataSource imple /** * Specify the map of target DataSources, with the lookup key as key. - * The mapped value can either be a corresponding {@link javax.sql.DataSource} + *

    The mapped value can either be a corresponding {@link javax.sql.DataSource} * instance or a data source name String (to be resolved via a * {@link #setDataSourceLookup DataSourceLookup}). *

    The key can be of arbitrary type; this class implements the @@ -213,6 +213,7 @@ public boolean isWrapperFor(Class iface) throws SQLException { return (iface.isInstance(this) || determineTargetDataSource().isWrapperFor(iface)); } + /** * Retrieve the current target DataSource. Determines the * {@link #determineCurrentLookupKey() current lookup key}, performs diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/object/RdbmsOperation.java b/spring-jdbc/src/main/java/org/springframework/jdbc/object/RdbmsOperation.java index cba88a46040b..fe7b05b11131 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/object/RdbmsOperation.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/object/RdbmsOperation.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -128,8 +128,8 @@ public void setFetchSize(int fetchSize) { /** * Set the maximum number of rows for this RDBMS operation. This is important - * for processing subsets of large result sets, avoiding to read and hold - * the entire result set in the database or in the JDBC driver. + * for processing subsets of large result sets, in order to avoid reading and + * holding the entire result set in the database or in the JDBC driver. *

    Default is -1, indicating to use the driver's default. * @see org.springframework.jdbc.core.JdbcTemplate#setMaxRows */ @@ -175,7 +175,7 @@ public int getResultSetType() { public void setUpdatableResults(boolean updatableResults) { if (isCompiled()) { throw new InvalidDataAccessApiUsageException( - "The updateableResults flag must be set before the operation is compiled"); + "The updatableResults flag must be set before the operation is compiled"); } this.updatableResults = updatableResults; } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/object/StoredProcedure.java b/spring-jdbc/src/main/java/org/springframework/jdbc/object/StoredProcedure.java index f740eb4b792f..d860eb206bdc 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/object/StoredProcedure.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/object/StoredProcedure.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-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,7 @@ /** * Superclass for object abstractions of RDBMS stored procedures. - * This class is abstract and it is intended that subclasses will provide a typed + * This class is abstract, and it is intended that subclasses will provide a typed * method for invocation that delegates to the supplied {@link #execute} method. * *

    The inherited {@link #setSql sql} property is the name of the stored procedure diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/AbstractFallbackSQLExceptionTranslator.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/AbstractFallbackSQLExceptionTranslator.java index 44b8838b2101..f1546734ac16 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/AbstractFallbackSQLExceptionTranslator.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/AbstractFallbackSQLExceptionTranslator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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. @@ -31,6 +31,8 @@ * * @author Juergen Hoeller * @since 2.5.6 + * @see #doTranslate + * @see #setFallbackTranslator */ public abstract class AbstractFallbackSQLExceptionTranslator implements SQLExceptionTranslator { @@ -42,8 +44,8 @@ public abstract class AbstractFallbackSQLExceptionTranslator implements SQLExcep /** - * Override the default SQL state fallback translator - * (typically a {@link SQLStateSQLExceptionTranslator}). + * Set the fallback translator to use when this translator cannot find a + * specific match itself. */ public void setFallbackTranslator(@Nullable SQLExceptionTranslator fallback) { this.fallbackTranslator = fallback; @@ -51,6 +53,7 @@ public void setFallbackTranslator(@Nullable SQLExceptionTranslator fallback) { /** * Return the fallback exception translator, if any. + * @see #setFallbackTranslator */ @Nullable public SQLExceptionTranslator getFallbackTranslator() { diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/CustomSQLExceptionTranslatorRegistry.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/CustomSQLExceptionTranslatorRegistry.java index 2908e65bac3f..259353a04f73 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/CustomSQLExceptionTranslatorRegistry.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/CustomSQLExceptionTranslatorRegistry.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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,9 +25,9 @@ import org.springframework.lang.Nullable; /** - * Registry for custom {@link org.springframework.jdbc.support.SQLExceptionTranslator} instances associated with - * specific databases allowing for overriding translation based on values contained in the configuration file - * named "sql-error-codes.xml". + * Registry for custom {@link SQLExceptionTranslator} instances associated with + * specific databases allowing for overriding translation based on values + * contained in the configuration file named "sql-error-codes.xml". * * @author Thomas Risberg * @since 3.1.1 @@ -38,7 +38,7 @@ public final class CustomSQLExceptionTranslatorRegistry { private static final Log logger = LogFactory.getLog(CustomSQLExceptionTranslatorRegistry.class); /** - * Keep track of a single instance so we can return it to classes that request it. + * Keep track of a single instance, so we can return it to classes that request it. */ private static final CustomSQLExceptionTranslatorRegistry instance = new CustomSQLExceptionTranslatorRegistry(); diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/JdbcUtils.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/JdbcUtils.java index 2deeb4c666fd..50ef5c2741df 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/JdbcUtils.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/JdbcUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -218,7 +218,7 @@ else if (obj instanceof Number) { return NumberUtils.convertNumberToTargetClass((Number) obj, Integer.class); } else { - // e.g. on Postgres: getObject returns a PGObject but we need a String + // e.g. on Postgres: getObject returns a PGObject, but we need a String return rs.getString(index); } } @@ -228,14 +228,14 @@ else if (obj instanceof Number) { try { return rs.getObject(index, requiredType); } - catch (AbstractMethodError err) { - logger.debug("JDBC driver does not implement JDBC 4.1 'getObject(int, Class)' method", err); - } - catch (SQLFeatureNotSupportedException ex) { + catch (SQLFeatureNotSupportedException | AbstractMethodError ex) { logger.debug("JDBC driver does not support JDBC 4.1 'getObject(int, Class)' method", ex); } catch (SQLException ex) { - logger.debug("JDBC driver has limited support for JDBC 4.1 'getObject(int, Class)' method", ex); + if (logger.isDebugEnabled()) { + logger.debug("JDBC driver has limited support for 'getObject(int, Class)' with column type: " + + requiredType.getName(), ex); + } } // Corresponding SQL types for JSR-310 / Joda-Time types, left up @@ -416,14 +416,14 @@ public static T extractDatabaseMetaData(DataSource dataSource, final String } /** - * Return whether the given JDBC driver supports JDBC 2.0 batch updates. + * Return whether the given JDBC driver supports JDBC batch updates. *

    Typically invoked right before execution of a given set of statements: * to decide whether the set of SQL statements should be executed through - * the JDBC 2.0 batch mechanism or simply in a traditional one-by-one fashion. + * the JDBC batch mechanism or simply in a traditional one-by-one fashion. *

    Logs a warning if the "supportsBatchUpdates" methods throws an exception * and simply returns {@code false} in that case. * @param con the Connection to check - * @return whether JDBC 2.0 batch updates are supported + * @return whether JDBC batch updates are supported * @see java.sql.DatabaseMetaData#supportsBatchUpdates() */ public static boolean supportsBatchUpdates(Connection con) { @@ -496,8 +496,8 @@ public static String resolveTypeName(int sqlType) { /** * Determine the column name to use. The column name is determined based on a * lookup using ResultSetMetaData. - *

    This method implementation takes into account recent clarifications - * expressed in the JDBC 4.0 specification: + *

    This method's implementation takes into account clarifications expressed + * in the JDBC 4.0 specification: *

    columnLabel - the label for the column specified with the SQL AS clause. * If the SQL AS clause was not specified, then the label is the name of the column. * @param resultSetMetaData the current meta-data to use diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLErrorCodeSQLExceptionTranslator.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLErrorCodeSQLExceptionTranslator.java index 80f89796049d..c4ba08dbfec3 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLErrorCodeSQLExceptionTranslator.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLErrorCodeSQLExceptionTranslator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -75,8 +75,6 @@ public class SQLErrorCodeSQLExceptionTranslator extends AbstractFallbackSQLExcep private static final int MESSAGE_SQL_THROWABLE_CONSTRUCTOR = 4; private static final int MESSAGE_SQL_SQLEX_CONSTRUCTOR = 5; - - /** Error codes used by this translator. */ @Nullable private SingletonSupplier sqlErrorCodes; @@ -194,9 +192,9 @@ protected DataAccessException doTranslate(String task, @Nullable String sql, SQL if (sqlErrorCodes != null) { SQLExceptionTranslator customTranslator = sqlErrorCodes.getCustomSqlExceptionTranslator(); if (customTranslator != null) { - DataAccessException customDex = customTranslator.translate(task, sql, sqlEx); - if (customDex != null) { - return customDex; + dae = customTranslator.translate(task, sql, sqlEx); + if (dae != null) { + return dae; } } } @@ -224,11 +222,10 @@ protected DataAccessException doTranslate(String task, @Nullable String sql, SQL for (CustomSQLErrorCodesTranslation customTranslation : customTranslations) { if (Arrays.binarySearch(customTranslation.getErrorCodes(), errorCode) >= 0 && customTranslation.getExceptionClass() != null) { - DataAccessException customException = createCustomException( - task, sql, sqlEx, customTranslation.getExceptionClass()); - if (customException != null) { + dae = createCustomException(task, sql, sqlEx, customTranslation.getExceptionClass()); + if (dae != null) { logTranslation(task, sql, sqlEx, true); - return customException; + return dae; } } } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLErrorCodesFactory.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLErrorCodesFactory.java index 2b78fa740f02..a751ef6c10c6 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLErrorCodesFactory.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLErrorCodesFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -67,7 +67,7 @@ public class SQLErrorCodesFactory { private static final Log logger = LogFactory.getLog(SQLErrorCodesFactory.class); /** - * Keep track of a single instance so we can return it to classes that request it. + * Keep track of a single instance, so we can return it to classes that request it. */ private static final SQLErrorCodesFactory instance = new SQLErrorCodesFactory(); @@ -101,6 +101,7 @@ public static SQLErrorCodesFactory getInstance() { * @see #loadResource(String) */ protected SQLErrorCodesFactory() { + Map errorCodes; try { diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLExceptionSubclassTranslator.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLExceptionSubclassTranslator.java index 7e86c84fce43..f13039ff5619 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLExceptionSubclassTranslator.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLExceptionSubclassTranslator.java @@ -69,10 +69,10 @@ protected DataAccessException doTranslate(String task, @Nullable String sql, SQL if (ex instanceof SQLTransientConnectionException) { return new TransientDataAccessResourceException(buildMessage(task, sql, ex), ex); } - else if (ex instanceof SQLTransactionRollbackException) { + if (ex instanceof SQLTransactionRollbackException) { return new ConcurrencyFailureException(buildMessage(task, sql, ex), ex); } - else if (ex instanceof SQLTimeoutException) { + if (ex instanceof SQLTimeoutException) { return new QueryTimeoutException(buildMessage(task, sql, ex), ex); } } @@ -80,19 +80,19 @@ else if (ex instanceof SQLNonTransientException) { if (ex instanceof SQLNonTransientConnectionException) { return new DataAccessResourceFailureException(buildMessage(task, sql, ex), ex); } - else if (ex instanceof SQLDataException) { + if (ex instanceof SQLDataException) { return new DataIntegrityViolationException(buildMessage(task, sql, ex), ex); } - else if (ex instanceof SQLIntegrityConstraintViolationException) { + if (ex instanceof SQLIntegrityConstraintViolationException) { return new DataIntegrityViolationException(buildMessage(task, sql, ex), ex); } - else if (ex instanceof SQLInvalidAuthorizationSpecException) { + if (ex instanceof SQLInvalidAuthorizationSpecException) { return new PermissionDeniedDataAccessException(buildMessage(task, sql, ex), ex); } - else if (ex instanceof SQLSyntaxErrorException) { + if (ex instanceof SQLSyntaxErrorException) { return new BadSqlGrammarException(task, (sql != null ? sql : ""), ex); } - else if (ex instanceof SQLFeatureNotSupportedException) { + if (ex instanceof SQLFeatureNotSupportedException) { return new InvalidDataAccessApiUsageException(buildMessage(task, sql, ex), ex); } } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/rowset/ResultSetWrappingSqlRowSet.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/rowset/ResultSetWrappingSqlRowSet.java index c399afa96126..56d3accb10d7 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/rowset/ResultSetWrappingSqlRowSet.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/rowset/ResultSetWrappingSqlRowSet.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -32,7 +32,7 @@ import org.springframework.util.CollectionUtils; /** - * The default implementation of Spring's {@link SqlRowSet} interface, wrapping a + * The common implementation of Spring's {@link SqlRowSet} interface, wrapping a * {@link java.sql.ResultSet}, catching any {@link SQLException SQLExceptions} and * translating them to a corresponding Spring {@link InvalidResultSetAccessException}. * @@ -43,17 +43,17 @@ *

    Note: Since JDBC 4.0, it has been clarified that any methods using a String to identify * the column should be using the column label. The column label is assigned using the ALIAS * keyword in the SQL query string. When the query doesn't use an ALIAS, the default label is - * the column name. Most JDBC ResultSet implementations follow this new pattern but there are + * the column name. Most JDBC ResultSet implementations follow this pattern, but there are * exceptions such as the {@code com.sun.rowset.CachedRowSetImpl} class which only uses - * the column name, ignoring any column labels. As of Spring 3.0.5, ResultSetWrappingSqlRowSet - * will translate column labels to the correct column index to provide better support for the + * the column name, ignoring any column labels. {@code ResultSetWrappingSqlRowSet} + * will translate column labels to the correct column index to provide better support for * {@code com.sun.rowset.CachedRowSetImpl} which is the default implementation used by * {@link org.springframework.jdbc.core.JdbcTemplate} when working with RowSets. * *

    Note: This class implements the {@code java.io.Serializable} marker interface * through the SqlRowSet interface, but is only actually serializable if the disconnected * ResultSet/RowSet contained in it is serializable. Most CachedRowSet implementations - * are actually serializable, so this should usually work out. + * are actually serializable, so serialization should usually work. * * @author Thomas Risberg * @author Juergen Hoeller @@ -67,16 +67,18 @@ public class ResultSetWrappingSqlRowSet implements SqlRowSet { /** use serialVersionUID from Spring 1.2 for interoperability. */ private static final long serialVersionUID = -4688694393146734764L; - + @SuppressWarnings("serial") private final ResultSet resultSet; + @SuppressWarnings("serial") private final SqlRowSetMetaData rowSetMetaData; + @SuppressWarnings("serial") private final Map columnLabelMap; /** - * Create a new ResultSetWrappingSqlRowSet for the given ResultSet. + * Create a new {@code ResultSetWrappingSqlRowSet} for the given {@link ResultSet}. * @param resultSet a disconnected ResultSet to wrap * (usually a {@code javax.sql.rowset.CachedRowSet}) * @throws InvalidResultSetAccessException if extracting diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/xml/SqlXmlValue.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/xml/SqlXmlValue.java index 3d45d467c28e..d662a9271ae3 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/xml/SqlXmlValue.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/xml/SqlXmlValue.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2008 the original author or authors. + * Copyright 2002-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,9 +20,7 @@ /** * Subinterface of {@link org.springframework.jdbc.support.SqlValue} - * that supports passing in XML data to specified column and adds a - * cleanup callback, to be invoked after the value has been set and - * the corresponding statement has been executed. + * that specifically indicates passing in XML data to a specified column. * * @author Thomas Risberg * @since 2.5.5 diff --git a/spring-jdbc/src/main/resources/org/springframework/jdbc/support/sql-error-codes.xml b/spring-jdbc/src/main/resources/org/springframework/jdbc/support/sql-error-codes.xml index d2e587967c16..2bb0e58b654a 100644 --- a/spring-jdbc/src/main/resources/org/springframework/jdbc/support/sql-error-codes.xml +++ b/spring-jdbc/src/main/resources/org/springframework/jdbc/support/sql-error-codes.xml @@ -172,7 +172,7 @@ 2601,2627 - 544,8114,8115 + 544,2628,8114,8115 4060 diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/BeanPropertyRowMapperTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/BeanPropertyRowMapperTests.java index 5ef1f57f8916..b83219be06f2 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/core/BeanPropertyRowMapperTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/BeanPropertyRowMapperTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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,8 +16,6 @@ package org.springframework.jdbc.core; -import java.util.List; - import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; @@ -44,12 +42,15 @@ */ class BeanPropertyRowMapperTests extends AbstractRowMapperTests { + private static final String SELECT_NULL_AS_AGE = "select null as age from people"; + + @Test @SuppressWarnings({"unchecked", "rawtypes"}) void overridingDifferentClassDefinedForMapping() { BeanPropertyRowMapper mapper = new BeanPropertyRowMapper(Person.class); - assertThatExceptionOfType(InvalidDataAccessApiUsageException.class).isThrownBy(() -> - mapper.setMappedClass(Long.class)); + assertThatExceptionOfType(InvalidDataAccessApiUsageException.class) + .isThrownBy(() -> mapper.setMappedClass(Long.class)); } @Test @@ -61,104 +62,106 @@ void overridingSameClassDefinedForMapping() { @Test void staticQueryWithRowMapper() throws Exception { Mock mock = new Mock(); - List result = mock.getJdbcTemplate().query( + Person person = mock.getJdbcTemplate().queryForObject( "select name, age, birth_date, balance from people", new BeanPropertyRowMapper<>(Person.class)); - assertThat(result).hasSize(1); - verifyPerson(result.get(0)); + verifyPerson(person); mock.verifyClosed(); } @Test void mappingWithInheritance() throws Exception { Mock mock = new Mock(); - List result = mock.getJdbcTemplate().query( + ConcretePerson person = mock.getJdbcTemplate().queryForObject( "select name, age, birth_date, balance from people", new BeanPropertyRowMapper<>(ConcretePerson.class)); - assertThat(result).hasSize(1); - verifyPerson(result.get(0)); + verifyPerson(person); mock.verifyClosed(); } @Test void mappingWithNoUnpopulatedFieldsFound() throws Exception { Mock mock = new Mock(); - List result = mock.getJdbcTemplate().query( + ConcretePerson person = mock.getJdbcTemplate().queryForObject( "select name, age, birth_date, balance from people", new BeanPropertyRowMapper<>(ConcretePerson.class, true)); - assertThat(result).hasSize(1); - verifyPerson(result.get(0)); + verifyPerson(person); mock.verifyClosed(); } @Test void mappingWithUnpopulatedFieldsNotChecked() throws Exception { Mock mock = new Mock(); - List result = mock.getJdbcTemplate().query( + ExtendedPerson person = mock.getJdbcTemplate().queryForObject( "select name, age, birth_date, balance from people", new BeanPropertyRowMapper<>(ExtendedPerson.class)); - assertThat(result).hasSize(1); - verifyPerson(result.get(0)); + verifyPerson(person); mock.verifyClosed(); } @Test void mappingWithUnpopulatedFieldsNotAccepted() throws Exception { + BeanPropertyRowMapper mapper = new BeanPropertyRowMapper<>(ExtendedPerson.class, true); Mock mock = new Mock(); - assertThatExceptionOfType(InvalidDataAccessApiUsageException.class).isThrownBy(() -> - mock.getJdbcTemplate().query("select name, age, birth_date, balance from people", - new BeanPropertyRowMapper<>(ExtendedPerson.class, true))); + assertThatExceptionOfType(InvalidDataAccessApiUsageException.class) + .isThrownBy(() -> mock.getJdbcTemplate().query("select name, age, birth_date, balance from people", mapper)); } @Test void mappingNullValue() throws Exception { BeanPropertyRowMapper mapper = new BeanPropertyRowMapper<>(Person.class); Mock mock = new Mock(MockType.TWO); - assertThatExceptionOfType(TypeMismatchException.class).isThrownBy(() -> - mock.getJdbcTemplate().query("select name, null as age, birth_date, balance from people", mapper)); + assertThatExceptionOfType(TypeMismatchException.class) + .isThrownBy(() -> mock.getJdbcTemplate().query(SELECT_NULL_AS_AGE, mapper)); + } + + @Test + void mappingNullValueWithPrimitivesDefaultedForNullValue() throws Exception { + BeanPropertyRowMapper mapper = new BeanPropertyRowMapper<>(Person.class); + mapper.setPrimitivesDefaultedForNullValue(true); + Mock mock = new Mock(MockType.TWO); + Person person = mock.getJdbcTemplate().queryForObject(SELECT_NULL_AS_AGE, mapper); + assertThat(person).extracting(Person::getAge).isEqualTo(42L); + mock.verifyClosed(); } @Test void queryWithSpaceInColumnNameAndLocalDateTime() throws Exception { Mock mock = new Mock(MockType.THREE); - List result = mock.getJdbcTemplate().query( + SpacePerson person = mock.getJdbcTemplate().queryForObject( "select last_name as \"Last Name\", age, birth_date, balance from people", new BeanPropertyRowMapper<>(SpacePerson.class)); - assertThat(result).hasSize(1); - verifyPerson(result.get(0)); + verifyPerson(person); mock.verifyClosed(); } @Test void queryWithSpaceInColumnNameAndLocalDate() throws Exception { Mock mock = new Mock(MockType.THREE); - List result = mock.getJdbcTemplate().query( + DatePerson person = mock.getJdbcTemplate().queryForObject( "select last_name as \"Last Name\", age, birth_date, balance from people", new BeanPropertyRowMapper<>(DatePerson.class)); - assertThat(result).hasSize(1); - verifyPerson(result.get(0)); + verifyPerson(person); mock.verifyClosed(); } @Test void queryWithDirectNameMatchOnBirthDate() throws Exception { Mock mock = new Mock(MockType.FOUR); - List result = mock.getJdbcTemplate().query( + ConcretePerson person = mock.getJdbcTemplate().queryForObject( "select name, age, birthdate, balance from people", new BeanPropertyRowMapper<>(ConcretePerson.class)); - assertThat(result).hasSize(1); - verifyPerson(result.get(0)); + verifyPerson(person); mock.verifyClosed(); } @Test void queryWithUnderscoreInColumnNameAndPersonWithMultipleAdjacentUppercaseLettersInPropertyName() throws Exception { Mock mock = new Mock(); - List result = mock.getJdbcTemplate().query( + EmailPerson person = mock.getJdbcTemplate().queryForObject( "select name, age, birth_date, balance, e_mail from people", new BeanPropertyRowMapper<>(EmailPerson.class)); - assertThat(result).hasSize(1); - verifyPerson(result.get(0)); + verifyPerson(person); mock.verifyClosed(); } diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/DataClassRowMapperTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/DataClassRowMapperTests.java index c612e5bcae63..3f569ca6c4db 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/core/DataClassRowMapperTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/DataClassRowMapperTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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,9 +17,7 @@ package org.springframework.jdbc.core; import java.math.BigDecimal; -import java.util.Collections; import java.util.Date; -import java.util.List; import org.junit.jupiter.api.Test; @@ -30,50 +28,48 @@ import static org.assertj.core.api.Assertions.assertThat; /** + * Tests for {@link DataClassRowMapper}. + * * @author Juergen Hoeller + * @author Sam Brannen * @since 5.3 */ -public class DataClassRowMapperTests extends AbstractRowMapperTests { +class DataClassRowMapperTests extends AbstractRowMapperTests { @Test - public void testStaticQueryWithDataClass() throws Exception { + void staticQueryWithDataClass() throws Exception { Mock mock = new Mock(); - List result = mock.getJdbcTemplate().query( + ConstructorPerson person = mock.getJdbcTemplate().queryForObject( "select name, age, birth_date, balance from people", new DataClassRowMapper<>(ConstructorPerson.class)); - assertThat(result.size()).isEqualTo(1); - verifyPerson(result.get(0)); + verifyPerson(person); mock.verifyClosed(); } @Test - public void testStaticQueryWithDataClassAndGenerics() throws Exception { + void staticQueryWithDataClassAndGenerics() throws Exception { Mock mock = new Mock(); - List result = mock.getJdbcTemplate().query( + ConstructorPersonWithGenerics person = mock.getJdbcTemplate().queryForObject( "select name, age, birth_date, balance from people", new DataClassRowMapper<>(ConstructorPersonWithGenerics.class)); - assertThat(result.size()).isEqualTo(1); - ConstructorPersonWithGenerics person = result.get(0); assertThat(person.name()).isEqualTo("Bubba"); assertThat(person.age()).isEqualTo(22L); - assertThat(person.birthDate()).usingComparator(Date::compareTo).isEqualTo(new java.util.Date(1221222L)); - assertThat(person.balance()).isEqualTo(Collections.singletonList(new BigDecimal("1234.56"))); + assertThat(person.birthDate()).usingComparator(Date::compareTo).isEqualTo(new Date(1221222L)); + assertThat(person.balance()).containsExactly(new BigDecimal("1234.56")); mock.verifyClosed(); } @Test - public void testStaticQueryWithDataClassAndSetters() throws Exception { + void staticQueryWithDataClassAndSetters() throws Exception { Mock mock = new Mock(MockType.FOUR); - List result = mock.getJdbcTemplate().query( + ConstructorPersonWithSetters person = mock.getJdbcTemplate().queryForObject( "select name, age, birthdate, balance from people", new DataClassRowMapper<>(ConstructorPersonWithSetters.class)); - assertThat(result.size()).isEqualTo(1); - ConstructorPersonWithSetters person = result.get(0); assertThat(person.name()).isEqualTo("BUBBA"); assertThat(person.age()).isEqualTo(22L); - assertThat(person.birthDate()).usingComparator(Date::compareTo).isEqualTo(new java.util.Date(1221222L)); + assertThat(person.birthDate()).usingComparator(Date::compareTo).isEqualTo(new Date(1221222L)); assertThat(person.balance()).isEqualTo(new BigDecimal("1234.56")); mock.verifyClosed(); diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/namedparam/NamedParameterQueryTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/namedparam/NamedParameterQueryTests.java index 1a2c61469908..8c47dcda143e 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/core/namedparam/NamedParameterQueryTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/namedparam/NamedParameterQueryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 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. @@ -34,8 +34,6 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.springframework.jdbc.core.RowMapper; - import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.BDDMockito.given; @@ -135,8 +133,7 @@ public void testQueryForListWithParamMapAndSingleRowAndColumn() throws Exception } @Test - public void testQueryForListWithParamMapAndIntegerElementAndSingleRowAndColumn() - throws Exception { + public void testQueryForListWithParamMapAndIntegerElementAndSingleRowAndColumn() throws Exception { given(resultSet.getMetaData()).willReturn(resultSetMetaData); given(resultSet.next()).willReturn(true, false); given(resultSet.getInt(1)).willReturn(11); @@ -174,11 +171,10 @@ public void testQueryForObjectWithParamMapAndRowMapper() throws Exception { MapSqlParameterSource params = new MapSqlParameterSource(); params.addValue("id", 3); - Object o = template.queryForObject("SELECT AGE FROM CUSTMR WHERE ID = :id", - params, (RowMapper) (rs, rowNum) -> rs.getInt(1)); + Integer value = template.queryForObject("SELECT AGE FROM CUSTMR WHERE ID = :id", + params, (rs, rowNum) -> rs.getInt(1)); - boolean condition = o instanceof Integer; - assertThat(condition).as("Correct result type").isTrue(); + assertThat(value).isEqualTo(22); verify(connection).prepareStatement("SELECT AGE FROM CUSTMR WHERE ID = ?"); verify(preparedStatement).setObject(1, 3); } @@ -191,11 +187,10 @@ public void testQueryForObjectWithMapAndInteger() throws Exception { Map params = new HashMap<>(); params.put("id", 3); - Object o = template.queryForObject("SELECT AGE FROM CUSTMR WHERE ID = :id", + Integer value = template.queryForObject("SELECT AGE FROM CUSTMR WHERE ID = :id", params, Integer.class); - boolean condition = o instanceof Integer; - assertThat(condition).as("Correct result type").isTrue(); + assertThat(value).isEqualTo(22); verify(connection).prepareStatement("SELECT AGE FROM CUSTMR WHERE ID = ?"); verify(preparedStatement).setObject(1, 3); } @@ -208,30 +203,26 @@ public void testQueryForObjectWithParamMapAndInteger() throws Exception { MapSqlParameterSource params = new MapSqlParameterSource(); params.addValue("id", 3); - Object o = template.queryForObject("SELECT AGE FROM CUSTMR WHERE ID = :id", + Integer value = template.queryForObject("SELECT AGE FROM CUSTMR WHERE ID = :id", params, Integer.class); - boolean condition = o instanceof Integer; - assertThat(condition).as("Correct result type").isTrue(); + assertThat(value).isEqualTo(22); verify(connection).prepareStatement("SELECT AGE FROM CUSTMR WHERE ID = ?"); verify(preparedStatement).setObject(1, 3); } @Test public void testQueryForObjectWithParamMapAndList() throws Exception { - String sql = "SELECT AGE FROM CUSTMR WHERE ID IN (:ids)"; - String sqlToUse = "SELECT AGE FROM CUSTMR WHERE ID IN (?, ?)"; given(resultSet.getMetaData()).willReturn(resultSetMetaData); given(resultSet.next()).willReturn(true, false); given(resultSet.getInt(1)).willReturn(22); MapSqlParameterSource params = new MapSqlParameterSource(); params.addValue("ids", Arrays.asList(3, 4)); - Object o = template.queryForObject(sql, params, Integer.class); + Integer value = template.queryForObject("SELECT AGE FROM CUSTMR WHERE ID IN (:ids)", params, Integer.class); - boolean condition = o instanceof Integer; - assertThat(condition).as("Correct result type").isTrue(); - verify(connection).prepareStatement(sqlToUse); + assertThat(value).isEqualTo(22); + verify(connection).prepareStatement("SELECT AGE FROM CUSTMR WHERE ID IN (?, ?)"); verify(preparedStatement).setObject(1, 3); } @@ -246,14 +237,11 @@ public void testQueryForObjectWithParamMapAndListOfExpressionLists() throws Exce l1.add(new Object[] {3, "Rod"}); l1.add(new Object[] {4, "Juergen"}); params.addValue("multiExpressionList", l1); - Object o = template.queryForObject( - "SELECT AGE FROM CUSTMR WHERE (ID, NAME) IN (:multiExpressionList)", + Integer value = template.queryForObject("SELECT AGE FROM CUSTMR WHERE (ID, NAME) IN (:multiExpressionList)", params, Integer.class); - boolean condition = o instanceof Integer; - assertThat(condition).as("Correct result type").isTrue(); - verify(connection).prepareStatement( - "SELECT AGE FROM CUSTMR WHERE (ID, NAME) IN ((?, ?), (?, ?))"); + assertThat(value).isEqualTo(22); + verify(connection).prepareStatement("SELECT AGE FROM CUSTMR WHERE (ID, NAME) IN ((?, ?), (?, ?))"); verify(preparedStatement).setObject(1, 3); } diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/support/LobSupportTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/support/LobSupportTests.java index d0ba6edf8f9c..da07e652edb7 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/core/support/LobSupportTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/support/LobSupportTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 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,6 @@ import org.junit.jupiter.api.Test; -import org.springframework.dao.DataAccessException; import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.jdbc.LobRetrievalFailureException; import org.springframework.jdbc.support.lob.LobCreator; @@ -55,11 +54,9 @@ class SetValuesCalled { final SetValuesCalled svc = new SetValuesCalled(); - AbstractLobCreatingPreparedStatementCallback psc = new AbstractLobCreatingPreparedStatementCallback( - handler) { + AbstractLobCreatingPreparedStatementCallback psc = new AbstractLobCreatingPreparedStatementCallback(handler) { @Override - protected void setValues(PreparedStatement ps, LobCreator lobCreator) - throws SQLException, DataAccessException { + protected void setValues(PreparedStatement ps, LobCreator lobCreator) { svc.b = true; } }; @@ -73,46 +70,43 @@ protected void setValues(PreparedStatement ps, LobCreator lobCreator) @Test public void testAbstractLobStreamingResultSetExtractorNoRows() throws SQLException { - ResultSet rset = mock(ResultSet.class); + ResultSet rs = mock(ResultSet.class); AbstractLobStreamingResultSetExtractor lobRse = getResultSetExtractor(false); - assertThatExceptionOfType(IncorrectResultSizeDataAccessException.class).isThrownBy(() -> - lobRse.extractData(rset)); - verify(rset).next(); + assertThatExceptionOfType(IncorrectResultSizeDataAccessException.class) + .isThrownBy(() -> lobRse.extractData(rs)); + verify(rs).next(); } @Test public void testAbstractLobStreamingResultSetExtractorOneRow() throws SQLException { - ResultSet rset = mock(ResultSet.class); - given(rset.next()).willReturn(true, false); + ResultSet rs = mock(ResultSet.class); + given(rs.next()).willReturn(true, false); AbstractLobStreamingResultSetExtractor lobRse = getResultSetExtractor(false); - lobRse.extractData(rset); - verify(rset).clearWarnings(); + lobRse.extractData(rs); + verify(rs).clearWarnings(); } @Test - public void testAbstractLobStreamingResultSetExtractorMultipleRows() - throws SQLException { - ResultSet rset = mock(ResultSet.class); - given(rset.next()).willReturn(true, true, false); + public void testAbstractLobStreamingResultSetExtractorMultipleRows() throws SQLException { + ResultSet rs = mock(ResultSet.class); + given(rs.next()).willReturn(true, true, false); AbstractLobStreamingResultSetExtractor lobRse = getResultSetExtractor(false); - assertThatExceptionOfType(IncorrectResultSizeDataAccessException.class).isThrownBy(() -> - lobRse.extractData(rset)); - verify(rset).clearWarnings(); + assertThatExceptionOfType(IncorrectResultSizeDataAccessException.class) + .isThrownBy(() -> lobRse.extractData(rs)); + verify(rs).clearWarnings(); } @Test - public void testAbstractLobStreamingResultSetExtractorCorrectException() - throws SQLException { - ResultSet rset = mock(ResultSet.class); - given(rset.next()).willReturn(true); + public void testAbstractLobStreamingResultSetExtractorCorrectException() throws SQLException { + ResultSet rs = mock(ResultSet.class); + given(rs.next()).willReturn(true); AbstractLobStreamingResultSetExtractor lobRse = getResultSetExtractor(true); - assertThatExceptionOfType(LobRetrievalFailureException.class).isThrownBy(() -> - lobRse.extractData(rset)); + assertThatExceptionOfType(LobRetrievalFailureException.class) + .isThrownBy(() -> lobRse.extractData(rs)); } private AbstractLobStreamingResultSetExtractor getResultSetExtractor(final boolean ex) { AbstractLobStreamingResultSetExtractor lobRse = new AbstractLobStreamingResultSetExtractor() { - @Override protected void streamData(ResultSet rs) throws SQLException, IOException { if (ex) { @@ -125,4 +119,5 @@ protected void streamData(ResultSet rs) throws SQLException, IOException { }; return lobRse; } + } diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/Person.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/Person.java index e1d9149d5120..b9be634d949c 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/Person.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/Person.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2023 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. @@ -31,6 +31,9 @@ public class Person { private BigDecimal balance; + public Person() { + this.age = 42; // custom "default" value for a primitive + } public String getName() { return name; diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLExceptionCustomTranslatorTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLExceptionCustomTranslatorTests.java index 8d46c0f578d4..0e1228df7821 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLExceptionCustomTranslatorTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLExceptionCustomTranslatorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2022 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.jdbc.support; +import java.sql.SQLDataException; import java.sql.SQLException; import org.junit.jupiter.api.Test; @@ -37,8 +38,8 @@ public class SQLExceptionCustomTranslatorTests { private static SQLErrorCodes ERROR_CODES = new SQLErrorCodes(); static { - ERROR_CODES.setBadSqlGrammarCodes(new String[] { "1" }); - ERROR_CODES.setDataAccessResourceFailureCodes(new String[] { "2" }); + ERROR_CODES.setBadSqlGrammarCodes("1"); + ERROR_CODES.setDataAccessResourceFailureCodes("2"); ERROR_CODES.setCustomSqlExceptionTranslatorClass(CustomSqlExceptionTranslator.class); } @@ -47,7 +48,7 @@ public class SQLExceptionCustomTranslatorTests { @Test public void badSqlGrammarException() { - SQLException badSqlGrammarExceptionEx = SQLExceptionSubclassFactory.newSQLDataException("", "", 1); + SQLException badSqlGrammarExceptionEx = new SQLDataException("", "", 1); DataAccessException dae = sext.translate("task", "SQL", badSqlGrammarExceptionEx); assertThat(dae.getCause()).isEqualTo(badSqlGrammarExceptionEx); assertThat(dae).isInstanceOf(BadSqlGrammarException.class); @@ -55,7 +56,7 @@ public void badSqlGrammarException() { @Test public void dataAccessResourceException() { - SQLException dataAccessResourceEx = SQLExceptionSubclassFactory.newSQLDataException("", "", 2); + SQLException dataAccessResourceEx = new SQLDataException("", "", 2); DataAccessException dae = sext.translate("task", "SQL", dataAccessResourceEx); assertThat(dae.getCause()).isEqualTo(dataAccessResourceEx); assertThat(dae).isInstanceOf(TransientDataAccessResourceException.class); diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLExceptionSubclassFactory.java b/spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLExceptionSubclassFactory.java deleted file mode 100644 index a172139de1e5..000000000000 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLExceptionSubclassFactory.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright 2002-2022 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.jdbc.support; - -import java.sql.SQLDataException; -import java.sql.SQLException; -import java.sql.SQLFeatureNotSupportedException; -import java.sql.SQLIntegrityConstraintViolationException; -import java.sql.SQLInvalidAuthorizationSpecException; -import java.sql.SQLNonTransientConnectionException; -import java.sql.SQLRecoverableException; -import java.sql.SQLSyntaxErrorException; -import java.sql.SQLTimeoutException; -import java.sql.SQLTransactionRollbackException; -import java.sql.SQLTransientConnectionException; - -/** - * Class to generate {@link SQLException} subclasses for testing purposes. - * - * @author Thomas Risberg - */ -public class SQLExceptionSubclassFactory { - - public static SQLException newSQLDataException(String reason, String SQLState, int vendorCode) { - return new SQLDataException(reason, SQLState, vendorCode); - } - - public static SQLException newSQLFeatureNotSupportedException(String reason, String SQLState, int vendorCode) { - return new SQLFeatureNotSupportedException(reason, SQLState, vendorCode); - } - - public static SQLException newSQLIntegrityConstraintViolationException(String reason, String SQLState, int vendorCode) { - return new SQLIntegrityConstraintViolationException(reason, SQLState, vendorCode); - } - - public static SQLException newSQLInvalidAuthorizationSpecException(String reason, String SQLState, int vendorCode) { - return new SQLInvalidAuthorizationSpecException(reason, SQLState, vendorCode); - } - - public static SQLException newSQLNonTransientConnectionException(String reason, String SQLState, int vendorCode) { - return new SQLNonTransientConnectionException(reason, SQLState, vendorCode); - } - - public static SQLException newSQLSyntaxErrorException(String reason, String SQLState, int vendorCode) { - return new SQLSyntaxErrorException(reason, SQLState, vendorCode); - } - - public static SQLException newSQLTransactionRollbackException(String reason, String SQLState, int vendorCode) { - return new SQLTransactionRollbackException(reason, SQLState, vendorCode); - } - - public static SQLException newSQLTransientConnectionException(String reason, String SQLState, int vendorCode) { - return new SQLTransientConnectionException(reason, SQLState, vendorCode); - } - - public static SQLException newSQLTimeoutException(String reason, String SQLState, int vendorCode) { - return new SQLTimeoutException(reason, SQLState, vendorCode); - } - - public static SQLException newSQLRecoverableException(String reason, String SQLState, int vendorCode) { - return new SQLRecoverableException(reason, SQLState, vendorCode); - } - -} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLExceptionSubclassTranslatorTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLExceptionSubclassTranslatorTests.java index 0be4d4b0d8d4..f24d101679b1 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLExceptionSubclassTranslatorTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLExceptionSubclassTranslatorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2022 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,11 +16,22 @@ package org.springframework.jdbc.support; +import java.sql.SQLDataException; import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; +import java.sql.SQLIntegrityConstraintViolationException; +import java.sql.SQLInvalidAuthorizationSpecException; +import java.sql.SQLNonTransientConnectionException; +import java.sql.SQLRecoverableException; +import java.sql.SQLSyntaxErrorException; +import java.sql.SQLTimeoutException; +import java.sql.SQLTransactionRollbackException; +import java.sql.SQLTransientConnectionException; import org.junit.jupiter.api.Test; import org.springframework.dao.ConcurrencyFailureException; +import org.springframework.dao.DataAccessException; import org.springframework.dao.DataAccessResourceFailureException; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.dao.InvalidDataAccessApiUsageException; @@ -34,78 +45,41 @@ /** * @author Thomas Risberg + * @author Juergen Hoeller */ public class SQLExceptionSubclassTranslatorTests { - private static SQLErrorCodes ERROR_CODES = new SQLErrorCodes(); - - static { - ERROR_CODES.setBadSqlGrammarCodes("1"); + @Test + public void exceptionClassTranslation() { + doTest(new SQLDataException("", "", 0), DataIntegrityViolationException.class); + doTest(new SQLFeatureNotSupportedException("", "", 0), InvalidDataAccessApiUsageException.class); + doTest(new SQLIntegrityConstraintViolationException("", "", 0), DataIntegrityViolationException.class); + doTest(new SQLInvalidAuthorizationSpecException("", "", 0), PermissionDeniedDataAccessException.class); + doTest(new SQLNonTransientConnectionException("", "", 0), DataAccessResourceFailureException.class); + doTest(new SQLRecoverableException("", "", 0), RecoverableDataAccessException.class); + doTest(new SQLSyntaxErrorException("", "", 0), BadSqlGrammarException.class); + doTest(new SQLTimeoutException("", "", 0), QueryTimeoutException.class); + doTest(new SQLTransactionRollbackException("", "", 0), ConcurrencyFailureException.class); + doTest(new SQLTransientConnectionException("", "", 0), TransientDataAccessResourceException.class); } - @Test - public void errorCodeTranslation() { - SQLExceptionTranslator sext = new SQLErrorCodeSQLExceptionTranslator(ERROR_CODES); - - SQLException dataIntegrityViolationEx = SQLExceptionSubclassFactory.newSQLDataException("", "", 0); - DataIntegrityViolationException divex = (DataIntegrityViolationException) sext.translate("task", "SQL", dataIntegrityViolationEx); - assertThat(divex.getCause()).isEqualTo(dataIntegrityViolationEx); - - SQLException featureNotSupEx = SQLExceptionSubclassFactory.newSQLFeatureNotSupportedException("", "", 0); - InvalidDataAccessApiUsageException idaex = (InvalidDataAccessApiUsageException) sext.translate("task", "SQL", featureNotSupEx); - assertThat(idaex.getCause()).isEqualTo(featureNotSupEx); - - SQLException dataIntegrityViolationEx2 = SQLExceptionSubclassFactory.newSQLIntegrityConstraintViolationException("", "", 0); - DataIntegrityViolationException divex2 = (DataIntegrityViolationException) sext.translate("task", "SQL", dataIntegrityViolationEx2); - assertThat(divex2.getCause()).isEqualTo(dataIntegrityViolationEx2); - - SQLException permissionDeniedEx = SQLExceptionSubclassFactory.newSQLInvalidAuthorizationSpecException("", "", 0); - PermissionDeniedDataAccessException pdaex = (PermissionDeniedDataAccessException) sext.translate("task", "SQL", permissionDeniedEx); - assertThat(pdaex.getCause()).isEqualTo(permissionDeniedEx); - - SQLException dataAccessResourceEx = SQLExceptionSubclassFactory.newSQLNonTransientConnectionException("", "", 0); - DataAccessResourceFailureException darex = (DataAccessResourceFailureException) sext.translate("task", "SQL", dataAccessResourceEx); - assertThat(darex.getCause()).isEqualTo(dataAccessResourceEx); - - SQLException badSqlEx2 = SQLExceptionSubclassFactory.newSQLSyntaxErrorException("", "", 0); - BadSqlGrammarException bsgex2 = (BadSqlGrammarException) sext.translate("task", "SQL2", badSqlEx2); - assertThat(bsgex2.getSql()).isEqualTo("SQL2"); - assertThat((Object) bsgex2.getSQLException()).isEqualTo(badSqlEx2); - - SQLException tranRollbackEx = SQLExceptionSubclassFactory.newSQLTransactionRollbackException("", "", 0); - ConcurrencyFailureException cfex = (ConcurrencyFailureException) sext.translate("task", "SQL", tranRollbackEx); - assertThat(cfex.getCause()).isEqualTo(tranRollbackEx); - - SQLException transientConnEx = SQLExceptionSubclassFactory.newSQLTransientConnectionException("", "", 0); - TransientDataAccessResourceException tdarex = (TransientDataAccessResourceException) sext.translate("task", "SQL", transientConnEx); - assertThat(tdarex.getCause()).isEqualTo(transientConnEx); - - SQLException transientConnEx2 = SQLExceptionSubclassFactory.newSQLTimeoutException("", "", 0); - QueryTimeoutException tdarex2 = (QueryTimeoutException) sext.translate("task", "SQL", transientConnEx2); - assertThat(tdarex2.getCause()).isEqualTo(transientConnEx2); - - SQLException recoverableEx = SQLExceptionSubclassFactory.newSQLRecoverableException("", "", 0); - RecoverableDataAccessException rdaex2 = (RecoverableDataAccessException) sext.translate("task", "SQL", recoverableEx); - assertThat(rdaex2.getCause()).isEqualTo(recoverableEx); - - // Test classic error code translation. We should move there next if the exception we pass in is not one - // of the new subclasses. - SQLException sexEct = new SQLException("", "", 1); - BadSqlGrammarException bsgEct = (BadSqlGrammarException) sext.translate("task", "SQL-ECT", sexEct); - assertThat(bsgEct.getSql()).isEqualTo("SQL-ECT"); - assertThat((Object) bsgEct.getSQLException()).isEqualTo(sexEct); - + public void fallbackStateTranslation() { // Test fallback. We assume that no database will ever return this error code, // but 07xxx will be bad grammar picked up by the fallback SQLState translator - SQLException sexFbt = new SQLException("", "07xxx", 666666666); - BadSqlGrammarException bsgFbt = (BadSqlGrammarException) sext.translate("task", "SQL-FBT", sexFbt); - assertThat(bsgFbt.getSql()).isEqualTo("SQL-FBT"); - assertThat((Object) bsgFbt.getSQLException()).isEqualTo(sexFbt); + doTest(new SQLException("", "07xxx", 666666666), BadSqlGrammarException.class); // and 08xxx will be data resource failure (non-transient) picked up by the fallback SQLState translator - SQLException sexFbt2 = new SQLException("", "08xxx", 666666666); - DataAccessResourceFailureException darfFbt = (DataAccessResourceFailureException) sext.translate("task", "SQL-FBT2", sexFbt2); - assertThat(darfFbt.getCause()).isEqualTo(sexFbt2); + doTest(new SQLException("", "08xxx", 666666666), DataAccessResourceFailureException.class); + } + + + private void doTest(SQLException ex, Class dataAccessExceptionType) { + SQLExceptionTranslator translator = new SQLExceptionSubclassTranslator(); + DataAccessException dax = translator.translate("task", "SQL", ex); + + assertThat(dax).as("Specific translation must not result in null").isNotNull(); + assertThat(dax).as("Wrong DataAccessException type returned").isExactlyInstanceOf(dataAccessExceptionType); + assertThat(dax.getCause()).as("The exact same original SQLException must be preserved").isSameAs(ex); } } diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLStateExceptionTranslatorTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLStateExceptionTranslatorTests.java deleted file mode 100644 index 608e0b5d3312..000000000000 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLStateExceptionTranslatorTests.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright 2002-2019 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.jdbc.support; - -import java.sql.SQLException; - -import org.junit.jupiter.api.Test; - -import org.springframework.jdbc.BadSqlGrammarException; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Rod Johnson - * @since 13-Jan-03 - */ -public class SQLStateExceptionTranslatorTests { - - private static final String sql = "SELECT FOO FROM BAR"; - - private final SQLStateSQLExceptionTranslator trans = new SQLStateSQLExceptionTranslator(); - - // ALSO CHECK CHAIN of SQLExceptions!? - // also allow chain of translators? default if can't do specific? - - @Test - public void badSqlGrammar() { - SQLException sex = new SQLException("Message", "42001", 1); - try { - throw this.trans.translate("task", sql, sex); - } - catch (BadSqlGrammarException ex) { - // OK - assertThat(sql.equals(ex.getSql())).as("SQL is correct").isTrue(); - assertThat(sex.equals(ex.getSQLException())).as("Exception matches").isTrue(); - } - } - - @Test - public void invalidSqlStateCode() { - SQLException sex = new SQLException("Message", "NO SUCH CODE", 1); - assertThat(this.trans.translate("task", sql, sex)).isNull(); - } - - /** - * PostgreSQL can return null. - * SAP DB can apparently return empty SQL code. - * Bug 729170 - */ - @Test - public void malformedSqlStateCodes() { - SQLException sex = new SQLException("Message", null, 1); - assertThat(this.trans.translate("task", sql, sex)).isNull(); - - sex = new SQLException("Message", "", 1); - assertThat(this.trans.translate("task", sql, sex)).isNull(); - - // One char's not allowed - sex = new SQLException("Message", "I", 1); - assertThat(this.trans.translate("task", sql, sex)).isNull(); - } - -} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLStateSQLExceptionTranslatorTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLStateSQLExceptionTranslatorTests.java index 98baf1ab7f9d..796847feb4b0 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLStateSQLExceptionTranslatorTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLStateSQLExceptionTranslatorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2022 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 org.springframework.dao.DataIntegrityViolationException; import org.springframework.dao.TransientDataAccessResourceException; import org.springframework.jdbc.BadSqlGrammarException; +import org.springframework.lang.Nullable; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; @@ -37,58 +38,73 @@ */ public class SQLStateSQLExceptionTranslatorTests { - private static final String REASON = "The game is afoot!"; - - private static final String TASK = "Counting sheep... yawn."; - - private static final String SQL = "select count(0) from t_sheep where over_fence = ... yawn... 1"; - - @Test - public void testTranslateNullException() { + public void translateNullException() { assertThatIllegalArgumentException().isThrownBy(() -> new SQLStateSQLExceptionTranslator().translate("", "", null)); } @Test - public void testTranslateBadSqlGrammar() { + public void translateBadSqlGrammar() { doTest("07", BadSqlGrammarException.class); } @Test - public void testTranslateDataIntegrityViolation() { + public void translateDataIntegrityViolation() { doTest("23", DataIntegrityViolationException.class); } @Test - public void testTranslateDataAccessResourceFailure() { + public void translateDataAccessResourceFailure() { doTest("53", DataAccessResourceFailureException.class); } @Test - public void testTranslateTransientDataAccessResourceFailure() { + public void translateTransientDataAccessResourceFailure() { doTest("S1", TransientDataAccessResourceException.class); } @Test - public void testTranslateConcurrencyFailure() { + public void translateConcurrencyFailure() { doTest("40", ConcurrencyFailureException.class); } @Test - public void testTranslateUncategorized() { - assertThat(new SQLStateSQLExceptionTranslator().translate("", "", new SQLException(REASON, "00000000"))).isNull(); + public void translateUncategorized() { + doTest("00000000", null); + } + + @Test + public void invalidSqlStateCode() { + doTest("NO SUCH CODE", null); + } + + /** + * PostgreSQL can return null. + * SAP DB can apparently return empty SQL code. + * Bug 729170 + */ + @Test + public void malformedSqlStateCodes() { + doTest(null, null); + doTest("", null); + doTest("I", null); } - private void doTest(String sqlState, Class dataAccessExceptionType) { - SQLException ex = new SQLException(REASON, sqlState); + private void doTest(@Nullable String sqlState, @Nullable Class dataAccessExceptionType) { SQLExceptionTranslator translator = new SQLStateSQLExceptionTranslator(); - DataAccessException dax = translator.translate(TASK, SQL, ex); - assertThat(dax).as("Specific translation must not result in a null DataAccessException being returned.").isNotNull(); - assertThat(dax.getClass()).as("Wrong DataAccessException type returned as the result of the translation").isEqualTo(dataAccessExceptionType); - assertThat(dax.getCause()).as("The original SQLException must be preserved in the translated DataAccessException").isNotNull(); - assertThat(dax.getCause()).as("The exact same original SQLException must be preserved in the translated DataAccessException").isSameAs(ex); + SQLException ex = new SQLException("reason", sqlState); + DataAccessException dax = translator.translate("task", "SQL", ex); + + if (dataAccessExceptionType == null) { + assertThat(dax).as("Expected translation to null").isNull(); + return; + } + + assertThat(dax).as("Specific translation must not result in null").isNotNull(); + assertThat(dax).as("Wrong DataAccessException type returned").isExactlyInstanceOf(dataAccessExceptionType); + assertThat(dax.getCause()).as("The exact same original SQLException must be preserved").isSameAs(ex); } } diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/support/rowset/ResultSetWrappingRowSetTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/support/rowset/ResultSetWrappingRowSetTests.java index 06ca49465989..8d946c13a907 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/support/rowset/ResultSetWrappingRowSetTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/support/rowset/ResultSetWrappingRowSetTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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,7 +25,6 @@ import java.sql.Time; import java.sql.Timestamp; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.jdbc.InvalidResultSetAccessException; @@ -38,174 +37,168 @@ /** * @author Thomas Risberg */ -public class ResultSetWrappingRowSetTests { +class ResultSetWrappingRowSetTests { - private ResultSet resultSet; + private final ResultSet resultSet = mock(ResultSet.class); - private ResultSetWrappingSqlRowSet rowSet; - - - @BeforeEach - public void setup() throws Exception { - resultSet = mock(ResultSet.class); - rowSet = new ResultSetWrappingSqlRowSet(resultSet); - } + private final ResultSetWrappingSqlRowSet rowSet = new ResultSetWrappingSqlRowSet(resultSet); @Test - public void testGetBigDecimalInt() throws Exception { + void testGetBigDecimalInt() throws Exception { Method rset = ResultSet.class.getDeclaredMethod("getBigDecimal", int.class); Method rowset = ResultSetWrappingSqlRowSet.class.getDeclaredMethod("getBigDecimal", int.class); doTest(rset, rowset, 1, BigDecimal.ONE); } @Test - public void testGetBigDecimalString() throws Exception { + void testGetBigDecimalString() throws Exception { Method rset = ResultSet.class.getDeclaredMethod("getBigDecimal", int.class); Method rowset = ResultSetWrappingSqlRowSet.class.getDeclaredMethod("getBigDecimal", String.class); doTest(rset, rowset, "test", BigDecimal.ONE); } @Test - public void testGetStringInt() throws Exception { + void testGetStringInt() throws Exception { Method rset = ResultSet.class.getDeclaredMethod("getString", int.class); Method rowset = ResultSetWrappingSqlRowSet.class.getDeclaredMethod("getString", int.class); doTest(rset, rowset, 1, "test"); } @Test - public void testGetStringString() throws Exception { + void testGetStringString() throws Exception { Method rset = ResultSet.class.getDeclaredMethod("getString", int.class); Method rowset = ResultSetWrappingSqlRowSet.class.getDeclaredMethod("getString", String.class); doTest(rset, rowset, "test", "test"); } @Test - public void testGetTimestampInt() throws Exception { + void testGetTimestampInt() throws Exception { Method rset = ResultSet.class.getDeclaredMethod("getTimestamp", int.class); Method rowset = ResultSetWrappingSqlRowSet.class.getDeclaredMethod("getTimestamp", int.class); doTest(rset, rowset, 1, new Timestamp(1234L)); } @Test - public void testGetTimestampString() throws Exception { + void testGetTimestampString() throws Exception { Method rset = ResultSet.class.getDeclaredMethod("getTimestamp", int.class); Method rowset = ResultSetWrappingSqlRowSet.class.getDeclaredMethod("getTimestamp", String.class); doTest(rset, rowset, "test", new Timestamp(1234L)); } @Test - public void testGetDateInt() throws Exception { + void testGetDateInt() throws Exception { Method rset = ResultSet.class.getDeclaredMethod("getDate", int.class); Method rowset = ResultSetWrappingSqlRowSet.class.getDeclaredMethod("getDate", int.class); doTest(rset, rowset, 1, new Date(1234L)); } @Test - public void testGetDateString() throws Exception { + void testGetDateString() throws Exception { Method rset = ResultSet.class.getDeclaredMethod("getDate", int.class); Method rowset = ResultSetWrappingSqlRowSet.class.getDeclaredMethod("getDate", String.class); doTest(rset, rowset, "test", new Date(1234L)); } @Test - public void testGetTimeInt() throws Exception { + void testGetTimeInt() throws Exception { Method rset = ResultSet.class.getDeclaredMethod("getTime", int.class); Method rowset = ResultSetWrappingSqlRowSet.class.getDeclaredMethod("getTime", int.class); doTest(rset, rowset, 1, new Time(1234L)); } @Test - public void testGetTimeString() throws Exception { + void testGetTimeString() throws Exception { Method rset = ResultSet.class.getDeclaredMethod("getTime", int.class); Method rowset = ResultSetWrappingSqlRowSet.class.getDeclaredMethod("getTime", String.class); doTest(rset, rowset, "test", new Time(1234L)); } @Test - public void testGetObjectInt() throws Exception { + void testGetObjectInt() throws Exception { Method rset = ResultSet.class.getDeclaredMethod("getObject", int.class); Method rowset = ResultSetWrappingSqlRowSet.class.getDeclaredMethod("getObject", int.class); doTest(rset, rowset, 1, new Object()); } @Test - public void testGetObjectString() throws Exception { + void testGetObjectString() throws Exception { Method rset = ResultSet.class.getDeclaredMethod("getObject", int.class); Method rowset = ResultSetWrappingSqlRowSet.class.getDeclaredMethod("getObject", String.class); doTest(rset, rowset, "test", new Object()); } @Test - public void testGetIntInt() throws Exception { + void testGetIntInt() throws Exception { Method rset = ResultSet.class.getDeclaredMethod("getInt", int.class); Method rowset = ResultSetWrappingSqlRowSet.class.getDeclaredMethod("getInt", int.class); doTest(rset, rowset, 1, 1); } @Test - public void testGetIntString() throws Exception { + void testGetIntString() throws Exception { Method rset = ResultSet.class.getDeclaredMethod("getInt", int.class); Method rowset = ResultSetWrappingSqlRowSet.class.getDeclaredMethod("getInt", String.class); doTest(rset, rowset, "test", 1); } @Test - public void testGetFloatInt() throws Exception { + void testGetFloatInt() throws Exception { Method rset = ResultSet.class.getDeclaredMethod("getFloat", int.class); Method rowset = ResultSetWrappingSqlRowSet.class.getDeclaredMethod("getFloat", int.class); doTest(rset, rowset, 1, 1.0f); } @Test - public void testGetFloatString() throws Exception { + void testGetFloatString() throws Exception { Method rset = ResultSet.class.getDeclaredMethod("getFloat", int.class); Method rowset = ResultSetWrappingSqlRowSet.class.getDeclaredMethod("getFloat", String.class); doTest(rset, rowset, "test", 1.0f); } @Test - public void testGetDoubleInt() throws Exception { + void testGetDoubleInt() throws Exception { Method rset = ResultSet.class.getDeclaredMethod("getDouble", int.class); Method rowset = ResultSetWrappingSqlRowSet.class.getDeclaredMethod("getDouble", int.class); doTest(rset, rowset, 1, 1.0d); } @Test - public void testGetDoubleString() throws Exception { + void testGetDoubleString() throws Exception { Method rset = ResultSet.class.getDeclaredMethod("getDouble", int.class); Method rowset = ResultSetWrappingSqlRowSet.class.getDeclaredMethod("getDouble", String.class); doTest(rset, rowset, "test", 1.0d); } @Test - public void testGetLongInt() throws Exception { + void testGetLongInt() throws Exception { Method rset = ResultSet.class.getDeclaredMethod("getLong", int.class); Method rowset = ResultSetWrappingSqlRowSet.class.getDeclaredMethod("getLong", int.class); doTest(rset, rowset, 1, 1L); } @Test - public void testGetLongString() throws Exception { + void testGetLongString() throws Exception { Method rset = ResultSet.class.getDeclaredMethod("getLong", int.class); Method rowset = ResultSetWrappingSqlRowSet.class.getDeclaredMethod("getLong", String.class); doTest(rset, rowset, "test", 1L); } @Test - public void testGetBooleanInt() throws Exception { + void testGetBooleanInt() throws Exception { Method rset = ResultSet.class.getDeclaredMethod("getBoolean", int.class); Method rowset = ResultSetWrappingSqlRowSet.class.getDeclaredMethod("getBoolean", int.class); doTest(rset, rowset, 1, true); } @Test - public void testGetBooleanString() throws Exception { + void testGetBooleanString() throws Exception { Method rset = ResultSet.class.getDeclaredMethod("getBoolean", int.class); Method rowset = ResultSetWrappingSqlRowSet.class.getDeclaredMethod("getBoolean", String.class); doTest(rset, rowset, "test", true); } + private void doTest(Method rsetMethod, Method rowsetMethod, Object arg, Object ret) throws Exception { if (arg instanceof String) { given(resultSet.findColumn((String) arg)).willReturn(1); @@ -215,9 +208,9 @@ private void doTest(Method rsetMethod, Method rowsetMethod, Object arg, Object r given(rsetMethod.invoke(resultSet, arg)).willReturn(ret).willThrow(new SQLException("test")); } rowsetMethod.invoke(rowSet, arg); - assertThatExceptionOfType(InvocationTargetException.class).isThrownBy(() -> - rowsetMethod.invoke(rowSet, arg)). - satisfies(ex -> assertThat(ex.getTargetException()).isExactlyInstanceOf(InvalidResultSetAccessException.class)); + assertThatExceptionOfType(InvocationTargetException.class) + .isThrownBy(() -> rowsetMethod.invoke(rowSet, arg)) + .satisfies(ex -> assertThat(ex.getTargetException()).isExactlyInstanceOf(InvalidResultSetAccessException.class)); } } diff --git a/spring-jms/src/main/java/org/springframework/jms/annotation/JmsListener.java b/spring-jms/src/main/java/org/springframework/jms/annotation/JmsListener.java index 20847545785b..7f3f685f9d4c 100644 --- a/spring-jms/src/main/java/org/springframework/jms/annotation/JmsListener.java +++ b/spring-jms/src/main/java/org/springframework/jms/annotation/JmsListener.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2023 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. @@ -76,6 +76,7 @@ * composed annotations with attribute overrides. * * @author Stephane Nicoll + * @author Sam Brannen * @since 4.1 * @see EnableJms * @see JmsListenerAnnotationBeanPostProcessor @@ -110,6 +111,12 @@ /** * The name for the durable subscription, if any. + *

    As of Spring Framework 5.3.26, if an explicit subscription name is not + * specified, a default subscription name will be generated based on the fully + * qualified name of the annotated listener method — for example, + * {@code "org.example.jms.ProductListener.processRequest"} for a + * {@code processRequest(...)} listener method in the + * {@code org.example.jms.ProductListener} class. */ String subscription() default ""; diff --git a/spring-jms/src/main/java/org/springframework/jms/config/DefaultJmsListenerContainerFactory.java b/spring-jms/src/main/java/org/springframework/jms/config/DefaultJmsListenerContainerFactory.java index 16440c1e9608..f11d9ae7d89e 100644 --- a/spring-jms/src/main/java/org/springframework/jms/config/DefaultJmsListenerContainerFactory.java +++ b/spring-jms/src/main/java/org/springframework/jms/config/DefaultJmsListenerContainerFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2023 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,8 +27,8 @@ * A {@link JmsListenerContainerFactory} implementation to build a regular * {@link DefaultMessageListenerContainer}. * - *

    This should be the default for most users and a good transition paths - * for those that are used to build such container definition manually. + *

    This should be the default for most users and a good transition path + * for those who are used to building such a container definition manually. * * @author Stephane Nicoll * @since 4.1 @@ -67,63 +67,63 @@ public class DefaultJmsListenerContainerFactory /** * @see DefaultMessageListenerContainer#setTaskExecutor */ - public void setTaskExecutor(Executor taskExecutor) { + public void setTaskExecutor(@Nullable Executor taskExecutor) { this.taskExecutor = taskExecutor; } /** * @see DefaultMessageListenerContainer#setTransactionManager */ - public void setTransactionManager(PlatformTransactionManager transactionManager) { + public void setTransactionManager(@Nullable PlatformTransactionManager transactionManager) { this.transactionManager = transactionManager; } /** * @see DefaultMessageListenerContainer#setCacheLevel */ - public void setCacheLevel(Integer cacheLevel) { + public void setCacheLevel(@Nullable Integer cacheLevel) { this.cacheLevel = cacheLevel; } /** * @see DefaultMessageListenerContainer#setCacheLevelName */ - public void setCacheLevelName(String cacheLevelName) { + public void setCacheLevelName(@Nullable String cacheLevelName) { this.cacheLevelName = cacheLevelName; } /** * @see DefaultMessageListenerContainer#setConcurrency */ - public void setConcurrency(String concurrency) { + public void setConcurrency(@Nullable String concurrency) { this.concurrency = concurrency; } /** * @see DefaultMessageListenerContainer#setMaxMessagesPerTask */ - public void setMaxMessagesPerTask(Integer maxMessagesPerTask) { + public void setMaxMessagesPerTask(@Nullable Integer maxMessagesPerTask) { this.maxMessagesPerTask = maxMessagesPerTask; } /** * @see DefaultMessageListenerContainer#setReceiveTimeout */ - public void setReceiveTimeout(Long receiveTimeout) { + public void setReceiveTimeout(@Nullable Long receiveTimeout) { this.receiveTimeout = receiveTimeout; } /** * @see DefaultMessageListenerContainer#setRecoveryInterval */ - public void setRecoveryInterval(Long recoveryInterval) { + public void setRecoveryInterval(@Nullable Long recoveryInterval) { this.recoveryInterval = recoveryInterval; } /** * @see DefaultMessageListenerContainer#setBackOff */ - public void setBackOff(BackOff backOff) { + public void setBackOff(@Nullable BackOff backOff) { this.backOff = backOff; } diff --git a/spring-jms/src/main/java/org/springframework/jms/connection/CachingConnectionFactory.java b/spring-jms/src/main/java/org/springframework/jms/connection/CachingConnectionFactory.java index 3bab8917c78f..627ccfa2e33f 100644 --- a/spring-jms/src/main/java/org/springframework/jms/connection/CachingConnectionFactory.java +++ b/spring-jms/src/main/java/org/springframework/jms/connection/CachingConnectionFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -622,7 +622,7 @@ public boolean equals(@Nullable Object other) { @Override public int hashCode() { - return (31 * super.hashCode() + ObjectUtils.nullSafeHashCode(this.selector)); + return super.hashCode() * 31 + ObjectUtils.nullSafeHashCode(this.selector); } @Override diff --git a/spring-jms/src/main/java/org/springframework/jms/connection/SingleConnectionFactory.java b/spring-jms/src/main/java/org/springframework/jms/connection/SingleConnectionFactory.java index 1a463c3b4fef..0c72a8dbb1fe 100644 --- a/spring-jms/src/main/java/org/springframework/jms/connection/SingleConnectionFactory.java +++ b/spring-jms/src/main/java/org/springframework/jms/connection/SingleConnectionFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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. @@ -347,8 +347,26 @@ public void initConnection() throws JMSException { if (this.connection != null) { closeConnection(this.connection); } - this.connection = doCreateConnection(); - prepareConnection(this.connection); + // Create new (method local) connection, which is later assigned to instance connection + // - prevention to hold instance connection without exception listener, in case when + // some subsequent methods (after creation of connection) throws JMSException + Connection con = doCreateConnection(); + try { + prepareConnection(con); + this.connection = con; + } + catch (JMSException ex) { + // Attempt to close new (not used) connection to release possible resources + try { + con.close(); + } + catch(Throwable th) { + if (logger.isDebugEnabled()) { + logger.debug("Could not close newly obtained JMS Connection that failed to prepare", th); + } + } + throw ex; + } if (this.startedCount > 0) { this.connection.start(); } diff --git a/spring-jms/src/main/java/org/springframework/jms/core/JmsMessagingTemplate.java b/spring-jms/src/main/java/org/springframework/jms/core/JmsMessagingTemplate.java index 7eaa1bd8d988..7d7baad5c39a 100644 --- a/spring-jms/src/main/java/org/springframework/jms/core/JmsMessagingTemplate.java +++ b/spring-jms/src/main/java/org/springframework/jms/core/JmsMessagingTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-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. @@ -189,11 +189,6 @@ public void send(Message message) { } } - @Override - public void convertAndSend(Object payload) throws MessagingException { - convertAndSend(payload, null); - } - @Override public void convertAndSend(Object payload, @Nullable MessagePostProcessor postProcessor) throws MessagingException { Destination defaultDestination = getDefaultDestination(); diff --git a/spring-jms/src/main/java/org/springframework/jms/core/JmsOperations.java b/spring-jms/src/main/java/org/springframework/jms/core/JmsOperations.java index 9b2cef401b92..94d1f475f224 100644 --- a/spring-jms/src/main/java/org/springframework/jms/core/JmsOperations.java +++ b/spring-jms/src/main/java/org/springframework/jms/core/JmsOperations.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-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. @@ -290,7 +290,7 @@ void convertAndSend(String destinationName, Object message, MessagePostProcessor *

    This method should be used carefully, since it will block the thread * until the message becomes available or until the timeout value is exceeded. *

    This will only work with a default destination specified! - * @return the message produced for the consumer or {@code null} if the timeout expires. + * @return the message produced for the consumer, or {@code null} if the timeout expires * @throws JmsException checked JMSException converted to unchecked */ @Nullable @@ -303,7 +303,7 @@ void convertAndSend(String destinationName, Object message, MessagePostProcessor *

    This method should be used carefully, since it will block the thread * until the message becomes available or until the timeout value is exceeded. * @param destination the destination to receive a message from - * @return the message produced for the consumer or {@code null} if the timeout expires. + * @return the message produced for the consumer, or {@code null} if the timeout expires * @throws JmsException checked JMSException converted to unchecked */ @Nullable @@ -317,7 +317,7 @@ void convertAndSend(String destinationName, Object message, MessagePostProcessor * until the message becomes available or until the timeout value is exceeded. * @param destinationName the name of the destination to send this message to * (to be resolved to an actual destination by a DestinationResolver) - * @return the message produced for the consumer or {@code null} if the timeout expires. + * @return the message produced for the consumer, or {@code null} if the timeout expires * @throws JmsException checked JMSException converted to unchecked */ @Nullable @@ -332,7 +332,7 @@ void convertAndSend(String destinationName, Object message, MessagePostProcessor *

    This will only work with a default destination specified! * @param messageSelector the JMS message selector expression (or {@code null} if none). * See the JMS specification for a detailed definition of selector expressions. - * @return the message produced for the consumer or {@code null} if the timeout expires. + * @return the message produced for the consumer, or {@code null} if the timeout expires * @throws JmsException checked JMSException converted to unchecked */ @Nullable @@ -347,7 +347,7 @@ void convertAndSend(String destinationName, Object message, MessagePostProcessor * @param destination the destination to receive a message from * @param messageSelector the JMS message selector expression (or {@code null} if none). * See the JMS specification for a detailed definition of selector expressions. - * @return the message produced for the consumer or {@code null} if the timeout expires. + * @return the message produced for the consumer, or {@code null} if the timeout expires * @throws JmsException checked JMSException converted to unchecked */ @Nullable @@ -363,7 +363,7 @@ void convertAndSend(String destinationName, Object message, MessagePostProcessor * (to be resolved to an actual destination by a DestinationResolver) * @param messageSelector the JMS message selector expression (or {@code null} if none). * See the JMS specification for a detailed definition of selector expressions. - * @return the message produced for the consumer or {@code null} if the timeout expires. + * @return the message produced for the consumer, or {@code null} if the timeout expires * @throws JmsException checked JMSException converted to unchecked */ @Nullable diff --git a/spring-jms/src/main/java/org/springframework/jms/core/support/JmsGatewaySupport.java b/spring-jms/src/main/java/org/springframework/jms/core/support/JmsGatewaySupport.java index 0e5e1f6c2c22..79d500593c72 100644 --- a/spring-jms/src/main/java/org/springframework/jms/core/support/JmsGatewaySupport.java +++ b/spring-jms/src/main/java/org/springframework/jms/core/support/JmsGatewaySupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2022 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-jms/src/main/java/org/springframework/jms/listener/AbstractJmsListeningContainer.java b/spring-jms/src/main/java/org/springframework/jms/listener/AbstractJmsListeningContainer.java index a9061e162fc5..21193f3b6401 100644 --- a/spring-jms/src/main/java/org/springframework/jms/listener/AbstractJmsListeningContainer.java +++ b/spring-jms/src/main/java/org/springframework/jms/listener/AbstractJmsListeningContainer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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. @@ -125,18 +125,19 @@ public boolean isAutoStartup() { } /** - * Specify the phase in which this container should be started and - * stopped. The startup order proceeds from lowest to highest, and - * the shutdown order is the reverse of that. By default this value - * is Integer.MAX_VALUE meaning that this container starts as late - * as possible and stops as soon as possible. + * Specify the lifecycle phase in which this container should be started and stopped. + *

    The startup order proceeds from lowest to highest, and the shutdown order + * is the reverse of that. The default is {@link #DEFAULT_PHASE} meaning that + * this container starts as late as possible and stops as soon as possible. + * @see SmartLifecycle#getPhase() */ public void setPhase(int phase) { this.phase = phase; } /** - * Return the phase in which this container will be started and stopped. + * Return the lifecycle phase in which this container will be started and stopped. + * @see #setPhase */ @Override public int getPhase() { diff --git a/spring-jms/src/main/java/org/springframework/jms/listener/adapter/AbstractAdaptableMessageListener.java b/spring-jms/src/main/java/org/springframework/jms/listener/adapter/AbstractAdaptableMessageListener.java index e4b9cd442617..e8d8fb8ce199 100644 --- a/spring-jms/src/main/java/org/springframework/jms/listener/adapter/AbstractAdaptableMessageListener.java +++ b/spring-jms/src/main/java/org/springframework/jms/listener/adapter/AbstractAdaptableMessageListener.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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. @@ -304,8 +304,8 @@ protected void handleResult(Object result, Message request, @Nullable Session se * @see #setMessageConverter */ protected Message buildMessage(Session session, Object result) throws JMSException { - Object content = preProcessResponse(result instanceof JmsResponse - ? ((JmsResponse) result).getResponse() : result); + Object content = preProcessResponse(result instanceof JmsResponse ? + ((JmsResponse) result).getResponse() : result); MessageConverter converter = getMessageConverter(); if (converter != null) { @@ -553,6 +553,29 @@ public MessageHeaders getHeaders() { } return this.headers; } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(getClass().getSimpleName()); + if (this.payload == null) { + sb.append(" [rawMessage=").append(this.message); + } + else { + sb.append(" [payload="); + if (this.payload instanceof byte[]) { + sb.append("byte[").append(((byte[]) this.payload).length).append(']'); + } + else { + sb.append(this.payload); + } + } + if (this.headers != null) { + sb.append(", headers=").append(this.headers); + } + sb.append(']'); + return sb.toString(); + } + } } diff --git a/spring-jms/src/main/java/org/springframework/jms/listener/adapter/MessagingMessageListenerAdapter.java b/spring-jms/src/main/java/org/springframework/jms/listener/adapter/MessagingMessageListenerAdapter.java index 91072e40cb5e..5f09fdf1fa83 100644 --- a/spring-jms/src/main/java/org/springframework/jms/listener/adapter/MessagingMessageListenerAdapter.java +++ b/spring-jms/src/main/java/org/springframework/jms/listener/adapter/MessagingMessageListenerAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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 javax.jms.Session; import org.springframework.core.MethodParameter; +import org.springframework.jms.listener.SubscriptionNameProvider; import org.springframework.jms.support.JmsHeaderMapper; import org.springframework.jms.support.converter.MessageConversionException; import org.springframework.lang.Nullable; @@ -34,7 +35,7 @@ * A {@link javax.jms.MessageListener} adapter that invokes a configurable * {@link InvocableHandlerMethod}. * - *

    Wraps the incoming {@link javax.jms.Message} to Spring's {@link Message} + *

    Wraps the incoming {@link javax.jms.Message} in Spring's {@link Message} * abstraction, copying the JMS standard headers using a configurable * {@link JmsHeaderMapper}. * @@ -42,13 +43,19 @@ * are provided as additional arguments so that these can be injected as * method arguments if necessary. * + *

    As of Spring Framework 5.3.26, {@code MessagingMessageListenerAdapter} implements + * {@link SubscriptionNameProvider} in order to provide a meaningful default + * subscription name. See {@link #getSubscriptionName()} for details. + * * @author Stephane Nicoll + * @author Sam Brannen * @since 4.1 * @see Message * @see JmsHeaderMapper * @see InvocableHandlerMethod */ -public class MessagingMessageListenerAdapter extends AbstractAdaptableMessageListener { +public class MessagingMessageListenerAdapter extends AbstractAdaptableMessageListener + implements SubscriptionNameProvider { @Nullable private InvocableHandlerMethod handlerMethod; @@ -83,17 +90,6 @@ public void onMessage(javax.jms.Message jmsMessage, @Nullable Session session) t } } - @Override - protected Object preProcessResponse(Object result) { - MethodParameter returnType = getHandlerMethod().getReturnType(); - if (result instanceof Message) { - return MessageBuilder.fromMessage((Message) result) - .setHeader(AbstractMessageSendingTemplate.CONVERSION_HINT_HEADER, returnType).build(); - } - return MessageBuilder.withPayload(result).setHeader( - AbstractMessageSendingTemplate.CONVERSION_HINT_HEADER, returnType).build(); - } - protected Message toMessagingMessage(javax.jms.Message jmsMessage) { try { return (Message) getMessagingMessageConverter().fromMessage(jmsMessage); @@ -104,7 +100,7 @@ protected Message toMessagingMessage(javax.jms.Message jmsMessage) { } /** - * Invoke the handler, wrapping any exception to a {@link ListenerExecutionFailedException} + * Invoke the handler, wrapping any exception in a {@link ListenerExecutionFailedException} * with a dedicated error message. */ @Nullable @@ -132,4 +128,39 @@ private String createMessagingErrorMessage(String description) { return sb.toString(); } + @Override + protected Object preProcessResponse(Object result) { + MethodParameter returnType = getHandlerMethod().getReturnType(); + MessageBuilder messageBuilder = (result instanceof Message ? + MessageBuilder.fromMessage((Message) result) : + MessageBuilder.withPayload(result)); + return messageBuilder + .setHeader(AbstractMessageSendingTemplate.CONVERSION_HINT_HEADER, returnType) + .build(); + } + + /** + * Generate a subscription name for this {@code MessageListener} adapter based + * on the following rules. + *

      + *
    • If the {@link #setHandlerMethod(InvocableHandlerMethod) handlerMethod} + * has been set, the generated subscription name takes the form of + * {@code handlerMethod.getBeanType().getName() + "." + handlerMethod.getMethod().getName()}.
    • + *
    • Otherwise, the generated subscription name is the result of invoking + * {@code getClass().getName()}, which aligns with the default behavior of + * {@link org.springframework.jms.listener.AbstractMessageListenerContainer}.
    • + *
    + * @since 5.3.26 + * @see SubscriptionNameProvider#getSubscriptionName() + */ + @Override + public String getSubscriptionName() { + if (this.handlerMethod != null) { + return this.handlerMethod.getBeanType().getName() + "." + this.handlerMethod.getMethod().getName(); + } + else { + return getClass().getName(); + } + } + } diff --git a/spring-jms/src/main/java/org/springframework/jms/support/JmsAccessor.java b/spring-jms/src/main/java/org/springframework/jms/support/JmsAccessor.java index 03f89d28a787..754c13e3bbde 100644 --- a/spring-jms/src/main/java/org/springframework/jms/support/JmsAccessor.java +++ b/spring-jms/src/main/java/org/springframework/jms/support/JmsAccessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2023 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. @@ -193,7 +193,13 @@ protected JmsException convertJmsAccessException(JMSException ex) { * @see javax.jms.ConnectionFactory#createConnection() */ protected Connection createConnection() throws JMSException { - return obtainConnectionFactory().createConnection(); + ConnectionFactory cf = obtainConnectionFactory(); + Connection con = cf.createConnection(); + if (con == null) { + throw new javax.jms.IllegalStateException( + "ConnectionFactory returned null from createConnection(): " + cf); + } + return con; } /** diff --git a/spring-jms/src/main/java/org/springframework/jms/support/JmsUtils.java b/spring-jms/src/main/java/org/springframework/jms/support/JmsUtils.java index 3f33ba102110..5e0418fde0b0 100644 --- a/spring-jms/src/main/java/org/springframework/jms/support/JmsUtils.java +++ b/spring-jms/src/main/java/org/springframework/jms/support/JmsUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-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. @@ -217,7 +217,7 @@ public static void commitIfNecessary(Session session) throws JMSException { try { session.commit(); } - catch (javax.jms.TransactionInProgressException | javax.jms.IllegalStateException ex) { + catch (javax.jms.TransactionInProgressException ex) { // Ignore -> can only happen in case of a JTA transaction. } } @@ -232,7 +232,7 @@ public static void rollbackIfNecessary(Session session) throws JMSException { try { session.rollback(); } - catch (javax.jms.TransactionInProgressException | javax.jms.IllegalStateException ex) { + catch (javax.jms.TransactionInProgressException ex) { // Ignore -> can only happen in case of a JTA transaction. } } diff --git a/spring-jms/src/test/java/org/springframework/jms/connection/SingleConnectionFactoryTests.java b/spring-jms/src/test/java/org/springframework/jms/connection/SingleConnectionFactoryTests.java index 85a6126dde2e..4a30219515ba 100644 --- a/spring-jms/src/test/java/org/springframework/jms/connection/SingleConnectionFactoryTests.java +++ b/spring-jms/src/test/java/org/springframework/jms/connection/SingleConnectionFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 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,9 @@ package org.springframework.jms.connection; +import java.lang.reflect.Field; +import java.util.concurrent.atomic.AtomicInteger; + import javax.jms.Connection; import javax.jms.ConnectionFactory; import javax.jms.ExceptionListener; @@ -30,7 +33,10 @@ import org.junit.jupiter.api.Test; +import org.springframework.util.ReflectionUtils; + import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; @@ -342,6 +348,76 @@ public void testWithConnectionFactoryAndExceptionListenerAndReconnectOnException assertThat(listener.getCount()).isEqualTo(1); } + @Test + public void testWithConnectionFactoryAndExceptionListenerAndReconnectOnExceptionWithJMSException() throws Exception { + // Throws JMSException on setExceptionListener() method, but only at the first time + class FailingTestConnection extends TestConnection { + private int setExceptionListenerInvocationCounter; + + @Override + public void setExceptionListener(ExceptionListener exceptionListener) throws JMSException { + setExceptionListenerInvocationCounter++; + // Throw JMSException on first invocation + if (setExceptionListenerInvocationCounter == 1) { + throw new JMSException("Test JMSException (setExceptionListener())"); + } + super.setExceptionListener(exceptionListener); + } + } + + // Prepare base JMS ConnectionFactory + // - createConnection(1st) -> TestConnection, + // - createConnection(2nd and next) -> FailingTestConnection + TestConnection testCon = new TestConnection(); + FailingTestConnection failingCon = new FailingTestConnection(); + AtomicInteger createConnectionMethodCounter = new AtomicInteger(); + ConnectionFactory cf = mock(ConnectionFactory.class); + given(cf.createConnection()).willAnswer(invocation -> { + int methodInvocationCounter = createConnectionMethodCounter.incrementAndGet(); + return methodInvocationCounter == 1 ? testCon : failingCon; + }); + + // Prepare SingleConnectionFactory (setReconnectOnException()) + // - internal connection exception listener should be registered + SingleConnectionFactory scf = new SingleConnectionFactory(cf); + scf.setReconnectOnException(true); + Field conField = ReflectionUtils.findField(SingleConnectionFactory.class, "connection"); + conField.setAccessible(true); + + // Get connection (1st) + Connection con1 = scf.getConnection(); + assertThat(createConnectionMethodCounter.get()).isEqualTo(1); + assertThat(con1).isNotNull(); + assertThat(con1.getExceptionListener()).isNotNull(); + assertThat(con1).isSameAs(testCon); + // Get connection again, the same should be returned (shared connection till some problem) + Connection con2 = scf.getConnection(); + assertThat(createConnectionMethodCounter.get()).isEqualTo(1); + assertThat(con2.getExceptionListener()).isNotNull(); + assertThat(con2).isSameAs(con1); + + // Invoke reset connection to simulate problem with connection + // - SCF exception listener should be invoked -> connection should be set to null + // - next attempt to invoke getConnection() must create new connection + scf.resetConnection(); + assertThat(conField.get(scf)).isNull(); + + // Attempt to get connection again + // - JMSException should be returned from FailingTestConnection + // - connection should be still null (no new connection without exception listener like before fix) + assertThatExceptionOfType(JMSException.class).isThrownBy(() -> scf.getConnection()); + assertThat(createConnectionMethodCounter.get()).isEqualTo(2); + assertThat(conField.get(scf)).isNull(); + + // Attempt to get connection again -> FailingTestConnection should be returned + // - no JMSException is thrown, exception listener should be present + Connection con3 = scf.getConnection(); + assertThat(createConnectionMethodCounter.get()).isEqualTo(3); + assertThat(con3).isNotNull(); + assertThat(con3).isSameAs(failingCon); + assertThat(con3.getExceptionListener()).isNotNull(); + } + @Test public void testWithConnectionFactoryAndLocalExceptionListenerWithCleanup() throws JMSException { ConnectionFactory cf = mock(ConnectionFactory.class); diff --git a/spring-jms/src/test/java/org/springframework/jms/listener/adapter/MessagingMessageListenerAdapterIntegrationTests.java b/spring-jms/src/test/java/org/springframework/jms/listener/adapter/MessagingMessageListenerAdapterIntegrationTests.java new file mode 100644 index 000000000000..9a63f325564d --- /dev/null +++ b/spring-jms/src/test/java/org/springframework/jms/listener/adapter/MessagingMessageListenerAdapterIntegrationTests.java @@ -0,0 +1,104 @@ +/* + * Copyright 2002-2023 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.jms.listener.adapter; + +import java.lang.reflect.Method; +import java.util.stream.Stream; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import org.springframework.jms.listener.SimpleMessageListenerContainer; +import org.springframework.messaging.handler.invocation.InvocableHandlerMethod; +import org.springframework.util.ReflectionUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Named.named; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +/** + * Integration tests for {@link MessagingMessageListenerAdapter}. + * + *

    These tests are similar to those in {@link MessagingMessageListenerAdapterTests}, + * except that these tests have a different scope and do not use mocks. + * + * @author Sam Brannen + * @since 5.3.26 + * @see MessagingMessageListenerAdapterTests + */ +class MessagingMessageListenerAdapterIntegrationTests { + + @ParameterizedTest + @MethodSource("subscriptionNames") + void defaultSubscriptionName(Method method, String subscriptionName) { + MessagingMessageListenerAdapter messageListenerAdaptor = new MessagingMessageListenerAdapter(); + InvocableHandlerMethod handlerMethod = new InvocableHandlerMethod(new CustomListener(), method); + messageListenerAdaptor.setHandlerMethod(handlerMethod); + + SimpleMessageListenerContainer listenerContainer = new SimpleMessageListenerContainer(); + assertThat(listenerContainer.getSubscriptionName()).isNull(); + + listenerContainer.setMessageListener(messageListenerAdaptor); + assertThat(listenerContainer.getSubscriptionName()).isEqualTo(subscriptionName); + } + + + private static Stream subscriptionNames() { + String method1 = "toUpperCase"; + String method2 = "toUpperCase(java.lang.String)"; + String method3 = "toUpperCase(java.lang.String,int)"; + String method4 = "toUpperCase(java.lang.String[])"; + String expectedName = CustomListener.class.getName() + ".toUpperCase"; + return Stream.of( + arguments(named(method1, findMethod()), expectedName), + arguments(named(method2, findMethod(String.class)), expectedName), + arguments(named(method3, findMethod(String.class, String.class)), expectedName), + arguments(named(method4, findMethod(byte[].class)), expectedName)); + } + + private static Method findMethod(Class... paramTypes) { + return ReflectionUtils.findMethod(CustomListener.class, "toUpperCase", paramTypes); + } + + + @SuppressWarnings("unused") + private static class CustomListener { + + // @JmsListener(...) + String toUpperCase() { + return "ENIGMA"; + } + + // @JmsListener(...) + String toUpperCase(String input) { + return "ENIGMA"; + } + + // @JmsListener(...) + String toUpperCase(String input, String customHeader) { + return "ENIGMA"; + } + + // @JmsListener(...) + String toUpperCase(byte[] input) { + return "ENIGMA"; + } + + } + +} diff --git a/spring-jms/src/test/java/org/springframework/jms/listener/adapter/MessagingMessageListenerAdapterTests.java b/spring-jms/src/test/java/org/springframework/jms/listener/adapter/MessagingMessageListenerAdapterTests.java index 4e0d7bfa363c..20de38040740 100644 --- a/spring-jms/src/test/java/org/springframework/jms/listener/adapter/MessagingMessageListenerAdapterTests.java +++ b/spring-jms/src/test/java/org/springframework/jms/listener/adapter/MessagingMessageListenerAdapterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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. @@ -40,6 +40,7 @@ import org.springframework.jms.support.converter.MappingJackson2MessageConverter; import org.springframework.jms.support.converter.MessageConverter; import org.springframework.jms.support.converter.MessageType; +import org.springframework.jms.support.converter.MessagingMessageConverter; import org.springframework.messaging.Message; import org.springframework.messaging.converter.MessageConversionException; import org.springframework.messaging.handler.annotation.support.DefaultMessageHandlerMethodFactory; @@ -340,6 +341,45 @@ public TextMessage testReplyWithJackson(String methodName, String replyContent) return responseMessage; } + @Test + void lazyResolutionMessageToStringProvidesBestEffortWithUnresolvedPayload() throws JMSException { + MessagingMessageListenerAdapter adapter = getSimpleInstance("echo", Message.class); + MessagingMessageConverter messagingMessageConverter = adapter.getMessagingMessageConverter(); + assertThat(messagingMessageConverter).isNotNull(); + TextMessage message = new StubTextMessage(); + assertThat(messagingMessageConverter.fromMessage(message)).isInstanceOfSatisfying(Message.class, msg -> + assertThat(msg.toString()).contains("rawMessage=").contains(message.toString()) + .doesNotContain("payload=").doesNotContain("headers=")); + } + + @Test + void lazyResolutionMessageToStringWithResolvedPayload() throws JMSException { + MessagingMessageListenerAdapter adapter = getSimpleInstance("echo", Message.class); + MessagingMessageConverter messagingMessageConverter = adapter.getMessagingMessageConverter(); + assertThat(messagingMessageConverter).isNotNull(); + TextMessage message = new StubTextMessage("Hello"); + assertThat(messagingMessageConverter.fromMessage(message)).isInstanceOfSatisfying(Message.class, msg -> { + msg.getPayload(); // force resolution + assertThat(msg.toString()).contains("payload=Hello") + .doesNotContain("rawMessage=").doesNotContain("headers="); + }); + } + + @Test + void lazyResolutionMessageToStringWithResolvedPayloadAndHeaders() throws JMSException { + MessagingMessageListenerAdapter adapter = getSimpleInstance("echo", Message.class); + MessagingMessageConverter messagingMessageConverter = adapter.getMessagingMessageConverter(); + assertThat(messagingMessageConverter).isNotNull(); + TextMessage message = new StubTextMessage("Hello"); + message.setJMSPriority(7); + assertThat(messagingMessageConverter.fromMessage(message)).isInstanceOfSatisfying(Message.class, msg -> { + msg.getPayload(); + msg.getHeaders(); // force resolution + assertThat(msg.toString()).contains("payload=Hello").contains("headers=").contains("jms_priority=7") + .doesNotContain("rawMessage="); + }); + } + protected MessagingMessageListenerAdapter getSimpleInstance(String methodName, Class... parameterTypes) { Method m = ReflectionUtils.findMethod(SampleBean.class, methodName, parameterTypes); diff --git a/spring-messaging/src/main/java/org/springframework/messaging/converter/JsonbMessageConverter.java b/spring-messaging/src/main/java/org/springframework/messaging/converter/JsonbMessageConverter.java index 3e282e912dab..de682f115d2a 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/converter/JsonbMessageConverter.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/converter/JsonbMessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2022 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-messaging/src/main/java/org/springframework/messaging/handler/HandlerMethod.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/HandlerMethod.java index 7a834c5d2f07..f646afba24d8 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/HandlerMethod.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/HandlerMethod.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -212,7 +212,7 @@ public Class getBeanType() { /** * If the bean method is a bridge method, this method returns the bridged - * (user-defined) method. Otherwise it returns the same method as {@link #getMethod()}. + * (user-defined) method. Otherwise, it returns the same method as {@link #getMethod()}. */ protected Method getBridgedMethod() { return this.bridgedMethod; @@ -298,8 +298,8 @@ public HandlerMethod createWithResolvedBean() { * Return a short representation of this handler method for log message purposes. */ public String getShortLogMessage() { - int args = this.method.getParameterCount(); - return getBeanType().getSimpleName() + "#" + this.method.getName() + "[" + args + " args]"; + return getBeanType().getSimpleName() + "#" + this.method.getName() + + "[" + this.method.getParameterCount() + " args]"; } @@ -365,13 +365,11 @@ protected void assertTargetBean(Method method, Object targetBean, Object[] args) } protected String formatInvokeError(String text, Object[] args) { - String formattedArgs = IntStream.range(0, args.length) .mapToObj(i -> (args[i] != null ? "[" + i + "] [type=" + args[i].getClass().getName() + "] [value=" + args[i] + "]" : "[" + i + "] [null]")) .collect(Collectors.joining(",\n", " ", " ")); - return text + "\n" + "Endpoint [" + getBeanType().getName() + "]\n" + "Method [" + getBridgedMethod().toGenericString() + "] " + diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/reactive/PayloadMethodArgumentResolver.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/reactive/PayloadMethodArgumentResolver.java index 3e3c930f05e5..4912e672d873 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/reactive/PayloadMethodArgumentResolver.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/reactive/PayloadMethodArgumentResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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. @@ -34,7 +34,6 @@ import org.springframework.core.ReactiveAdapter; import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.core.ResolvableType; -import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.codec.Decoder; import org.springframework.core.codec.DecodingException; import org.springframework.core.io.buffer.DataBuffer; @@ -55,17 +54,17 @@ import org.springframework.validation.BeanPropertyBindingResult; import org.springframework.validation.SmartValidator; import org.springframework.validation.Validator; -import org.springframework.validation.annotation.Validated; +import org.springframework.validation.annotation.ValidationAnnotationUtils; /** * A resolver to extract and decode the payload of a message using a - * {@link Decoder}, where the payload is expected to be a {@link Publisher} of - * {@link DataBuffer DataBuffer}. + * {@link Decoder}, where the payload is expected to be a {@link Publisher} + * of {@link DataBuffer DataBuffer}. * *

    Validation is applied if the method argument is annotated with - * {@code @javax.validation.Valid} or - * {@link org.springframework.validation.annotation.Validated}. Validation - * failure results in an {@link MethodArgumentNotValidException}. + * {@link org.springframework.validation.annotation.Validated} or + * {@code @javax.validation.Valid}. Validation failure results in an + * {@link MethodArgumentNotValidException}. * *

    This resolver should be ordered last if {@link #useDefaultResolution} is * set to {@code true} since in that case it supports all types and does not @@ -287,10 +286,8 @@ private Consumer getValidator(Message message, MethodParameter parame return null; } for (Annotation ann : parameter.getParameterAnnotations()) { - Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class); - if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) { - Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann)); - Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints}); + Object[] validationHints = ValidationAnnotationUtils.determineValidationHints(ann); + if (validationHints != null) { String name = Conventions.getVariableNameForParameter(parameter); return target -> { BeanPropertyBindingResult bindingResult = new BeanPropertyBindingResult(target, name); diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/PayloadMethodArgumentResolver.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/PayloadMethodArgumentResolver.java index 2ced8c86cd45..3ef7b86c189d 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/PayloadMethodArgumentResolver.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/PayloadMethodArgumentResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 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,7 +19,6 @@ import java.lang.annotation.Annotation; import org.springframework.core.MethodParameter; -import org.springframework.core.annotation.AnnotationUtils; import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.converter.MessageConversionException; @@ -36,12 +35,16 @@ import org.springframework.validation.ObjectError; import org.springframework.validation.SmartValidator; import org.springframework.validation.Validator; -import org.springframework.validation.annotation.Validated; +import org.springframework.validation.annotation.ValidationAnnotationUtils; /** * A resolver to extract and convert the payload of a message using a - * {@link MessageConverter}. It also validates the payload using a - * {@link Validator} if the argument is annotated with a Validation annotation. + * {@link MessageConverter}. + * + *

    Validation is applied if the method argument is annotated with + * {@link org.springframework.validation.annotation.Validated} or + * {@code @javax.validation.Valid}. Validation failure results in an + * {@link MethodArgumentNotValidException}. * *

    This {@link HandlerMethodArgumentResolver} should be ordered last as it * supports all types and does not require the {@link Payload} annotation. @@ -190,9 +193,6 @@ protected Class resolveTargetClass(MethodParameter parameter, Message mess /** * Validate the payload if applicable. - *

    The default implementation checks for {@code @javax.validation.Valid}, - * Spring's {@link Validated}, - * and custom annotations whose name starts with "Valid". * @param message the currently processed message * @param parameter the method parameter * @param target the target payload object @@ -203,10 +203,8 @@ protected void validate(Message message, MethodParameter parameter, Object ta return; } for (Annotation ann : parameter.getParameterAnnotations()) { - Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class); - if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) { - Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann)); - Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints}); + Object[] validationHints = ValidationAnnotationUtils.determineValidationHints(ann); + if (validationHints != null) { BeanPropertyBindingResult bindingResult = new BeanPropertyBindingResult(target, getParameterName(parameter)); if (!ObjectUtils.isEmpty(validationHints) && this.validator instanceof SmartValidator) { diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/reactive/ChannelSendOperator.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/reactive/ChannelSendOperator.java index 108d14929906..ddcf7e709c2f 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/reactive/ChannelSendOperator.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/reactive/ChannelSendOperator.java @@ -281,6 +281,10 @@ public void request(long n) { return; } synchronized (this) { + if (this.state == State.READY_TO_WRITE) { + s.request(n); + return; + } if (this.writeSubscriber != null) { if (this.state == State.EMITTING_CACHED_SIGNALS) { this.demandBeforeReadyToWrite = n; diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/broker/AbstractBrokerMessageHandler.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/broker/AbstractBrokerMessageHandler.java index 58f83e9ef427..05e8d6a0b85e 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/broker/AbstractBrokerMessageHandler.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/broker/AbstractBrokerMessageHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2022 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-messaging/src/main/java/org/springframework/messaging/support/MessageBuilder.java b/spring-messaging/src/main/java/org/springframework/messaging/support/MessageBuilder.java index 4e58b578db90..6b5d91fe2eaf 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/support/MessageBuilder.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/support/MessageBuilder.java @@ -200,7 +200,7 @@ public static MessageBuilder withPayload(T payload) { * @since 4.1 */ @SuppressWarnings("unchecked") - public static Message createMessage(@Nullable T payload, MessageHeaders messageHeaders) { + public static Message createMessage(T payload, MessageHeaders messageHeaders) { Assert.notNull(payload, "Payload must not be null"); Assert.notNull(messageHeaders, "MessageHeaders must not be null"); if (payload instanceof Throwable) { diff --git a/spring-messaging/src/main/java/org/springframework/messaging/support/MessageHeaderAccessor.java b/spring-messaging/src/main/java/org/springframework/messaging/support/MessageHeaderAccessor.java index afbdd8d28888..bfd087e5a593 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/support/MessageHeaderAccessor.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/support/MessageHeaderAccessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2022 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-messaging/src/test/java/org/springframework/messaging/handler/annotation/support/PayloadMethodArgumentResolverTests.java b/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/support/PayloadMethodArgumentResolverTests.java index 2e71ce8815e5..1535df80cbab 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/support/PayloadMethodArgumentResolverTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/support/PayloadMethodArgumentResolverTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2022 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,7 +26,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.springframework.core.LocalVariableTableParameterNameDiscoverer; +import org.springframework.core.DefaultParameterNameDiscoverer; import org.springframework.core.MethodParameter; import org.springframework.core.annotation.SynthesizingMethodParameter; import org.springframework.messaging.Message; @@ -81,7 +81,7 @@ public void setup() throws Exception { this.paramAnnotatedRequired = new SynthesizingMethodParameter(payloadMethod, 2); this.paramWithSpelExpression = new SynthesizingMethodParameter(payloadMethod, 3); this.paramValidated = new SynthesizingMethodParameter(payloadMethod, 4); - this.paramValidated.initParameterNameDiscovery(new LocalVariableTableParameterNameDiscoverer()); + this.paramValidated.initParameterNameDiscovery(new DefaultParameterNameDiscoverer()); this.paramValidatedNotAnnotated = new SynthesizingMethodParameter(payloadMethod, 5); this.paramNotAnnotated = new SynthesizingMethodParameter(payloadMethod, 6); } diff --git a/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/ResolvableMethod.java b/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/ResolvableMethod.java index 1abfbcfd8a94..b38e1ca4f47a 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/ResolvableMethod.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/ResolvableMethod.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 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.messaging.handler.invocation; import java.lang.annotation.Annotation; +import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Parameter; +import java.lang.reflect.Proxy; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -28,18 +30,16 @@ import java.util.function.Predicate; import java.util.function.Supplier; -import org.aopalliance.intercept.MethodInterceptor; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.springframework.aop.framework.ProxyFactory; -import org.springframework.aop.target.EmptyTargetSource; import org.springframework.cglib.core.SpringNamingPolicy; import org.springframework.cglib.proxy.Callback; import org.springframework.cglib.proxy.Enhancer; import org.springframework.cglib.proxy.Factory; +import org.springframework.cglib.proxy.MethodInterceptor; import org.springframework.cglib.proxy.MethodProxy; -import org.springframework.core.LocalVariableTableParameterNameDiscoverer; +import org.springframework.core.DefaultParameterNameDiscoverer; import org.springframework.core.MethodIntrospector; import org.springframework.core.MethodParameter; import org.springframework.core.ParameterNameDiscoverer; @@ -123,6 +123,7 @@ * * * @author Rossen Stoyanchev + * @author Juergen Hoeller * @since 5.2 */ public class ResolvableMethod { @@ -131,7 +132,7 @@ public class ResolvableMethod { private static final SpringObjenesis objenesis = new SpringObjenesis(); - private static final ParameterNameDiscoverer nameDiscoverer = new LocalVariableTableParameterNameDiscoverer(); + private static final ParameterNameDiscoverer nameDiscoverer = new DefaultParameterNameDiscoverer(); // Matches ValueConstants.DEFAULT_NONE (spring-web and spring-messaging) private static final String DEFAULT_VALUE_NONE = "\n\t\t\n\t\t\n\uE000\uE001\uE002\n\t\t\t\t\n"; @@ -189,7 +190,6 @@ public MethodParameter arg(ResolvableType type) { /** * Filter on method arguments with annotation. - * See {@link org.springframework.web.method.MvcAnnotationPredicates}. */ @SafeVarargs public final ArgResolver annot(Predicate... filter) { @@ -302,7 +302,6 @@ public Builder argTypes(Class... argTypes) { /** * Filter on annotated methods. - * See {@link org.springframework.web.method.MvcAnnotationPredicates}. */ @SafeVarargs public final Builder annot(Predicate... filters) { @@ -313,7 +312,6 @@ public final Builder annot(Predicate... filters) { /** * Filter on methods annotated with the given annotation type. * @see #annot(Predicate[]) - * See {@link org.springframework.web.method.MvcAnnotationPredicates}. */ @SafeVarargs public final Builder annotPresent(Class... annotationTypes) { @@ -530,7 +528,6 @@ private ArgResolver(Predicate... filter) { /** * Filter on method arguments with annotations. - * See {@link org.springframework.web.method.MvcAnnotationPredicates}. */ @SafeVarargs public final ArgResolver annot(Predicate... filters) { @@ -542,7 +539,6 @@ public final ArgResolver annot(Predicate... filters) { * Filter on method arguments that have the given annotations. * @param annotationTypes the annotation types * @see #annot(Predicate[]) - * See {@link org.springframework.web.method.MvcAnnotationPredicates}. */ @SafeVarargs public final ArgResolver annotPresent(Class... annotationTypes) { @@ -615,19 +611,14 @@ private List applyFilters() { } - private static class MethodInvocationInterceptor - implements org.springframework.cglib.proxy.MethodInterceptor, MethodInterceptor { + private static class MethodInvocationInterceptor implements MethodInterceptor, InvocationHandler { + @Nullable private Method invokedMethod; - - Method getInvokedMethod() { - return this.invokedMethod; - } - @Override @Nullable - public Object intercept(Object object, Method method, Object[] args, MethodProxy proxy) { + public Object intercept(Object object, Method method, @Nullable Object[] args, @Nullable MethodProxy proxy) { if (ReflectionUtils.isObjectMethod(method)) { return ReflectionUtils.invokeMethod(method, object, args); } @@ -639,20 +630,24 @@ public Object intercept(Object object, Method method, Object[] args, MethodProxy @Override @Nullable - public Object invoke(org.aopalliance.intercept.MethodInvocation inv) throws Throwable { - return intercept(inv.getThis(), inv.getMethod(), inv.getArguments(), null); + public Object invoke(Object proxy, Method method, @Nullable Object[] args) { + return intercept(proxy, method, args, null); + } + + @Nullable + Method getInvokedMethod() { + return this.invokedMethod; } } + @SuppressWarnings("unchecked") private static T initProxy(Class type, MethodInvocationInterceptor interceptor) { Assert.notNull(type, "'type' must not be null"); if (type.isInterface()) { - ProxyFactory factory = new ProxyFactory(EmptyTargetSource.INSTANCE); - factory.addInterface(type); - factory.addInterface(Supplier.class); - factory.addAdvice(interceptor); - return (T) factory.getProxy(); + return (T) Proxy.newProxyInstance(type.getClassLoader(), + new Class[] {type, Supplier.class}, + interceptor); } else { diff --git a/spring-orm/src/main/java/org/springframework/orm/hibernate5/HibernateExceptionTranslator.java b/spring-orm/src/main/java/org/springframework/orm/hibernate5/HibernateExceptionTranslator.java index bfd086eb626e..3e336b40b328 100644 --- a/spring-orm/src/main/java/org/springframework/orm/hibernate5/HibernateExceptionTranslator.java +++ b/spring-orm/src/main/java/org/springframework/orm/hibernate5/HibernateExceptionTranslator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2023 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. @@ -96,7 +96,7 @@ protected DataAccessException convertHibernateAccessException(HibernateException DataAccessException dae = this.jdbcExceptionTranslator.translate( "Hibernate operation: " + jdbcEx.getMessage(), jdbcEx.getSQL(), jdbcEx.getSQLException()); if (dae != null) { - throw dae; + return dae; } } return SessionFactoryUtils.convertHibernateAccessException(ex); diff --git a/spring-orm/src/main/java/org/springframework/orm/hibernate5/HibernateTransactionManager.java b/spring-orm/src/main/java/org/springframework/orm/hibernate5/HibernateTransactionManager.java index 65990f3e8333..80849930c296 100644 --- a/spring-orm/src/main/java/org/springframework/orm/hibernate5/HibernateTransactionManager.java +++ b/spring-orm/src/main/java/org/springframework/orm/hibernate5/HibernateTransactionManager.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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. @@ -100,13 +100,11 @@ * @author Juergen Hoeller * @since 4.2 * @see #setSessionFactory - * @see #setDataSource * @see SessionFactory#getCurrentSession() - * @see DataSourceUtils#getConnection - * @see DataSourceUtils#releaseConnection * @see org.springframework.jdbc.core.JdbcTemplate - * @see org.springframework.jdbc.datasource.DataSourceTransactionManager - * @see org.springframework.transaction.jta.JtaTransactionManager + * @see org.springframework.jdbc.support.JdbcTransactionManager + * @see org.springframework.orm.jpa.JpaTransactionManager + * @see org.springframework.orm.jpa.vendor.HibernateJpaDialect */ @SuppressWarnings("serial") public class HibernateTransactionManager extends AbstractPlatformTransactionManager @@ -271,7 +269,11 @@ public void setPrepareConnection(boolean prepareConnection) { * @see Connection#setHoldability * @see ResultSet#HOLD_CURSORS_OVER_COMMIT * @see #disconnectOnCompletion(Session) + * @deprecated as of 5.3.29 since Hibernate 5.x aggressively closes ResultSets on commit, + * making it impossible to rely on ResultSet holdability. Also, Spring does not provide + * an equivalent setting on {@link org.springframework.orm.jpa.JpaTransactionManager}. */ + @Deprecated public void setAllowResultAccessAfterCompletion(boolean allowResultAccessAfterCompletion) { this.allowResultAccessAfterCompletion = allowResultAccessAfterCompletion; } @@ -775,8 +777,6 @@ protected void disconnectOnCompletion(Session session) { /** * Convert the given HibernateException to an appropriate exception * from the {@code org.springframework.dao} hierarchy. - *

    Will automatically apply a specified SQLExceptionTranslator to a - * Hibernate JDBCException, else rely on Hibernate's default translation. * @param ex the HibernateException that occurred * @return a corresponding DataAccessException * @see SessionFactoryUtils#convertHibernateAccessException diff --git a/spring-orm/src/main/java/org/springframework/orm/hibernate5/SpringBeanContainer.java b/spring-orm/src/main/java/org/springframework/orm/hibernate5/SpringBeanContainer.java index b96d9df89b6b..88422aeba409 100644 --- a/spring-orm/src/main/java/org/springframework/orm/hibernate5/SpringBeanContainer.java +++ b/spring-orm/src/main/java/org/springframework/orm/hibernate5/SpringBeanContainer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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. @@ -180,14 +180,23 @@ private SpringContainedBean createBean( try { if (lifecycleOptions.useJpaCompliantCreation()) { - Object bean = this.beanFactory.autowire(beanType, AutowireCapableBeanFactory.AUTOWIRE_CONSTRUCTOR, false); - this.beanFactory.autowireBeanProperties(bean, AutowireCapableBeanFactory.AUTOWIRE_NO, false); - this.beanFactory.applyBeanPropertyValues(bean, name); - bean = this.beanFactory.initializeBean(bean, name); - return new SpringContainedBean<>(bean, beanInstance -> this.beanFactory.destroyBean(name, beanInstance)); + if (this.beanFactory.containsBean(name)) { + Object bean = this.beanFactory.autowire(beanType, AutowireCapableBeanFactory.AUTOWIRE_CONSTRUCTOR, false); + this.beanFactory.autowireBeanProperties(bean, AutowireCapableBeanFactory.AUTOWIRE_NO, false); + this.beanFactory.applyBeanPropertyValues(bean, name); + bean = this.beanFactory.initializeBean(bean, name); + return new SpringContainedBean<>(bean, beanInstance -> this.beanFactory.destroyBean(name, beanInstance)); + } + else { + return new SpringContainedBean<>( + this.beanFactory.createBean(beanType, AutowireCapableBeanFactory.AUTOWIRE_CONSTRUCTOR, false), + this.beanFactory::destroyBean); + } } else { - return new SpringContainedBean<>(this.beanFactory.getBean(name, beanType)); + return (this.beanFactory.containsBean(name) ? + new SpringContainedBean<>(this.beanFactory.getBean(name, beanType)) : + new SpringContainedBean<>(this.beanFactory.getBean(beanType))); } } catch (BeansException ex) { diff --git a/spring-orm/src/main/java/org/springframework/orm/hibernate5/SpringFlushSynchronization.java b/spring-orm/src/main/java/org/springframework/orm/hibernate5/SpringFlushSynchronization.java index 94579851887d..cd64a15e80ed 100644 --- a/spring-orm/src/main/java/org/springframework/orm/hibernate5/SpringFlushSynchronization.java +++ b/spring-orm/src/main/java/org/springframework/orm/hibernate5/SpringFlushSynchronization.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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. @@ -43,7 +43,6 @@ public void flush() { SessionFactoryUtils.flush(this.session, false); } - @Override public boolean equals(@Nullable Object other) { return (this == other || (other instanceof SpringFlushSynchronization && diff --git a/spring-orm/src/main/java/org/springframework/orm/hibernate5/support/HibernateDaoSupport.java b/spring-orm/src/main/java/org/springframework/orm/hibernate5/support/HibernateDaoSupport.java index a65d52555615..965334421dfd 100644 --- a/spring-orm/src/main/java/org/springframework/orm/hibernate5/support/HibernateDaoSupport.java +++ b/spring-orm/src/main/java/org/springframework/orm/hibernate5/support/HibernateDaoSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2022 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-orm/src/main/java/org/springframework/orm/jpa/JpaTransactionManager.java b/spring-orm/src/main/java/org/springframework/orm/jpa/JpaTransactionManager.java index 118048d83dcc..d6555d06ec32 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/JpaTransactionManager.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/JpaTransactionManager.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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. @@ -109,7 +109,7 @@ * @see org.springframework.jdbc.datasource.DataSourceUtils#getConnection * @see org.springframework.jdbc.datasource.DataSourceUtils#releaseConnection * @see org.springframework.jdbc.core.JdbcTemplate - * @see org.springframework.jdbc.datasource.DataSourceTransactionManager + * @see org.springframework.jdbc.support.JdbcTransactionManager * @see org.springframework.transaction.jta.JtaTransactionManager */ @SuppressWarnings("serial") diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/LocalContainerEntityManagerFactoryBean.java b/spring-orm/src/main/java/org/springframework/orm/jpa/LocalContainerEntityManagerFactoryBean.java index 6fc17b5c5fdc..aded2b9c9f0b 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/LocalContainerEntityManagerFactoryBean.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/LocalContainerEntityManagerFactoryBean.java @@ -253,8 +253,9 @@ public void setValidationMode(ValidationMode validationMode) { * @see javax.persistence.spi.PersistenceUnitInfo#getNonJtaDataSource() * @see #setPersistenceUnitManager */ - public void setDataSource(DataSource dataSource) { - this.internalPersistenceUnitManager.setDataSourceLookup(new SingleDataSourceLookup(dataSource)); + public void setDataSource(@Nullable DataSource dataSource) { + this.internalPersistenceUnitManager.setDataSourceLookup( + dataSource != null ? new SingleDataSourceLookup(dataSource) : null); this.internalPersistenceUnitManager.setDefaultDataSource(dataSource); } @@ -270,8 +271,9 @@ public void setDataSource(DataSource dataSource) { * @see javax.persistence.spi.PersistenceUnitInfo#getJtaDataSource() * @see #setPersistenceUnitManager */ - public void setJtaDataSource(DataSource jtaDataSource) { - this.internalPersistenceUnitManager.setDataSourceLookup(new SingleDataSourceLookup(jtaDataSource)); + public void setJtaDataSource(@Nullable DataSource jtaDataSource) { + this.internalPersistenceUnitManager.setDataSourceLookup( + jtaDataSource != null ? new SingleDataSourceLookup(jtaDataSource) : null); this.internalPersistenceUnitManager.setDefaultJtaDataSource(jtaDataSource); } @@ -416,6 +418,7 @@ public String getPersistenceUnitName() { } @Override + @Nullable public DataSource getDataSource() { if (this.persistenceUnitInfo != null) { return (this.persistenceUnitInfo.getJtaDataSource() != null ? diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/LocalEntityManagerFactoryBean.java b/spring-orm/src/main/java/org/springframework/orm/jpa/LocalEntityManagerFactoryBean.java index dcc9961f55c0..398fa693380b 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/LocalEntityManagerFactoryBean.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/LocalEntityManagerFactoryBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-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. @@ -28,28 +28,18 @@ * shared JPA EntityManagerFactory in a Spring application context; the * EntityManagerFactory can then be passed to JPA-based DAOs via * dependency injection. Note that switching to a JNDI lookup or to a - * {@link LocalContainerEntityManagerFactoryBean} - * definition is just a matter of configuration! + * {@link LocalContainerEntityManagerFactoryBean} definition based on the + * JPA container contract is just a matter of configuration! * *

    Configuration settings are usually read from a {@code META-INF/persistence.xml} * config file, residing in the class path, according to the JPA standalone bootstrap - * contract. Additionally, most JPA providers will require a special VM agent - * (specified on JVM startup) that allows them to instrument application classes. - * See the Java Persistence API specification and your provider documentation - * for setup details. - * - *

    This EntityManagerFactory bootstrap is appropriate for standalone applications - * which solely use JPA for data access. If you want to set up your persistence - * provider for an external DataSource and/or for global transactions which span - * multiple resources, you will need to either deploy it into a full Java EE - * application server and access the deployed EntityManagerFactory via JNDI, - * or use Spring's {@link LocalContainerEntityManagerFactoryBean} with appropriate - * configuration for local setup according to JPA's container contract. + * contract. See the Java Persistence API specification and your persistence provider + * documentation for setup details. Additionally, JPA properties can also be added + * on this FactoryBean via {@link #setJpaProperties}/{@link #setJpaPropertyMap}. * *

    Note: This FactoryBean has limited configuration power in terms of - * what configuration it is able to pass to the JPA provider. If you need more - * flexible configuration, for example passing a Spring-managed JDBC DataSource - * to the JPA provider, consider using Spring's more powerful + * the configuration that it is able to pass to the JPA provider. If you need + * more flexible configuration options, consider using Spring's more powerful * {@link LocalContainerEntityManagerFactoryBean} instead. * *

    NOTE: Spring's JPA support requires JPA 2.1 or higher, as of Spring 5.0. diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/SharedEntityManagerCreator.java b/spring-orm/src/main/java/org/springframework/orm/jpa/SharedEntityManagerCreator.java index 5b109cf89864..e44b0369dae5 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/SharedEntityManagerCreator.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/SharedEntityManagerCreator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -62,6 +62,7 @@ * @author Rod Johnson * @author Oliver Gierke * @author Mark Paluch + * @author Sam Brannen * @since 2.0 * @see javax.persistence.PersistenceContext * @see javax.persistence.PersistenceContextType#TRANSACTION @@ -74,9 +75,9 @@ public abstract class SharedEntityManagerCreator { private static final Map, Class[]> cachedQueryInterfaces = new ConcurrentReferenceHashMap<>(4); - private static final Set transactionRequiringMethods = new HashSet<>(8); + private static final Set transactionRequiringMethods = new HashSet<>(6); - private static final Set queryTerminatingMethods = new HashSet<>(8); + private static final Set queryTerminatingMethods = new HashSet<>(10); static { transactionRequiringMethods.add("joinTransaction"); @@ -86,12 +87,16 @@ public abstract class SharedEntityManagerCreator { transactionRequiringMethods.add("remove"); transactionRequiringMethods.add("refresh"); - queryTerminatingMethods.add("execute"); // JPA 2.1 StoredProcedureQuery - queryTerminatingMethods.add("executeUpdate"); - queryTerminatingMethods.add("getSingleResult"); - queryTerminatingMethods.add("getResultStream"); - queryTerminatingMethods.add("getResultList"); - queryTerminatingMethods.add("list"); // Hibernate Query.list() method + queryTerminatingMethods.add("execute"); // javax.persistence.StoredProcedureQuery.execute() + queryTerminatingMethods.add("executeUpdate"); // javax.persistence.Query.executeUpdate() + queryTerminatingMethods.add("getSingleResult"); // javax.persistence.Query.getSingleResult() + queryTerminatingMethods.add("getResultStream"); // javax.persistence.Query.getResultStream() + queryTerminatingMethods.add("getResultList"); // javax.persistence.Query.getResultList() + queryTerminatingMethods.add("list"); // org.hibernate.query.Query.list() + queryTerminatingMethods.add("scroll"); // org.hibernate.query.Query.scroll() + queryTerminatingMethods.add("stream"); // org.hibernate.query.Query.stream() + queryTerminatingMethods.add("uniqueResult"); // org.hibernate.query.Query.uniqueResult() + queryTerminatingMethods.add("uniqueResultOptional"); // org.hibernate.query.Query.uniqueResultOptional() } @@ -385,7 +390,9 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl else if (targetClass.isInstance(proxy)) { return proxy; } - break; + else { + return this.target.unwrap(targetClass); + } case "getOutputParameterValue": if (this.entityManager == null) { Object key = args[0]; @@ -432,7 +439,7 @@ else if (targetClass.isInstance(proxy)) { entry.setValue(storedProc.getOutputParameterValue(key.toString())); } } - catch (IllegalArgumentException ex) { + catch (RuntimeException ex) { entry.setValue(ex); } } diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/PersistenceUnitReader.java b/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/PersistenceUnitReader.java index 092285cd25fd..1c1f0aa959cd 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/PersistenceUnitReader.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/PersistenceUnitReader.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -154,7 +154,7 @@ public SpringPersistenceUnitInfo[] readPersistenceUnitInfos(String[] persistence /** * Validate the given stream and return a valid DOM document for parsing. */ - protected Document buildDocument(ErrorHandler handler, InputStream stream) + Document buildDocument(ErrorHandler handler, InputStream stream) throws ParserConfigurationException, SAXException, IOException { DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); @@ -168,9 +168,7 @@ protected Document buildDocument(ErrorHandler handler, InputStream stream) /** * Parse the validated document and add entries to the given unit info list. */ - protected List parseDocument( - Resource resource, Document document, List infos) throws IOException { - + void parseDocument(Resource resource, Document document, List infos) throws IOException { Element persistence = document.getDocumentElement(); String version = persistence.getAttribute(PERSISTENCE_VERSION); URL rootUrl = determinePersistenceUnitRootUrl(resource); @@ -179,14 +177,12 @@ protected List parseDocument( for (Element unit : units) { infos.add(parsePersistenceUnitInfo(unit, version, rootUrl)); } - - return infos; } /** * Parse the unit info DOM element. */ - protected SpringPersistenceUnitInfo parsePersistenceUnitInfo( + SpringPersistenceUnitInfo parsePersistenceUnitInfo( Element persistenceUnit, String version, @Nullable URL rootUrl) throws IOException { SpringPersistenceUnitInfo unitInfo = new SpringPersistenceUnitInfo(); @@ -253,7 +249,7 @@ protected SpringPersistenceUnitInfo parsePersistenceUnitInfo( /** * Parse the {@code property} XML elements. */ - protected void parseProperties(Element persistenceUnit, SpringPersistenceUnitInfo unitInfo) { + void parseProperties(Element persistenceUnit, SpringPersistenceUnitInfo unitInfo) { Element propRoot = DomUtils.getChildElementByTagName(persistenceUnit, PROPERTIES); if (propRoot == null) { return; @@ -269,7 +265,7 @@ protected void parseProperties(Element persistenceUnit, SpringPersistenceUnitInf /** * Parse the {@code class} XML elements. */ - protected void parseManagedClasses(Element persistenceUnit, SpringPersistenceUnitInfo unitInfo) { + void parseManagedClasses(Element persistenceUnit, SpringPersistenceUnitInfo unitInfo) { List classes = DomUtils.getChildElementsByTagName(persistenceUnit, MANAGED_CLASS_NAME); for (Element element : classes) { String value = DomUtils.getTextValue(element).trim(); @@ -282,7 +278,7 @@ protected void parseManagedClasses(Element persistenceUnit, SpringPersistenceUni /** * Parse the {@code mapping-file} XML elements. */ - protected void parseMappingFiles(Element persistenceUnit, SpringPersistenceUnitInfo unitInfo) { + void parseMappingFiles(Element persistenceUnit, SpringPersistenceUnitInfo unitInfo) { List files = DomUtils.getChildElementsByTagName(persistenceUnit, MAPPING_FILE_NAME); for (Element element : files) { String value = DomUtils.getTextValue(element).trim(); @@ -295,7 +291,7 @@ protected void parseMappingFiles(Element persistenceUnit, SpringPersistenceUnitI /** * Parse the {@code jar-file} XML elements. */ - protected void parseJarFiles(Element persistenceUnit, SpringPersistenceUnitInfo unitInfo) throws IOException { + void parseJarFiles(Element persistenceUnit, SpringPersistenceUnitInfo unitInfo) throws IOException { List jars = DomUtils.getChildElementsByTagName(persistenceUnit, JAR_FILE_URL); for (Element element : jars) { String value = DomUtils.getTextValue(element).trim(); diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/EclipseLinkJpaDialect.java b/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/EclipseLinkJpaDialect.java index 30db932f1677..a02e4a8fb469 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/EclipseLinkJpaDialect.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/EclipseLinkJpaDialect.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2023 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,6 +22,7 @@ import javax.persistence.EntityManager; import javax.persistence.PersistenceException; +import org.eclipse.persistence.sessions.DatabaseLogin; import org.eclipse.persistence.sessions.UnitOfWork; import org.springframework.jdbc.datasource.ConnectionHandle; @@ -35,14 +36,20 @@ * Persistence Services (EclipseLink). Developed and tested against EclipseLink 2.7; * backwards-compatible with EclipseLink 2.5 and 2.6 at runtime. * - *

    By default, this class acquires an early EclipseLink transaction with an early - * JDBC Connection for non-read-only transactions. This allows for mixing JDBC and - * JPA/EclipseLink operations in the same transaction, with cross visibility of + *

    By default, this dialect acquires an early EclipseLink transaction with an + * early JDBC Connection for non-read-only transactions. This allows for mixing + * JDBC and JPA operations in the same transaction, with cross visibility of * their impact. If this is not needed, set the "lazyDatabaseTransaction" flag to * {@code true} or consistently declare all affected transactions as read-only. * As of Spring 4.1.2, this will reliably avoid early JDBC Connection retrieval * and therefore keep EclipseLink in shared cache mode. * + *

    NOTE: This dialect supports custom isolation levels with limitations. + * Consistent isolation level handling is only guaranteed when all Spring transaction + * definitions specify a concrete isolation level, without any default transactions + * running concurrently. Otherwise, you may see non-default isolation levels exposed + * to default transactions when custom isolation is used in concurrent transactions. + * * @author Juergen Hoeller * @since 2.5.2 * @see #setLazyDatabaseTransaction @@ -78,19 +85,38 @@ public void setLazyDatabaseTransaction(boolean lazyDatabaseTransaction) { public Object beginTransaction(EntityManager entityManager, TransactionDefinition definition) throws PersistenceException, SQLException, TransactionException { - if (definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT) { + int currentIsolationLevel = definition.getIsolationLevel(); + if (currentIsolationLevel != TransactionDefinition.ISOLATION_DEFAULT) { // Pass custom isolation level on to EclipseLink's DatabaseLogin configuration - // (since Spring 4.1.2) + // (since Spring 4.1.2 / revised in 5.3.28) UnitOfWork uow = entityManager.unwrap(UnitOfWork.class); - uow.getLogin().setTransactionIsolation(definition.getIsolationLevel()); + DatabaseLogin databaseLogin = uow.getLogin(); + // Synchronize on shared DatabaseLogin instance for consistent isolation level + // set and reset in case of concurrent transactions with different isolation. + synchronized (databaseLogin) { + int originalIsolationLevel = databaseLogin.getTransactionIsolation(); + // Apply current isolation level value, if necessary. + if (currentIsolationLevel != originalIsolationLevel) { + databaseLogin.setTransactionIsolation(currentIsolationLevel); + } + // Transaction begin including enforced JDBC Connection access + // (picking up current isolation level from DatabaseLogin) + entityManager.getTransaction().begin(); + uow.beginEarlyTransaction(); + entityManager.unwrap(Connection.class); + // Restore original isolation level value, if necessary. + if (currentIsolationLevel != originalIsolationLevel) { + databaseLogin.setTransactionIsolation(originalIsolationLevel); + } + } } - - entityManager.getTransaction().begin(); - - if (!definition.isReadOnly() && !this.lazyDatabaseTransaction) { - // Begin an early transaction to force EclipseLink to get a JDBC Connection - // so that Spring can manage transactions with JDBC as well as EclipseLink. - entityManager.unwrap(UnitOfWork.class).beginEarlyTransaction(); + else { + entityManager.getTransaction().begin(); + if (!definition.isReadOnly() && !this.lazyDatabaseTransaction) { + // Begin an early transaction to force EclipseLink to get a JDBC Connection + // so that Spring can manage transactions with JDBC as well as EclipseLink. + entityManager.unwrap(UnitOfWork.class).beginEarlyTransaction(); + } } return null; diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/HibernateJpaDialect.java b/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/HibernateJpaDialect.java index 72ff21ce8d9f..b5c2c599e1b2 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/HibernateJpaDialect.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/HibernateJpaDialect.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -124,8 +124,8 @@ public void setPrepareConnection(boolean prepareConnection) { * @since 5.1 * @see java.sql.SQLException * @see org.hibernate.JDBCException + * @see org.springframework.jdbc.support.SQLExceptionSubclassTranslator * @see org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator - * @see org.springframework.jdbc.support.SQLStateSQLExceptionTranslator */ public void setJdbcExceptionTranslator(SQLExceptionTranslator jdbcExceptionTranslator) { this.jdbcExceptionTranslator = jdbcExceptionTranslator; @@ -247,7 +247,7 @@ protected DataAccessException convertHibernateAccessException(HibernateException DataAccessException dae = this.jdbcExceptionTranslator.translate( "Hibernate operation: " + jdbcEx.getMessage(), jdbcEx.getSQL(), jdbcEx.getSQLException()); if (dae != null) { - throw dae; + return dae; } } diff --git a/spring-orm/src/test/java/org/springframework/orm/jpa/AbstractContainerEntityManagerFactoryIntegrationTests.java b/spring-orm/src/test/java/org/springframework/orm/jpa/AbstractContainerEntityManagerFactoryIntegrationTests.java index 81c7b0b912a5..c403a9aa680b 100644 --- a/spring-orm/src/test/java/org/springframework/orm/jpa/AbstractContainerEntityManagerFactoryIntegrationTests.java +++ b/spring-orm/src/test/java/org/springframework/orm/jpa/AbstractContainerEntityManagerFactoryIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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,8 +27,10 @@ import org.junit.jupiter.api.Test; import org.springframework.core.testfixture.io.SerializationTestUtils; +import org.springframework.jdbc.datasource.DataSourceUtils; import org.springframework.orm.jpa.domain.DriversLicense; import org.springframework.orm.jpa.domain.Person; +import org.springframework.transaction.TransactionDefinition; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatException; @@ -114,24 +116,34 @@ public void testGetReferenceWhenNoRow() { } @Test - public void testLazyLoading() { + public void testLazyLoading() throws Exception { try { Person tony = new Person(); tony.setFirstName("Tony"); tony.setLastName("Blair"); tony.setDriversLicense(new DriversLicense("8439DK")); sharedEntityManager.persist(tony); + assertThat(DataSourceUtils.getConnection(jdbcTemplate.getDataSource()).getTransactionIsolation()) + .isEqualTo(TransactionDefinition.ISOLATION_READ_COMMITTED); setComplete(); endTransaction(); + transactionDefinition.setIsolationLevel(TransactionDefinition.ISOLATION_SERIALIZABLE); startNewTransaction(); + assertThat(DataSourceUtils.getConnection(jdbcTemplate.getDataSource()).getTransactionIsolation()) + .isEqualTo(TransactionDefinition.ISOLATION_SERIALIZABLE); sharedEntityManager.clear(); Person newTony = entityManagerFactory.createEntityManager().getReference(Person.class, tony.getId()); assertThat(tony).isNotSameAs(newTony); endTransaction(); - assertThat(newTony.getDriversLicense()).isNotNull(); + transactionDefinition.setIsolationLevel(TransactionDefinition.ISOLATION_DEFAULT); + startNewTransaction(); + assertThat(DataSourceUtils.getConnection(jdbcTemplate.getDataSource()).getTransactionIsolation()) + .isEqualTo(TransactionDefinition.ISOLATION_READ_COMMITTED); + endTransaction(); + assertThat(newTony.getDriversLicense()).isNotNull(); newTony.getDriversLicense().getSerialNumber(); } finally { diff --git a/spring-orm/src/test/java/org/springframework/orm/jpa/SharedEntityManagerCreatorTests.java b/spring-orm/src/test/java/org/springframework/orm/jpa/SharedEntityManagerCreatorTests.java index 86df24330b85..653402c94d3e 100644 --- a/spring-orm/src/test/java/org/springframework/orm/jpa/SharedEntityManagerCreatorTests.java +++ b/spring-orm/src/test/java/org/springframework/orm/jpa/SharedEntityManagerCreatorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -38,16 +38,16 @@ import static org.mockito.Mockito.withSettings; /** - * Unit tests for {@link SharedEntityManagerCreator}. + * Tests for {@link SharedEntityManagerCreator}. * * @author Oliver Gierke * @author Juergen Hoeller */ @ExtendWith(MockitoExtension.class) -public class SharedEntityManagerCreatorTests { +class SharedEntityManagerCreatorTests { @Test - public void proxyingWorksIfInfoReturnsNullEntityManagerInterface() { + void proxyingWorksIfInfoReturnsNullEntityManagerInterface() { EntityManagerFactory emf = mock(EntityManagerFactory.class, withSettings().extraInterfaces(EntityManagerFactoryInfo.class)); // EntityManagerFactoryInfo.getEntityManagerInterface returns null @@ -55,7 +55,7 @@ public void proxyingWorksIfInfoReturnsNullEntityManagerInterface() { } @Test - public void transactionRequiredExceptionOnJoinTransaction() { + void transactionRequiredExceptionOnJoinTransaction() { EntityManagerFactory emf = mock(EntityManagerFactory.class); EntityManager em = SharedEntityManagerCreator.createSharedEntityManager(emf); assertThatExceptionOfType(TransactionRequiredException.class).isThrownBy( @@ -63,7 +63,7 @@ public void transactionRequiredExceptionOnJoinTransaction() { } @Test - public void transactionRequiredExceptionOnFlush() { + void transactionRequiredExceptionOnFlush() { EntityManagerFactory emf = mock(EntityManagerFactory.class); EntityManager em = SharedEntityManagerCreator.createSharedEntityManager(emf); assertThatExceptionOfType(TransactionRequiredException.class).isThrownBy( @@ -71,7 +71,7 @@ public void transactionRequiredExceptionOnFlush() { } @Test - public void transactionRequiredExceptionOnPersist() { + void transactionRequiredExceptionOnPersist() { EntityManagerFactory emf = mock(EntityManagerFactory.class); EntityManager em = SharedEntityManagerCreator.createSharedEntityManager(emf); assertThatExceptionOfType(TransactionRequiredException.class).isThrownBy(() -> @@ -79,7 +79,7 @@ public void transactionRequiredExceptionOnPersist() { } @Test - public void transactionRequiredExceptionOnMerge() { + void transactionRequiredExceptionOnMerge() { EntityManagerFactory emf = mock(EntityManagerFactory.class); EntityManager em = SharedEntityManagerCreator.createSharedEntityManager(emf); assertThatExceptionOfType(TransactionRequiredException.class).isThrownBy(() -> @@ -87,7 +87,7 @@ public void transactionRequiredExceptionOnMerge() { } @Test - public void transactionRequiredExceptionOnRemove() { + void transactionRequiredExceptionOnRemove() { EntityManagerFactory emf = mock(EntityManagerFactory.class); EntityManager em = SharedEntityManagerCreator.createSharedEntityManager(emf); assertThatExceptionOfType(TransactionRequiredException.class).isThrownBy(() -> @@ -95,7 +95,7 @@ public void transactionRequiredExceptionOnRemove() { } @Test - public void transactionRequiredExceptionOnRefresh() { + void transactionRequiredExceptionOnRefresh() { EntityManagerFactory emf = mock(EntityManagerFactory.class); EntityManager em = SharedEntityManagerCreator.createSharedEntityManager(emf); assertThatExceptionOfType(TransactionRequiredException.class).isThrownBy(() -> @@ -103,78 +103,98 @@ public void transactionRequiredExceptionOnRefresh() { } @Test - public void deferredQueryWithUpdate() { + void deferredQueryWithUpdate() { EntityManagerFactory emf = mock(EntityManagerFactory.class); EntityManager targetEm = mock(EntityManager.class); - Query query = mock(Query.class); + Query targetQuery = mock(Query.class); given(emf.createEntityManager()).willReturn(targetEm); - given(targetEm.createQuery("x")).willReturn(query); + given(targetEm.createQuery("x")).willReturn(targetQuery); given(targetEm.isOpen()).willReturn(true); + given((Query) targetQuery.unwrap(targetQuery.getClass())).willReturn(targetQuery); EntityManager em = SharedEntityManagerCreator.createSharedEntityManager(emf); - em.createQuery("x").executeUpdate(); + Query query = em.createQuery("x"); + assertThat((Query) query.unwrap(null)).isSameAs(targetQuery); + assertThat((Query) query.unwrap(targetQuery.getClass())).isSameAs(targetQuery); + assertThat(query.unwrap(Query.class)).isSameAs(query); + query.executeUpdate(); - verify(query).executeUpdate(); + verify(targetQuery).executeUpdate(); verify(targetEm).close(); } @Test - public void deferredQueryWithSingleResult() { + void deferredQueryWithSingleResult() { EntityManagerFactory emf = mock(EntityManagerFactory.class); EntityManager targetEm = mock(EntityManager.class); - Query query = mock(Query.class); + Query targetQuery = mock(Query.class); given(emf.createEntityManager()).willReturn(targetEm); - given(targetEm.createQuery("x")).willReturn(query); + given(targetEm.createQuery("x")).willReturn(targetQuery); given(targetEm.isOpen()).willReturn(true); + given((Query) targetQuery.unwrap(targetQuery.getClass())).willReturn(targetQuery); EntityManager em = SharedEntityManagerCreator.createSharedEntityManager(emf); - em.createQuery("x").getSingleResult(); + Query query = em.createQuery("x"); + assertThat((Query) query.unwrap(null)).isSameAs(targetQuery); + assertThat((Query) query.unwrap(targetQuery.getClass())).isSameAs(targetQuery); + assertThat(query.unwrap(Query.class)).isSameAs(query); + query.getSingleResult(); - verify(query).getSingleResult(); + verify(targetQuery).getSingleResult(); verify(targetEm).close(); } @Test - public void deferredQueryWithResultList() { + void deferredQueryWithResultList() { EntityManagerFactory emf = mock(EntityManagerFactory.class); EntityManager targetEm = mock(EntityManager.class); - Query query = mock(Query.class); + Query targetQuery = mock(Query.class); given(emf.createEntityManager()).willReturn(targetEm); - given(targetEm.createQuery("x")).willReturn(query); + given(targetEm.createQuery("x")).willReturn(targetQuery); given(targetEm.isOpen()).willReturn(true); + given((Query) targetQuery.unwrap(targetQuery.getClass())).willReturn(targetQuery); EntityManager em = SharedEntityManagerCreator.createSharedEntityManager(emf); - em.createQuery("x").getResultList(); + Query query = em.createQuery("x"); + assertThat((Query) query.unwrap(null)).isSameAs(targetQuery); + assertThat((Query) query.unwrap(targetQuery.getClass())).isSameAs(targetQuery); + assertThat(query.unwrap(Query.class)).isSameAs(query); + query.getResultList(); - verify(query).getResultList(); + verify(targetQuery).getResultList(); verify(targetEm).close(); } @Test - public void deferredQueryWithResultStream() { + void deferredQueryWithResultStream() { EntityManagerFactory emf = mock(EntityManagerFactory.class); EntityManager targetEm = mock(EntityManager.class); - Query query = mock(Query.class); + Query targetQuery = mock(Query.class); given(emf.createEntityManager()).willReturn(targetEm); - given(targetEm.createQuery("x")).willReturn(query); + given(targetEm.createQuery("x")).willReturn(targetQuery); given(targetEm.isOpen()).willReturn(true); + given((Query) targetQuery.unwrap(targetQuery.getClass())).willReturn(targetQuery); EntityManager em = SharedEntityManagerCreator.createSharedEntityManager(emf); - em.createQuery("x").getResultStream(); + Query query = em.createQuery("x"); + assertThat((Query) query.unwrap(null)).isSameAs(targetQuery); + assertThat((Query) query.unwrap(targetQuery.getClass())).isSameAs(targetQuery); + assertThat(query.unwrap(Query.class)).isSameAs(query); + query.getResultStream(); - verify(query).getResultStream(); + verify(targetQuery).getResultStream(); verify(targetEm).close(); } @Test - public void deferredStoredProcedureQueryWithIndexedParameters() { + void deferredStoredProcedureQueryWithIndexedParameters() { EntityManagerFactory emf = mock(EntityManagerFactory.class); EntityManager targetEm = mock(EntityManager.class); - StoredProcedureQuery query = mock(StoredProcedureQuery.class); + StoredProcedureQuery targetQuery = mock(StoredProcedureQuery.class); given(emf.createEntityManager()).willReturn(targetEm); - given(targetEm.createStoredProcedureQuery("x")).willReturn(query); - willReturn("y").given(query).getOutputParameterValue(0); - willReturn("z").given(query).getOutputParameterValue(2); + given(targetEm.createStoredProcedureQuery("x")).willReturn(targetQuery); + willReturn("y").given(targetQuery).getOutputParameterValue(0); + willReturn("z").given(targetQuery).getOutputParameterValue(2); given(targetEm.isOpen()).willReturn(true); EntityManager em = SharedEntityManagerCreator.createSharedEntityManager(emf); @@ -188,24 +208,24 @@ public void deferredStoredProcedureQueryWithIndexedParameters() { spq.getOutputParameterValue(1)); assertThat(spq.getOutputParameterValue(2)).isEqualTo("z"); - verify(query).registerStoredProcedureParameter(0, String.class, ParameterMode.OUT); - verify(query).registerStoredProcedureParameter(1, Number.class, ParameterMode.IN); - verify(query).registerStoredProcedureParameter(2, Object.class, ParameterMode.INOUT); - verify(query).execute(); + verify(targetQuery).registerStoredProcedureParameter(0, String.class, ParameterMode.OUT); + verify(targetQuery).registerStoredProcedureParameter(1, Number.class, ParameterMode.IN); + verify(targetQuery).registerStoredProcedureParameter(2, Object.class, ParameterMode.INOUT); + verify(targetQuery).execute(); verify(targetEm).close(); - verifyNoMoreInteractions(query); + verifyNoMoreInteractions(targetQuery); verifyNoMoreInteractions(targetEm); } @Test - public void deferredStoredProcedureQueryWithNamedParameters() { + void deferredStoredProcedureQueryWithNamedParameters() { EntityManagerFactory emf = mock(EntityManagerFactory.class); EntityManager targetEm = mock(EntityManager.class); - StoredProcedureQuery query = mock(StoredProcedureQuery.class); + StoredProcedureQuery targetQuery = mock(StoredProcedureQuery.class); given(emf.createEntityManager()).willReturn(targetEm); - given(targetEm.createStoredProcedureQuery("x")).willReturn(query); - willReturn("y").given(query).getOutputParameterValue("a"); - willReturn("z").given(query).getOutputParameterValue("c"); + given(targetEm.createStoredProcedureQuery("x")).willReturn(targetQuery); + willReturn("y").given(targetQuery).getOutputParameterValue("a"); + willReturn("z").given(targetQuery).getOutputParameterValue("c"); given(targetEm.isOpen()).willReturn(true); EntityManager em = SharedEntityManagerCreator.createSharedEntityManager(emf); @@ -219,12 +239,12 @@ public void deferredStoredProcedureQueryWithNamedParameters() { spq.getOutputParameterValue("b")); assertThat(spq.getOutputParameterValue("c")).isEqualTo("z"); - verify(query).registerStoredProcedureParameter("a", String.class, ParameterMode.OUT); - verify(query).registerStoredProcedureParameter("b", Number.class, ParameterMode.IN); - verify(query).registerStoredProcedureParameter("c", Object.class, ParameterMode.INOUT); - verify(query).execute(); + verify(targetQuery).registerStoredProcedureParameter("a", String.class, ParameterMode.OUT); + verify(targetQuery).registerStoredProcedureParameter("b", Number.class, ParameterMode.IN); + verify(targetQuery).registerStoredProcedureParameter("c", Object.class, ParameterMode.INOUT); + verify(targetQuery).execute(); verify(targetEm).close(); - verifyNoMoreInteractions(query); + verifyNoMoreInteractions(targetQuery); verifyNoMoreInteractions(targetEm); } diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/ConnectionFactoryUtils.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/ConnectionFactoryUtils.java index dc3fab49d326..c378b52b8170 100644 --- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/ConnectionFactoryUtils.java +++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/ConnectionFactoryUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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. @@ -85,7 +85,7 @@ public abstract class ConnectionFactoryUtils { */ public static Mono getConnection(ConnectionFactory connectionFactory) { return doGetConnection(connectionFactory) - .onErrorMap(e -> new DataAccessResourceFailureException("Failed to obtain R2DBC Connection", e)); + .onErrorMap(ex -> new DataAccessResourceFailureException("Failed to obtain R2DBC Connection", ex)); } /** @@ -124,17 +124,17 @@ public static Mono doGetConnection(ConnectionFactory connectionFacto holderToUse.setConnection(conn); } holderToUse.requested(); - synchronizationManager - .registerSynchronization(new ConnectionSynchronization(holderToUse, connectionFactory)); + synchronizationManager.registerSynchronization( + new ConnectionSynchronization(holderToUse, connectionFactory)); holderToUse.setSynchronizedWithTransaction(true); if (holderToUse != conHolder) { synchronizationManager.bindResource(connectionFactory, holderToUse); } }) // Unexpected exception from external delegation call -> close Connection and rethrow. - .onErrorResume(e -> releaseConnection(connection, connectionFactory).then(Mono.error(e)))); + .onErrorResume(ex -> releaseConnection(connection, connectionFactory).then(Mono.error(ex)))); } return con; - }).onErrorResume(NoTransactionException.class, e -> Mono.from(connectionFactory.create())); + }).onErrorResume(NoTransactionException.class, ex -> Mono.from(connectionFactory.create())); } /** @@ -159,7 +159,7 @@ private static Mono fetchConnection(ConnectionFactory connectionFact */ public static Mono releaseConnection(Connection con, ConnectionFactory connectionFactory) { return doReleaseConnection(con, connectionFactory) - .onErrorMap(e -> new DataAccessResourceFailureException("Failed to close R2DBC Connection", e)); + .onErrorMap(ex -> new DataAccessResourceFailureException("Failed to close R2DBC Connection", ex)); } /** @@ -171,15 +171,15 @@ public static Mono releaseConnection(Connection con, ConnectionFactory con * @see #doGetConnection */ public static Mono doReleaseConnection(Connection connection, ConnectionFactory connectionFactory) { - return TransactionSynchronizationManager.forCurrentTransaction() - .flatMap(synchronizationManager -> { + return TransactionSynchronizationManager.forCurrentTransaction().flatMap(synchronizationManager -> { ConnectionHolder conHolder = (ConnectionHolder) synchronizationManager.getResource(connectionFactory); if (conHolder != null && connectionEquals(conHolder, connection)) { // It's the transactional Connection: Don't close it. conHolder.released(); + return Mono.empty(); } return Mono.from(connection.close()); - }).onErrorResume(NoTransactionException.class, e -> Mono.from(connection.close())); + }).onErrorResume(NoTransactionException.class, ex -> Mono.from(connection.close())); } /** @@ -221,7 +221,7 @@ public static DataAccessException convertR2dbcException(String task, @Nullable S return new QueryTimeoutException(buildMessage(task, sql, ex), ex); } } - if (ex instanceof R2dbcNonTransientException) { + else if (ex instanceof R2dbcNonTransientException) { if (ex instanceof R2dbcNonTransientResourceException) { return new DataAccessResourceFailureException(buildMessage(task, sql, ex), ex); } @@ -268,7 +268,8 @@ private static boolean connectionEquals(ConnectionHolder conHolder, Connection p Connection heldCon = conHolder.getConnection(); // Explicitly check for identity too: for Connection handles that do not implement // "equals" properly). - return (heldCon == passedInCon || heldCon.equals(passedInCon) || getTargetConnection(heldCon).equals(passedInCon)); + return (heldCon == passedInCon || heldCon.equals(passedInCon) || + getTargetConnection(heldCon).equals(passedInCon)); } /** diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/R2dbcTransactionManager.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/R2dbcTransactionManager.java index bf7c437eabb8..46dc6b0a9195 100644 --- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/R2dbcTransactionManager.java +++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/R2dbcTransactionManager.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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,48 +29,42 @@ import org.springframework.lang.Nullable; import org.springframework.transaction.CannotCreateTransactionException; import org.springframework.transaction.TransactionDefinition; -import org.springframework.transaction.TransactionException; import org.springframework.transaction.reactive.AbstractReactiveTransactionManager; import org.springframework.transaction.reactive.GenericReactiveTransaction; import org.springframework.transaction.reactive.TransactionSynchronizationManager; import org.springframework.util.Assert; /** - * {@link org.springframework.transaction.ReactiveTransactionManager} - * implementation for a single R2DBC {@link ConnectionFactory}. This class is - * capable of working in any environment with any R2DBC driver, as long as the - * setup uses a {@code ConnectionFactory} as its {@link Connection} factory - * mechanism. Binds a R2DBC {@code Connection} from the specified - * {@code ConnectionFactory} to the current subscriber context, potentially - * allowing for one context-bound {@code Connection} per {@code ConnectionFactory}. + * {@link org.springframework.transaction.ReactiveTransactionManager} implementation + * for a single R2DBC {@link ConnectionFactory}. This class is capable of working + * in any environment with any R2DBC driver, as long as the setup uses a + * {@code ConnectionFactory} as its {@link Connection} factory mechanism. + * Binds a R2DBC {@code Connection} from the specified {@code ConnectionFactory} + * to the current subscriber context, potentially allowing for one context-bound + * {@code Connection} per {@code ConnectionFactory}. * - *

    Note: The {@code ConnectionFactory} that this transaction manager - * operates on needs to return independent {@code Connection}s. - * The {@code Connection}s may come from a pool (the typical case), but the - * {@code ConnectionFactory} must not return scoped {@code Connection}s - * or the like. This transaction manager will associate {@code Connection} - * with context-bound transactions itself, according to the specified propagation - * behavior. It assumes that a separate, independent {@code Connection} can - * be obtained even during an ongoing transaction. + *

    Note: The {@code ConnectionFactory} that this transaction manager operates + * on needs to return independent {@code Connection}s. The {@code Connection}s + * typically come from a connection pool but the {@code ConnectionFactory} must not + * return specifically scoped or constrained {@code Connection}s. This transaction + * manager will associate {@code Connection} with context-bound transactions, + * according to the specified propagation behavior. It assumes that a separate, + * independent {@code Connection} can be obtained even during an ongoing transaction. * *

    Application code is required to retrieve the R2DBC Connection via * {@link ConnectionFactoryUtils#getConnection(ConnectionFactory)} * instead of a standard R2DBC-style {@link ConnectionFactory#create()} call. * Spring classes such as {@code DatabaseClient} use this strategy implicitly. * If not used in combination with this transaction manager, the - * {@link ConnectionFactoryUtils} lookup strategy behaves exactly like the - * native {@code ConnectionFactory} lookup; it can thus be used in a portable fashion. + * {@link ConnectionFactoryUtils} lookup strategy behaves exactly like the native + * {@code ConnectionFactory} lookup; it can thus be used in a portable fashion. * - *

    Alternatively, you can allow application code to work with the standard - * R2DBC lookup pattern {@link ConnectionFactory#create()}, for example for code - * that is not aware of Spring at all. In that case, define a - * {@link TransactionAwareConnectionFactoryProxy} for your target {@code ConnectionFactory}, - * and pass that proxy {@code ConnectionFactory} to your DAOs, which will automatically - * participate in Spring-managed transactions when accessing it. - * - *

    This transaction manager triggers flush callbacks on registered transaction - * synchronizations (if synchronization is generally active), assuming resources - * operating on the underlying R2DBC {@code Connection}. + *

    Alternatively, you can allow application code to work with the lookup pattern + * {@link ConnectionFactory#create()}, for example for code not aware of Spring. + * In that case, define a {@link TransactionAwareConnectionFactoryProxy} for your + * target {@code ConnectionFactory}, and pass that proxy {@code ConnectionFactory} + * to your DAOs which will automatically participate in Spring-managed transactions + * when accessing it. * * @author Mark Paluch * @since 5.3 @@ -89,7 +83,7 @@ public class R2dbcTransactionManager extends AbstractReactiveTransactionManager /** * Create a new {@code R2dbcTransactionManager} instance. - * A ConnectionFactory has to be set to be able to use it. + * A {@code ConnectionFactory} has to be set to be able to use it. * @see #setConnectionFactory */ public R2dbcTransactionManager() {} @@ -106,12 +100,13 @@ public R2dbcTransactionManager(ConnectionFactory connectionFactory) { /** - * Set the R2DBC {@link ConnectionFactory} that this instance should manage transactions for. - *

    This will typically be a locally defined {@code ConnectionFactory}, for example a connection pool. - *

    The {@code ConnectionFactory} passed in here needs to return independent {@link Connection}s. - * The {@code Connection}s may come from a pool (the typical case), but the {@code ConnectionFactory} - * must not return scoped {@code Connection}s or the like. - * @see TransactionAwareConnectionFactoryProxy + * Set the R2DBC {@link ConnectionFactory} that this instance should manage transactions + * for. This will typically be a locally defined {@code ConnectionFactory}, for example + * an R2DBC connection pool. + *

    The {@code ConnectionFactory} passed in here needs to return independent + * {@link Connection}s. The {@code Connection}s typically come from a connection + * pool but the {@code ConnectionFactory} must not return specifically scoped or + * constrained {@code Connection}s. */ public void setConnectionFactory(@Nullable ConnectionFactory connectionFactory) { this.connectionFactory = connectionFactory; @@ -142,8 +137,8 @@ protected ConnectionFactory obtainConnectionFactory() { * transactional connection: "SET TRANSACTION READ ONLY" as understood by Oracle, * MySQL and Postgres. *

    The exact treatment, including any SQL statement executed on the connection, - * can be customized through {@link #prepareTransactionalConnection(Connection, TransactionDefinition)}. - * @see #prepareTransactionalConnection(Connection, TransactionDefinition) + * can be customized through {@link #prepareTransactionalConnection}. + * @see #prepareTransactionalConnection */ public void setEnforceReadOnly(boolean enforceReadOnly) { this.enforceReadOnly = enforceReadOnly; @@ -166,7 +161,7 @@ public void afterPropertiesSet() { } @Override - protected Object doGetTransaction(TransactionSynchronizationManager synchronizationManager) throws TransactionException { + protected Object doGetTransaction(TransactionSynchronizationManager synchronizationManager) { ConnectionFactoryTransactionObject txObject = new ConnectionFactoryTransactionObject(); ConnectionHolder conHolder = (ConnectionHolder) synchronizationManager.getResource(obtainConnectionFactory()); txObject.setConnectionHolder(conHolder, false); @@ -182,7 +177,7 @@ protected boolean isExistingTransaction(Object transaction) { @SuppressWarnings("deprecation") @Override protected Mono doBegin(TransactionSynchronizationManager synchronizationManager, Object transaction, - TransactionDefinition definition) throws TransactionException { + TransactionDefinition definition) { ConnectionFactoryTransactionObject txObject = (ConnectionFactoryTransactionObject) transaction; @@ -193,7 +188,7 @@ protected Mono doBegin(TransactionSynchronizationManager synchronizationMa Mono newCon = Mono.from(obtainConnectionFactory().create()); connectionMono = newCon.doOnNext(connection -> { if (logger.isDebugEnabled()) { - logger.debug("Acquired Connection [" + newCon + "] for R2DBC transaction"); + logger.debug("Acquired Connection [" + connection + "] for R2DBC transaction"); } txObject.setConnectionHolder(new ConnectionHolder(connection), true); }); @@ -247,9 +242,7 @@ protected Duration determineTimeout(TransactionDefinition definition) { } @Override - protected Mono doSuspend(TransactionSynchronizationManager synchronizationManager, Object transaction) - throws TransactionException { - + protected Mono doSuspend(TransactionSynchronizationManager synchronizationManager, Object transaction) { return Mono.defer(() -> { ConnectionFactoryTransactionObject txObject = (ConnectionFactoryTransactionObject) transaction; txObject.setConnectionHolder(null); @@ -259,7 +252,7 @@ protected Mono doSuspend(TransactionSynchronizationManager synchronizati @Override protected Mono doResume(TransactionSynchronizationManager synchronizationManager, - @Nullable Object transaction, Object suspendedResources) throws TransactionException { + @Nullable Object transaction, Object suspendedResources) { return Mono.defer(() -> { synchronizationManager.bindResource(obtainConnectionFactory(), suspendedResources); @@ -269,7 +262,7 @@ protected Mono doResume(TransactionSynchronizationManager synchronizationM @Override protected Mono doCommit(TransactionSynchronizationManager TransactionSynchronizationManager, - GenericReactiveTransaction status) throws TransactionException { + GenericReactiveTransaction status) { ConnectionFactoryTransactionObject txObject = (ConnectionFactoryTransactionObject) status.getTransaction(); Connection connection = txObject.getConnectionHolder().getConnection(); @@ -282,7 +275,7 @@ protected Mono doCommit(TransactionSynchronizationManager TransactionSynch @Override protected Mono doRollback(TransactionSynchronizationManager TransactionSynchronizationManager, - GenericReactiveTransaction status) throws TransactionException { + GenericReactiveTransaction status) { ConnectionFactoryTransactionObject txObject = (ConnectionFactoryTransactionObject) status.getTransaction(); Connection connection = txObject.getConnectionHolder().getConnection(); @@ -295,7 +288,7 @@ protected Mono doRollback(TransactionSynchronizationManager TransactionSyn @Override protected Mono doSetRollbackOnly(TransactionSynchronizationManager synchronizationManager, - GenericReactiveTransaction status) throws TransactionException { + GenericReactiveTransaction status) { return Mono.fromRunnable(() -> { ConnectionFactoryTransactionObject txObject = (ConnectionFactoryTransactionObject) status.getTransaction(); @@ -325,31 +318,48 @@ protected Mono doCleanupAfterCompletion(TransactionSynchronizationManager Mono afterCleanup = Mono.empty(); if (txObject.isMustRestoreAutoCommit()) { - afterCleanup = afterCleanup.then(Mono.from(con.setAutoCommit(true))); + Mono restoreAutoCommitStep = safeCleanupStep( + "doCleanupAfterCompletion when restoring autocommit", Mono.from(con.setAutoCommit(true))); + afterCleanup = afterCleanup.then(restoreAutoCommitStep); } if (txObject.getPreviousIsolationLevel() != null) { - afterCleanup = afterCleanup - .then(Mono.from(con.setTransactionIsolationLevel(txObject.getPreviousIsolationLevel()))); + Mono restoreIsolationStep = safeCleanupStep( + "doCleanupAfterCompletion when restoring isolation level", + Mono.from(con.setTransactionIsolationLevel(txObject.getPreviousIsolationLevel()))); + afterCleanup = afterCleanup.then(restoreIsolationStep); } - return afterCleanup.then(Mono.defer(() -> { + Mono releaseConnectionStep = Mono.defer(() -> { try { if (txObject.isNewConnectionHolder()) { if (logger.isDebugEnabled()) { logger.debug("Releasing R2DBC Connection [" + con + "] after transaction"); } - return ConnectionFactoryUtils.releaseConnection(con, obtainConnectionFactory()); + return safeCleanupStep("doCleanupAfterCompletion when releasing R2DBC Connection", + ConnectionFactoryUtils.releaseConnection(con, obtainConnectionFactory())); } } finally { txObject.getConnectionHolder().clear(); } return Mono.empty(); - })); + }); + return afterCleanup.then(releaseConnectionStep); }); } + private Mono safeCleanupStep(String stepDescription, Mono stepMono) { + if (!logger.isDebugEnabled()) { + return stepMono.onErrorComplete(); + } + else { + return stepMono.doOnError(e -> + logger.debug(String.format("Error ignored during %s: %s", stepDescription, e))) + .onErrorComplete(); + } + } + /** * Prepare the transactional {@link Connection} right before transaction begin. * @deprecated in favor of {@link #prepareTransactionalConnection(Connection, TransactionDefinition)} diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/SingleConnectionFactory.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/SingleConnectionFactory.java index f40d34ea99bd..6d16e8c1b6f0 100644 --- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/SingleConnectionFactory.java +++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/SingleConnectionFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-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. @@ -70,13 +70,15 @@ public class SingleConnectionFactory extends DelegatingConnectionFactory private boolean suppressClose; /** Override auto-commit state?. */ - private @Nullable Boolean autoCommit; + @Nullable + private Boolean autoCommit; /** Wrapped Connection. */ private final AtomicReference target = new AtomicReference<>(); /** Proxy Connection. */ - private @Nullable Connection connection; + @Nullable + private Connection connection; private final Mono connectionEmitter; @@ -231,7 +233,7 @@ protected Mono prepareConnection(Connection connection) { */ protected Connection getCloseSuppressingConnectionProxy(Connection target) { return (Connection) Proxy.newProxyInstance(SingleConnectionFactory.class.getClassLoader(), - new Class[] { Connection.class, Wrapped.class }, new CloseSuppressingInvocationHandler(target)); + new Class[] {Connection.class, Wrapped.class}, new CloseSuppressingInvocationHandler(target)); } diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/init/DatabasePopulator.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/init/DatabasePopulator.java index 467a25549cfd..8adf9831b30c 100644 --- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/init/DatabasePopulator.java +++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/init/DatabasePopulator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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,7 +20,6 @@ import io.r2dbc.spi.ConnectionFactory; import reactor.core.publisher.Mono; -import org.springframework.dao.DataAccessException; import org.springframework.r2dbc.connection.ConnectionFactoryUtils; import org.springframework.util.Assert; @@ -44,17 +43,18 @@ public interface DatabasePopulator { * already configured and ready to use, must not be {@code null} * @return {@link Mono} that initiates script execution and is * notified upon completion - * @throws ScriptException in all other error cases + * @throws ScriptException in case of any errors */ - Mono populate(Connection connection) throws ScriptException; + Mono populate(Connection connection); /** * Execute the given {@link DatabasePopulator} against the given {@link ConnectionFactory}. * @param connectionFactory the {@link ConnectionFactory} to execute against * @return {@link Mono} that initiates {@link DatabasePopulator#populate(Connection)} * and is notified upon completion + * @throws ScriptException in case of any errors */ - default Mono populate(ConnectionFactory connectionFactory) throws DataAccessException { + default Mono populate(ConnectionFactory connectionFactory) { Assert.notNull(connectionFactory, "ConnectionFactory must not be null"); return Mono.usingWhen(ConnectionFactoryUtils.getConnection(connectionFactory), // this::populate, // diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/init/ResourceDatabasePopulator.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/init/ResourceDatabasePopulator.java index a6aeac31364c..86dc2f4fcf23 100644 --- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/init/ResourceDatabasePopulator.java +++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/init/ResourceDatabasePopulator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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. @@ -260,7 +260,7 @@ public void setDataBufferFactory(DataBufferFactory dataBufferFactory) { @Override - public Mono populate(Connection connection) throws ScriptException { + public Mono populate(Connection connection) { Assert.notNull(connection, "Connection must not be null"); return Flux.fromIterable(this.scripts).concatMap(resource -> { EncodedResource encodedScript = new EncodedResource(resource, this.sqlScriptEncoding); diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/init/ScriptUtils.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/init/ScriptUtils.java index c374d68c05a4..18437ddca93a 100644 --- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/init/ScriptUtils.java +++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/init/ScriptUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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. @@ -125,7 +125,7 @@ public abstract class ScriptUtils { * @see org.springframework.r2dbc.connection.ConnectionFactoryUtils#getConnection * @see org.springframework.r2dbc.connection.ConnectionFactoryUtils#releaseConnection */ - public static Mono executeSqlScript(Connection connection, Resource resource) throws ScriptException { + public static Mono executeSqlScript(Connection connection, Resource resource) { return executeSqlScript(connection, new EncodedResource(resource)); } @@ -149,7 +149,7 @@ public static Mono executeSqlScript(Connection connection, Resource resour * @see org.springframework.r2dbc.connection.ConnectionFactoryUtils#getConnection * @see org.springframework.r2dbc.connection.ConnectionFactoryUtils#releaseConnection */ - public static Mono executeSqlScript(Connection connection, EncodedResource resource) throws ScriptException { + public static Mono executeSqlScript(Connection connection, EncodedResource resource) { return executeSqlScript(connection, resource, DefaultDataBufferFactory.sharedInstance, false, false, DEFAULT_COMMENT_PREFIXES, DEFAULT_STATEMENT_SEPARATOR, DEFAULT_BLOCK_COMMENT_START_DELIMITER, DEFAULT_BLOCK_COMMENT_END_DELIMITER); @@ -189,7 +189,7 @@ public static Mono executeSqlScript(Connection connection, EncodedResource public static Mono executeSqlScript(Connection connection, EncodedResource resource, DataBufferFactory dataBufferFactory, boolean continueOnError, boolean ignoreFailedDrops, String commentPrefix, @Nullable String separator, String blockCommentStartDelimiter, - String blockCommentEndDelimiter) throws ScriptException { + String blockCommentEndDelimiter) { return executeSqlScript(connection, resource, dataBufferFactory, continueOnError, ignoreFailedDrops, new String[] { commentPrefix }, separator, @@ -230,7 +230,7 @@ public static Mono executeSqlScript(Connection connection, EncodedResource public static Mono executeSqlScript(Connection connection, EncodedResource resource, DataBufferFactory dataBufferFactory, boolean continueOnError, boolean ignoreFailedDrops, String[] commentPrefixes, @Nullable String separator, String blockCommentStartDelimiter, - String blockCommentEndDelimiter) throws ScriptException { + String blockCommentEndDelimiter) { if (logger.isDebugEnabled()) { logger.debug("Executing SQL script from " + resource); @@ -365,7 +365,7 @@ private static void appendSeparatorToScriptIfNecessary(StringBuilder scriptBuild */ static boolean containsStatementSeparator(EncodedResource resource, String script, String separator, String[] commentPrefixes, String blockCommentStartDelimiter, - String blockCommentEndDelimiter) throws ScriptException { + String blockCommentEndDelimiter) { boolean inSingleQuote = false; boolean inDoubleQuote = false; @@ -448,7 +448,7 @@ else if (script.startsWith(blockCommentStartDelimiter, i)) { */ static List splitSqlScript(EncodedResource resource, String script, String separator, String[] commentPrefixes, String blockCommentStartDelimiter, - String blockCommentEndDelimiter) throws ScriptException { + String blockCommentEndDelimiter) { Assert.hasText(script, "'script' must not be null or empty"); Assert.notNull(separator, "'separator' must not be null"); diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/ConnectionAccessor.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/ConnectionAccessor.java index 9f794ce67101..d3424708960d 100644 --- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/ConnectionAccessor.java +++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/ConnectionAccessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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,8 +22,6 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import org.springframework.dao.DataAccessException; - /** * Interface declaring methods that accept callback {@link Function} * to operate within the scope of a {@link Connection}. @@ -31,13 +29,16 @@ * close the connection as the connections may be pooled or be * subject to other kinds of resource management. * - *

    Callback functions are responsible for creating a + *

    Callback functions are responsible for creating a * {@link org.reactivestreams.Publisher} that defines the scope of how * long the allocated {@link Connection} is valid. Connections are * released after the publisher terminates. * + *

    This serves as a base interface for {@link DatabaseClient}. + * * @author Mark Paluch * @since 5.3 + * @see DatabaseClient */ public interface ConnectionAccessor { @@ -49,8 +50,9 @@ public interface ConnectionAccessor { * {@link Function} closure, otherwise resources may get defunct. * @param action the callback object that specifies the connection action * @return the resulting {@link Mono} + * @throws org.springframework.dao.DataAccessException in case of any errors */ - Mono inConnection(Function> action) throws DataAccessException; + Mono inConnection(Function> action); /** * Execute a callback {@link Function} within a {@link Connection} scope. @@ -60,7 +62,8 @@ public interface ConnectionAccessor { * {@link Function} closure, otherwise resources may get defunct. * @param action the callback object that specifies the connection action * @return the resulting {@link Flux} + * @throws org.springframework.dao.DataAccessException in case of any errors */ - Flux inConnectionMany(Function> action) throws DataAccessException; + Flux inConnectionMany(Function> action); } diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/ConnectionFunction.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/ConnectionFunction.java index 4d1b26529474..12a9f8fc80ae 100644 --- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/ConnectionFunction.java +++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/ConnectionFunction.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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,34 +20,18 @@ import io.r2dbc.spi.Connection; + /** * Union type combining {@link Function} and {@link SqlProvider} to expose the SQL that is - * related to the underlying action. + * related to the underlying action. The SqlProvider can support lazy / generate once semantics, + * in which case {@link #getSql()} can be {@code null} until the {@code #apply(Connection)} + * method is invoked. * * @author Mark Paluch + * @author Simon Baslé * @since 5.3 * @param the type of the result of the function. */ -class ConnectionFunction implements Function, SqlProvider { - - private final String sql; - - private final Function function; - - - ConnectionFunction(String sql, Function function) { - this.sql = sql; - this.function = function; - } - - - @Override - public R apply(Connection t) { - return this.function.apply(t); - } - - @Override - public String getSql() { - return this.sql; - } +interface ConnectionFunction extends Function, SqlProvider { } + diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/DatabaseClient.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/DatabaseClient.java index 1b7b7b7b6030..f9a6daef4d18 100644 --- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/DatabaseClient.java +++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/DatabaseClient.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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. @@ -32,9 +32,9 @@ import org.springframework.util.Assert; /** - * A non-blocking, reactive client for performing database calls requests with - * Reactive Streams back pressure. Provides a higher level, common API over - * R2DBC client libraries. + * A non-blocking, reactive client for performing database calls with Reactive Streams + * back pressure. Provides a higher level, common API over R2DBC client libraries. + * Propagates {@link org.springframework.dao.DataAccessException} variants for errors. * *

    Use one of the static factory methods {@link #create(ConnectionFactory)} * or obtain a {@link DatabaseClient#builder()} to create an instance. @@ -79,7 +79,10 @@ public interface DatabaseClient extends ConnectionAccessor { * the execution. The SQL string can contain either native parameter * bind markers or named parameters (e.g. {@literal :foo, :bar}) when * {@link NamedParameterExpander} is enabled. - *

    Accepts {@link PreparedOperation} as SQL and binding {@link Supplier} + *

    Accepts {@link PreparedOperation} as SQL and binding {@link Supplier}. + *

    {@code DatabaseClient} implementations should defer the resolution of + * the SQL string as much as possible, ideally up to the point where a + * {@code Subscription} happens. This is the case for the default implementation. * @param sqlSupplier a supplier for the SQL statement * @return a new {@link GenericExecuteSpec} * @see NamedParameterExpander diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/DefaultDatabaseClient.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/DefaultDatabaseClient.java index 91b21d74f225..bfa6aa4be466 100644 --- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/DefaultDatabaseClient.java +++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/DefaultDatabaseClient.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -44,7 +44,6 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import org.springframework.dao.DataAccessException; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.lang.Nullable; import org.springframework.r2dbc.connection.ConnectionFactoryUtils; @@ -60,6 +59,7 @@ * @author Mark Paluch * @author Mingyuan Wu * @author Bogdan Ilchyshyn + * @author Simon Baslé * @since 5.3 */ class DefaultDatabaseClient implements DatabaseClient { @@ -104,7 +104,7 @@ public GenericExecuteSpec sql(Supplier sqlSupplier) { } @Override - public Mono inConnection(Function> action) throws DataAccessException { + public Mono inConnection(Function> action) { Assert.notNull(action, "Callback object must not be null"); Mono connectionMono = getConnection().map( connection -> new ConnectionCloseHolder(connection, this::closeConnection)); @@ -126,7 +126,7 @@ public Mono inConnection(Function> action) throws Dat } @Override - public Flux inConnectionMany(Function> action) throws DataAccessException { + public Flux inConnectionMany(Function> action) { Assert.notNull(action, "Callback object must not be null"); Mono connectionMono = getConnection().map( connection -> new ConnectionCloseHolder(connection, this::closeConnection)); @@ -158,11 +158,9 @@ private Mono getConnection() { /** * Release the {@link Connection}. * @param connection to close. - * @return a {@link Publisher} that completes successfully when the connection is - * closed + * @return a {@link Publisher} that completes successfully when the connection is closed */ private Publisher closeConnection(Connection connection) { - return ConnectionFactoryUtils.currentConnectionFactory( obtainConnectionFactory()).then().onErrorResume(Exception.class, e -> Mono.from(connection.close())); @@ -184,28 +182,26 @@ private ConnectionFactory obtainConnectionFactory() { */ private static Connection createConnectionProxy(Connection con) { return (Connection) Proxy.newProxyInstance(DatabaseClient.class.getClassLoader(), - new Class[] { Connection.class, Wrapped.class }, + new Class[] {Connection.class, Wrapped.class}, new CloseSuppressingInvocationHandler(con)); } - private static Mono sumRowsUpdated( - Function> resultFunction, Connection it) { + private static Mono sumRowsUpdated(Function> resultFunction, Connection it) { return resultFunction.apply(it) .flatMap(Result::getRowsUpdated) .collect(Collectors.summingInt(Integer::intValue)); } /** - * Determine SQL from potential provider object. - * @param sqlProvider object that's potentially a SqlProvider + * Get SQL from a potential provider object. + * @param object an object that is potentially an SqlProvider * @return the SQL string, or {@code null} * @see SqlProvider */ @Nullable - private static String getSql(Object sqlProvider) { - - if (sqlProvider instanceof SqlProvider) { - return ((SqlProvider) sqlProvider).getSql(); + private static String getSql(Object object) { + if (object instanceof SqlProvider) { + return ((SqlProvider) object).getSql(); } else { return null; @@ -214,7 +210,7 @@ private static String getSql(Object sqlProvider) { /** - * Base class for {@link DatabaseClient.GenericExecuteSpec} implementations. + * Default {@link DatabaseClient.GenericExecuteSpec} implementation. */ class DefaultGenericExecuteSpec implements GenericExecuteSpec { @@ -322,9 +318,8 @@ public Mono then() { return fetch().rowsUpdated().then(); } - private FetchSpec execute(Supplier sqlSupplier, BiFunction mappingFunction) { - String sql = getRequiredSql(sqlSupplier); - Function statementFunction = connection -> { + private ResultFunction getResultFunction(Supplier sqlSupplier) { + BiFunction statementFunction = (connection, sql) -> { if (logger.isDebugEnabled()) { logger.debug("Executing SQL statement [" + sql + "]"); } @@ -370,16 +365,16 @@ private FetchSpec execute(Supplier sqlSupplier, BiFunction> resultFunction = connection -> { - Statement statement = statementFunction.apply(connection); - return Flux.from(this.filterFunction.filter(statement, DefaultDatabaseClient.this.executeFunction)) - .cast(Result.class).checkpoint("SQL \"" + sql + "\" [DatabaseClient]"); - }; + return new ResultFunction(sqlSupplier, statementFunction, this.filterFunction, DefaultDatabaseClient.this.executeFunction); + } + + private FetchSpec execute(Supplier sqlSupplier, BiFunction mappingFunction) { + ResultFunction resultHandler = getResultFunction(sqlSupplier); return new DefaultFetchSpec<>( - DefaultDatabaseClient.this, sql, - new ConnectionFunction<>(sql, resultFunction), - new ConnectionFunction<>(sql, connection -> sumRowsUpdated(resultFunction, connection)), + DefaultDatabaseClient.this, + resultHandler, + connection -> sumRowsUpdated(resultHandler, connection), mappingFunction); } @@ -505,12 +500,11 @@ static class ConnectionCloseHolder extends AtomicBoolean { private static final long serialVersionUID = -8994138383301201380L; - final Connection connection; + final transient Connection connection; - final Function> closeFunction; + final transient Function> closeFunction; - ConnectionCloseHolder(Connection connection, - Function> closeFunction) { + ConnectionCloseHolder(Connection connection, Function> closeFunction) { this.connection = connection; this.closeFunction = closeFunction; } diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/DefaultFetchSpec.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/DefaultFetchSpec.java index 85d8bd311a15..b92f96c24621 100644 --- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/DefaultFetchSpec.java +++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/DefaultFetchSpec.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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,7 +20,6 @@ import java.util.function.Function; import io.r2dbc.spi.Connection; -import io.r2dbc.spi.Result; import io.r2dbc.spi.Row; import io.r2dbc.spi.RowMetadata; import reactor.core.publisher.Flux; @@ -32,6 +31,7 @@ * Default {@link FetchSpec} implementation. * * @author Mark Paluch + * @author Simon Baslé * @since 5.3 * @param the row result type */ @@ -39,24 +39,21 @@ class DefaultFetchSpec implements FetchSpec { private final ConnectionAccessor connectionAccessor; - private final String sql; - - private final Function> resultFunction; + private final ResultFunction resultFunction; private final Function> updatedRowsFunction; private final BiFunction mappingFunction; - DefaultFetchSpec(ConnectionAccessor connectionAccessor, String sql, - Function> resultFunction, + DefaultFetchSpec(ConnectionAccessor connectionAccessor, + ResultFunction resultFunction, Function> updatedRowsFunction, BiFunction mappingFunction) { - this.sql = sql; this.connectionAccessor = connectionAccessor; this.resultFunction = resultFunction; - this.updatedRowsFunction = updatedRowsFunction; + this.updatedRowsFunction = new DelegateConnectionFunction<>(resultFunction, updatedRowsFunction); this.mappingFunction = mappingFunction; } @@ -70,7 +67,7 @@ public Mono one() { } if (list.size() > 1) { return Mono.error(new IncorrectResultSizeDataAccessException( - String.format("Query [%s] returned non unique result.", this.sql), + String.format("Query [%s] returned non unique result.", this.resultFunction.getSql()), 1)); } return Mono.just(list.get(0)); @@ -84,7 +81,7 @@ public Mono first() { @Override public Flux all() { - return this.connectionAccessor.inConnectionMany(new ConnectionFunction<>(this.sql, + return this.connectionAccessor.inConnectionMany(new DelegateConnectionFunction<>(this.resultFunction, connection -> this.resultFunction.apply(connection) .flatMap(result -> result.map(this.mappingFunction)))); } diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/DelegateConnectionFunction.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/DelegateConnectionFunction.java new file mode 100644 index 000000000000..a25bddd7f665 --- /dev/null +++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/DelegateConnectionFunction.java @@ -0,0 +1,56 @@ +/* + * Copyright 2002-2023 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.r2dbc.core; + +import java.util.function.Function; + +import io.r2dbc.spi.Connection; + +import org.springframework.lang.Nullable; + +/** + * A {@link ConnectionFunction} that delegates to a {@code SqlProvider} and a plain + * {@code Function}. + * + * @author Simon Baslé + * @since 5.3.26 + * @param the type of the result of the function. + */ +final class DelegateConnectionFunction implements ConnectionFunction { + + private final SqlProvider sql; + + private final Function function; + + + DelegateConnectionFunction(SqlProvider sql, Function function) { + this.sql = sql; + this.function = function; + } + + + @Override + public R apply(Connection t) { + return this.function.apply(t); + } + + @Nullable + @Override + public String getSql() { + return this.sql.getSql(); + } +} diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/NamedParameterUtils.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/NamedParameterUtils.java index e89255b42c04..ce9d1bc5a195 100644 --- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/NamedParameterUtils.java +++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/NamedParameterUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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,7 +22,6 @@ import java.util.Iterator; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Set; import java.util.TreeMap; @@ -373,6 +372,7 @@ private static class ParameterHolder { private final int endIndex; ParameterHolder(String parameterName, int startIndex, int endIndex) { + Assert.notNull(parameterName, "Parameter name must not be null"); this.parameterName = parameterName; this.startIndex = startIndex; this.endIndex = endIndex; @@ -391,21 +391,21 @@ int getEndIndex() { } @Override - public boolean equals(Object o) { - if (this == o) { + public boolean equals(Object other) { + if (this == other) { return true; } - if (!(o instanceof ParameterHolder)) { + if (!(other instanceof ParameterHolder)) { return false; } - ParameterHolder that = (ParameterHolder) o; - return this.startIndex == that.startIndex && this.endIndex == that.endIndex - && Objects.equals(this.parameterName, that.parameterName); + ParameterHolder that = (ParameterHolder) other; + return (this.startIndex == that.startIndex && this.endIndex == that.endIndex && + this.parameterName.equals(that.parameterName)); } @Override public int hashCode() { - return Objects.hash(this.parameterName, this.startIndex, this.endIndex); + return this.parameterName.hashCode(); } } diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/Parameter.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/Parameter.java index 7615477fcedf..bea746611a3b 100644 --- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/Parameter.java +++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/Parameter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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,8 +16,6 @@ package org.springframework.r2dbc.core; -import java.util.Objects; - import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -109,21 +107,21 @@ public boolean isEmpty() { @Override - public boolean equals(Object obj) { - if (this == obj) { + public boolean equals(Object other) { + if (this == other) { return true; } - if (!(obj instanceof Parameter)) { + if (!(other instanceof Parameter)) { return false; } - Parameter other = (Parameter) obj; - return (ObjectUtils.nullSafeEquals(this.value, other.value) && - ObjectUtils.nullSafeEquals(this.type, other.type)); + Parameter that = (Parameter) other; + return (ObjectUtils.nullSafeEquals(this.value, that.value) && + ObjectUtils.nullSafeEquals(this.type, that.type)); } @Override public int hashCode() { - return Objects.hash(this.value, this.type); + return ObjectUtils.nullSafeHashCode(this.value) + ObjectUtils.nullSafeHashCode(this.type); } @Override diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/ResultFunction.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/ResultFunction.java new file mode 100644 index 000000000000..1204eac5df01 --- /dev/null +++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/ResultFunction.java @@ -0,0 +1,74 @@ +/* + * Copyright 2002-2023 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.r2dbc.core; + +import java.util.function.BiFunction; +import java.util.function.Supplier; + +import io.r2dbc.spi.Connection; +import io.r2dbc.spi.Result; +import io.r2dbc.spi.Statement; +import reactor.core.publisher.Flux; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * A {@link ConnectionFunction} that produces a {@code Flux} of {@link Result} and that + * defers generation of the SQL until the function has been applied. + * Beforehand, the {@code getSql()} method simply returns {@code null}. The sql String is + * also memoized during application, so that subsequent calls to {@link #getSql()} return + * the same {@code String} without further calls to the {@code Supplier}. + * + * @author Mark Paluch + * @author Simon Baslé + * @since 5.3.26 + */ +final class ResultFunction implements ConnectionFunction> { + + final Supplier sqlSupplier; + final BiFunction statementFunction; + final StatementFilterFunction filterFunction; + final ExecuteFunction executeFunction; + + @Nullable + String resolvedSql = null; + + ResultFunction(Supplier sqlSupplier, BiFunction statementFunction, StatementFilterFunction filterFunction, ExecuteFunction executeFunction) { + this.sqlSupplier = sqlSupplier; + this.statementFunction = statementFunction; + this.filterFunction = filterFunction; + this.executeFunction = executeFunction; + } + + @Override + public Flux apply(Connection connection) { + String sql = this.sqlSupplier.get(); + Assert.state(StringUtils.hasText(sql), "SQL returned by supplier must not be empty"); + this.resolvedSql = sql; + Statement statement = this.statementFunction.apply(connection, sql); + return Flux.from(this.filterFunction.filter(statement, this.executeFunction)) + .cast(Result.class).checkpoint("SQL \"" + sql + "\" [DatabaseClient]"); + } + + @Nullable + @Override + public String getSql() { + return this.resolvedSql; + } +} diff --git a/spring-r2dbc/src/test/java/org/springframework/r2dbc/connection/DelegatingConnectionFactoryUnitTests.java b/spring-r2dbc/src/test/java/org/springframework/r2dbc/connection/DelegatingConnectionFactoryUnitTests.java index fb7e6fa9e17c..16b7a39be867 100644 --- a/spring-r2dbc/src/test/java/org/springframework/r2dbc/connection/DelegatingConnectionFactoryUnitTests.java +++ b/spring-r2dbc/src/test/java/org/springframework/r2dbc/connection/DelegatingConnectionFactoryUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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. @@ -40,7 +40,7 @@ class DelegatingConnectionFactoryUnitTests { @Test - @SuppressWarnings({ "rawtypes", "unchecked" }) + @SuppressWarnings({"rawtypes", "unchecked"}) void shouldDelegateGetConnection() { Mono connectionMono = Mono.just(connectionMock); when(delegate.create()).thenReturn((Mono) connectionMono); @@ -53,6 +53,7 @@ void shouldDelegateUnwrapWithoutImplementing() { assertThat(connectionFactory.unwrap()).isSameAs(delegate); } + static class ExampleConnectionFactory extends DelegatingConnectionFactory { ExampleConnectionFactory(ConnectionFactory targetConnectionFactory) { diff --git a/spring-r2dbc/src/test/java/org/springframework/r2dbc/connection/R2dbcTransactionManagerUnitTests.java b/spring-r2dbc/src/test/java/org/springframework/r2dbc/connection/R2dbcTransactionManagerUnitTests.java index fa3d7d6db346..e8134c1d3691 100644 --- a/spring-r2dbc/src/test/java/org/springframework/r2dbc/connection/R2dbcTransactionManagerUnitTests.java +++ b/spring-r2dbc/src/test/java/org/springframework/r2dbc/connection/R2dbcTransactionManagerUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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,12 +22,14 @@ import io.r2dbc.spi.ConnectionFactory; import io.r2dbc.spi.IsolationLevel; import io.r2dbc.spi.R2dbcBadGrammarException; +import io.r2dbc.spi.R2dbcTimeoutException; import io.r2dbc.spi.Statement; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; +import org.springframework.r2dbc.BadSqlGrammarException; import org.springframework.transaction.CannotCreateTransactionException; import org.springframework.transaction.IllegalTransactionStateException; import org.springframework.transaction.TransactionDefinition; @@ -52,6 +54,7 @@ * Unit tests for {@link R2dbcTransactionManager}. * * @author Mark Paluch + * @author Juergen Hoeller */ class R2dbcTransactionManagerUnitTests { @@ -61,8 +64,9 @@ class R2dbcTransactionManagerUnitTests { private R2dbcTransactionManager tm; + @BeforeEach - @SuppressWarnings({ "unchecked", "rawtypes" }) + @SuppressWarnings({"rawtypes", "unchecked"}) void before() { when(connectionFactoryMock.create()).thenReturn((Mono) Mono.just(connectionMock)); when(connectionMock.beginTransaction()).thenReturn(Mono.empty()); @@ -70,6 +74,7 @@ void before() { tm = new R2dbcTransactionManager(connectionFactoryMock); } + @Test void testSimpleTransaction() { TestTransactionSynchronization sync = new TestTransactionSynchronization( @@ -82,8 +87,7 @@ void testSimpleTransaction() { ConnectionFactoryUtils.getConnection(connectionFactoryMock) .flatMap(connection -> TransactionSynchronizationManager.forCurrentTransaction() - .doOnNext(synchronizationManager -> synchronizationManager.registerSynchronization( - sync))) + .doOnNext(synchronizationManager -> synchronizationManager.registerSynchronization(sync))) .as(operator::transactional) .as(StepVerifier::create) .expectNextCount(1) @@ -115,12 +119,11 @@ void testBeginFails() { TransactionalOperator operator = TransactionalOperator.create(tm, definition); - ConnectionFactoryUtils.getConnection(connectionFactoryMock).as( - operator::transactional) + ConnectionFactoryUtils.getConnection(connectionFactoryMock) + .as(operator::transactional) .as(StepVerifier::create) .expectErrorSatisfies(actual -> assertThat(actual).isInstanceOf( - CannotCreateTransactionException.class).hasCauseInstanceOf( - R2dbcBadGrammarException.class)) + CannotCreateTransactionException.class).hasCauseInstanceOf(R2dbcBadGrammarException.class)) .verify(); } @@ -136,8 +139,8 @@ void appliesIsolationLevel() { TransactionalOperator operator = TransactionalOperator.create(tm, definition); - ConnectionFactoryUtils.getConnection(connectionFactoryMock).as( - operator::transactional) + ConnectionFactoryUtils.getConnection(connectionFactoryMock) + .as(operator::transactional) .as(StepVerifier::create) .expectNextCount(1) .verifyComplete(); @@ -161,8 +164,8 @@ void doesNotSetIsolationLevelIfMatch() { TransactionalOperator operator = TransactionalOperator.create(tm, definition); - ConnectionFactoryUtils.getConnection(connectionFactoryMock).as( - operator::transactional) + ConnectionFactoryUtils.getConnection(connectionFactoryMock) + .as(operator::transactional) .as(StepVerifier::create) .expectNextCount(1) .verifyComplete(); @@ -181,8 +184,8 @@ void doesNotSetAutoCommitDisabled() { TransactionalOperator operator = TransactionalOperator.create(tm, definition); - ConnectionFactoryUtils.getConnection(connectionFactoryMock).as( - operator::transactional) + ConnectionFactoryUtils.getConnection(connectionFactoryMock) + .as(operator::transactional) .as(StepVerifier::create) .expectNextCount(1) .verifyComplete(); @@ -229,8 +232,8 @@ void appliesReadOnly() { TransactionalOperator operator = TransactionalOperator.create(tm, definition); - ConnectionFactoryUtils.getConnection(connectionFactoryMock).as( - operator::transactional) + ConnectionFactoryUtils.getConnection(connectionFactoryMock) + .as(operator::transactional) .as(StepVerifier::create) .expectNextCount(1) .verifyComplete(); @@ -246,7 +249,6 @@ void appliesReadOnly() { @Test void testCommitFails() { when(connectionMock.commitTransaction()).thenReturn(Mono.defer(() -> Mono.error(new R2dbcBadGrammarException("Commit should fail")))); - when(connectionMock.rollbackTransaction()).thenReturn(Mono.empty()); TransactionalOperator operator = TransactionalOperator.create(tm); @@ -267,7 +269,6 @@ void testCommitFails() { @Test void testRollback() { - AtomicInteger commits = new AtomicInteger(); when(connectionMock.commitTransaction()).thenReturn( Mono.fromRunnable(commits::incrementAndGet)); @@ -279,11 +280,9 @@ void testRollback() { TransactionalOperator operator = TransactionalOperator.create(tm); ConnectionFactoryUtils.getConnection(connectionFactoryMock) - .doOnNext(connection -> { - throw new IllegalStateException(); - }).as(operator::transactional) - .as(StepVerifier::create) - .verifyError(IllegalStateException.class); + .doOnNext(connection -> { throw new IllegalStateException(); }) + .as(operator::transactional) + .as(StepVerifier::create).verifyError(IllegalStateException.class); assertThat(commits).hasValue(0); assertThat(rollbacks).hasValue(1); @@ -300,15 +299,11 @@ void testRollbackFails() { when(connectionMock.rollbackTransaction()).thenReturn(Mono.defer(() -> Mono.error(new R2dbcBadGrammarException("Commit should fail"))), Mono.empty()); TransactionalOperator operator = TransactionalOperator.create(tm); - operator.execute(reactiveTransaction -> { - reactiveTransaction.setRollbackOnly(); - return ConnectionFactoryUtils.getConnection(connectionFactoryMock) .doOnNext(connection -> connection.createStatement("foo")).then(); - }).as(StepVerifier::create) - .verifyError(IllegalTransactionStateException.class); + }).as(StepVerifier::create).verifyError(IllegalTransactionStateException.class); verify(connectionMock).isAutoCommit(); verify(connectionMock).beginTransaction(); @@ -319,6 +314,34 @@ void testRollbackFails() { verifyNoMoreInteractions(connectionMock); } + @Test + @SuppressWarnings("unchecked") + void testConnectionReleasedWhenRollbackFails() { + when(connectionMock.rollbackTransaction()).thenReturn(Mono.defer(() -> Mono.error(new R2dbcBadGrammarException("Rollback should fail"))), Mono.empty()); + + TransactionalOperator operator = TransactionalOperator.create(tm); + + when(connectionMock.isAutoCommit()).thenReturn(true); + when(connectionMock.setAutoCommit(true)).thenReturn(Mono.defer(() -> Mono.error(new R2dbcTimeoutException("SET AUTOCOMMIT = 1 timed out")))); + when(connectionMock.setTransactionIsolationLevel(any())).thenReturn(Mono.empty()); + when(connectionMock.setAutoCommit(false)).thenReturn(Mono.empty()); + + operator.execute(reactiveTransaction -> ConnectionFactoryUtils.getConnection(connectionFactoryMock) + .doOnNext(connection -> { + throw new IllegalStateException("Intentional error to trigger rollback"); + }).then()).as(StepVerifier::create) + .verifyErrorSatisfies(ex -> assertThat(ex) + .isInstanceOf(BadSqlGrammarException.class) + .hasCause(new R2dbcBadGrammarException("Rollback should fail")) + ); + + verify(connectionMock).isAutoCommit(); + verify(connectionMock).beginTransaction(); + verify(connectionMock, never()).commitTransaction(); + verify(connectionMock).rollbackTransaction(); + verify(connectionMock).close(); + } + @Test void testTransactionSetRollbackOnly() { when(connectionMock.rollbackTransaction()).thenReturn(Mono.empty()); @@ -326,19 +349,15 @@ void testTransactionSetRollbackOnly() { TransactionSynchronization.STATUS_ROLLED_BACK); TransactionalOperator operator = TransactionalOperator.create(tm); - operator.execute(tx -> { - tx.setRollbackOnly(); assertThat(tx.isNewTransaction()).isTrue(); - return TransactionSynchronizationManager.forCurrentTransaction().doOnNext( synchronizationManager -> { assertThat(synchronizationManager.hasResource(connectionFactoryMock)).isTrue(); synchronizationManager.registerSynchronization(sync); }).then(); - }).as(StepVerifier::create) - .verifyComplete(); + }).as(StepVerifier::create).verifyComplete(); verify(connectionMock).isAutoCommit(); verify(connectionMock).beginTransaction(); @@ -358,20 +377,16 @@ void testPropagationNeverWithExistingTransaction() { DefaultTransactionDefinition definition = new DefaultTransactionDefinition(); definition.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); - TransactionalOperator operator = TransactionalOperator.create(tm, definition); + TransactionalOperator operator = TransactionalOperator.create(tm, definition); operator.execute(tx1 -> { - assertThat(tx1.isNewTransaction()).isTrue(); - definition.setPropagationBehavior(TransactionDefinition.PROPAGATION_NEVER); return operator.execute(tx2 -> { - fail("Should have thrown IllegalTransactionStateException"); return Mono.empty(); }); - }).as(StepVerifier::create) - .verifyError(IllegalTransactionStateException.class); + }).as(StepVerifier::create).verifyError(IllegalTransactionStateException.class); verify(connectionMock).rollbackTransaction(); verify(connectionMock).close(); @@ -383,32 +398,49 @@ void testPropagationSupportsAndRequiresNew() { DefaultTransactionDefinition definition = new DefaultTransactionDefinition(); definition.setPropagationBehavior(TransactionDefinition.PROPAGATION_SUPPORTS); - TransactionalOperator operator = TransactionalOperator.create(tm, definition); + TransactionalOperator operator = TransactionalOperator.create(tm, definition); operator.execute(tx1 -> { - assertThat(tx1.isNewTransaction()).isFalse(); - DefaultTransactionDefinition innerDef = new DefaultTransactionDefinition(); - innerDef.setPropagationBehavior( - TransactionDefinition.PROPAGATION_REQUIRES_NEW); + innerDef.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); TransactionalOperator inner = TransactionalOperator.create(tm, innerDef); - return inner.execute(tx2 -> { - assertThat(tx2.isNewTransaction()).isTrue(); return Mono.empty(); }); - }).as(StepVerifier::create) - .verifyComplete(); + }).as(StepVerifier::create).verifyComplete(); verify(connectionMock).commitTransaction(); verify(connectionMock).close(); } + @Test + void testPropagationSupportsAndRequiresNewWithRollback() { + when(connectionMock.rollbackTransaction()).thenReturn(Mono.empty()); - private static class TestTransactionSynchronization - implements TransactionSynchronization { + DefaultTransactionDefinition definition = new DefaultTransactionDefinition(); + definition.setPropagationBehavior(TransactionDefinition.PROPAGATION_SUPPORTS); + + TransactionalOperator operator = TransactionalOperator.create(tm, definition); + operator.execute(tx1 -> { + assertThat(tx1.isNewTransaction()).isFalse(); + DefaultTransactionDefinition innerDef = new DefaultTransactionDefinition(); + innerDef.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); + TransactionalOperator inner = TransactionalOperator.create(tm, innerDef); + return inner.execute(tx2 -> { + assertThat(tx2.isNewTransaction()).isTrue(); + tx2.setRollbackOnly(); + return Mono.empty(); + }); + }).as(StepVerifier::create).verifyComplete(); + + verify(connectionMock).rollbackTransaction(); + verify(connectionMock).close(); + } + + + private static class TestTransactionSynchronization implements TransactionSynchronization { private int status; @@ -481,7 +513,6 @@ protected void doAfterCompletion(int status) { this.afterCompletionCalled = true; assertThat(status).isEqualTo(this.status); } - } } diff --git a/spring-r2dbc/src/test/java/org/springframework/r2dbc/connection/TransactionAwareConnectionFactoryProxyUnitTests.java b/spring-r2dbc/src/test/java/org/springframework/r2dbc/connection/TransactionAwareConnectionFactoryProxyUnitTests.java index 83a0d13e9431..15fafccafe8a 100644 --- a/spring-r2dbc/src/test/java/org/springframework/r2dbc/connection/TransactionAwareConnectionFactoryProxyUnitTests.java +++ b/spring-r2dbc/src/test/java/org/springframework/r2dbc/connection/TransactionAwareConnectionFactoryProxyUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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. @@ -54,14 +54,16 @@ class TransactionAwareConnectionFactoryProxyUnitTests { R2dbcTransactionManager tm; + @BeforeEach - @SuppressWarnings({ "rawtypes", "unchecked" }) + @SuppressWarnings({"rawtypes", "unchecked"}) void before() { when(connectionFactoryMock.create()).thenReturn((Mono) Mono.just(connectionMock1), (Mono) Mono.just(connectionMock2), (Mono) Mono.just(connectionMock3)); tm = new R2dbcTransactionManager(connectionFactoryMock); } + @Test void createShouldWrapConnection() { new TransactionAwareConnectionFactoryProxy(connectionFactoryMock).create() @@ -143,14 +145,15 @@ void shouldEmitBoundConnection() { ConnectionFactoryUtils.getConnection(connectionFactoryMock) .doOnNext(transactionalConnection::set).flatMap(connection -> proxyCf.create() .doOnNext(wrappedConnection -> assertThat(((Wrapped) wrappedConnection).unwrap()).isSameAs(connection))) - .as(rxtx::transactional) .flatMapMany(Connection::close) + .as(rxtx::transactional) .as(StepVerifier::create) .verifyComplete(); + verify(connectionFactoryMock, times(1)).create(); + verify(connectionMock1, times(1)).close(); verifyNoInteractions(connectionMock2); verifyNoInteractions(connectionMock3); - verify(connectionFactoryMock, times(1)).create(); } } diff --git a/spring-r2dbc/src/test/java/org/springframework/r2dbc/connection/lookup/MapConnectionFactoryLookupUnitTests.java b/spring-r2dbc/src/test/java/org/springframework/r2dbc/connection/lookup/MapConnectionFactoryLookupUnitTests.java index bf2dcd5ff6dc..40ac7940094b 100644 --- a/spring-r2dbc/src/test/java/org/springframework/r2dbc/connection/lookup/MapConnectionFactoryLookupUnitTests.java +++ b/spring-r2dbc/src/test/java/org/springframework/r2dbc/connection/lookup/MapConnectionFactoryLookupUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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. @@ -34,30 +34,27 @@ public class MapConnectionFactoryLookupUnitTests { private static final String CONNECTION_FACTORY_NAME = "connectionFactory"; + @Test public void getConnectionFactoriesReturnsUnmodifiableMap() { MapConnectionFactoryLookup lookup = new MapConnectionFactoryLookup(); Map connectionFactories = lookup.getConnectionFactories(); - assertThatThrownBy(() -> connectionFactories.put("", - new DummyConnectionFactory())).isInstanceOf( - UnsupportedOperationException.class); + assertThatThrownBy(() -> connectionFactories.put("", new DummyConnectionFactory())) + .isInstanceOf(UnsupportedOperationException.class); } @Test public void shouldLookupConnectionFactory() { Map connectionFactories = new HashMap<>(); DummyConnectionFactory expectedConnectionFactory = new DummyConnectionFactory(); - connectionFactories.put(CONNECTION_FACTORY_NAME, expectedConnectionFactory); - MapConnectionFactoryLookup lookup = new MapConnectionFactoryLookup(); + MapConnectionFactoryLookup lookup = new MapConnectionFactoryLookup(); lookup.setConnectionFactories(connectionFactories); - ConnectionFactory connectionFactory = lookup.getConnectionFactory( - CONNECTION_FACTORY_NAME); - - assertThat(connectionFactory).isNotNull().isSameAs(expectedConnectionFactory); + assertThat(lookup.getConnectionFactory(CONNECTION_FACTORY_NAME)) + .isNotNull().isSameAs(expectedConnectionFactory); } @Test @@ -68,27 +65,22 @@ public void addingConnectionFactoryPermitsOverride() { connectionFactories.put(CONNECTION_FACTORY_NAME, overriddenConnectionFactory); MapConnectionFactoryLookup lookup = new MapConnectionFactoryLookup(); - lookup.setConnectionFactories(connectionFactories); lookup.addConnectionFactory(CONNECTION_FACTORY_NAME, expectedConnectionFactory); - ConnectionFactory connectionFactory = lookup.getConnectionFactory( - CONNECTION_FACTORY_NAME); - - assertThat(connectionFactory).isNotNull().isSameAs(expectedConnectionFactory); + assertThat(lookup.getConnectionFactory(CONNECTION_FACTORY_NAME)) + .isNotNull().isSameAs(expectedConnectionFactory); } @Test - @SuppressWarnings({ "unchecked", "rawtypes" }) + @SuppressWarnings({"rawtypes", "unchecked"}) public void getConnectionFactoryWhereSuppliedMapHasNonConnectionFactoryTypeUnderSpecifiedKey() { Map connectionFactories = new HashMap<>(); connectionFactories.put(CONNECTION_FACTORY_NAME, new Object()); - MapConnectionFactoryLookup lookup = new MapConnectionFactoryLookup( - connectionFactories); + MapConnectionFactoryLookup lookup = new MapConnectionFactoryLookup(connectionFactories); - assertThatThrownBy( - () -> lookup.getConnectionFactory(CONNECTION_FACTORY_NAME)).isInstanceOf( - ClassCastException.class); + assertThatThrownBy(() -> lookup.getConnectionFactory(CONNECTION_FACTORY_NAME)) + .isInstanceOf(ClassCastException.class); } @Test @@ -99,4 +91,5 @@ public void getConnectionFactoryWhereSuppliedMapHasNoEntryForSpecifiedKey() { () -> lookup.getConnectionFactory(CONNECTION_FACTORY_NAME)).isInstanceOf( ConnectionFactoryLookupFailureException.class); } + } diff --git a/spring-r2dbc/src/test/java/org/springframework/r2dbc/core/DefaultDatabaseClientUnitTests.java b/spring-r2dbc/src/test/java/org/springframework/r2dbc/core/DefaultDatabaseClientUnitTests.java index 6f03e82bd424..e6b0e09cb6d3 100644 --- a/spring-r2dbc/src/test/java/org/springframework/r2dbc/core/DefaultDatabaseClientUnitTests.java +++ b/spring-r2dbc/src/test/java/org/springframework/r2dbc/core/DefaultDatabaseClientUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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,8 @@ package org.springframework.r2dbc.core; import java.util.Arrays; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; import io.r2dbc.spi.Connection; import io.r2dbc.spi.ConnectionFactory; @@ -64,6 +66,7 @@ * @author Mark Paluch * @author Ferdinand Jacobs * @author Jens Schauder + * @author Simon Baslé */ @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) @@ -74,8 +77,9 @@ class DefaultDatabaseClientUnitTests { private DatabaseClient.Builder databaseClientBuilder; + @BeforeEach - @SuppressWarnings({ "rawtypes", "unchecked" }) + @SuppressWarnings({"rawtypes", "unchecked"}) void before() { ConnectionFactory connectionFactory = mock(ConnectionFactory.class); @@ -86,6 +90,7 @@ void before() { connectionFactory).bindMarkers(BindMarkersFactory.indexed("$", 1)); } + @Test void connectionFactoryIsExposed() { ConnectionFactory connectionFactory = mock(ConnectionFactory.class); @@ -397,6 +402,47 @@ void shouldApplySimpleStatementFilterFunctions() { inOrder.verifyNoMoreInteractions(); } + @Test + void sqlSupplierInvocationIsDeferredUntilSubscription() { + // We'll have either 2 or 3 rows, depending on the subscription and the generated SQL + MockRowMetadata metadata = MockRowMetadata.builder().columnMetadata( + MockColumnMetadata.builder().name("id").javaType(Integer.class).build()).build(); + final MockRow row1 = MockRow.builder().identified("id", Integer.class, 1).build(); + final MockRow row2 = MockRow.builder().identified("id", Integer.class, 2).build(); + final MockRow row3 = MockRow.builder().identified("id", Integer.class, 3).build(); + // Set up 2 mock statements + mockStatementFor("SELECT id FROM test WHERE id < '3'", MockResult.builder() + .rowMetadata(metadata) + .row(row1, row2).build()); + mockStatementFor("SELECT id FROM test WHERE id < '4'", MockResult.builder() + .rowMetadata(metadata) + .row(row1, row2, row3).build()); + // Create the client + DatabaseClient databaseClient = this.databaseClientBuilder.build(); + + AtomicInteger invoked = new AtomicInteger(); + // Assemble a publisher, but don't subscribe yet + Mono> operation = databaseClient + .sql(() -> { + int idMax = 2 + invoked.incrementAndGet(); + return String.format("SELECT id FROM test WHERE id < '%s'", idMax); + }) + .map(r -> r.get("id", Integer.class)) + .all() + .collectList(); + + assertThat(invoked).as("invoked (before subscription)").hasValue(0); + + List rows = operation.block(); + assertThat(invoked).as("invoked (after 1st subscription)").hasValue(1); + assertThat(rows).containsExactly(1, 2); + + rows = operation.block(); + assertThat(invoked).as("invoked (after 2nd subscription)").hasValue(2); + assertThat(rows).containsExactly(1, 2, 3); + } + + private Statement mockStatement() { return mockStatementFor(null, null); } diff --git a/spring-r2dbc/src/test/java/org/springframework/r2dbc/core/NamedParameterUtilsUnitTests.java b/spring-r2dbc/src/test/java/org/springframework/r2dbc/core/NamedParameterUtilsUnitTests.java index c26ba5acb6c3..83038dcc14a2 100644 --- a/spring-r2dbc/src/test/java/org/springframework/r2dbc/core/NamedParameterUtilsUnitTests.java +++ b/spring-r2dbc/src/test/java/org/springframework/r2dbc/core/NamedParameterUtilsUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -82,8 +82,9 @@ public void substituteNamedParameters() { @Test public void substituteObjectArray() { MapBindParameterSource namedParams = new MapBindParameterSource(new HashMap<>()); - namedParams.addValue("a", Arrays.asList(new Object[] { "Walter", "Heisenberg" }, - new Object[] { "Walt Jr.", "Flynn" })); + namedParams.addValue("a", + Arrays.asList(new Object[] {"Walter", "Heisenberg"}, + new Object[] {"Walt Jr.", "Flynn"})); PreparedOperation operation = NamedParameterUtils.substituteNamedParameters( "xxx :a", BIND_MARKERS, namedParams); @@ -94,8 +95,9 @@ public void substituteObjectArray() { @Test public void shouldBindObjectArray() { MapBindParameterSource namedParams = new MapBindParameterSource(new HashMap<>()); - namedParams.addValue("a", Arrays.asList(new Object[] { "Walter", "Heisenberg" }, - new Object[] { "Walt Jr.", "Flynn" })); + namedParams.addValue("a", + Arrays.asList(new Object[] {"Walter", "Heisenberg"}, + new Object[] {"Walt Jr.", "Flynn"})); BindTarget bindTarget = mock(BindTarget.class); diff --git a/spring-r2dbc/src/test/java/org/springframework/r2dbc/core/binding/IndexedBindMarkersUnitTests.java b/spring-r2dbc/src/test/java/org/springframework/r2dbc/core/binding/IndexedBindMarkersUnitTests.java index 9bc389d71887..e94b2cb512a8 100644 --- a/spring-r2dbc/src/test/java/org/springframework/r2dbc/core/binding/IndexedBindMarkersUnitTests.java +++ b/spring-r2dbc/src/test/java/org/springframework/r2dbc/core/binding/IndexedBindMarkersUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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. @@ -32,7 +32,6 @@ class IndexedBindMarkersUnitTests { @Test void shouldCreateNewBindMarkers() { BindMarkersFactory factory = BindMarkersFactory.indexed("$", 0); - BindMarkers bindMarkers1 = factory.create(); BindMarkers bindMarkers2 = factory.create(); @@ -43,7 +42,6 @@ void shouldCreateNewBindMarkers() { @Test void shouldCreateNewBindMarkersWithOffset() { BindTarget bindTarget = mock(BindTarget.class); - BindMarkers bindMarkers = BindMarkersFactory.indexed("$", 1).create(); BindMarker first = bindMarkers.next(); @@ -60,10 +58,9 @@ void shouldCreateNewBindMarkersWithOffset() { @Test void nextShouldIncrementBindMarker() { - String[] prefixes = { "$", "?" }; + String[] prefixes = {"$", "?"}; for (String prefix : prefixes) { - BindMarkers bindMarkers = BindMarkersFactory.indexed(prefix, 0).create(); BindMarker marker1 = bindMarkers.next(); @@ -76,9 +73,7 @@ void nextShouldIncrementBindMarker() { @Test void bindValueShouldBindByIndex() { - BindTarget bindTarget = mock(BindTarget.class); - BindMarkers bindMarkers = BindMarkersFactory.indexed("$", 0).create(); bindMarkers.next().bind(bindTarget, "foo"); @@ -91,7 +86,6 @@ void bindValueShouldBindByIndex() { @Test void bindNullShouldBindByIndex() { BindTarget bindTarget = mock(BindTarget.class); - BindMarkers bindMarkers = BindMarkersFactory.indexed("$", 0).create(); bindMarkers.next(); // ignore diff --git a/spring-r2dbc/src/test/java/org/springframework/r2dbc/core/binding/NamedBindMarkersUnitTests.java b/spring-r2dbc/src/test/java/org/springframework/r2dbc/core/binding/NamedBindMarkersUnitTests.java index d83691e5b2d7..5c2f56d08904 100644 --- a/spring-r2dbc/src/test/java/org/springframework/r2dbc/core/binding/NamedBindMarkersUnitTests.java +++ b/spring-r2dbc/src/test/java/org/springframework/r2dbc/core/binding/NamedBindMarkersUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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. @@ -43,7 +43,7 @@ void shouldCreateNewBindMarkers() { } @ParameterizedTest - @ValueSource(strings = { "$", "?" }) + @ValueSource(strings = {"$", "?"}) void nextShouldIncrementBindMarker(String prefix) { BindMarkers bindMarkers = BindMarkersFactory.named(prefix, "p", 32).create(); diff --git a/spring-test/src/main/java/org/springframework/mock/env/MockEnvironment.java b/spring-test/src/main/java/org/springframework/mock/env/MockEnvironment.java index 8495e7c37b06..88072db943d3 100644 --- a/spring-test/src/main/java/org/springframework/mock/env/MockEnvironment.java +++ b/spring-test/src/main/java/org/springframework/mock/env/MockEnvironment.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2023 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,8 +21,7 @@ /** * Simple {@link ConfigurableEnvironment} implementation exposing - * {@link #setProperty(String, String)} and {@link #withProperty(String, String)} - * methods for testing purposes. + * {@link #setProperty} and {@link #withProperty} methods for testing purposes. * * @author Chris Beams * @author Sam Brannen @@ -31,7 +30,8 @@ */ public class MockEnvironment extends AbstractEnvironment { - private MockPropertySource propertySource = new MockPropertySource(); + private final MockPropertySource propertySource = new MockPropertySource(); + /** * Create a new {@code MockEnvironment} with a single {@link MockPropertySource}. @@ -40,6 +40,7 @@ public MockEnvironment() { getPropertySources().addLast(this.propertySource); } + /** * Set a property on the underlying {@link MockPropertySource} for this environment. */ @@ -54,7 +55,7 @@ public void setProperty(String key, String value) { * @see MockPropertySource#withProperty */ public MockEnvironment withProperty(String key, String value) { - this.setProperty(key, value); + setProperty(key, value); return this; } diff --git a/spring-test/src/main/java/org/springframework/mock/env/MockPropertySource.java b/spring-test/src/main/java/org/springframework/mock/env/MockPropertySource.java index 2f3eb44151f8..3ef180fcf22b 100644 --- a/spring-test/src/main/java/org/springframework/mock/env/MockPropertySource.java +++ b/spring-test/src/main/java/org/springframework/mock/env/MockPropertySource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2023 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. @@ -48,6 +48,7 @@ public class MockPropertySource extends PropertiesPropertySource { */ public static final String MOCK_PROPERTIES_PROPERTY_SOURCE_NAME = "mockProperties"; + /** * Create a new {@code MockPropertySource} named {@value #MOCK_PROPERTIES_PROPERTY_SOURCE_NAME} * that will maintain its own internal {@link Properties} instance. @@ -84,6 +85,7 @@ public MockPropertySource(String name, Properties properties) { super(name, properties); } + /** * Set the given property on the underlying {@link Properties} object. */ @@ -97,7 +99,7 @@ public void setProperty(String name, Object value) { * @return this {@link MockPropertySource} instance */ public MockPropertySource withProperty(String name, Object value) { - this.setProperty(name, value); + setProperty(name, value); return this; } diff --git a/spring-test/src/main/java/org/springframework/mock/web/MockHttpServletRequest.java b/spring-test/src/main/java/org/springframework/mock/web/MockHttpServletRequest.java index f34a6bda5fef..7d3981f5320d 100644 --- a/spring-test/src/main/java/org/springframework/mock/web/MockHttpServletRequest.java +++ b/spring-test/src/main/java/org/springframework/mock/web/MockHttpServletRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-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. @@ -60,7 +60,6 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; -import org.springframework.lang.NonNull; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.LinkedCaseInsensitiveMap; @@ -990,7 +989,7 @@ public void setCookies(@Nullable Cookie... cookies) { } } - private static String encodeCookies(@NonNull Cookie... cookies) { + private static String encodeCookies(Cookie... cookies) { return Arrays.stream(cookies) .map(c -> c.getName() + '=' + (c.getValue() == null ? "" : c.getValue())) .collect(Collectors.joining("; ")); diff --git a/spring-test/src/main/java/org/springframework/test/context/DynamicPropertySource.java b/spring-test/src/main/java/org/springframework/test/context/DynamicPropertySource.java index 0474f9856bda..57732968e75e 100644 --- a/spring-test/src/main/java/org/springframework/test/context/DynamicPropertySource.java +++ b/spring-test/src/main/java/org/springframework/test/context/DynamicPropertySource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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. @@ -70,14 +70,15 @@ * class ExampleIntegrationTests { * * @Container - * static RedisContainer redis = new RedisContainer(); + * static GenericContainer redis = + * new GenericContainer("redis:5.0.3-alpine").withExposedPorts(6379); * * // ... * * @DynamicPropertySource * static void redisProperties(DynamicPropertyRegistry registry) { - * registry.add("redis.host", redis::getContainerIpAddress); - * registry.add("redis.port", redis::getMappedPort); + * registry.add("redis.host", redis::getHost); + * registry.add("redis.port", redis::getFirstMappedPort); * } * * } diff --git a/spring-test/src/main/java/org/springframework/test/context/jdbc/SqlScriptsTestExecutionListener.java b/spring-test/src/main/java/org/springframework/test/context/jdbc/SqlScriptsTestExecutionListener.java index 6f1ca25e97ad..48ad7a0d0788 100644 --- a/spring-test/src/main/java/org/springframework/test/context/jdbc/SqlScriptsTestExecutionListener.java +++ b/spring-test/src/main/java/org/springframework/test/context/jdbc/SqlScriptsTestExecutionListener.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -31,7 +31,6 @@ import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator; -import org.springframework.lang.NonNull; import org.springframework.lang.Nullable; import org.springframework.test.context.TestContext; import org.springframework.test.context.TestContextAnnotationUtils; @@ -280,7 +279,6 @@ private void executeSqlScripts( } } - @NonNull private ResourceDatabasePopulator createDatabasePopulator(MergedSqlConfig mergedSqlConfig) { ResourceDatabasePopulator populator = new ResourceDatabasePopulator(); populator.setSqlScriptEncoding(mergedSqlConfig.getEncoding()); diff --git a/spring-test/src/main/java/org/springframework/test/context/support/AbstractGenericContextLoader.java b/spring-test/src/main/java/org/springframework/test/context/support/AbstractGenericContextLoader.java index 6b94938bb9f1..b5b99405fb65 100644 --- a/spring-test/src/main/java/org/springframework/test/context/support/AbstractGenericContextLoader.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/AbstractGenericContextLoader.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 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-test/src/main/java/org/springframework/test/context/support/DynamicPropertiesContextCustomizer.java b/spring-test/src/main/java/org/springframework/test/context/support/DynamicPropertiesContextCustomizer.java index d7093a066a7a..b419f0b9e412 100644 --- a/spring-test/src/main/java/org/springframework/test/context/support/DynamicPropertiesContextCustomizer.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/DynamicPropertiesContextCustomizer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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. @@ -46,7 +46,6 @@ class DynamicPropertiesContextCustomizer implements ContextCustomizer { private static final String PROPERTY_SOURCE_NAME = "Dynamic Test Properties"; - private final Set methods; @@ -65,9 +64,7 @@ private void assertValid(Method method) { } @Override - public void customizeContext(ConfigurableApplicationContext context, - MergedContextConfiguration mergedConfig) { - + public void customizeContext(ConfigurableApplicationContext context, MergedContextConfiguration mergedConfig) { MutablePropertySources sources = context.getEnvironment().getPropertySources(); sources.addFirst(new DynamicValuesPropertySource(PROPERTY_SOURCE_NAME, buildDynamicPropertiesMap())); } @@ -90,10 +87,6 @@ Set getMethods() { return this.methods; } - @Override - public int hashCode() { - return this.methods.hashCode(); - } @Override public boolean equals(Object obj) { @@ -106,4 +99,9 @@ public boolean equals(Object obj) { return this.methods.equals(((DynamicPropertiesContextCustomizer) obj).methods); } + @Override + public int hashCode() { + return this.methods.hashCode(); + } + } diff --git a/spring-test/src/main/java/org/springframework/test/context/support/DynamicPropertiesContextCustomizerFactory.java b/spring-test/src/main/java/org/springframework/test/context/support/DynamicPropertiesContextCustomizerFactory.java index 11779cc12339..7f9e3da7798a 100644 --- a/spring-test/src/main/java/org/springframework/test/context/support/DynamicPropertiesContextCustomizerFactory.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/DynamicPropertiesContextCustomizerFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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. @@ -35,6 +35,7 @@ * * @author Phillip Webb * @author Sam Brannen + * @author Yanming Zhou * @since 5.2.5 * @see DynamicPropertiesContextCustomizer */ @@ -54,10 +55,15 @@ public DynamicPropertiesContextCustomizer createContextCustomizer(Class testC } private void findMethods(Class testClass, Set methods) { - methods.addAll(MethodIntrospector.selectMethods(testClass, this::isAnnotated)); + // Beginning with Java 16, inner classes may contain static members. + // We therefore need to search for @DynamicPropertySource methods in the + // current class after searching enclosing classes so that a local + // @DynamicPropertySource method can override properties registered in + // an enclosing class. if (TestContextAnnotationUtils.searchEnclosingClass(testClass)) { findMethods(testClass.getEnclosingClass(), methods); } + methods.addAll(MethodIntrospector.selectMethods(testClass, this::isAnnotated)); } private boolean isAnnotated(Method method) { diff --git a/spring-test/src/main/java/org/springframework/test/web/client/MockMvcClientHttpRequestFactory.java b/spring-test/src/main/java/org/springframework/test/web/client/MockMvcClientHttpRequestFactory.java index 42c7a03cefff..136cf2250235 100644 --- a/spring-test/src/main/java/org/springframework/test/web/client/MockMvcClientHttpRequestFactory.java +++ b/spring-test/src/main/java/org/springframework/test/web/client/MockMvcClientHttpRequestFactory.java @@ -35,7 +35,6 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.request; - /** * A {@link ClientHttpRequestFactory} for requests executed via {@link MockMvc}. * diff --git a/spring-test/src/main/java/org/springframework/test/web/client/match/ContentRequestMatchers.java b/spring-test/src/main/java/org/springframework/test/web/client/match/ContentRequestMatchers.java index dd91496fa5df..dd1770c5b667 100644 --- a/spring-test/src/main/java/org/springframework/test/web/client/match/ContentRequestMatchers.java +++ b/spring-test/src/main/java/org/springframework/test/web/client/match/ContentRequestMatchers.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 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-test/src/main/java/org/springframework/test/web/client/match/MockRestRequestMatchers.java b/spring-test/src/main/java/org/springframework/test/web/client/match/MockRestRequestMatchers.java index ff3f6c27a7df..efa97c27194c 100644 --- a/spring-test/src/main/java/org/springframework/test/web/client/match/MockRestRequestMatchers.java +++ b/spring-test/src/main/java/org/springframework/test/web/client/match/MockRestRequestMatchers.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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. @@ -47,6 +47,7 @@ * @author Craig Walls * @author Rossen Stoyanchev * @author Sam Brannen + * @author Simon Baslé * @since 3.2 */ public abstract class MockRestRequestMatchers { @@ -112,8 +113,49 @@ public static RequestMatcher requestTo(URI uri) { return request -> assertEquals("Unexpected request", uri, request.getURI()); } + /** + * Assert request query parameter values with the given Hamcrest matcher, + * matching on the entire {@link List} of values. + *

    For example, this can be used to check that the list of query parameter + * values has at least one value matching a given Hamcrest matcher (such as + * {@link org.hamcrest.Matchers#hasItem(Matcher)}), that every value in the list + * matches common criteria (such as {@link org.hamcrest.Matchers#everyItem(Matcher)}), + * that each value in the list matches corresponding dedicated criteria + * (such as {@link org.hamcrest.Matchers#contains(Matcher[])}), etc. + * @param name the name of the query parameter whose value(s) will be asserted + * @param matcher the Hamcrest matcher to apply to the entire list of values + * for the given query parameter + * @since 5.3.27 + * @see #queryParam(String, Matcher...) + * @see #queryParam(String, String...) + */ + public static RequestMatcher queryParamList(String name, Matcher> matcher) { + return request -> { + MultiValueMap params = getQueryParams(request); + List paramValues = params.get(name); + if (paramValues == null) { + fail("Expected query param <" + name + "> to exist but was null"); + } + assertThat("Query param [" + name + "] values", paramValues, matcher); + }; + } + /** * Assert request query parameter values with the given Hamcrest matcher(s). + *

    If the query parameter value list is larger than the number of provided + * {@code matchers}, no matchers will be applied to the extra query parameter + * values, effectively ignoring the additional parameter values. If the number + * of provided {@code matchers} exceeds the number of query parameter values, + * an {@link AssertionError} will be thrown to signal the mismatch. + *

    See {@link #queryParamList(String, Matcher)} for a variant which accepts a + * {@code Matcher} that applies to the entire list of values as opposed to + * applying only to individual values. + * @param name the name of the query parameter whose value(s) will be asserted + * @param matchers the Hamcrest matchers to apply to individual query parameter + * values; the nth matcher is applied to the nth query + * parameter value + * @see #queryParamList(String, Matcher) + * @see #queryParam(String, String...) */ @SafeVarargs public static RequestMatcher queryParam(String name, Matcher... matchers) { @@ -128,6 +170,20 @@ public static RequestMatcher queryParam(String name, Matcher... /** * Assert request query parameter values. + *

    If the query parameter value list is larger than the number of + * {@code expectedValues}, no assertions will be applied to the extra query + * parameter values, effectively ignoring the additional parameter values. If + * the number of {@code expectedValues} exceeds the number of query parameter + * values, an {@link AssertionError} will be thrown to signal the mismatch. + *

    See {@link #queryParamList(String, Matcher)} for a variant which accepts a + * Hamcrest {@code Matcher} that applies to the entire list of values as opposed + * to asserting only individual values. + * @param name the name of the query parameter whose value(s) will be asserted + * @param expectedValues the expected values of individual query parameter values; + * the nth expected value is compared to the nth query + * parameter value + * @see #queryParamList(String, Matcher) + * @see #queryParam(String, Matcher...) */ public static RequestMatcher queryParam(String name, String... expectedValues) { return request -> { @@ -143,21 +199,47 @@ private static MultiValueMap getQueryParams(ClientHttpRequest re return UriComponentsBuilder.fromUri(request.getURI()).build().getQueryParams(); } - private static void assertValueCount( - String valueType, String name, MultiValueMap map, int count) { - - List values = map.get(name); - String message = "Expected " + valueType + " <" + name + ">"; - if (values == null) { - fail(message + " to exist but was null"); - } - if (count > values.size()) { - fail(message + " to have at least <" + count + "> values but found " + values); - } + /** + * Assert request header values with the given Hamcrest matcher, matching on + * the entire {@link List} of values. + *

    For example, this can be used to check that the list of header values + * has at least one value matching a given Hamcrest matcher (such as + * {@link org.hamcrest.Matchers#hasItem(Matcher)}), that every value in the list + * matches common criteria (such as {@link org.hamcrest.Matchers#everyItem(Matcher)}), + * that each value in the list matches corresponding dedicated criteria + * (such as {@link org.hamcrest.Matchers#contains(Matcher[])}), etc. + * @param name the name of the header whose value(s) will be asserted + * @param matcher the Hamcrest matcher to apply to the entire list of values + * for the given header + * @since 5.3.27 + * @see #header(String, Matcher...) + * @see #header(String, String...) + */ + public static RequestMatcher headerList(String name, Matcher> matcher) { + return request -> { + List headerValues = request.getHeaders().get(name); + if (headerValues == null) { + fail("Expected header <" + name + "> to exist but was null"); + } + assertThat("Request header [" + name + "] values", headerValues, matcher); + }; } /** * Assert request header values with the given Hamcrest matcher(s). + *

    If the header value list is larger than the number of provided + * {@code matchers}, no matchers will be applied to the extra header values, + * effectively ignoring the additional header values. If the number of + * provided {@code matchers} exceeds the number of header values, an + * {@link AssertionError} will be thrown to signal the mismatch. + *

    See {@link #headerList(String, Matcher)} for a variant which accepts a + * Hamcrest {@code Matcher} that applies to the entire list of values as + * opposed to applying only to individual values. + * @param name the name of the header whose value(s) will be asserted + * @param matchers the Hamcrest matchers to apply to individual header values; + * the nth matcher is applied to the nth header value + * @see #headerList(String, Matcher) + * @see #header(String, String...) */ @SafeVarargs public static RequestMatcher header(String name, Matcher... matchers) { @@ -173,6 +255,19 @@ public static RequestMatcher header(String name, Matcher... matc /** * Assert request header values. + *

    If the header value list is larger than the number of {@code expectedValues}, + * no matchers will be applied to the extra header values, effectively ignoring the + * additional header values. If the number of {@code expectedValues} exceeds the + * number of header values, an {@link AssertionError} will be thrown to signal the + * mismatch. + *

    See {@link #headerList(String, Matcher)} for a variant which accepts a + * Hamcrest {@code Matcher} that applies to the entire list of values as + * opposed to applying only to individual values. + * @param name the name of the header whose value(s) will be asserted + * @param expectedValues the expected values of individual header values; the + * nth expected value is compared to the nth header value + * @see #headerList(String, Matcher) + * @see #header(String, Matcher...) */ public static RequestMatcher header(String name, String... expectedValues) { return request -> { @@ -258,4 +353,18 @@ public static XpathRequestMatchers xpath(String expression, Map return new XpathRequestMatchers(expression, namespaces, args); } + + private static void assertValueCount( + String valueType, String name, MultiValueMap map, int count) { + + List values = map.get(name); + String message = "Expected " + valueType + " <" + name + ">"; + if (values == null) { + fail(message + " to exist but was null"); + } + if (count > values.size()) { + fail(message + " to have at least <" + count + "> values but found " + values); + } + } + } diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClientBuilder.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClientBuilder.java index 06c0cb11d75f..839ede992465 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClientBuilder.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClientBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 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-test/src/main/java/org/springframework/test/web/servlet/MvcResult.java b/spring-test/src/main/java/org/springframework/test/web/servlet/MvcResult.java index 6245fa0a6163..31b2e15b4721 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/MvcResult.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/MvcResult.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-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. @@ -34,13 +34,13 @@ public interface MvcResult { /** * Return the performed request. - * @return the request, never {@code null} + * @return the request (never {@code null}) */ MockHttpServletRequest getRequest(); /** * Return the resulting response. - * @return the response, never {@code null} + * @return the response (never {@code null}) */ MockHttpServletResponse getResponse(); diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/MockMvcWebConnection.java b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/MockMvcWebConnection.java index 7c67ab3340db..e134a74d26ea 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/MockMvcWebConnection.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/MockMvcWebConnection.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 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. @@ -68,6 +68,8 @@ public final class MockMvcWebConnection implements WebConnection { private WebClient webClient; + private static int MAX_FORWARDS = 100; + /** * Create a new instance that assumes the context path of the application @@ -133,10 +135,15 @@ public WebResponse getResponse(WebRequest webRequest) throws IOException { MockHttpServletResponse httpServletResponse = getResponse(requestBuilder); String forwardedUrl = httpServletResponse.getForwardedUrl(); - while (forwardedUrl != null) { + int forwards = 0; + while (forwardedUrl != null && forwards < MAX_FORWARDS) { requestBuilder.setForwardPostProcessor(new ForwardRequestPostProcessor(forwardedUrl)); httpServletResponse = getResponse(requestBuilder); forwardedUrl = httpServletResponse.getForwardedUrl(); + forwards += 1; + } + if (forwards == MAX_FORWARDS) { + throw new IllegalStateException("Forwarded more than " + forwards + " times in a row, potential infinite forward loop"); } storeCookies(webRequest, httpServletResponse.getCookies()); diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/result/ContentResultMatchers.java b/spring-test/src/main/java/org/springframework/test/web/servlet/result/ContentResultMatchers.java index 79ecb78127e2..9703efa48cdc 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/result/ContentResultMatchers.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/result/ContentResultMatchers.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 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-test/src/main/java/org/springframework/test/web/servlet/result/XpathResultMatchers.java b/spring-test/src/main/java/org/springframework/test/web/servlet/result/XpathResultMatchers.java index 4528d2b40d4d..0d1af749a312 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/result/XpathResultMatchers.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/result/XpathResultMatchers.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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 @@ public ResultMatcher nodeList(Matcher matcher) { } /** - * Get the response encoding if explicitly defined in the response, {code null} otherwise. + * Get the response encoding if explicitly defined in the response, {@code null} otherwise. */ @Nullable private String getDefinedEncoding(MockHttpServletResponse response) { diff --git a/spring-test/src/main/kotlin/org/springframework/test/web/servlet/MockMvcExtensions.kt b/spring-test/src/main/kotlin/org/springframework/test/web/servlet/MockMvcExtensions.kt index 83894e34092d..222c9e1f3725 100644 --- a/spring-test/src/main/kotlin/org/springframework/test/web/servlet/MockMvcExtensions.kt +++ b/spring-test/src/main/kotlin/org/springframework/test/web/servlet/MockMvcExtensions.kt @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 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. @@ -224,6 +224,18 @@ fun MockMvc.multipart(urlTemplate: String, vararg vars: Any?, dsl: MockMultipart return MockMultipartHttpServletRequestDsl(requestBuilder).apply(dsl).perform(this) } +/** + * [MockMvc] extension providing access to [MockMultipartHttpServletRequestDsl] Kotlin DSL. + * + * @see MockMvcRequestBuilders.multipart + * @author Sebastien Deleuze + * @since 5.3.26 + */ +fun MockMvc.multipart(httpMethod: HttpMethod, urlTemplate: String, vararg vars: Any?, dsl: MockMultipartHttpServletRequestDsl.() -> Unit = {}): ResultActionsDsl { + val requestBuilder = MockMvcRequestBuilders.multipart(httpMethod, urlTemplate, *vars) + return MockMultipartHttpServletRequestDsl(requestBuilder).apply(dsl).perform(this) +} + /** * [MockMvc] extension providing access to [MockMultipartHttpServletRequestDsl] Kotlin DSL. * @@ -235,3 +247,15 @@ fun MockMvc.multipart(uri: URI, dsl: MockMultipartHttpServletRequestDsl.() -> Un val requestBuilder = MockMvcRequestBuilders.multipart(uri) return MockMultipartHttpServletRequestDsl(requestBuilder).apply(dsl).perform(this) } + +/** + * [MockMvc] extension providing access to [MockMultipartHttpServletRequestDsl] Kotlin DSL. + * + * @see MockMvcRequestBuilders.multipart + * @author Sebastien Deleuze + * @since 5.3.26 + */ +fun MockMvc.multipart(httpMethod: HttpMethod, uri: URI, dsl: MockMultipartHttpServletRequestDsl.() -> Unit = {}): ResultActionsDsl { + val requestBuilder = MockMvcRequestBuilders.multipart(httpMethod, uri) + return MockMultipartHttpServletRequestDsl(requestBuilder).apply(dsl).perform(this) +} diff --git a/spring-test/src/test/java/org/springframework/test/web/client/match/MockRestRequestMatchersTests.java b/spring-test/src/test/java/org/springframework/test/web/client/match/MockRestRequestMatchersTests.java index 6481a3313bd7..aaecda7d6e91 100644 --- a/spring-test/src/test/java/org/springframework/test/web/client/match/MockRestRequestMatchersTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/client/match/MockRestRequestMatchersTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 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,18 +16,31 @@ package org.springframework.test.web.client.match; +import java.io.IOException; import java.net.URI; import java.util.Arrays; -import java.util.Collections; import java.util.List; +import org.assertj.core.api.ThrowableTypeAssert; import org.junit.jupiter.api.Test; import org.springframework.http.HttpMethod; import org.springframework.mock.http.client.MockClientHttpRequest; -import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.anything; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.endsWith; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.everyItem; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.startsWith; /** * Unit tests for {@link MockRestRequestMatchers}. @@ -35,179 +48,313 @@ * @author Craig Walls * @author Rossen Stoyanchev * @author Sam Brannen + * @author Simon Baslé */ -public class MockRestRequestMatchersTests { +class MockRestRequestMatchersTests { private final MockClientHttpRequest request = new MockClientHttpRequest(); @Test - public void requestTo() throws Exception { + void requestTo() throws Exception { this.request.setURI(new URI("http://www.foo.example/bar")); MockRestRequestMatchers.requestTo("http://www.foo.example/bar").match(this.request); } @Test // SPR-15819 - public void requestToUriTemplate() throws Exception { + void requestToUriTemplate() throws Exception { this.request.setURI(new URI("http://www.foo.example/bar")); MockRestRequestMatchers.requestToUriTemplate("http://www.foo.example/{bar}", "bar").match(this.request); } @Test - public void requestToNoMatch() throws Exception { + void requestToNoMatch() throws Exception { this.request.setURI(new URI("http://www.foo.example/bar")); - assertThatThrownBy( - () -> MockRestRequestMatchers.requestTo("http://www.foo.example/wrong").match(this.request)) - .isInstanceOf(AssertionError.class); + assertThatAssertionError() + .isThrownBy(() -> MockRestRequestMatchers.requestTo("http://www.foo.example/wrong").match(this.request)); } @Test - public void requestToContains() throws Exception { + void requestToContains() throws Exception { this.request.setURI(new URI("http://www.foo.example/bar")); MockRestRequestMatchers.requestTo(containsString("bar")).match(this.request); } @Test - public void method() throws Exception { + void method() throws Exception { this.request.setMethod(HttpMethod.GET); MockRestRequestMatchers.method(HttpMethod.GET).match(this.request); } @Test - public void methodNoMatch() throws Exception { + void methodNoMatch() throws Exception { this.request.setMethod(HttpMethod.POST); - assertThatThrownBy(() -> MockRestRequestMatchers.method(HttpMethod.GET).match(this.request)) - .isInstanceOf(AssertionError.class) - .hasMessageContaining("expected: but was:"); + assertThatAssertionError() + .isThrownBy(() -> MockRestRequestMatchers.method(HttpMethod.GET).match(this.request)) + .withMessageContaining("expected: but was:"); } @Test - public void header() throws Exception { + void header() throws Exception { this.request.getHeaders().put("foo", Arrays.asList("bar", "baz")); MockRestRequestMatchers.header("foo", "bar", "baz").match(this.request); } @Test - public void headerDoesNotExist() throws Exception { + void headerDoesNotExist() throws Exception { MockRestRequestMatchers.headerDoesNotExist(null).match(this.request); MockRestRequestMatchers.headerDoesNotExist("").match(this.request); MockRestRequestMatchers.headerDoesNotExist("foo").match(this.request); List values = Arrays.asList("bar", "baz"); this.request.getHeaders().put("foo", values); - assertThatThrownBy(() -> MockRestRequestMatchers.headerDoesNotExist("foo").match(this.request)) - .isInstanceOf(AssertionError.class) - .hasMessage("Expected header not to exist, but it exists with values: " + values); + assertThatAssertionError() + .isThrownBy(() -> MockRestRequestMatchers.headerDoesNotExist("foo").match(this.request)) + .withMessage("Expected header not to exist, but it exists with values: " + values); } @Test - public void headerMissing() throws Exception { - assertThatThrownBy(() -> MockRestRequestMatchers.header("foo", "bar").match(this.request)) - .isInstanceOf(AssertionError.class) - .hasMessageContaining("was null"); + void headerMissing() { + assertThatAssertionError() + .isThrownBy(() -> MockRestRequestMatchers.header("foo", "bar").match(this.request)) + .withMessageContaining("was null"); } @Test - public void headerMissingValue() throws Exception { + void headerMissingValue() { this.request.getHeaders().put("foo", Arrays.asList("bar", "baz")); - assertThatThrownBy(() -> MockRestRequestMatchers.header("foo", "bad").match(this.request)) - .isInstanceOf(AssertionError.class) - .hasMessageContaining("expected: but was:"); + assertThatAssertionError() + .isThrownBy(() -> MockRestRequestMatchers.header("foo", "bad").match(this.request)) + .withMessageContaining("expected: but was:"); } @Test - public void headerContains() throws Exception { + void headerContains() throws Exception { this.request.getHeaders().put("foo", Arrays.asList("bar", "baz")); MockRestRequestMatchers.header("foo", containsString("ba")).match(this.request); } @Test - public void headerContainsWithMissingHeader() throws Exception { - assertThatThrownBy(() -> MockRestRequestMatchers.header("foo", containsString("baz")).match(this.request)) - .isInstanceOf(AssertionError.class) - .hasMessageContaining("but was null"); + void headerContainsWithMissingHeader() { + assertThatAssertionError() + .isThrownBy(() -> MockRestRequestMatchers.header("foo", containsString("baz")).match(this.request)) + .withMessage("Expected header to exist but was null"); } @Test - public void headerContainsWithMissingValue() throws Exception { + void headerContainsWithMissingValue() { this.request.getHeaders().put("foo", Arrays.asList("bar", "baz")); - assertThatThrownBy(() -> MockRestRequestMatchers.header("foo", containsString("bx")).match(this.request)) - .isInstanceOf(AssertionError.class) - .hasMessageContaining("was \"bar\""); + assertThatAssertionError() + .isThrownBy(() -> MockRestRequestMatchers.header("foo", containsString("bx")).match(this.request)) + .withMessageContaining("was \"bar\""); } @Test - public void headers() throws Exception { + void headerListMissing() { + assertThatAssertionError() + .isThrownBy(() -> MockRestRequestMatchers.headerList("foo", hasSize(2)).match(this.request)) + .withMessage("Expected header to exist but was null"); + } + + @Test + void headerListMatchers() throws IOException { + this.request.getHeaders().put("foo", Arrays.asList("bar", "baz")); + + MockRestRequestMatchers.headerList("foo", containsInAnyOrder(endsWith("baz"), endsWith("bar"))).match(this.request); + MockRestRequestMatchers.headerList("foo", contains(is("bar"), is("baz"))).match(this.request); + MockRestRequestMatchers.headerList("foo", contains(is("bar"), anything())).match(this.request); + MockRestRequestMatchers.headerList("foo", hasItem(endsWith("baz"))).match(this.request); + MockRestRequestMatchers.headerList("foo", everyItem(startsWith("ba"))).match(this.request); + MockRestRequestMatchers.headerList("foo", hasSize(2)).match(this.request); + + MockRestRequestMatchers.headerList("foo", notNullValue()).match(this.request); + MockRestRequestMatchers.headerList("foo", is(anything())).match(this.request); + MockRestRequestMatchers.headerList("foo", allOf(notNullValue(), notNullValue())).match(this.request); + + MockRestRequestMatchers.headerList("foo", allOf(notNullValue(), hasSize(2))).match(this.request); + } + + @Test + void headerListContainsMismatch() { + this.request.getHeaders().put("foo", Arrays.asList("bar", "baz")); + + assertThatAssertionError() + .isThrownBy(() -> MockRestRequestMatchers.headerList("foo", contains(containsString("ba"))).match(this.request)) + .withMessageContainingAll( + "Request header [foo] values", + "Expected: iterable containing [a string containing \"ba\"]", + "but: not matched: \"baz\""); + + assertThatAssertionError() + .isThrownBy(() -> MockRestRequestMatchers.headerList("foo", hasItem(endsWith("ba"))).match(this.request)) + .withMessageContainingAll( + "Request header [foo] values", + "Expected: a collection containing a string ending with \"ba\"", + "but: mismatches were: [was \"bar\", was \"baz\"]"); + + assertThatAssertionError() + .isThrownBy(() -> MockRestRequestMatchers.headerList("foo", everyItem(endsWith("ar"))).match(this.request)) + .withMessageContainingAll( + "Request header [foo] values", + "Expected: every item is a string ending with \"ar\"", + "but: an item was \"baz\""); + } + + @Test + void headerListDoesntHideHeaderWithSingleMatcher() throws IOException { + this.request.getHeaders().put("foo", Arrays.asList("bar", "baz")); + + MockRestRequestMatchers.header("foo", equalTo("bar")).match(this.request); + + assertThatAssertionError() + .isThrownBy(() -> MockRestRequestMatchers.headerList("foo", equalTo("bar")).match(this.request)) + .withMessageContainingAll( + "Request header [foo] values", + "Expected: \"bar\"", + "but: was <[bar, baz]>"); + } + + @Test + void headers() throws Exception { this.request.getHeaders().put("foo", Arrays.asList("bar", "baz")); MockRestRequestMatchers.header("foo", "bar", "baz").match(this.request); } @Test - public void headersWithMissingHeader() throws Exception { - assertThatThrownBy(() -> MockRestRequestMatchers.header("foo", "bar").match(this.request)) - .isInstanceOf(AssertionError.class) - .hasMessageContaining("but was null"); + void headersWithMissingHeader() { + assertThatAssertionError() + .isThrownBy(() -> MockRestRequestMatchers.header("foo", "bar").match(this.request)) + .withMessage("Expected header to exist but was null"); } @Test - public void headersWithMissingValue() throws Exception { - this.request.getHeaders().put("foo", Collections.singletonList("bar")); + void headersWithMissingValue() { + this.request.getHeaders().put("foo", Arrays.asList("bar")); - assertThatThrownBy(() -> MockRestRequestMatchers.header("foo", "bar", "baz").match(this.request)) - .isInstanceOf(AssertionError.class) - .hasMessageContaining("to have at least <2> values"); + assertThatAssertionError() + .isThrownBy(() -> MockRestRequestMatchers.header("foo", "bar", "baz").match(this.request)) + .withMessageContaining("to have at least <2> values"); } @Test - public void queryParam() throws Exception { + void queryParam() throws Exception { this.request.setURI(new URI("http://www.foo.example/a?foo=bar&foo=baz")); MockRestRequestMatchers.queryParam("foo", "bar", "baz").match(this.request); } @Test - public void queryParamMissing() throws Exception { + void queryParamMissing() throws Exception { this.request.setURI(new URI("http://www.foo.example/a")); - assertThatThrownBy(() -> MockRestRequestMatchers.queryParam("foo", "bar").match(this.request)) - .isInstanceOf(AssertionError.class) - .hasMessageContaining("but was null"); + assertThatAssertionError() + .isThrownBy(() -> MockRestRequestMatchers.queryParam("foo", "bar").match(this.request)) + .withMessage("Expected query param to exist but was null"); } @Test - public void queryParamMissingValue() throws Exception { + void queryParamMissingValue() throws Exception { this.request.setURI(new URI("http://www.foo.example/a?foo=bar&foo=baz")); - assertThatThrownBy(() -> MockRestRequestMatchers.queryParam("foo", "bad").match(this.request)) - .isInstanceOf(AssertionError.class) - .hasMessageContaining("expected: but was:"); + assertThatAssertionError() + .isThrownBy(() -> MockRestRequestMatchers.queryParam("foo", "bad").match(this.request)) + .withMessageContaining("expected: but was:"); } @Test - public void queryParamContains() throws Exception { + void queryParamContains() throws Exception { this.request.setURI(new URI("http://www.foo.example/a?foo=bar&foo=baz")); MockRestRequestMatchers.queryParam("foo", containsString("ba")).match(this.request); } @Test - public void queryParamContainsWithMissingValue() throws Exception { + void queryParamContainsWithMissingValue() throws Exception { this.request.setURI(new URI("http://www.foo.example/a?foo=bar&foo=baz")); - assertThatThrownBy(() -> MockRestRequestMatchers.queryParam("foo", containsString("bx")).match(this.request)) - .isInstanceOf(AssertionError.class) - .hasMessageContaining("was \"bar\""); + assertThatAssertionError() + .isThrownBy(() -> MockRestRequestMatchers.queryParam("foo", containsString("bx")).match(this.request)) + .withMessageContaining("was \"bar\""); + } + + @Test + void queryParamListMissing() { + assertThatAssertionError() + .isThrownBy(() -> MockRestRequestMatchers.queryParamList("foo", hasSize(2)).match(this.request)) + .withMessage("Expected query param to exist but was null"); + } + + @Test + void queryParamListMatchers() throws IOException { + this.request.setURI(URI.create("http://www.foo.example/a?foo=bar&foo=baz")); + + MockRestRequestMatchers.queryParamList("foo", containsInAnyOrder(endsWith("baz"), endsWith("bar"))).match(this.request); + MockRestRequestMatchers.queryParamList("foo", contains(is("bar"), is("baz"))).match(this.request); + MockRestRequestMatchers.queryParamList("foo", contains(is("bar"), anything())).match(this.request); + MockRestRequestMatchers.queryParamList("foo", hasItem(endsWith("baz"))).match(this.request); + MockRestRequestMatchers.queryParamList("foo", everyItem(startsWith("ba"))).match(this.request); + MockRestRequestMatchers.queryParamList("foo", hasSize(2)).match(this.request); + + MockRestRequestMatchers.queryParamList("foo", notNullValue()).match(this.request); + MockRestRequestMatchers.queryParamList("foo", is(anything())).match(this.request); + MockRestRequestMatchers.queryParamList("foo", allOf(notNullValue(), notNullValue())).match(this.request); + + MockRestRequestMatchers.queryParamList("foo", allOf(notNullValue(), hasSize(2))).match(this.request); + } + + @Test + void queryParamListContainsMismatch() { + this.request.setURI(URI.create("http://www.foo.example/a?foo=bar&foo=baz")); + + assertThatAssertionError() + .isThrownBy(() -> MockRestRequestMatchers.queryParamList("foo", contains(containsString("ba"))).match(this.request)) + .withMessageContainingAll( + "Query param [foo] values", + "Expected: iterable containing [a string containing \"ba\"]", + "but: not matched: \"baz\""); + + assertThatAssertionError() + .isThrownBy(() -> MockRestRequestMatchers.queryParamList("foo", hasItem(endsWith("ba"))).match(this.request)) + .withMessageContainingAll( + "Query param [foo] values", + "Expected: a collection containing a string ending with \"ba\"", + "but: mismatches were: [was \"bar\", was \"baz\"]"); + + assertThatAssertionError() + .isThrownBy(() -> MockRestRequestMatchers.queryParamList("foo", everyItem(endsWith("ar"))).match(this.request)) + .withMessageContainingAll( + "Query param [foo] values", + "Expected: every item is a string ending with \"ar\"", + "but: an item was \"baz\""); + } + + @Test + void queryParamListDoesntHideQueryParamWithSingleMatcher() throws IOException { + this.request.setURI(URI.create("http://www.foo.example/a?foo=bar&foo=baz")); + + MockRestRequestMatchers.queryParam("foo", equalTo("bar")).match(this.request); + + assertThatAssertionError() + .isThrownBy(() -> MockRestRequestMatchers.queryParamList("foo", equalTo("bar")).match(this.request)) + .withMessageContainingAll( + "Query param [foo] values", + "Expected: \"bar\"", + "but: was <[bar, baz]>"); + } + + private static ThrowableTypeAssert assertThatAssertionError() { + return assertThatExceptionOfType(AssertionError.class); } } diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/ForwardController.java b/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/ForwardController.java index 0a96d54bac9c..ee405c609a69 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/ForwardController.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/ForwardController.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 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. @@ -31,4 +31,9 @@ public String forward() { return "forward:/a"; } + @RequestMapping("/infiniteForward") + public String infiniteForward() { + return "forward:/infiniteForward"; + } + } diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/MockMvcWebConnectionTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/MockMvcWebConnectionTests.java index 051cabaf0187..cca4bfa2e0e9 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/MockMvcWebConnectionTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/MockMvcWebConnectionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 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,6 +29,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; /** @@ -80,6 +81,13 @@ public void forward() throws IOException { assertThat(page.getWebResponse().getContentAsString()).isEqualTo("hello"); } + @Test + public void infiniteForward() { + this.webClient.setWebConnection(new MockMvcWebConnection(this.mockMvc, this.webClient, "")); + assertThatIllegalStateException().isThrownBy(() -> this.webClient.getPage("http://localhost/infiniteForward")) + .withMessage("Forwarded more than 100 times in a row, potential infinite forward loop"); + } + @Test @SuppressWarnings("resource") public void contextPathDoesNotStartWithSlash() throws IOException { diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/spr/MockMvcBuilderMethodChainTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/spr/MockMvcBuilderMethodChainTests.java index 6d4fc912c796..c30dc7eeafc8 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/spr/MockMvcBuilderMethodChainTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/spr/MockMvcBuilderMethodChainTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2022 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,19 +17,15 @@ package org.springframework.test.web.servlet.samples.spr; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit.jupiter.SpringExtension; -import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.context.junit.jupiter.web.SpringJUnitWebConfig; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.filter.CharacterEncodingFilter; import org.springframework.web.servlet.config.annotation.EnableWebMvc; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import static org.assertj.core.api.Assertions.assertThatNoException; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; /** @@ -37,26 +33,23 @@ * * @author Wesley Hall */ -@ExtendWith(SpringExtension.class) -@WebAppConfiguration -@ContextConfiguration -public class MockMvcBuilderMethodChainTests { - - @Autowired - private WebApplicationContext wac; +@SpringJUnitWebConfig +class MockMvcBuilderMethodChainTests { @Test - public void chainMultiple() { - MockMvcBuilders + void chainMultiple(WebApplicationContext wac) { + assertThatNoException().isThrownBy(() -> + MockMvcBuilders .webAppContextSetup(wac) .addFilter(new CharacterEncodingFilter() ) .defaultRequest(get("/").contextPath("/mywebapp")) - .build(); + .build() + ); } @Configuration @EnableWebMvc - static class WebConfig implements WebMvcConfigurer { + static class WebConfig { } } diff --git a/spring-tx/src/main/java/org/springframework/dao/CannotAcquireLockException.java b/spring-tx/src/main/java/org/springframework/dao/CannotAcquireLockException.java index d5b113e9f5e2..5a36c2381545 100644 --- a/spring-tx/src/main/java/org/springframework/dao/CannotAcquireLockException.java +++ b/spring-tx/src/main/java/org/springframework/dao/CannotAcquireLockException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2022 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,9 @@ * Exception thrown on failure to acquire a lock during an update, * for example during a "select for update" statement. * + *

    Consider handling the general {@link PessimisticLockingFailureException} + * instead, semantically including a wider range of locking-related failures. + * * @author Rod Johnson */ @SuppressWarnings("serial") diff --git a/spring-tx/src/main/java/org/springframework/dao/CannotSerializeTransactionException.java b/spring-tx/src/main/java/org/springframework/dao/CannotSerializeTransactionException.java index 1dfe7f541674..7b6223e0ebed 100644 --- a/spring-tx/src/main/java/org/springframework/dao/CannotSerializeTransactionException.java +++ b/spring-tx/src/main/java/org/springframework/dao/CannotSerializeTransactionException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2022 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,9 @@ * Exception thrown on failure to complete a transaction in serialized mode * due to update conflicts. * + *

    Consider handling the general {@link PessimisticLockingFailureException} + * instead, semantically including a wider range of locking-related failures. + * * @author Rod Johnson */ @SuppressWarnings("serial") diff --git a/spring-tx/src/main/java/org/springframework/dao/ConcurrencyFailureException.java b/spring-tx/src/main/java/org/springframework/dao/ConcurrencyFailureException.java index cc990803f483..4f1031ce3f14 100644 --- a/spring-tx/src/main/java/org/springframework/dao/ConcurrencyFailureException.java +++ b/spring-tx/src/main/java/org/springframework/dao/ConcurrencyFailureException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2022 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,17 +19,15 @@ import org.springframework.lang.Nullable; /** - * Exception thrown on concurrency failure. + * Exception thrown on various data access concurrency failures. * - *

    This exception should be subclassed to indicate the type of failure: - * optimistic locking, failure to acquire lock, etc. + *

    This exception provides subclasses for specific types of failure, + * in particular optimistic locking versus pessimistic locking. * * @author Thomas Risberg * @since 1.1 * @see OptimisticLockingFailureException * @see PessimisticLockingFailureException - * @see CannotAcquireLockException - * @see DeadlockLoserDataAccessException */ @SuppressWarnings("serial") public class ConcurrencyFailureException extends TransientDataAccessException { diff --git a/spring-tx/src/main/java/org/springframework/dao/DataIntegrityViolationException.java b/spring-tx/src/main/java/org/springframework/dao/DataIntegrityViolationException.java index b1347e1ee946..ad1cc68bb972 100644 --- a/spring-tx/src/main/java/org/springframework/dao/DataIntegrityViolationException.java +++ b/spring-tx/src/main/java/org/springframework/dao/DataIntegrityViolationException.java @@ -19,8 +19,13 @@ /** * Exception thrown when an attempt to insert or update data * results in violation of an integrity constraint. Note that this - * is not purely a relational concept; unique primary keys are - * required by most database types. + * is not purely a relational concept; integrity constraints such + * as unique primary keys are required by most database types. + * + *

    Serves as a superclass for more specific exceptions, e.g. + * {@link DuplicateKeyException}. However, it is generally + * recommended to handle {@code DataIntegrityViolationException} + * itself instead of relying on specific exception subclasses. * * @author Rod Johnson */ diff --git a/spring-tx/src/main/java/org/springframework/dao/DeadlockLoserDataAccessException.java b/spring-tx/src/main/java/org/springframework/dao/DeadlockLoserDataAccessException.java index 3c946cd56408..9965e0fffe92 100644 --- a/spring-tx/src/main/java/org/springframework/dao/DeadlockLoserDataAccessException.java +++ b/spring-tx/src/main/java/org/springframework/dao/DeadlockLoserDataAccessException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2022 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,9 @@ * Generic exception thrown when the current process was * a deadlock loser, and its transaction rolled back. * + *

    Consider handling the general {@link PessimisticLockingFailureException} + * instead, semantically including a wider range of locking-related failures. + * * @author Rod Johnson */ @SuppressWarnings("serial") diff --git a/spring-tx/src/main/java/org/springframework/dao/DuplicateKeyException.java b/spring-tx/src/main/java/org/springframework/dao/DuplicateKeyException.java index b406391a3ff3..36ee084c9e7b 100644 --- a/spring-tx/src/main/java/org/springframework/dao/DuplicateKeyException.java +++ b/spring-tx/src/main/java/org/springframework/dao/DuplicateKeyException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2022 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,6 +22,9 @@ * Note that this is not necessarily a purely relational concept; * unique primary keys are required by most database types. * + *

    Consider handling the general {@link DataIntegrityViolationException} + * instead, semantically including a wider range of constraint violations. + * * @author Thomas Risberg */ @SuppressWarnings("serial") diff --git a/spring-tx/src/main/java/org/springframework/dao/IncorrectUpdateSemanticsDataAccessException.java b/spring-tx/src/main/java/org/springframework/dao/IncorrectUpdateSemanticsDataAccessException.java index 70689977cd2e..2eb1749da9e9 100644 --- a/spring-tx/src/main/java/org/springframework/dao/IncorrectUpdateSemanticsDataAccessException.java +++ b/spring-tx/src/main/java/org/springframework/dao/IncorrectUpdateSemanticsDataAccessException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-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. @@ -44,10 +44,11 @@ public IncorrectUpdateSemanticsDataAccessException(String msg, Throwable cause) super(msg, cause); } + /** * Return whether data was updated. - * If this method returns false, there's nothing to roll back. - *

    The default implementation always returns true. + * If this method returns {@code false}, there is nothing to roll back. + *

    The default implementation always returns {@code true}. * This can be overridden in subclasses. */ public boolean wasDataUpdated() { diff --git a/spring-tx/src/main/java/org/springframework/dao/PessimisticLockingFailureException.java b/spring-tx/src/main/java/org/springframework/dao/PessimisticLockingFailureException.java index 3e60eb141bca..31599203a997 100644 --- a/spring-tx/src/main/java/org/springframework/dao/PessimisticLockingFailureException.java +++ b/spring-tx/src/main/java/org/springframework/dao/PessimisticLockingFailureException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2022 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,13 +21,13 @@ * Thrown by Spring's SQLException translation mechanism * if a corresponding database error is encountered. * - *

    Serves as superclass for more specific exceptions, like - * CannotAcquireLockException and DeadlockLoserDataAccessException. + *

    Serves as a superclass for more specific exceptions, e.g. + * {@link CannotAcquireLockException}. However, it is generally + * recommended to handle {@code PessimisticLockingFailureException} + * itself instead of relying on specific exception subclasses. * * @author Thomas Risberg * @since 1.2 - * @see CannotAcquireLockException - * @see DeadlockLoserDataAccessException * @see OptimisticLockingFailureException */ @SuppressWarnings("serial") diff --git a/spring-tx/src/main/java/org/springframework/dao/support/DataAccessUtils.java b/spring-tx/src/main/java/org/springframework/dao/support/DataAccessUtils.java index adb80a3afb7a..a72df8db9dc1 100644 --- a/spring-tx/src/main/java/org/springframework/dao/support/DataAccessUtils.java +++ b/spring-tx/src/main/java/org/springframework/dao/support/DataAccessUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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,8 @@ /** * Miscellaneous utility methods for DAO implementations. - * Useful with any data access technology. + * + *

    Useful with any data access technology. * * @author Juergen Hoeller * @since 1.0.2 diff --git a/spring-tx/src/main/java/org/springframework/jca/endpoint/GenericMessageEndpointFactory.java b/spring-tx/src/main/java/org/springframework/jca/endpoint/GenericMessageEndpointFactory.java index e4b5b7e61083..520045ff4bb7 100644 --- a/spring-tx/src/main/java/org/springframework/jca/endpoint/GenericMessageEndpointFactory.java +++ b/spring-tx/src/main/java/org/springframework/jca/endpoint/GenericMessageEndpointFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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. @@ -84,11 +84,12 @@ protected Object getMessageListener() { @Override public MessageEndpoint createEndpoint(XAResource xaResource) throws UnavailableException { GenericMessageEndpoint endpoint = (GenericMessageEndpoint) super.createEndpoint(xaResource); - ProxyFactory proxyFactory = new ProxyFactory(getMessageListener()); + Object target = getMessageListener(); + ProxyFactory proxyFactory = new ProxyFactory(target); DelegatingIntroductionInterceptor introduction = new DelegatingIntroductionInterceptor(endpoint); introduction.suppressInterface(MethodInterceptor.class); proxyFactory.addAdvice(introduction); - return (MessageEndpoint) proxyFactory.getProxy(); + return (MessageEndpoint) proxyFactory.getProxy(target.getClass().getClassLoader()); } /** diff --git a/spring-tx/src/main/java/org/springframework/transaction/PlatformTransactionManager.java b/spring-tx/src/main/java/org/springframework/transaction/PlatformTransactionManager.java index f15e2f3700c6..ae192da79c18 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/PlatformTransactionManager.java +++ b/spring-tx/src/main/java/org/springframework/transaction/PlatformTransactionManager.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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. @@ -31,10 +31,10 @@ * template methods for specific states of the underlying transaction, * for example: begin, suspend, resume, commit. * - *

    The default implementations of this strategy interface are - * {@link org.springframework.transaction.jta.JtaTransactionManager} and - * {@link org.springframework.jdbc.datasource.DataSourceTransactionManager}, - * which can serve as an implementation guide for other transaction strategies. + *

    A classic implementation of this strategy interface is + * {@link org.springframework.transaction.jta.JtaTransactionManager}. However, + * in common single-resource scenarios, Spring's specific transaction managers + * for e.g. JDBC, JPA, JMS are preferred choices. * * @author Rod Johnson * @author Juergen Hoeller @@ -68,8 +68,7 @@ public interface PlatformTransactionManager extends TransactionManager { * @see TransactionDefinition#getTimeout * @see TransactionDefinition#isReadOnly */ - TransactionStatus getTransaction(@Nullable TransactionDefinition definition) - throws TransactionException; + TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException; /** * Commit the given transaction, with regard to its status. If the transaction @@ -81,12 +80,9 @@ TransactionStatus getTransaction(@Nullable TransactionDefinition definition) *

    Note that when the commit call completes, no matter if normally or * throwing an exception, the transaction must be fully completed and * cleaned up. No rollback call should be expected in such a case. - *

    If this method throws an exception other than a TransactionException, - * then some before-commit error caused the commit attempt to fail. For - * example, an O/R Mapping tool might have tried to flush changes to the - * database right before commit, with the resulting DataAccessException - * causing the transaction to fail. The original exception will be - * propagated to the caller of this commit method in such a case. + *

    Depending on the concrete transaction manager setup, {@code commit} + * may propagate {@link org.springframework.dao.DataAccessException} as well, + * either from before-commit flushes or from the actual commit step. * @param status object returned by the {@code getTransaction} method * @throws UnexpectedRollbackException in case of an unexpected rollback * that the transaction coordinator initiated @@ -110,6 +106,8 @@ TransactionStatus getTransaction(@Nullable TransactionDefinition definition) * The transaction will already have been completed and cleaned up when commit * returns, even in case of a commit exception. Consequently, a rollback call * after commit failure will lead to an IllegalTransactionStateException. + *

    Depending on the concrete transaction manager setup, {@code rollback} + * may propagate {@link org.springframework.dao.DataAccessException} as well. * @param status object returned by the {@code getTransaction} method * @throws TransactionSystemException in case of rollback or system errors * (typically caused by fundamental resource failures) diff --git a/spring-tx/src/main/java/org/springframework/transaction/ReactiveTransaction.java b/spring-tx/src/main/java/org/springframework/transaction/ReactiveTransaction.java index 11a33e681b52..2c3a03e37855 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/ReactiveTransaction.java +++ b/spring-tx/src/main/java/org/springframework/transaction/ReactiveTransaction.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 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,7 +17,7 @@ package org.springframework.transaction; /** - * Representation of an ongoing reactive transaction. + * Representation of an ongoing {@link ReactiveTransactionManager} transaction. * This is currently a marker interface extending {@link TransactionExecution} * but may acquire further methods in a future revision. * @@ -30,6 +30,7 @@ * @since 5.2 * @see #setRollbackOnly() * @see ReactiveTransactionManager#getReactiveTransaction + * @see org.springframework.transaction.reactive.TransactionCallback#doInTransaction */ public interface ReactiveTransaction extends TransactionExecution { diff --git a/spring-tx/src/main/java/org/springframework/transaction/ReactiveTransactionManager.java b/spring-tx/src/main/java/org/springframework/transaction/ReactiveTransactionManager.java index 64341ebb0310..d55a13472dda 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/ReactiveTransactionManager.java +++ b/spring-tx/src/main/java/org/springframework/transaction/ReactiveTransactionManager.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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. @@ -46,6 +46,8 @@ public interface ReactiveTransactionManager extends TransactionManager { *

    An exception to the above rule is the read-only flag, which should be * ignored if no explicit read-only mode is supported. Essentially, the * read-only flag is just a hint for potential optimization. + *

    Note: In contrast to {@link PlatformTransactionManager}, exceptions + * are propagated through the reactive pipeline returned from this method. * @param definition the TransactionDefinition instance, * describing propagation behavior, isolation level, timeout etc. * @return transaction status object representing the new or current transaction @@ -58,8 +60,7 @@ public interface ReactiveTransactionManager extends TransactionManager { * @see TransactionDefinition#getTimeout * @see TransactionDefinition#isReadOnly */ - Mono getReactiveTransaction(@Nullable TransactionDefinition definition) - throws TransactionException; + Mono getReactiveTransaction(@Nullable TransactionDefinition definition); /** * Commit the given transaction, with regard to its status. If the transaction @@ -69,14 +70,12 @@ Mono getReactiveTransaction(@Nullable TransactionDefinition * has been suspended to be able to create a new one, resume the previous * transaction after committing the new one. *

    Note that when the commit call completes, no matter if normally or - * throwing an exception, the transaction must be fully completed and + * propagating an exception, the transaction must be fully completed and * cleaned up. No rollback call should be expected in such a case. - *

    If this method throws an exception other than a TransactionException, - * then some before-commit error caused the commit attempt to fail. For - * example, an O/R Mapping tool might have tried to flush changes to the - * database right before commit, with the resulting DataAccessException - * causing the transaction to fail. The original exception will be - * propagated to the caller of this commit method in such a case. + *

    Note: In contrast to {@link PlatformTransactionManager}, exceptions + * are propagated through the reactive pipeline returned from this method. + * Also, depending on the transaction manager implementation, {@code commit} + * may propagate {@link org.springframework.dao.DataAccessException} as well. * @param transaction object returned by the {@code getTransaction} method * @throws UnexpectedRollbackException in case of an unexpected rollback * that the transaction coordinator initiated @@ -88,7 +87,7 @@ Mono getReactiveTransaction(@Nullable TransactionDefinition * is already completed (that is, committed or rolled back) * @see ReactiveTransaction#setRollbackOnly */ - Mono commit(ReactiveTransaction transaction) throws TransactionException; + Mono commit(ReactiveTransaction transaction); /** * Perform a rollback of the given transaction. @@ -96,16 +95,20 @@ Mono getReactiveTransaction(@Nullable TransactionDefinition * participation in the surrounding transaction. If a previous transaction * has been suspended to be able to create a new one, resume the previous * transaction after rolling back the new one. - *

    Do not call rollback on a transaction if commit threw an exception. + *

    Do not call rollback on a transaction if commit failed. * The transaction will already have been completed and cleaned up when commit * returns, even in case of a commit exception. Consequently, a rollback call * after commit failure will lead to an IllegalTransactionStateException. + *

    Note: In contrast to {@link PlatformTransactionManager}, exceptions + * are propagated through the reactive pipeline returned from this method. + * Also, depending on the transaction manager implementation, {@code rollback} + * may propagate {@link org.springframework.dao.DataAccessException} as well. * @param transaction object returned by the {@code getTransaction} method * @throws TransactionSystemException in case of rollback or system errors * (typically caused by fundamental resource failures) * @throws IllegalTransactionStateException if the given transaction * is already completed (that is, committed or rolled back) */ - Mono rollback(ReactiveTransaction transaction) throws TransactionException; + Mono rollback(ReactiveTransaction transaction); } diff --git a/spring-tx/src/main/java/org/springframework/transaction/TransactionStatus.java b/spring-tx/src/main/java/org/springframework/transaction/TransactionStatus.java index 5968c57fd7bb..d61ed25f618d 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/TransactionStatus.java +++ b/spring-tx/src/main/java/org/springframework/transaction/TransactionStatus.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 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,7 +19,8 @@ import java.io.Flushable; /** - * Representation of the status of a transaction. + * Representation of an ongoing {@link PlatformTransactionManager} transaction. + * Extends the common {@link TransactionExecution} interface. * *

    Transactional code can use this to retrieve status information, * and to programmatically request a rollback (instead of throwing diff --git a/spring-tx/src/main/java/org/springframework/transaction/event/TransactionalApplicationListenerMethodAdapter.java b/spring-tx/src/main/java/org/springframework/transaction/event/TransactionalApplicationListenerMethodAdapter.java index ce39e136c1a0..b12aff8e17b0 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/event/TransactionalApplicationListenerMethodAdapter.java +++ b/spring-tx/src/main/java/org/springframework/transaction/event/TransactionalApplicationListenerMethodAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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. @@ -48,10 +48,10 @@ public class TransactionalApplicationListenerMethodAdapter extends ApplicationListenerMethodAdapter implements TransactionalApplicationListener { - private final TransactionalEventListener annotation; - private final TransactionPhase transactionPhase; + private final boolean fallbackExecution; + private final List callbacks = new CopyOnWriteArrayList<>(); @@ -63,13 +63,13 @@ public class TransactionalApplicationListenerMethodAdapter extends ApplicationLi */ public TransactionalApplicationListenerMethodAdapter(String beanName, Class targetClass, Method method) { super(beanName, targetClass, method); - TransactionalEventListener ann = - AnnotatedElementUtils.findMergedAnnotation(method, TransactionalEventListener.class); - if (ann == null) { + TransactionalEventListener eventAnn = + AnnotatedElementUtils.findMergedAnnotation(getTargetMethod(), TransactionalEventListener.class); + if (eventAnn == null) { throw new IllegalStateException("No TransactionalEventListener annotation found on method: " + method); } - this.annotation = ann; - this.transactionPhase = ann.phase(); + this.transactionPhase = eventAnn.phase(); + this.fallbackExecution = eventAnn.fallbackExecution(); } @@ -92,8 +92,8 @@ public void onApplicationEvent(ApplicationEvent event) { TransactionSynchronizationManager.registerSynchronization( new TransactionalApplicationListenerSynchronization<>(event, this, this.callbacks)); } - else if (this.annotation.fallbackExecution()) { - if (this.annotation.phase() == TransactionPhase.AFTER_ROLLBACK && logger.isWarnEnabled()) { + else if (this.fallbackExecution) { + if (getTransactionPhase() == TransactionPhase.AFTER_ROLLBACK && logger.isWarnEnabled()) { logger.warn("Processing " + event + " as a fallback execution on AFTER_ROLLBACK phase"); } processEvent(event); diff --git a/spring-tx/src/main/java/org/springframework/transaction/interceptor/DefaultTransactionAttribute.java b/spring-tx/src/main/java/org/springframework/transaction/interceptor/DefaultTransactionAttribute.java index ab3df073c7c6..76e72bcd9d04 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/interceptor/DefaultTransactionAttribute.java +++ b/spring-tx/src/main/java/org/springframework/transaction/interceptor/DefaultTransactionAttribute.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-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. @@ -51,7 +51,7 @@ public class DefaultTransactionAttribute extends DefaultTransactionDefinition im /** - * Create a new DefaultTransactionAttribute, with default settings. + * Create a new {@code DefaultTransactionAttribute} with default settings. * Can be modified through bean property setters. * @see #setPropagationBehavior * @see #setIsolationLevel @@ -76,7 +76,7 @@ public DefaultTransactionAttribute(TransactionAttribute other) { } /** - * Create a new DefaultTransactionAttribute with the given + * Create a new {@code DefaultTransactionAttribute} with the given * propagation behavior. Can be modified through bean property setters. * @param propagationBehavior one of the propagation constants in the * TransactionDefinition interface diff --git a/spring-tx/src/main/java/org/springframework/transaction/interceptor/RollbackRuleAttribute.java b/spring-tx/src/main/java/org/springframework/transaction/interceptor/RollbackRuleAttribute.java index 49327d934521..6f5c419aa998 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/interceptor/RollbackRuleAttribute.java +++ b/spring-tx/src/main/java/org/springframework/transaction/interceptor/RollbackRuleAttribute.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -159,8 +159,8 @@ public boolean equals(@Nullable Object other) { if (!(other instanceof RollbackRuleAttribute)) { return false; } - RollbackRuleAttribute rhs = (RollbackRuleAttribute) other; - return this.exceptionPattern.equals(rhs.exceptionPattern); + RollbackRuleAttribute otherAttr = (RollbackRuleAttribute) other; + return this.exceptionPattern.equals(otherAttr.exceptionPattern); } @Override diff --git a/spring-tx/src/main/java/org/springframework/transaction/interceptor/TransactionAspectSupport.java b/spring-tx/src/main/java/org/springframework/transaction/interceptor/TransactionAspectSupport.java index 1dbbdaa6055f..fd674834ef0a 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/interceptor/TransactionAspectSupport.java +++ b/spring-tx/src/main/java/org/springframework/transaction/interceptor/TransactionAspectSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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. @@ -355,8 +355,8 @@ protected Object invokeWithinTransaction(Method method, @Nullable Class targe (isSuspendingFunction ? (hasSuspendingFlowReturnType ? Flux.class : Mono.class) : method.getReturnType()); ReactiveAdapter adapter = this.reactiveAdapterRegistry.getAdapter(reactiveType); if (adapter == null) { - throw new IllegalStateException("Cannot apply reactive transaction to non-reactive return type: " + - method.getReturnType()); + throw new IllegalStateException("Cannot apply reactive transaction to non-reactive return type [" + + method.getReturnType() + "] with specified transaction manager: " + tm); } return new ReactiveTransactionSupport(adapter); }); diff --git a/spring-tx/src/main/java/org/springframework/transaction/interceptor/TransactionAttributeSourceEditor.java b/spring-tx/src/main/java/org/springframework/transaction/interceptor/TransactionAttributeSourceEditor.java index eef4a8cc9fff..f1d8c1d33324 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/interceptor/TransactionAttributeSourceEditor.java +++ b/spring-tx/src/main/java/org/springframework/transaction/interceptor/TransactionAttributeSourceEditor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2022 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-tx/src/main/java/org/springframework/transaction/reactive/AbstractReactiveTransactionManager.java b/spring-tx/src/main/java/org/springframework/transaction/reactive/AbstractReactiveTransactionManager.java index cdc434fc7361..2aaa26180796 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/reactive/AbstractReactiveTransactionManager.java +++ b/spring-tx/src/main/java/org/springframework/transaction/reactive/AbstractReactiveTransactionManager.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-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. @@ -95,9 +95,7 @@ public abstract class AbstractReactiveTransactionManager implements ReactiveTran * @see #doBegin */ @Override - public final Mono getReactiveTransaction(@Nullable TransactionDefinition definition) - throws TransactionException { - + public final Mono getReactiveTransaction(@Nullable TransactionDefinition definition) { // Use defaults if no transaction definition given. TransactionDefinition def = (definition != null ? definition : TransactionDefinition.withDefaults()); @@ -165,7 +163,7 @@ else if (def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUI * Create a ReactiveTransaction for an existing transaction. */ private Mono handleExistingTransaction(TransactionSynchronizationManager synchronizationManager, - TransactionDefinition definition, Object transaction, boolean debugEnabled) throws TransactionException { + TransactionDefinition definition, Object transaction, boolean debugEnabled) { if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NEVER) { return Mono.error(new IllegalTransactionStateException( @@ -212,11 +210,13 @@ private Mono handleExistingTransaction(TransactionSynchroni prepareSynchronization(synchronizationManager, status, definition)).thenReturn(status); } - // Assumably PROPAGATION_SUPPORTS or PROPAGATION_REQUIRED. + // PROPAGATION_REQUIRED, PROPAGATION_SUPPORTS, PROPAGATION_MANDATORY: + // regular participation in existing transaction. if (debugEnabled) { logger.debug("Participating in existing transaction"); } - return Mono.just(prepareReactiveTransaction(synchronizationManager, definition, transaction, false, debugEnabled, null)); + return Mono.just(prepareReactiveTransaction( + synchronizationManager, definition, transaction, false, debugEnabled, null)); } /** @@ -277,7 +277,7 @@ private void prepareSynchronization(TransactionSynchronizationManager synchroniz * @see #resume */ private Mono suspend(TransactionSynchronizationManager synchronizationManager, - @Nullable Object transaction) throws TransactionException { + @Nullable Object transaction) { if (synchronizationManager.isSynchronizationActive()) { Mono> suspendedSynchronizations = doSuspendSynchronization(synchronizationManager); @@ -325,15 +325,14 @@ else if (transaction != null) { * @see #suspend */ private Mono resume(TransactionSynchronizationManager synchronizationManager, - @Nullable Object transaction, @Nullable SuspendedResourcesHolder resourcesHolder) - throws TransactionException { + @Nullable Object transaction, @Nullable SuspendedResourcesHolder resourcesHolder) { Mono resume = Mono.empty(); if (resourcesHolder != null) { Object suspendedResources = resourcesHolder.suspendedResources; if (suspendedResources != null) { - resume = doResume(synchronizationManager, transaction, suspendedResources); + resume = doResume(synchronizationManager, transaction, suspendedResources); } List suspendedSynchronizations = resourcesHolder.suspendedSynchronizations; if (suspendedSynchronizations != null) { @@ -403,7 +402,7 @@ private Mono doResumeSynchronization(TransactionSynchronizationManager syn * @see #rollback */ @Override - public final Mono commit(ReactiveTransaction transaction) throws TransactionException { + public final Mono commit(ReactiveTransaction transaction) { if (transaction.isCompleted()) { return Mono.error(new IllegalTransactionStateException( "Transaction is already completed - do not call commit or rollback more than once per transaction")); @@ -426,10 +425,9 @@ public final Mono commit(ReactiveTransaction transaction) throws Transacti * Rollback-only flags have already been checked and applied. * @param synchronizationManager the synchronization manager bound to the current transaction * @param status object representing the transaction - * @throws TransactionException in case of commit failure */ private Mono processCommit(TransactionSynchronizationManager synchronizationManager, - GenericReactiveTransaction status) throws TransactionException { + GenericReactiveTransaction status) { AtomicBoolean beforeCompletionInvoked = new AtomicBoolean(); @@ -487,7 +485,7 @@ else if (ErrorPredicates.RUNTIME_OR_ERROR.test(ex)) { * @see #doSetRollbackOnly */ @Override - public final Mono rollback(ReactiveTransaction transaction) throws TransactionException { + public final Mono rollback(ReactiveTransaction transaction) { if (transaction.isCompleted()) { return Mono.error(new IllegalTransactionStateException( "Transaction is already completed - do not call commit or rollback more than once per transaction")); @@ -503,7 +501,6 @@ public final Mono rollback(ReactiveTransaction transaction) throws Transac * The completed flag has already been checked. * @param synchronizationManager the synchronization manager bound to the current transaction * @param status object representing the transaction - * @throws TransactionException in case of rollback failure */ private Mono processRollback(TransactionSynchronizationManager synchronizationManager, GenericReactiveTransaction status) { @@ -542,11 +539,10 @@ private Mono processRollback(TransactionSynchronizationManager synchroniza * @param synchronizationManager the synchronization manager bound to the current transaction * @param status object representing the transaction * @param ex the thrown application exception or error - * @throws TransactionException in case of rollback failure * @see #doRollback */ private Mono doRollbackOnCommitException(TransactionSynchronizationManager synchronizationManager, - GenericReactiveTransaction status, Throwable ex) throws TransactionException { + GenericReactiveTransaction status, Throwable ex) { return Mono.defer(() -> { if (status.isNewTransaction()) { @@ -714,14 +710,12 @@ private Mono cleanupAfterCompletion(TransactionSynchronizationManager sync * @return the current transaction object * @throws org.springframework.transaction.CannotCreateTransactionException * if transaction support is not available - * @throws TransactionException in case of lookup or system errors * @see #doBegin * @see #doCommit * @see #doRollback * @see GenericReactiveTransaction#getTransaction */ - protected abstract Object doGetTransaction(TransactionSynchronizationManager synchronizationManager) - throws TransactionException; + protected abstract Object doGetTransaction(TransactionSynchronizationManager synchronizationManager); /** * Check if the given transaction object indicates an existing transaction @@ -735,10 +729,9 @@ protected abstract Object doGetTransaction(TransactionSynchronizationManager syn * Subclasses are of course encouraged to provide such support. * @param transaction the transaction object returned by doGetTransaction * @return if there is an existing transaction - * @throws TransactionException in case of system errors * @see #doGetTransaction */ - protected boolean isExistingTransaction(Object transaction) throws TransactionException { + protected boolean isExistingTransaction(Object transaction) { return false; } @@ -757,12 +750,11 @@ protected boolean isExistingTransaction(Object transaction) throws TransactionEx * @param transaction the transaction object returned by {@code doGetTransaction} * @param definition a TransactionDefinition instance, describing propagation * behavior, isolation level, read-only flag, timeout, and transaction name - * @throws TransactionException in case of creation or system errors * @throws org.springframework.transaction.NestedTransactionNotSupportedException * if the underlying transaction does not support nesting (e.g. through savepoints) */ protected abstract Mono doBegin(TransactionSynchronizationManager synchronizationManager, - Object transaction, TransactionDefinition definition) throws TransactionException; + Object transaction, TransactionDefinition definition); /** * Suspend the resources of the current transaction. @@ -775,11 +767,10 @@ protected abstract Mono doBegin(TransactionSynchronizationManager synchron * (will be kept unexamined for passing it into doResume) * @throws org.springframework.transaction.TransactionSuspensionNotSupportedException * if suspending is not supported by the transaction manager implementation - * @throws TransactionException in case of system errors * @see #doResume */ protected Mono doSuspend(TransactionSynchronizationManager synchronizationManager, - Object transaction) throws TransactionException { + Object transaction) { throw new TransactionSuspensionNotSupportedException( "Transaction manager [" + getClass().getName() + "] does not support transaction suspension"); @@ -796,11 +787,10 @@ protected Mono doSuspend(TransactionSynchronizationManager synchronizati * as returned by doSuspend * @throws org.springframework.transaction.TransactionSuspensionNotSupportedException * if suspending is not supported by the transaction manager implementation - * @throws TransactionException in case of system errors * @see #doSuspend */ protected Mono doResume(TransactionSynchronizationManager synchronizationManager, - @Nullable Object transaction, Object suspendedResources) throws TransactionException { + @Nullable Object transaction, Object suspendedResources) { throw new TransactionSuspensionNotSupportedException( "Transaction manager [" + getClass().getName() + "] does not support transaction suspension"); @@ -830,11 +820,10 @@ protected Mono prepareForCommit(TransactionSynchronizationManager synchron * contained in the passed-in status. * @param synchronizationManager the synchronization manager bound to the current transaction * @param status the status representation of the transaction - * @throws TransactionException in case of commit or system errors * @see GenericReactiveTransaction#getTransaction */ protected abstract Mono doCommit(TransactionSynchronizationManager synchronizationManager, - GenericReactiveTransaction status) throws TransactionException; + GenericReactiveTransaction status); /** * Perform an actual rollback of the given transaction. @@ -843,11 +832,10 @@ protected abstract Mono doCommit(TransactionSynchronizationManager synchro * will be performed on the transaction object contained in the passed-in status. * @param synchronizationManager the synchronization manager bound to the current transaction * @param status the status representation of the transaction - * @throws TransactionException in case of system errors * @see GenericReactiveTransaction#getTransaction */ protected abstract Mono doRollback(TransactionSynchronizationManager synchronizationManager, - GenericReactiveTransaction status) throws TransactionException; + GenericReactiveTransaction status); /** * Set the given transaction rollback-only. Only called on rollback @@ -857,10 +845,9 @@ protected abstract Mono doRollback(TransactionSynchronizationManager synch * supported. Subclasses are of course encouraged to provide such support. * @param synchronizationManager the synchronization manager bound to the current transaction * @param status the status representation of the transaction - * @throws TransactionException in case of system errors */ protected Mono doSetRollbackOnly(TransactionSynchronizationManager synchronizationManager, - GenericReactiveTransaction status) throws TransactionException { + GenericReactiveTransaction status) { throw new IllegalTransactionStateException( "Participating in existing transactions is not supported - when 'isExistingTransaction' " + @@ -878,13 +865,12 @@ protected Mono doSetRollbackOnly(TransactionSynchronizationManager synchro * @param synchronizationManager the synchronization manager bound to the current transaction * @param transaction the transaction object returned by {@code doGetTransaction} * @param synchronizations a List of TransactionSynchronization objects - * @throws TransactionException in case of system errors * @see #invokeAfterCompletion(TransactionSynchronizationManager, List, int) * @see TransactionSynchronization#afterCompletion(int) * @see TransactionSynchronization#STATUS_UNKNOWN */ protected Mono registerAfterCompletionWithExistingTransaction(TransactionSynchronizationManager synchronizationManager, - Object transaction, List synchronizations) throws TransactionException { + Object transaction, List synchronizations) { logger.debug("Cannot register Spring after-completion synchronization with existing transaction - " + "processing Spring after-completion callbacks immediately, with outcome status 'unknown'"); diff --git a/spring-tx/src/main/java/org/springframework/transaction/reactive/TransactionContext.java b/spring-tx/src/main/java/org/springframework/transaction/reactive/TransactionContext.java index 280669b83f04..930fb09bf302 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/reactive/TransactionContext.java +++ b/spring-tx/src/main/java/org/springframework/transaction/reactive/TransactionContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-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. @@ -39,7 +39,8 @@ */ public class TransactionContext { - private final @Nullable TransactionContext parent; + @Nullable + private final TransactionContext parent; private final SingletonSupplier contextId = SingletonSupplier.of(UUID::randomUUID); @@ -48,11 +49,13 @@ public class TransactionContext { @Nullable private Set synchronizations; - private volatile @Nullable String currentTransactionName; + @Nullable + private volatile String currentTransactionName; private volatile boolean currentTransactionReadOnly; - private volatile @Nullable Integer currentTransactionIsolationLevel; + @Nullable + private volatile Integer currentTransactionIsolationLevel; private volatile boolean actualTransactionActive; diff --git a/spring-tx/src/main/java/org/springframework/transaction/reactive/TransactionContextManager.java b/spring-tx/src/main/java/org/springframework/transaction/reactive/TransactionContextManager.java index db6f4fb414ce..cac1f04133c7 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/reactive/TransactionContextManager.java +++ b/spring-tx/src/main/java/org/springframework/transaction/reactive/TransactionContextManager.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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. @@ -46,10 +46,10 @@ private TransactionContextManager() { * transactional context holder. Context retrieval fails with NoTransactionException * if no context or context holder is registered. * @return the current {@link TransactionContext} - * @throws NoTransactionException if no TransactionContext was found in the subscriber context - * or no context found in a holder + * @throws NoTransactionException if no TransactionContext was found in the + * subscriber context or no context found in a holder */ - public static Mono currentContext() throws NoTransactionException { + public static Mono currentContext() { return Mono.deferContextual(ctx -> { if (ctx.hasKey(TransactionContext.class)) { return Mono.just(ctx.get(TransactionContext.class)); diff --git a/spring-tx/src/main/java/org/springframework/transaction/reactive/TransactionSynchronizationManager.java b/spring-tx/src/main/java/org/springframework/transaction/reactive/TransactionSynchronizationManager.java index 54f7cd17a03d..27ed7f41739b 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/reactive/TransactionSynchronizationManager.java +++ b/spring-tx/src/main/java/org/springframework/transaction/reactive/TransactionSynchronizationManager.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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. @@ -32,8 +32,8 @@ /** * Central delegate that manages resources and transaction synchronizations per - * subscriber context. - * To be used by resource management code but not by typical application code. + * subscriber context. To be used by resource management code but not by typical + * application code. * *

    Supports one resource per key without overwriting, that is, a resource needs * to be removed before a new one can be set for the same key. @@ -73,6 +73,7 @@ public class TransactionSynchronizationManager { public TransactionSynchronizationManager(TransactionContext transactionContext) { + Assert.notNull(transactionContext, "TransactionContext must not be null"); this.transactionContext = transactionContext; } @@ -88,10 +89,11 @@ public static Mono forCurrentTransaction() { return TransactionContextManager.currentContext().map(TransactionSynchronizationManager::new); } + /** - * Check if there is a resource for the given key bound to the current thread. + * Check if there is a resource for the given key bound to the current context. * @param key the key to check (usually the resource factory) - * @return if there is a value bound to the current thread + * @return if there is a value bound to the current context */ public boolean hasResource(Object key) { Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key); @@ -100,9 +102,9 @@ public boolean hasResource(Object key) { } /** - * Retrieve a resource for the given key that is bound to the current thread. + * Retrieve a resource for the given key that is bound to the current context. * @param key the key to check (usually the resource factory) - * @return a value bound to the current thread (usually the active + * @return a value bound to the current context (usually the active * resource object), or {@code null} if none */ @Nullable diff --git a/spring-tx/src/main/java/org/springframework/transaction/reactive/TransactionSynchronizationUtils.java b/spring-tx/src/main/java/org/springframework/transaction/reactive/TransactionSynchronizationUtils.java index 902bd2012992..f12b921d891c 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/reactive/TransactionSynchronizationUtils.java +++ b/spring-tx/src/main/java/org/springframework/transaction/reactive/TransactionSynchronizationUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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. @@ -85,7 +85,7 @@ public static Mono triggerBeforeCommit(Collection triggerBeforeCompletion(Collection synchronizations) { return Flux.fromIterable(synchronizations) .concatMap(TransactionSynchronization::beforeCompletion).onErrorContinue((t, o) -> - logger.debug("TransactionSynchronization.beforeCompletion threw exception", t)).then(); + logger.error("TransactionSynchronization.beforeCompletion threw exception", t)).then(); } /** @@ -115,7 +115,7 @@ public static Mono invokeAfterCompletion( Collection synchronizations, int completionStatus) { return Flux.fromIterable(synchronizations).concatMap(it -> it.afterCompletion(completionStatus)) - .onErrorContinue((t, o) -> logger.debug("TransactionSynchronization.afterCompletion threw exception", t)).then(); + .onErrorContinue((t, o) -> logger.error("TransactionSynchronization.afterCompletion threw exception", t)).then(); } diff --git a/spring-tx/src/main/java/org/springframework/transaction/support/AbstractPlatformTransactionManager.java b/spring-tx/src/main/java/org/springframework/transaction/support/AbstractPlatformTransactionManager.java index e3f52fded7ad..c27c4bd2fd6c 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/support/AbstractPlatformTransactionManager.java +++ b/spring-tx/src/main/java/org/springframework/transaction/support/AbstractPlatformTransactionManager.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-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. @@ -451,7 +451,7 @@ private TransactionStatus handleExistingTransaction( if (useSavepointForNestedTransaction()) { // Create savepoint within existing Spring-managed transaction, // through the SavepointManager API implemented by TransactionStatus. - // Usually uses JDBC 3.0 savepoints. Never activates Spring synchronization. + // Usually uses JDBC savepoints. Never activates Spring synchronization. DefaultTransactionStatus status = prepareTransactionStatus(definition, transaction, false, false, debugEnabled, null); status.createAndHoldSavepoint(); @@ -465,7 +465,8 @@ private TransactionStatus handleExistingTransaction( } } - // Assumably PROPAGATION_SUPPORTS or PROPAGATION_REQUIRED. + // PROPAGATION_REQUIRED, PROPAGATION_SUPPORTS, PROPAGATION_MANDATORY: + // regular participation in existing transaction. if (debugEnabled) { logger.debug("Participating in existing transaction"); } diff --git a/spring-tx/src/main/java/org/springframework/transaction/support/DefaultTransactionDefinition.java b/spring-tx/src/main/java/org/springframework/transaction/support/DefaultTransactionDefinition.java index c42a0f8208e4..8cce62d42375 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/support/DefaultTransactionDefinition.java +++ b/spring-tx/src/main/java/org/springframework/transaction/support/DefaultTransactionDefinition.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-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. @@ -65,7 +65,7 @@ public class DefaultTransactionDefinition implements TransactionDefinition, Seri /** - * Create a new DefaultTransactionDefinition, with default settings. + * Create a new {@code DefaultTransactionDefinition} with default settings. * Can be modified through bean property setters. * @see #setPropagationBehavior * @see #setIsolationLevel @@ -93,7 +93,7 @@ public DefaultTransactionDefinition(TransactionDefinition other) { } /** - * Create a new DefaultTransactionDefinition with the given + * Create a new {@code DefaultTransactionDefinition} with the given * propagation behavior. Can be modified through bean property setters. * @param propagationBehavior one of the propagation constants in the * TransactionDefinition interface diff --git a/spring-tx/src/main/java/org/springframework/transaction/support/SmartTransactionObject.java b/spring-tx/src/main/java/org/springframework/transaction/support/SmartTransactionObject.java index 1bb2bf173173..d774381ccbde 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/support/SmartTransactionObject.java +++ b/spring-tx/src/main/java/org/springframework/transaction/support/SmartTransactionObject.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2023 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,13 +23,13 @@ * return an internal rollback-only marker, typically from another * transaction that has participated and marked it as rollback-only. * - *

    Autodetected by DefaultTransactionStatus, to always return a - * current rollbackOnly flag even if not resulting from the current + *

    Autodetected by {@link DefaultTransactionStatus} in order to always + * return a current rollbackOnly flag even if not resulting from the current * TransactionStatus. * * @author Juergen Hoeller * @since 1.1 - * @see DefaultTransactionStatus#isRollbackOnly + * @see DefaultTransactionStatus#isGlobalRollbackOnly() */ public interface SmartTransactionObject extends Flushable { diff --git a/spring-tx/src/main/java/org/springframework/transaction/support/TransactionSynchronizationUtils.java b/spring-tx/src/main/java/org/springframework/transaction/support/TransactionSynchronizationUtils.java index 8621d14988fb..05eb140f4ad7 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/support/TransactionSynchronizationUtils.java +++ b/spring-tx/src/main/java/org/springframework/transaction/support/TransactionSynchronizationUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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. @@ -108,7 +108,7 @@ public static void triggerBeforeCompletion() { synchronization.beforeCompletion(); } catch (Throwable ex) { - logger.debug("TransactionSynchronization.beforeCompletion threw exception", ex); + logger.error("TransactionSynchronization.beforeCompletion threw exception", ex); } } } @@ -172,7 +172,7 @@ public static void invokeAfterCompletion(@Nullable List { + getContext().publishEvent("SKIP"); + getEventCollector().assertNoEventReceived(); + return null; + }); + getEventCollector().assertNoEventReceived(); + } + @Test public void afterRollback() { load(AfterCompletionExplicitTestListener.class); @@ -307,13 +315,12 @@ public void conditionFoundOnTransactionalEventListener() { } @Test - public void afterCommitMetaAnnotation() throws Exception { + public void afterCommitMetaAnnotation() { load(AfterCommitMetaAnnotationTestListener.class); this.transactionTemplate.execute(status -> { getContext().publishEvent("test"); getEventCollector().assertNoEventReceived(); return null; - }); getEventCollector().assertEvents(EventCollector.AFTER_COMMIT, "test"); getEventCollector().assertTotalEventsCount(1); @@ -326,7 +333,6 @@ public void conditionFoundOnMetaAnnotation() { getContext().publishEvent("SKIP"); getEventCollector().assertNoEventReceived(); return null; - }); getEventCollector().assertNoEventReceived(); } @@ -530,6 +536,25 @@ public void handleAfterCommit(String data) { } + interface TransactionalComponentTestInterface { + + void handleAfterCommit(String data); + } + + + @Transactional + @Component + static class TransactionalComponentTestListenerWithInterface extends BaseTransactionalTestListener implements + TransactionalComponentTestInterface { + + @TransactionalEventListener(condition = "!'SKIP'.equals(#data)") + @Override + public void handleAfterCommit(String data) { + handleEvent(EventCollector.AFTER_COMMIT, data); + } + } + + @Component static class BeforeCommitTestListener extends BaseTransactionalTestListener { diff --git a/spring-web/spring-web.gradle b/spring-web/spring-web.gradle index 1b331f7d1ff7..03ef2d51c036 100644 --- a/spring-web/spring-web.gradle +++ b/spring-web/spring-web.gradle @@ -66,6 +66,7 @@ dependencies { testImplementation("com.fasterxml.jackson.datatype:jackson-datatype-joda") testImplementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310") testImplementation("com.fasterxml.jackson.module:jackson-module-kotlin") + testImplementation("com.fasterxml.jackson.dataformat:jackson-dataformat-csv") testImplementation("org.apache.tomcat:tomcat-util") testImplementation("org.apache.tomcat.embed:tomcat-embed-core") testImplementation("org.eclipse.jetty:jetty-server") diff --git a/spring-web/src/main/java/org/springframework/http/ContentDisposition.java b/spring-web/src/main/java/org/springframework/http/ContentDisposition.java index 119d0a29f2e1..f436b3f56567 100644 --- a/spring-web/src/main/java/org/springframework/http/ContentDisposition.java +++ b/spring-web/src/main/java/org/springframework/http/ContentDisposition.java @@ -233,9 +233,9 @@ public int hashCode() { result = 31 * result + ObjectUtils.nullSafeHashCode(this.filename); result = 31 * result + ObjectUtils.nullSafeHashCode(this.charset); result = 31 * result + ObjectUtils.nullSafeHashCode(this.size); - result = 31 * result + (this.creationDate != null ? this.creationDate.hashCode() : 0); - result = 31 * result + (this.modificationDate != null ? this.modificationDate.hashCode() : 0); - result = 31 * result + (this.readDate != null ? this.readDate.hashCode() : 0); + result = 31 * result + ObjectUtils.nullSafeHashCode(this.creationDate); + result = 31 * result + ObjectUtils.nullSafeHashCode(this.modificationDate); + result = 31 * result + ObjectUtils.nullSafeHashCode(this.readDate); return result; } @@ -360,7 +360,7 @@ else if (attribute.equals("filename*") ) { if (idx1 != -1 && idx2 != -1) { charset = Charset.forName(value.substring(0, idx1).trim()); Assert.isTrue(UTF_8.equals(charset) || ISO_8859_1.equals(charset), - "Charset should be UTF-8 or ISO-8859-1"); + "Charset must be UTF-8 or ISO-8859-1"); filename = decodeFilename(value.substring(idx2 + 1), charset); } else { @@ -532,10 +532,11 @@ private static String escapeQuotationsInFilename(String filename) { * @see RFC 5987 */ private static String encodeFilename(String input, Charset charset) { - Assert.notNull(input, "'input' is required"); - Assert.notNull(charset, "'charset' is required"); + Assert.notNull(input, "'input' must not be null"); + Assert.notNull(charset, "'charset' must not be null"); Assert.isTrue(!StandardCharsets.US_ASCII.equals(charset), "ASCII does not require encoding"); Assert.isTrue(UTF_8.equals(charset) || ISO_8859_1.equals(charset), "Only UTF-8 and ISO-8859-1 are supported"); + byte[] source = input.getBytes(charset); int len = source.length; StringBuilder sb = new StringBuilder(len << 1); diff --git a/spring-web/src/main/java/org/springframework/http/ETag.java b/spring-web/src/main/java/org/springframework/http/ETag.java new file mode 100644 index 000000000000..c4d670d50cbc --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/ETag.java @@ -0,0 +1,161 @@ +/* + * Copyright 2002-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.http; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.util.StringUtils; + +/** + * Represents an ETag for HTTP conditional requests. + * + * @author Rossen Stoyanchev + * @since 5.3.38 + * @see RFC 7232 + */ +public class ETag { + + private static final Log logger = LogFactory.getLog(ETag.class); + + private static final ETag WILDCARD = new ETag("*", false); + + + private final String tag; + + private final boolean weak; + + + public ETag(String tag, boolean weak) { + this.tag = tag; + this.weak = weak; + } + + + public String tag() { + return this.tag; + } + + public boolean weak() { + return this.weak; + } + + /** + * Whether this a wildcard tag matching to any entity tag value. + */ + public boolean isWildcard() { + return (this == WILDCARD); + } + + /** + * Return the fully formatted tag including "W/" prefix and quotes. + */ + public String formattedTag() { + if (this == WILDCARD) { + return "*"; + } + return (this.weak ? "W/" : "") + "\"" + this.tag + "\""; + } + + @Override + public String toString() { + return formattedTag(); + } + + + /** + * Parse entity tags from an "If-Match" or "If-None-Match" header. + * @param source the source string to parse + * @return the parsed ETags + */ + public static List parse(String source) { + + List result = new ArrayList<>(); + State state = State.BEFORE_QUOTES; + int startIndex = -1; + boolean weak = false; + + for (int i = 0; i < source.length(); i++) { + char c = source.charAt(i); + + if (state == State.IN_QUOTES) { + if (c == '"') { + String tag = source.substring(startIndex, i); + if (StringUtils.hasText(tag)) { + result.add(new ETag(tag, weak)); + } + state = State.AFTER_QUOTES; + startIndex = -1; + weak = false; + } + continue; + } + + if (Character.isWhitespace(c)) { + continue; + } + + if (c == ',') { + state = State.BEFORE_QUOTES; + continue; + } + + if (state == State.BEFORE_QUOTES) { + if (c == '*') { + result.add(WILDCARD); + state = State.AFTER_QUOTES; + continue; + } + if (c == '"') { + state = State.IN_QUOTES; + startIndex = i + 1; + continue; + } + if (c == 'W' && source.length() > i + 2) { + if (source.charAt(i + 1) == '/' && source.charAt(i + 2) == '"') { + state = State.IN_QUOTES; + i = i + 2; + startIndex = i + 1; + weak = true; + continue; + } + } + } + + if (logger.isDebugEnabled()) { + logger.debug("Unexpected char at index " + i); + } + } + + if (state != State.IN_QUOTES && logger.isDebugEnabled()) { + logger.debug("Expected closing '\"'"); + } + + return result; + } + + + private enum State { + + BEFORE_QUOTES, IN_QUOTES, AFTER_QUOTES + + } + +} diff --git a/spring-web/src/main/java/org/springframework/http/HttpCookie.java b/spring-web/src/main/java/org/springframework/http/HttpCookie.java index 9ce7fde3a26e..279015a91a69 100644 --- a/spring-web/src/main/java/org/springframework/http/HttpCookie.java +++ b/spring-web/src/main/java/org/springframework/http/HttpCookie.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2023 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. @@ -56,11 +56,6 @@ public String getValue() { } - @Override - public int hashCode() { - return this.name.hashCode(); - } - @Override public boolean equals(@Nullable Object other) { if (this == other) { @@ -73,6 +68,11 @@ public boolean equals(@Nullable Object other) { return (this.name.equalsIgnoreCase(otherCookie.getName())); } + @Override + public int hashCode() { + return this.name.hashCode(); + } + @Override public String toString() { return this.name + '=' + this.value; diff --git a/spring-web/src/main/java/org/springframework/http/HttpHeaders.java b/spring-web/src/main/java/org/springframework/http/HttpHeaders.java index a6ac7a4b1090..5fe14a360be4 100644 --- a/spring-web/src/main/java/org/springframework/http/HttpHeaders.java +++ b/spring-web/src/main/java/org/springframework/http/HttpHeaders.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-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. @@ -40,8 +40,6 @@ import java.util.Map; import java.util.Set; import java.util.StringJoiner; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import java.util.stream.Collectors; import org.springframework.lang.Nullable; @@ -393,12 +391,6 @@ public class HttpHeaders implements MultiValueMap, Serializable */ public static final HttpHeaders EMPTY = new ReadOnlyHttpHeaders(new LinkedMultiValueMap<>()); - /** - * Pattern matching ETag multiple field values in headers such as "If-Match", "If-None-Match". - * @see Section 2.3 of RFC 7232 - */ - private static final Pattern ETAG_HEADER_VALUE_PATTERN = Pattern.compile("\\*|\\s*((W\\/)?(\"[^\"]*\"))\\s*,?"); - private static final DecimalFormatSymbols DECIMAL_FORMAT_SYMBOLS = new DecimalFormatSymbols(Locale.ENGLISH); private static final ZoneId GMT = ZoneId.of("GMT"); @@ -984,7 +976,8 @@ public void setContentType(@Nullable MediaType mediaType) { /** * Return the {@linkplain MediaType media type} of the body, as specified * by the {@code Content-Type} header. - *

    Returns {@code null} when the content-type is unknown. + *

    Returns {@code null} when the {@code Content-Type} header is not set. + * @throws InvalidMediaTypeException if the media type value cannot be parsed */ @Nullable public MediaType getContentType() { @@ -1036,9 +1029,8 @@ public long getDate() { */ public void setETag(@Nullable String etag) { if (etag != null) { - Assert.isTrue(etag.startsWith("\"") || etag.startsWith("W/"), - "Invalid ETag: does not start with W/ or \""); - Assert.isTrue(etag.endsWith("\""), "Invalid ETag: does not end with \""); + Assert.isTrue(etag.startsWith("\"") || etag.startsWith("W/\""), "ETag does not start with W/\" or \""); + Assert.isTrue(etag.endsWith("\""), "ETag does not end with \""); set(ETAG, etag); } else { @@ -1568,35 +1560,27 @@ public void clearContentHeaders() { /** * Retrieve a combined result from the field values of the ETag header. - * @param headerName the header name + * @param name the header name * @return the combined result * @throws IllegalArgumentException if parsing fails * @since 4.3 */ - protected List getETagValuesAsList(String headerName) { - List values = get(headerName); - if (values != null) { - List result = new ArrayList<>(); - for (String value : values) { - if (value != null) { - Matcher matcher = ETAG_HEADER_VALUE_PATTERN.matcher(value); - while (matcher.find()) { - if ("*".equals(matcher.group())) { - result.add(matcher.group()); - } - else { - result.add(matcher.group(1)); - } - } - if (result.isEmpty()) { - throw new IllegalArgumentException( - "Could not parse header '" + headerName + "' with value '" + value + "'"); - } + protected List getETagValuesAsList(String name) { + List values = get(name); + if (values == null) { + return Collections.emptyList(); + } + List result = new ArrayList<>(); + for (String value : values) { + if (value != null) { + List tags = ETag.parse(value); + Assert.notEmpty(tags, "Could not parse header '" + name + "' with value '" + value + "'"); + for (ETag tag : tags) { + result.add(tag.formattedTag()); } } - return result; } - return Collections.emptyList(); + return result; } /** diff --git a/spring-web/src/main/java/org/springframework/http/RequestEntity.java b/spring-web/src/main/java/org/springframework/http/RequestEntity.java index b5bd28da5c39..473e95f52a5b 100644 --- a/spring-web/src/main/java/org/springframework/http/RequestEntity.java +++ b/spring-web/src/main/java/org/springframework/http/RequestEntity.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 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-web/src/main/java/org/springframework/http/ResponseCookie.java b/spring-web/src/main/java/org/springframework/http/ResponseCookie.java index 386fabd5db61..8c6120d37c0d 100644 --- a/spring-web/src/main/java/org/springframework/http/ResponseCookie.java +++ b/spring-web/src/main/java/org/springframework/http/ResponseCookie.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-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. @@ -110,7 +110,7 @@ public boolean isSecure() { /** * Return {@code true} if the cookie has the "HttpOnly" attribute. - * @see https://www.owasp.org/index.php/HTTPOnly + * @see https://owasp.org/www-community/HttpOnly */ public boolean isHttpOnly() { return this.httpOnly; @@ -326,7 +326,7 @@ public interface ResponseCookieBuilder { /** * Add the "HttpOnly" attribute to the cookie. - * @see https://www.owasp.org/index.php/HTTPOnly + * @see https://owasp.org/www-community/HttpOnly */ ResponseCookieBuilder httpOnly(boolean httpOnly); diff --git a/spring-web/src/main/java/org/springframework/http/client/reactive/AbstractClientHttpResponse.java b/spring-web/src/main/java/org/springframework/http/client/reactive/AbstractClientHttpResponse.java new file mode 100644 index 000000000000..a54129ea6eae --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/client/reactive/AbstractClientHttpResponse.java @@ -0,0 +1,124 @@ +/* + * Copyright 2002-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.http.client.reactive; + +import java.util.Objects; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; +import reactor.core.publisher.Flux; + +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseCookie; +import org.springframework.util.Assert; +import org.springframework.util.MultiValueMap; + +/** + * Base class for {@link ClientHttpResponse} implementations. + * + * @author Arjen Poutsma + * @since 5.3.32 + */ +public abstract class AbstractClientHttpResponse implements ClientHttpResponse { + + private final int statusCode; + + private final HttpHeaders headers; + + private final MultiValueMap cookies; + + private final Flux body; + + + + protected AbstractClientHttpResponse(int statusCode, HttpHeaders headers, + MultiValueMap cookies, Flux body) { + + Assert.notNull(headers, "Headers must not be null"); + Assert.notNull(body, "Body must not be null"); + + this.statusCode = statusCode; + this.headers = headers; + this.cookies = cookies; + this.body = Flux.from(new SingleSubscriberPublisher<>(body)); + } + + + @Override + public HttpStatus getStatusCode() { + return HttpStatus.valueOf(this.statusCode); + } + + @Override + public int getRawStatusCode() { + return this.statusCode; + } + + @Override + public HttpHeaders getHeaders() { + return this.headers; + } + + @Override + public MultiValueMap getCookies() { + return this.cookies; + } + + @Override + public Flux getBody() { + return this.body; + } + + + private static final class SingleSubscriberPublisher implements Publisher { + + private static final Subscription NO_OP_SUBSCRIPTION = new Subscription() { + @Override + public void request(long l) { + } + + @Override + public void cancel() { + } + }; + + private final Publisher delegate; + + private final AtomicBoolean subscribed = new AtomicBoolean(); + + + public SingleSubscriberPublisher(Publisher delegate) { + this.delegate = delegate; + } + + @Override + public void subscribe(Subscriber subscriber) { + Objects.requireNonNull(subscriber, "Subscriber must not be null"); + if (this.subscribed.compareAndSet(false, true)) { + this.delegate.subscribe(subscriber); + } + else { + subscriber.onSubscribe(NO_OP_SUBSCRIPTION); + subscriber.onError(new IllegalStateException("The client response body can only be consumed once")); + } + } + } +} diff --git a/spring-web/src/main/java/org/springframework/http/client/reactive/HttpComponentsClientHttpConnector.java b/spring-web/src/main/java/org/springframework/http/client/reactive/HttpComponentsClientHttpConnector.java index 612da6ec3480..ea3fa56d5fe6 100644 --- a/spring-web/src/main/java/org/springframework/http/client/reactive/HttpComponentsClientHttpConnector.java +++ b/spring-web/src/main/java/org/springframework/http/client/reactive/HttpComponentsClientHttpConnector.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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. @@ -106,14 +106,12 @@ public Mono connect(HttpMethod method, URI uri, Function> requestCallback) { HttpClientContext context = this.contextProvider.apply(method, uri); - if (context.getCookieStore() == null) { context.setCookieStore(new BasicCookieStore()); } - HttpComponentsClientHttpRequest request = new HttpComponentsClientHttpRequest(method, uri, - context, this.dataBufferFactory); - + HttpComponentsClientHttpRequest request = + new HttpComponentsClientHttpRequest(method, uri, context, this.dataBufferFactory); return requestCallback.apply(request).then(Mono.defer(() -> execute(request, context))); } @@ -123,7 +121,6 @@ private Mono execute(HttpComponentsClientHttpRequest request return Mono.create(sink -> { ReactiveResponseConsumer reactiveResponseConsumer = new ReactiveResponseConsumer(new MonoFutureCallbackAdapter(sink, this.dataBufferFactory, context)); - this.client.execute(requestProducer, reactiveResponseConsumer, context, null); }); } @@ -133,6 +130,7 @@ public void close() throws IOException { this.client.close(); } + private static class MonoFutureCallbackAdapter implements FutureCallback>> { @@ -144,6 +142,7 @@ private static class MonoFutureCallbackAdapter public MonoFutureCallbackAdapter(MonoSink sink, DataBufferFactory dataBufferFactory, HttpClientContext context) { + this.sink = sink; this.dataBufferFactory = dataBufferFactory; this.context = context; @@ -151,19 +150,12 @@ public MonoFutureCallbackAdapter(MonoSink sink, @Override public void completed(Message> result) { - HttpComponentsClientHttpResponse response = - new HttpComponentsClientHttpResponse(this.dataBufferFactory, result, this.context); - this.sink.success(response); + this.sink.success(new HttpComponentsClientHttpResponse(this.dataBufferFactory, result, this.context)); } @Override public void failed(Exception ex) { - Throwable t = ex; - if (t instanceof HttpStreamResetException) { - HttpStreamResetException httpStreamResetException = (HttpStreamResetException) ex; - t = httpStreamResetException.getCause(); - } - this.sink.error(t); + this.sink.error(ex instanceof HttpStreamResetException && ex.getCause() != null ? ex.getCause() : ex); } @Override diff --git a/spring-web/src/main/java/org/springframework/http/client/reactive/HttpComponentsClientHttpRequest.java b/spring-web/src/main/java/org/springframework/http/client/reactive/HttpComponentsClientHttpRequest.java index fd48945d5fe2..d79bae649211 100644 --- a/spring-web/src/main/java/org/springframework/http/client/reactive/HttpComponentsClientHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/client/reactive/HttpComponentsClientHttpRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-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. @@ -39,11 +39,10 @@ import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; import org.springframework.lang.Nullable; import org.springframework.util.Assert; -import static org.springframework.http.MediaType.ALL_VALUE; - /** * {@link ClientHttpRequest} implementation for the Apache HttpComponents HttpClient 5.x. * @author Martin Tarjányi @@ -123,16 +122,14 @@ public Mono setComplete() { @Override protected void applyHeaders() { HttpHeaders headers = getHeaders(); - headers.entrySet() .stream() .filter(entry -> !HttpHeaders.CONTENT_LENGTH.equals(entry.getKey())) .forEach(entry -> entry.getValue().forEach(v -> this.httpRequest.addHeader(entry.getKey(), v))); if (!this.httpRequest.containsHeader(HttpHeaders.ACCEPT)) { - this.httpRequest.addHeader(HttpHeaders.ACCEPT, ALL_VALUE); + this.httpRequest.addHeader(HttpHeaders.ACCEPT, MediaType.ALL_VALUE); } - this.contentLength = headers.getContentLength(); } @@ -143,7 +140,6 @@ protected void applyCookies() { } CookieStore cookieStore = this.context.getCookieStore(); - getCookies().values() .stream() .flatMap(Collection::stream) diff --git a/spring-web/src/main/java/org/springframework/http/client/reactive/HttpComponentsClientHttpResponse.java b/spring-web/src/main/java/org/springframework/http/client/reactive/HttpComponentsClientHttpResponse.java index c284834e1501..e72285a69bd3 100644 --- a/spring-web/src/main/java/org/springframework/http/client/reactive/HttpComponentsClientHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/client/reactive/HttpComponentsClientHttpResponse.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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,7 +17,6 @@ package org.springframework.http.client.reactive; import java.nio.ByteBuffer; -import java.util.concurrent.atomic.AtomicBoolean; import org.apache.hc.client5.http.cookie.Cookie; import org.apache.hc.client5.http.protocol.HttpClientContext; @@ -26,10 +25,8 @@ import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; -import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; import org.springframework.http.ResponseCookie; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; @@ -42,45 +39,22 @@ * @since 5.3 * @see Apache HttpComponents */ -class HttpComponentsClientHttpResponse implements ClientHttpResponse { - - private final DataBufferFactory dataBufferFactory; - - private final Message> message; - - private final HttpHeaders headers; - - private final HttpClientContext context; - - private final AtomicBoolean rejectSubscribers = new AtomicBoolean(); +class HttpComponentsClientHttpResponse extends AbstractClientHttpResponse { public HttpComponentsClientHttpResponse(DataBufferFactory dataBufferFactory, Message> message, HttpClientContext context) { - this.dataBufferFactory = dataBufferFactory; - this.message = message; - this.context = context; - - MultiValueMap adapter = new HttpComponentsHeadersAdapter(message.getHead()); - this.headers = HttpHeaders.readOnlyHttpHeaders(adapter); - } - - - @Override - public HttpStatus getStatusCode() { - return HttpStatus.valueOf(this.message.getHead().getCode()); - } - - @Override - public int getRawStatusCode() { - return this.message.getHead().getCode(); + super(message.getHead().getCode(), + HttpHeaders.readOnlyHttpHeaders(new HttpComponentsHeadersAdapter(message.getHead())), + adaptCookies(context), + Flux.from(message.getBody()).map(dataBufferFactory::wrap) + ); } - @Override - public MultiValueMap getCookies() { + private static MultiValueMap adaptCookies(HttpClientContext context) { LinkedMultiValueMap result = new LinkedMultiValueMap<>(); - this.context.getCookieStore().getCookies().forEach(cookie -> + context.getCookieStore().getCookies().forEach(cookie -> result.add(cookie.getName(), ResponseCookie.fromClientResponse(cookie.getName(), cookie.getValue()) .domain(cookie.getDomain()) @@ -93,25 +67,10 @@ public MultiValueMap getCookies() { return result; } - private long getMaxAgeSeconds(Cookie cookie) { + private static long getMaxAgeSeconds(Cookie cookie) { String maxAgeAttribute = cookie.getAttribute(Cookie.MAX_AGE_ATTR); return (maxAgeAttribute != null ? Long.parseLong(maxAgeAttribute) : -1); } - @Override - public Flux getBody() { - return Flux.from(this.message.getBody()) - .doOnSubscribe(s -> { - if (!this.rejectSubscribers.compareAndSet(false, true)) { - throw new IllegalStateException("The client response body can only be consumed once."); - } - }) - .map(this.dataBufferFactory::wrap); - } - - @Override - public HttpHeaders getHeaders() { - return this.headers; - } } diff --git a/spring-web/src/main/java/org/springframework/http/client/reactive/HttpComponentsHeadersAdapter.java b/spring-web/src/main/java/org/springframework/http/client/reactive/HttpComponentsHeadersAdapter.java index 944703f8e166..aa82346d7453 100644 --- a/spring-web/src/main/java/org/springframework/http/client/reactive/HttpComponentsHeadersAdapter.java +++ b/spring-web/src/main/java/org/springframework/http/client/reactive/HttpComponentsHeadersAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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,6 +53,7 @@ class HttpComponentsHeadersAdapter implements MultiValueMap { @Override + @Nullable public String getFirst(String key) { Header header = this.message.getFirstHeader(key); return (header != null ? header.getValue() : null); diff --git a/spring-web/src/main/java/org/springframework/http/client/reactive/Jetty10HttpFieldsHelper.java b/spring-web/src/main/java/org/springframework/http/client/reactive/Jetty10HttpFieldsHelper.java new file mode 100644 index 000000000000..83ff92994d4e --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/client/reactive/Jetty10HttpFieldsHelper.java @@ -0,0 +1,93 @@ +/* + * Copyright 2002-2023 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.http.client.reactive; + +import java.lang.reflect.Method; + +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.client.api.Response; + +import org.springframework.http.HttpHeaders; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ReflectionUtils; + +/** + * Used to support Jetty 10. + * + * @author Rossen Stoyanchev + * @author Arjen Poutsma + * @since 5.3.26 + */ +abstract class Jetty10HttpFieldsHelper { + + private static final boolean jetty10Present; + + private static final Method requestGetHeadersMethod; + + private static final Method responseGetHeadersMethod; + + private static final Method getNameMethod; + + private static final Method getValueMethod; + + + static { + try { + ClassLoader classLoader = JettyClientHttpResponse.class.getClassLoader(); + Class httpFieldsClass = classLoader.loadClass("org.eclipse.jetty.http.HttpFields"); + jetty10Present = httpFieldsClass.isInterface(); + requestGetHeadersMethod = Request.class.getMethod("getHeaders"); + responseGetHeadersMethod = Response.class.getMethod("getHeaders"); + Class httpFieldClass = classLoader.loadClass("org.eclipse.jetty.http.HttpField"); + getNameMethod = httpFieldClass.getMethod("getName"); + getValueMethod = httpFieldClass.getMethod("getValue"); + } + catch (ClassNotFoundException | NoSuchMethodException ex) { + throw new IllegalStateException("No compatible Jetty version found", ex); + } + } + + + public static boolean jetty10Present() { + return jetty10Present; + } + + public static HttpHeaders getHttpHeaders(Request request) { + Iterable iterator = (Iterable) + ReflectionUtils.invokeMethod(requestGetHeadersMethod, request); + return getHttpHeadersInternal(iterator); + } + + public static HttpHeaders getHttpHeaders(Response response) { + Iterable iterator = (Iterable) + ReflectionUtils.invokeMethod(responseGetHeadersMethod, response); + return getHttpHeadersInternal(iterator); + } + + private static HttpHeaders getHttpHeadersInternal(@Nullable Iterable iterator) { + Assert.notNull(iterator, "Iterator must not be null"); + HttpHeaders headers = new HttpHeaders(); + for (Object field : iterator) { + String name = (String) ReflectionUtils.invokeMethod(getNameMethod, field); + Assert.notNull(name, "Header name must not be null"); + String value = (String) ReflectionUtils.invokeMethod(getValueMethod, field); + headers.add(name, value); + } + return headers; + } +} diff --git a/spring-web/src/main/java/org/springframework/http/client/reactive/JettyClientHttpRequest.java b/spring-web/src/main/java/org/springframework/http/client/reactive/JettyClientHttpRequest.java index 4e1df906eb21..c3c75c26d144 100644 --- a/spring-web/src/main/java/org/springframework/http/client/reactive/JettyClientHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/client/reactive/JettyClientHttpRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -37,6 +37,7 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; +import org.springframework.util.MultiValueMap; /** * {@link ClientHttpRequest} implementation for the Jetty ReactiveStreams HTTP client. @@ -54,7 +55,6 @@ class JettyClientHttpRequest extends AbstractClientHttpRequest { private final ReactiveRequest.Builder builder; - public JettyClientHttpRequest(Request jettyRequest, DataBufferFactory bufferFactory) { this.jettyRequest = jettyRequest; this.bufferFactory = bufferFactory; @@ -96,8 +96,7 @@ public Mono writeWith(Publisher body) { .as(chunks -> ReactiveRequest.Content.fromPublisher(chunks, getContentType())); this.builder.content(content); sink.success(); - }) - .then(doCommit()); + }).then(doCommit()); } @Override @@ -144,7 +143,11 @@ protected void applyHeaders() { @Override protected HttpHeaders initReadOnlyHeaders() { - return HttpHeaders.readOnlyHttpHeaders(new JettyHeadersAdapter(this.jettyRequest.getHeaders())); + MultiValueMap headers = (Jetty10HttpFieldsHelper.jetty10Present() ? + Jetty10HttpFieldsHelper.getHttpHeaders(this.jettyRequest) : + new JettyHeadersAdapter(this.jettyRequest.getHeaders())); + + return HttpHeaders.readOnlyHttpHeaders(headers); } public ReactiveRequest toReactiveRequest() { diff --git a/spring-web/src/main/java/org/springframework/http/client/reactive/JettyClientHttpResponse.java b/spring-web/src/main/java/org/springframework/http/client/reactive/JettyClientHttpResponse.java index 5d88c83ddf67..e6c8bf6c685f 100644 --- a/spring-web/src/main/java/org/springframework/http/client/reactive/JettyClientHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/client/reactive/JettyClientHttpResponse.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-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,27 +16,21 @@ package org.springframework.http.client.reactive; -import java.lang.reflect.Method; import java.net.HttpCookie; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; -import org.eclipse.jetty.client.api.Response; import org.eclipse.jetty.reactive.client.ReactiveResponse; -import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; import org.springframework.http.ResponseCookie; import org.springframework.lang.Nullable; -import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; -import org.springframework.util.ReflectionUtils; /** * {@link ClientHttpResponse} implementation for the Jetty ReactiveStreams HTTP client. @@ -46,60 +40,31 @@ * @see * Jetty ReactiveStreams HttpClient */ -class JettyClientHttpResponse implements ClientHttpResponse { +class JettyClientHttpResponse extends AbstractClientHttpResponse { private static final Pattern SAMESITE_PATTERN = Pattern.compile("(?i).*SameSite=(Strict|Lax|None).*"); - private static final ClassLoader classLoader = JettyClientHttpResponse.class.getClassLoader(); - private static final boolean jetty10Present; + public JettyClientHttpResponse(ReactiveResponse reactiveResponse, Flux content) { - - private final ReactiveResponse reactiveResponse; - - private final Flux content; - - private final HttpHeaders headers; - - - static { - try { - Class httpFieldsClass = classLoader.loadClass("org.eclipse.jetty.http.HttpFields"); - jetty10Present = httpFieldsClass.isInterface(); - } - catch (ClassNotFoundException ex) { - throw new IllegalStateException("No compatible Jetty version found", ex); - } - } - - - public JettyClientHttpResponse(ReactiveResponse reactiveResponse, Publisher content) { - this.reactiveResponse = reactiveResponse; - this.content = Flux.from(content); - - MultiValueMap headers = (jetty10Present ? - Jetty10HttpFieldsHelper.getHttpHeaders(reactiveResponse) : - new JettyHeadersAdapter(reactiveResponse.getHeaders())); - - this.headers = HttpHeaders.readOnlyHttpHeaders(headers); - } - - - @Override - public HttpStatus getStatusCode() { - return HttpStatus.valueOf(getRawStatusCode()); + super(reactiveResponse.getStatus(), + adaptHeaders(reactiveResponse), + adaptCookies(reactiveResponse), + content); } - @Override - public int getRawStatusCode() { - return this.reactiveResponse.getStatus(); + private static HttpHeaders adaptHeaders(ReactiveResponse response) { + MultiValueMap headers = (Jetty10HttpFieldsHelper.jetty10Present() ? + Jetty10HttpFieldsHelper.getHttpHeaders(response.getResponse()) : + new JettyHeadersAdapter(response.getHeaders())); + return HttpHeaders.readOnlyHttpHeaders(headers); } - @Override - public MultiValueMap getCookies() { + private static MultiValueMap adaptCookies(ReactiveResponse response) { MultiValueMap result = new LinkedMultiValueMap<>(); - List cookieHeader = getHeaders().get(HttpHeaders.SET_COOKIE); - if (cookieHeader != null) { + MultiValueMap headers = adaptHeaders(response); + List cookieHeader = headers.get(HttpHeaders.SET_COOKIE); + if (!CollectionUtils.isEmpty(cookieHeader)) { cookieHeader.forEach(header -> HttpCookie.parse(header).forEach(cookie -> result.add(cookie.getName(), ResponseCookie.fromClientResponse(cookie.getName(), cookie.getValue()) @@ -121,51 +86,4 @@ private static String parseSameSite(String headerValue) { return (matcher.matches() ? matcher.group(1) : null); } - - @Override - public Flux getBody() { - return this.content; - } - - @Override - public HttpHeaders getHeaders() { - return this.headers; - } - - - private static class Jetty10HttpFieldsHelper { - - private static final Method getHeadersMethod; - - private static final Method getNameMethod; - - private static final Method getValueMethod; - - static { - try { - getHeadersMethod = Response.class.getMethod("getHeaders"); - Class type = classLoader.loadClass("org.eclipse.jetty.http.HttpField"); - getNameMethod = type.getMethod("getName"); - getValueMethod = type.getMethod("getValue"); - } - catch (ClassNotFoundException | NoSuchMethodException ex) { - throw new IllegalStateException("No compatible Jetty version found", ex); - } - } - - public static HttpHeaders getHttpHeaders(ReactiveResponse response) { - HttpHeaders headers = new HttpHeaders(); - Iterable iterator = (Iterable) - ReflectionUtils.invokeMethod(getHeadersMethod, response.getResponse()); - Assert.notNull(iterator, "Iterator must not be null"); - for (Object field : iterator) { - String name = (String) ReflectionUtils.invokeMethod(getNameMethod, field); - Assert.notNull(name, "Header name must not be null"); - String value = (String) ReflectionUtils.invokeMethod(getValueMethod, field); - headers.add(name, value); - } - return headers; - } - } - } diff --git a/spring-web/src/main/java/org/springframework/http/client/reactive/JettyHeadersAdapter.java b/spring-web/src/main/java/org/springframework/http/client/reactive/JettyHeadersAdapter.java index f44362c9c3a2..7b2f3a245884 100644 --- a/spring-web/src/main/java/org/springframework/http/client/reactive/JettyHeadersAdapter.java +++ b/spring-web/src/main/java/org/springframework/http/client/reactive/JettyHeadersAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 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. @@ -170,7 +170,6 @@ public Set>> entrySet() { public Iterator>> iterator() { return new EntryIterator(); } - @Override public int size() { return headers.size(); diff --git a/spring-web/src/main/java/org/springframework/http/codec/ClientCodecConfigurer.java b/spring-web/src/main/java/org/springframework/http/codec/ClientCodecConfigurer.java index 070b0610ee1f..4bcd156a46ff 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/ClientCodecConfigurer.java +++ b/spring-web/src/main/java/org/springframework/http/codec/ClientCodecConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2022 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. @@ -92,11 +92,11 @@ interface ClientDefaultCodecs extends DefaultCodecs { /** * Configure the {@code Decoder} to use for Server-Sent Events. - *

    By default if this is not set, and Jackson is available, the - * {@link #jackson2JsonDecoder} override is used instead. Use this property - * if you want to further customize the SSE decoder. - *

    Note that {@link #maxInMemorySize(int)}, if configured, will be - * applied to the given decoder. + *

    By default if this is not set, and Jackson is available, + * the {@link #jackson2JsonDecoder} override is used instead. + * Use this method to customize the SSE decoder. + *

    Note that {@link #maxInMemorySize(int)}, if configured, + * will be applied to the given decoder. * @param decoder the decoder to use */ void serverSentEventDecoder(Decoder decoder); diff --git a/spring-web/src/main/java/org/springframework/http/codec/ServerCodecConfigurer.java b/spring-web/src/main/java/org/springframework/http/codec/ServerCodecConfigurer.java index 4bbefc8c939c..bd23db409d8a 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/ServerCodecConfigurer.java +++ b/spring-web/src/main/java/org/springframework/http/codec/ServerCodecConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2022 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. @@ -78,7 +78,7 @@ static ServerCodecConfigurer create() { /** - * {@link CodecConfigurer.DefaultCodecs} extension with extra client-side options. + * {@link CodecConfigurer.DefaultCodecs} extension with extra server-side options. */ interface ServerDefaultCodecs extends DefaultCodecs { @@ -101,9 +101,9 @@ interface ServerDefaultCodecs extends DefaultCodecs { /** * Configure the {@code Encoder} to use for Server-Sent Events. - *

    By default if this is not set, and Jackson is available, the - * {@link #jackson2JsonEncoder} override is used instead. Use this method - * to customize the SSE encoder. + *

    By default if this is not set, and Jackson is available, + * the {@link #jackson2JsonEncoder} override is used instead. + * Use this method to customize the SSE encoder. */ void serverSentEventEncoder(Encoder encoder); } diff --git a/spring-web/src/main/java/org/springframework/http/codec/json/AbstractJackson2Encoder.java b/spring-web/src/main/java/org/springframework/http/codec/json/AbstractJackson2Encoder.java index ffb979f27c4d..ddff8fea1ea3 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/json/AbstractJackson2Encoder.java +++ b/spring-web/src/main/java/org/springframework/http/codec/json/AbstractJackson2Encoder.java @@ -169,8 +169,8 @@ public Flux encode(Publisher inputStream, DataBufferFactory buffe separator)) .doAfterTerminate(() -> { try { - byteBuilder.release(); generator.close(); + byteBuilder.release(); } catch (IOException ex) { logger.error("Could not close Encoder resources", ex); diff --git a/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2CodecSupport.java b/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2CodecSupport.java index 538c2e41a86b..8151064fa901 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2CodecSupport.java +++ b/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2CodecSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 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-web/src/main/java/org/springframework/http/codec/protobuf/ProtobufDecoder.java b/spring-web/src/main/java/org/springframework/http/codec/protobuf/ProtobufDecoder.java index 4804dcf1f4f5..5be998536ea6 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/protobuf/ProtobufDecoder.java +++ b/spring-web/src/main/java/org/springframework/http/codec/protobuf/ProtobufDecoder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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. @@ -265,7 +265,7 @@ public Iterable apply(DataBuffer input) { * Parse message size as a varint from the input stream, updating {@code messageBytesToRead} and * {@code offset} fields if needed to allow processing of upcoming chunks. * Inspired from {@link CodedInputStream#readRawVarint32(int, java.io.InputStream)} - * @return {code true} when the message size is parsed successfully, {code false} when the message size is + * @return {@code true} when the message size is parsed successfully, {@code false} when the message size is * truncated * @see Base 128 Varints */ diff --git a/spring-web/src/main/java/org/springframework/http/converter/FormHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/FormHttpMessageConverter.java index 105e94b86ba8..4119649b601f 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/FormHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/FormHttpMessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2022 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-web/src/main/java/org/springframework/http/converter/ResourceHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/ResourceHttpMessageConverter.java index 17aa290ca9ab..8c60749ba125 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/ResourceHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/ResourceHttpMessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-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. @@ -81,6 +81,7 @@ protected Resource readInternal(Class clazz, HttpInputMessag if (this.supportsReadStreaming && InputStreamResource.class == clazz) { return new InputStreamResource(inputMessage.getBody()) { @Override + @Nullable public String getFilename() { return inputMessage.getHeaders().getContentDisposition().getFilename(); } @@ -106,12 +107,20 @@ public String getFilename() { } } + @Override + protected void writeInternal(Resource resource, HttpOutputMessage outputMessage) + throws IOException, HttpMessageNotWritableException { + + writeContent(resource, outputMessage); + } + @Override protected MediaType getDefaultContentType(Resource resource) { return MediaTypeFactory.getMediaType(resource).orElse(MediaType.APPLICATION_OCTET_STREAM); } @Override + @Nullable protected Long getContentLength(Resource resource, @Nullable MediaType contentType) throws IOException { // Don't try to determine contentLength on InputStreamResource - cannot be read afterwards... // Note: custom InputStreamResource subclasses could provide a pre-calculated content length! @@ -122,15 +131,10 @@ protected Long getContentLength(Resource resource, @Nullable MediaType contentTy return (contentLength < 0 ? null : contentLength); } - @Override - protected void writeInternal(Resource resource, HttpOutputMessage outputMessage) - throws IOException, HttpMessageNotWritableException { - - writeContent(resource, outputMessage); - } protected void writeContent(Resource resource, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException { + // We cannot use try-with-resources here for the InputStream, since we have // custom handling of the close() method in a finally-block. try { diff --git a/spring-web/src/main/java/org/springframework/http/converter/ResourceRegionHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/ResourceRegionHttpMessageConverter.java index 511544a95614..8cc17fd7d968 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/ResourceRegionHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/ResourceRegionHttpMessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-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. @@ -37,8 +37,8 @@ import org.springframework.util.StreamUtils; /** - * Implementation of {@link HttpMessageConverter} that can write a single {@link ResourceRegion}, - * or Collections of {@link ResourceRegion ResourceRegions}. + * Implementation of {@link HttpMessageConverter} that can write a single + * {@link ResourceRegion} or Collections of {@link ResourceRegion ResourceRegions}. * * @author Brian Clozel * @author Juergen Hoeller @@ -51,22 +51,6 @@ public ResourceRegionHttpMessageConverter() { } - @Override - @SuppressWarnings("unchecked") - protected MediaType getDefaultContentType(Object object) { - Resource resource = null; - if (object instanceof ResourceRegion) { - resource = ((ResourceRegion) object).getResource(); - } - else { - Collection regions = (Collection) object; - if (!regions.isEmpty()) { - resource = regions.iterator().next().getResource(); - } - } - return MediaTypeFactory.getMediaType(resource).orElse(MediaType.APPLICATION_OCTET_STREAM); - } - @Override public boolean canRead(Class clazz, @Nullable MediaType mediaType) { return false; @@ -123,7 +107,6 @@ public boolean canWrite(@Nullable Type type, @Nullable Class clazz, @Nullable } @Override - @SuppressWarnings("unchecked") protected void writeInternal(Object object, @Nullable Type type, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException { @@ -131,16 +114,33 @@ protected void writeInternal(Object object, @Nullable Type type, HttpOutputMessa writeResourceRegion((ResourceRegion) object, outputMessage); } else { + @SuppressWarnings("unchecked") Collection regions = (Collection) object; if (regions.size() == 1) { writeResourceRegion(regions.iterator().next(), outputMessage); } else { - writeResourceRegionCollection((Collection) object, outputMessage); + writeResourceRegionCollection(regions, outputMessage); } } } + @Override + protected MediaType getDefaultContentType(Object object) { + Resource resource = null; + if (object instanceof ResourceRegion) { + resource = ((ResourceRegion) object).getResource(); + } + else { + @SuppressWarnings("unchecked") + Collection regions = (Collection) object; + if (!regions.isEmpty()) { + resource = regions.iterator().next().getResource(); + } + } + return MediaTypeFactory.getMediaType(resource).orElse(MediaType.APPLICATION_OCTET_STREAM); + } + protected void writeResourceRegion(ResourceRegion region, HttpOutputMessage outputMessage) throws IOException { Assert.notNull(region, "ResourceRegion must not be null"); diff --git a/spring-web/src/main/java/org/springframework/http/converter/StringHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/StringHttpMessageConverter.java index b45c9ddeb82b..6da38db05bfa 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/StringHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/StringHttpMessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -106,10 +106,9 @@ protected Long getContentLength(String str, @Nullable MediaType contentType) { @Override protected void addDefaultHeaders(HttpHeaders headers, String s, @Nullable MediaType type) throws IOException { if (headers.getContentType() == null ) { - if (type != null && type.isConcrete() && - (type.isCompatibleWith(MediaType.APPLICATION_JSON) || + if (type != null && type.isConcrete() && (type.isCompatibleWith(MediaType.APPLICATION_JSON) || type.isCompatibleWith(APPLICATION_PLUS_JSON))) { - // Prevent charset parameter for JSON.. + // Prevent charset parameter for JSON. headers.setContentType(type); } } diff --git a/spring-web/src/main/java/org/springframework/http/converter/json/AbstractJackson2HttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/json/AbstractJackson2HttpMessageConverter.java index 657fe94804fe..31d547d6498e 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/json/AbstractJackson2HttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/json/AbstractJackson2HttpMessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-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. @@ -519,6 +519,7 @@ protected MediaType getDefaultContentType(Object object) throws IOException { } @Override + @Nullable protected Long getContentLength(Object object, @Nullable MediaType contentType) throws IOException { if (object instanceof MappingJacksonValue) { object = ((MappingJacksonValue) object).getValue(); diff --git a/spring-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilder.java b/spring-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilder.java index ae541b3195bc..88469357027f 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilder.java +++ b/spring-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -57,7 +57,6 @@ import com.fasterxml.jackson.dataformat.xml.XmlMapper; import org.springframework.beans.BeanUtils; -import org.springframework.beans.FatalBeanException; import org.springframework.context.ApplicationContext; import org.springframework.core.KotlinDetector; import org.springframework.lang.Nullable; @@ -581,7 +580,7 @@ public Jackson2ObjectMapperBuilder modules(Consumer> consumer) { * @see com.fasterxml.jackson.databind.Module */ public Jackson2ObjectMapperBuilder modulesToInstall(Module... modules) { - this.modules = Arrays.asList(modules); + this.modules = new ArrayList<>(Arrays.asList(modules)); this.findWellKnownModules = true; return this; } @@ -837,7 +836,7 @@ else if (feature instanceof MapperFeature) { objectMapper.configure((MapperFeature) feature, enabled); } else { - throw new FatalBeanException("Unknown feature class: " + feature.getClass().getName()); + throw new IllegalArgumentException("Unknown feature class: " + feature.getClass().getName()); } } diff --git a/spring-web/src/main/java/org/springframework/http/converter/json/JsonbHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/json/JsonbHttpMessageConverter.java index 9571ca9b0fa5..bd4a79c719fd 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/json/JsonbHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/json/JsonbHttpMessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2022 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-web/src/main/java/org/springframework/http/converter/support/AllEncompassingFormHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/support/AllEncompassingFormHttpMessageConverter.java index f75f85400fc8..a1dba24d268b 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/support/AllEncompassingFormHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/support/AllEncompassingFormHttpMessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2022 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-web/src/main/java/org/springframework/http/converter/xml/SourceHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/xml/SourceHttpMessageConverter.java index 468152e2ce3f..edfc01de23a4 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/xml/SourceHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/xml/SourceHttpMessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-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. @@ -93,7 +93,7 @@ public class SourceHttpMessageConverter extends AbstractHttpMe /** * Sets the {@link #setSupportedMediaTypes(java.util.List) supportedMediaTypes} - * to {@code text/xml} and {@code application/xml}, and {@code application/*-xml}. + * to {@code text/xml} and {@code application/xml}, and {@code application/*+xml}. */ public SourceHttpMessageConverter() { super(MediaType.APPLICATION_XML, MediaType.TEXT_XML, new MediaType("application", "*+xml")); diff --git a/spring-web/src/main/java/org/springframework/http/server/ServletServerHttpAsyncRequestControl.java b/spring-web/src/main/java/org/springframework/http/server/ServletServerHttpAsyncRequestControl.java index 866b2422456f..e4840e921520 100644 --- a/spring-web/src/main/java/org/springframework/http/server/ServletServerHttpAsyncRequestControl.java +++ b/spring-web/src/main/java/org/springframework/http/server/ServletServerHttpAsyncRequestControl.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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. @@ -115,13 +115,11 @@ public void complete() { // --------------------------------------------------------------------- @Override - public void onComplete(AsyncEvent event) throws IOException { - this.asyncContext = null; - this.asyncCompleted.set(true); + public void onStartAsync(AsyncEvent event) throws IOException { } @Override - public void onStartAsync(AsyncEvent event) throws IOException { + public void onTimeout(AsyncEvent event) throws IOException { } @Override @@ -129,7 +127,9 @@ public void onError(AsyncEvent event) throws IOException { } @Override - public void onTimeout(AsyncEvent event) throws IOException { + public void onComplete(AsyncEvent event) throws IOException { + this.asyncContext = null; + this.asyncCompleted.set(true); } } diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/ChannelSendOperator.java b/spring-web/src/main/java/org/springframework/http/server/reactive/ChannelSendOperator.java index da46443edc2f..8a757f92a196 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/ChannelSendOperator.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/ChannelSendOperator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -39,7 +39,7 @@ * to defer the invocation of the write function, until we know if the source * publisher will begin publishing without an error. If the first emission is * an error, the write function is bypassed, and the error is sent directly - * through the result publisher. Otherwise the write function is invoked. + * through the result publisher. Otherwise, the write function is invoked. * * @author Rossen Stoyanchev * @author Stephane Maldini @@ -273,6 +273,10 @@ public void request(long n) { return; } synchronized (this) { + if (this.state == State.READY_TO_WRITE) { + s.request(n); + return; + } if (this.writeSubscriber != null) { if (this.state == State.EMITTING_CACHED_SIGNALS) { this.demandBeforeReadyToWrite = n; diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyHeadersAdapter.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyHeadersAdapter.java index 357662872860..d325dc4c7a24 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyHeadersAdapter.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyHeadersAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 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. @@ -170,7 +170,6 @@ public Set>> entrySet() { public Iterator>> iterator() { return new EntryIterator(); } - @Override public int size() { return headers.size(); diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyHttpHandlerAdapter.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyHttpHandlerAdapter.java index 89a315068359..041f8fd3839b 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyHttpHandlerAdapter.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyHttpHandlerAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 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. @@ -65,12 +65,12 @@ public JettyHttpHandlerAdapter(HttpHandler httpHandler) { protected ServletServerHttpRequest createRequest(HttpServletRequest request, AsyncContext context) throws IOException, URISyntaxException { - // TODO: need to compile against Jetty 10 to use HttpFields (class->interface) if (jetty10Present) { + // No HttpFields optimization on Jetty 10 due to binary incompatibility return super.createRequest(request, context); } - Assert.notNull(getServletPath(), "Servlet path is not initialized"); + Assert.state(getServletPath() != null, "Servlet path is not initialized"); return new JettyServerHttpRequest( request, context, getServletPath(), getDataBufferFactory(), getBufferSize()); } @@ -79,15 +79,14 @@ protected ServletServerHttpRequest createRequest(HttpServletRequest request, Asy protected ServletServerHttpResponse createResponse(HttpServletResponse response, AsyncContext context, ServletServerHttpRequest request) throws IOException { - // TODO: need to compile against Jetty 10 to use HttpFields (class->interface) if (jetty10Present) { + // No HttpFields optimization on Jetty 10 due to binary incompatibility return new BaseJettyServerHttpResponse( response, context, getDataBufferFactory(), getBufferSize(), request); } - else { - return new JettyServerHttpResponse( - response, context, getDataBufferFactory(), getBufferSize(), request); - } + + return new JettyServerHttpResponse( + response, context, getDataBufferFactory(), getBufferSize(), request); } @@ -120,8 +119,6 @@ else if (request instanceof HttpServletRequestWrapper) { "] to org.eclipse.jetty.server.Request"); } } - - } diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpRequest.java b/spring-web/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpRequest.java index 72cdc1f9fc21..abc506830267 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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,7 +24,6 @@ import javax.net.ssl.SSLSession; import io.netty.channel.Channel; -import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.cookie.Cookie; import io.netty.handler.ssl.SslHandler; import org.apache.commons.logging.Log; @@ -81,39 +80,16 @@ private static URI initUri(HttpServerRequest request) throws URISyntaxException } private static URI resolveBaseUrl(HttpServerRequest request) throws URISyntaxException { - String scheme = getScheme(request); - String header = request.requestHeaders().get(HttpHeaderNames.HOST); - if (header != null) { - final int portIndex; - if (header.startsWith("[")) { - portIndex = header.indexOf(':', header.indexOf(']')); - } - else { - portIndex = header.indexOf(':'); - } - if (portIndex != -1) { - try { - return new URI(scheme, null, header.substring(0, portIndex), - Integer.parseInt(header.substring(portIndex + 1)), null, null, null); - } - catch (NumberFormatException ex) { - throw new URISyntaxException(header, "Unable to parse port", portIndex); - } - } - else { - return new URI(scheme, header, null, null); - } - } - else { - InetSocketAddress localAddress = request.hostAddress(); - Assert.state(localAddress != null, "No host address available"); - return new URI(scheme, null, localAddress.getHostString(), - localAddress.getPort(), null, null, null); - } + String scheme = request.scheme(); + int port = request.hostPort(); + return (usePort(scheme, port) ? + new URI(scheme, null, request.hostName(), port, null, null, null) : + new URI(scheme, request.hostName(), null, null)); } - private static String getScheme(HttpServerRequest request) { - return request.scheme(); + private static boolean usePort(String scheme, int port) { + return ((scheme.equals("http") || scheme.equals("ws")) && (port != 80)) || + ((scheme.equals("https") || scheme.equals("wss")) && (port != 443)); } private static String resolveRequestUri(HttpServerRequest request) { diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java b/spring-web/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java index c38837c7ed03..b18f1f1f42f0 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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. @@ -170,7 +170,7 @@ public void service(ServletRequest request, ServletResponse response) throws Ser AsyncListener requestListener; String logPrefix; try { - httpRequest = createRequest(((HttpServletRequest) request), asyncContext); + httpRequest = createRequest((HttpServletRequest) request, asyncContext); requestListener = httpRequest.getAsyncListener(); logPrefix = httpRequest.getLogPrefix(); } @@ -183,8 +183,10 @@ public void service(ServletRequest request, ServletResponse response) throws Ser return; } - ServerHttpResponse httpResponse = createResponse(((HttpServletResponse) response), asyncContext, httpRequest); - AsyncListener responseListener = ((ServletServerHttpResponse) httpResponse).getAsyncListener(); + ServletServerHttpResponse wrappedResponse = + createResponse((HttpServletResponse) response, asyncContext, httpRequest); + ServerHttpResponse httpResponse = wrappedResponse; + AsyncListener responseListener = wrappedResponse.getAsyncListener(); if (httpRequest.getMethod() == HttpMethod.HEAD) { httpResponse = new HttpHeadResponseDecorator(httpResponse); } @@ -201,7 +203,7 @@ public void service(ServletRequest request, ServletResponse response) throws Ser protected ServletServerHttpRequest createRequest(HttpServletRequest request, AsyncContext context) throws IOException, URISyntaxException { - Assert.notNull(this.servletPath, "Servlet path is not initialized"); + Assert.state(this.servletPath != null, "Servlet path is not initialized"); return new ServletServerHttpRequest( request, context, this.servletPath, getDataBufferFactory(), getBufferSize()); } @@ -264,9 +266,7 @@ private static class HttpHandlerAsyncListener implements AsyncListener { private final String logPrefix; - - public HttpHandlerAsyncListener( - AsyncListener requestAsyncListener, AsyncListener responseAsyncListener, + public HttpHandlerAsyncListener(AsyncListener requestAsyncListener, AsyncListener responseAsyncListener, Runnable handlerDisposeTask, AtomicBoolean completionFlag, String logPrefix) { this.requestAsyncListener = requestAsyncListener; @@ -276,6 +276,10 @@ public HttpHandlerAsyncListener( this.logPrefix = logPrefix; } + @Override + public void onStartAsync(AsyncEvent event) { + // no-op + } @Override public void onTimeout(AsyncEvent event) { @@ -343,11 +347,6 @@ private void handleTimeoutOrError(AsyncEvent event) { } }); } - - @Override - public void onStartAsync(AsyncEvent event) { - // no-op - } } @@ -362,9 +361,7 @@ private static class HandlerResultSubscriber implements Subscriber, Runnab @Nullable private volatile Subscription subscription; - public HandlerResultSubscriber( - AsyncContext asyncContext, AtomicBoolean completionFlag, String logPrefix) { - + public HandlerResultSubscriber(AsyncContext asyncContext, AtomicBoolean completionFlag, String logPrefix) { this.asyncContext = asyncContext; this.completionFlag = completionFlag; this.logPrefix = logPrefix; diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/TomcatHttpHandlerAdapter.java b/spring-web/src/main/java/org/springframework/http/server/reactive/TomcatHttpHandlerAdapter.java index b8c78fdbf4e4..c53da8b30f49 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/TomcatHttpHandlerAdapter.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/TomcatHttpHandlerAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2022 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,7 +58,6 @@ */ public class TomcatHttpHandlerAdapter extends ServletHttpHandlerAdapter { - public TomcatHttpHandlerAdapter(HttpHandler httpHandler) { super(httpHandler); } @@ -68,7 +67,7 @@ public TomcatHttpHandlerAdapter(HttpHandler httpHandler) { protected ServletServerHttpRequest createRequest(HttpServletRequest request, AsyncContext asyncContext) throws IOException, URISyntaxException { - Assert.notNull(getServletPath(), "Servlet path is not initialized"); + Assert.state(getServletPath() != null, "Servlet path is not initialized"); return new TomcatServerHttpRequest( request, asyncContext, getServletPath(), getDataBufferFactory(), getBufferSize()); } diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java index 8394a47d03de..423a4067d873 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 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-web/src/main/java/org/springframework/web/accept/AbstractMappingContentNegotiationStrategy.java b/spring-web/src/main/java/org/springframework/web/accept/AbstractMappingContentNegotiationStrategy.java index 62a650d3c4d3..22c1d1e3f67f 100644 --- a/spring-web/src/main/java/org/springframework/web/accept/AbstractMappingContentNegotiationStrategy.java +++ b/spring-web/src/main/java/org/springframework/web/accept/AbstractMappingContentNegotiationStrategy.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2023 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 @@ * *

    The method {@link #handleNoMatch} allow subclasses to plug in additional * ways of looking up media types (e.g. through the Java Activation framework, - * or {@link jakarta.servlet.ServletContext#getMimeType}). Media types resolved + * or {@link javax.servlet.ServletContext#getMimeType}). Media types resolved * via base classes are then added to the base class * {@link MappingMediaTypeFileExtensionResolver}, i.e. cached for new lookups. * diff --git a/spring-web/src/main/java/org/springframework/web/bind/EscapedErrors.java b/spring-web/src/main/java/org/springframework/web/bind/EscapedErrors.java index a2cb47e4787a..b9a75b541a44 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/EscapedErrors.java +++ b/spring-web/src/main/java/org/springframework/web/bind/EscapedErrors.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2023 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. @@ -110,8 +110,8 @@ public void rejectValue(@Nullable String field, String errorCode, String default } @Override - public void rejectValue(@Nullable String field, String errorCode, @Nullable Object[] errorArgs, - @Nullable String defaultMessage) { + public void rejectValue(@Nullable String field, String errorCode, + @Nullable Object[] errorArgs, @Nullable String defaultMessage) { this.source.rejectValue(field, errorCode, errorArgs, defaultMessage); } diff --git a/spring-web/src/main/java/org/springframework/web/bind/ServletRequestDataBinder.java b/spring-web/src/main/java/org/springframework/web/bind/ServletRequestDataBinder.java index 1c6f0218d2e7..b2f7b8cd3fcf 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/ServletRequestDataBinder.java +++ b/spring-web/src/main/java/org/springframework/web/bind/ServletRequestDataBinder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -110,7 +110,7 @@ public ServletRequestDataBinder(@Nullable Object target, String objectName) { * @see org.springframework.web.multipart.MultipartHttpServletRequest * @see org.springframework.web.multipart.MultipartRequest * @see org.springframework.web.multipart.MultipartFile - * @see jakarta.servlet.http.Part + * @see javax.servlet.http.Part * @see #bind(org.springframework.beans.PropertyValues) */ public void bind(ServletRequest request) { diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/CrossOrigin.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/CrossOrigin.java index 0a568d25db2b..0cc69be17276 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/annotation/CrossOrigin.java +++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/CrossOrigin.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-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. @@ -97,31 +97,23 @@ /** * The list of request headers that are permitted in actual requests, - * possibly {@code "*"} to allow all headers. - *

    Allowed headers are listed in the {@code Access-Control-Allow-Headers} - * response header of preflight requests. - *

    A header name is not required to be listed if it is one of: - * {@code Cache-Control}, {@code Content-Language}, {@code Expires}, - * {@code Last-Modified}, or {@code Pragma} as per the CORS spec. + * possibly {@code "*"} to allow all headers. Please, see + * {@link CorsConfiguration#setAllowedHeaders(List)} for details. *

    By default all requested headers are allowed. */ String[] allowedHeaders() default {}; /** * The List of response headers that the user-agent will allow the client - * to access on an actual response, other than "simple" headers, i.e. - * {@code Cache-Control}, {@code Content-Language}, {@code Content-Type}, - * {@code Expires}, {@code Last-Modified}, or {@code Pragma}, - *

    Exposed headers are listed in the {@code Access-Control-Expose-Headers} - * response header of actual CORS requests. - *

    The special value {@code "*"} allows all headers to be exposed for - * non-credentialed requests. + * to access on an actual response, possibly {@code "*"} to expose all headers. + * Please, see {@link CorsConfiguration#setExposedHeaders(List)} for details. *

    By default no headers are listed as exposed. */ String[] exposedHeaders() default {}; /** - * The list of supported HTTP request methods. + * The list of supported HTTP request methods. Please, see + * {@link CorsConfiguration#setAllowedMethods(List)} for details. *

    By default the supported methods are the same as the ones to which a * controller method is mapped. */ @@ -129,9 +121,8 @@ /** * Whether the browser should send credentials, such as cookies along with - * cross domain requests, to the annotated endpoint. The configured value is - * set on the {@code Access-Control-Allow-Credentials} response header of - * preflight requests. + * cross domain requests, to the annotated endpoint. Please, see + * {@link CorsConfiguration#setAllowCredentials(Boolean)} for details. *

    NOTE: Be aware that this option establishes a high * level of trust with the configured domains and also increases the surface * attack of the web application by exposing sensitive user-specific @@ -142,6 +133,14 @@ */ String allowCredentials() default ""; + /** + * Whether private network access is supported. Please, see + * {@link CorsConfiguration#setAllowPrivateNetwork(Boolean)} for details. + *

    By default this is not set (i.e. private network access is not supported). + * @since 5.3.32 + */ + String allowPrivateNetwork() default ""; + /** * The maximum age (in seconds) of the cache duration for preflight responses. *

    This property controls the value of the {@code Access-Control-Max-Age} diff --git a/spring-web/src/main/java/org/springframework/web/bind/support/WebExchangeBindException.java b/spring-web/src/main/java/org/springframework/web/bind/support/WebExchangeBindException.java index 8c605fc66254..f330ff8b4cb5 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/support/WebExchangeBindException.java +++ b/spring-web/src/main/java/org/springframework/web/bind/support/WebExchangeBindException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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. @@ -54,14 +54,13 @@ public WebExchangeBindException(MethodParameter parameter, BindingResult binding /** * Return the BindingResult that this BindException wraps. - * Will typically be a BeanPropertyBindingResult. + *

    Will typically be a BeanPropertyBindingResult. * @see BeanPropertyBindingResult */ public final BindingResult getBindingResult() { return this.bindingResult; } - @Override public String getObjectName() { return this.bindingResult.getObjectName(); @@ -87,7 +86,6 @@ public void popNestedPath() throws IllegalStateException { this.bindingResult.popNestedPath(); } - @Override public void reject(String errorCode) { this.bindingResult.reject(errorCode); @@ -114,8 +112,8 @@ public void rejectValue(@Nullable String field, String errorCode, String default } @Override - public void rejectValue( - @Nullable String field, String errorCode, @Nullable Object[] errorArgs, @Nullable String defaultMessage) { + public void rejectValue(@Nullable String field, String errorCode, + @Nullable Object[] errorArgs, @Nullable String defaultMessage) { this.bindingResult.rejectValue(field, errorCode, errorArgs, defaultMessage); } @@ -125,7 +123,6 @@ public void addAllErrors(Errors errors) { this.bindingResult.addAllErrors(errors); } - @Override public boolean hasErrors() { return this.bindingResult.hasErrors(); @@ -276,7 +273,6 @@ public String[] getSuppressedFields() { return this.bindingResult.getSuppressedFields(); } - /** * Returns diagnostic information about the errors held in this object. */ @@ -287,8 +283,8 @@ public String getMessage() { StringBuilder sb = new StringBuilder("Validation failed for argument at index ") .append(parameter.getParameterIndex()).append(" in method: ") .append(parameter.getExecutable().toGenericString()) - .append(", with ").append(this.bindingResult.getErrorCount()).append(" error(s): "); - for (ObjectError error : this.bindingResult.getAllErrors()) { + .append(", with ").append(getErrorCount()).append(" error(s): "); + for (ObjectError error : getAllErrors()) { sb.append('[').append(error).append("] "); } return sb.toString(); diff --git a/spring-web/src/main/java/org/springframework/web/client/RestOperations.java b/spring-web/src/main/java/org/springframework/web/client/RestOperations.java index 0217c4f9bbd1..c468339725e8 100644 --- a/spring-web/src/main/java/org/springframework/web/client/RestOperations.java +++ b/spring-web/src/main/java/org/springframework/web/client/RestOperations.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-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,7 +30,8 @@ /** * Interface specifying a basic set of RESTful operations. - * Implemented by {@link RestTemplate}. Not often used directly, but a useful + * + *

    Implemented by {@link RestTemplate}. Not often used directly, but a useful * option to enhance testability, as it can easily be mocked or stubbed. * * @author Arjen Poutsma @@ -67,7 +68,7 @@ public interface RestOperations { T getForObject(String url, Class responseType, Map uriVariables) throws RestClientException; /** - * Retrieve a representation by doing a GET on the URL . + * Retrieve a representation by doing a GET on the URL. * The response (if any) is converted and returned. * @param url the URL * @param responseType the type of the return value @@ -78,7 +79,7 @@ public interface RestOperations { /** * Retrieve an entity by doing a GET on the specified URL. - * The response is converted and stored in an {@link ResponseEntity}. + * The response is converted and stored in a {@link ResponseEntity}. *

    URI Template variables are expanded using the given URI variables, if any. * @param url the URL * @param responseType the type of the return value @@ -91,7 +92,7 @@ ResponseEntity getForEntity(String url, Class responseType, Object... /** * Retrieve a representation by doing a GET on the URI template. - * The response is converted and stored in an {@link ResponseEntity}. + * The response is converted and stored in a {@link ResponseEntity}. *

    URI Template variables are expanded using the given map. * @param url the URL * @param responseType the type of the return value @@ -103,8 +104,8 @@ ResponseEntity getForEntity(String url, Class responseType, Map ResponseEntity getForEntity(String url, Class responseType, MapURI Template variables are expanded using the given URI variables, if any. *

    The {@code request} parameter can be a {@link HttpEntity} in order to @@ -164,7 +165,7 @@ ResponseEntity getForEntity(String url, Class responseType, MapURI Template variables are expanded using the given map. *

    The {@code request} parameter can be a {@link HttpEntity} in order to @@ -185,7 +186,7 @@ URI postForLocation(String url, @Nullable Object request, Map uriVari throws RestClientException; /** - * Create a new resource by POSTing the given object to the URL, and returns the value of the + * Create a new resource by POSTing the given object to the URL, and return the value of the * {@code Location} header. This header typically indicates where the new resource is stored. *

    The {@code request} parameter can be a {@link HttpEntity} in order to * add additional HTTP headers to the request. @@ -204,7 +205,7 @@ URI postForLocation(String url, @Nullable Object request, Map uriVari /** * Create a new resource by POSTing the given object to the URI template, - * and returns the representation found in the response. + * and return the representation found in the response. *

    URI Template variables are expanded using the given URI variables, if any. *

    The {@code request} parameter can be a {@link HttpEntity} in order to * add additional HTTP headers to the request. @@ -226,7 +227,7 @@ T postForObject(String url, @Nullable Object request, Class responseType, /** * Create a new resource by POSTing the given object to the URI template, - * and returns the representation found in the response. + * and return the representation found in the response. *

    URI Template variables are expanded using the given map. *

    The {@code request} parameter can be a {@link HttpEntity} in order to * add additional HTTP headers to the request. @@ -248,7 +249,7 @@ T postForObject(String url, @Nullable Object request, Class responseType, /** * Create a new resource by POSTing the given object to the URL, - * and returns the representation found in the response. + * and return the representation found in the response. *

    The {@code request} parameter can be a {@link HttpEntity} in order to * add additional HTTP headers to the request. *

    The body of the entity, or {@code request} itself, can be a @@ -267,7 +268,7 @@ T postForObject(String url, @Nullable Object request, Class responseType, /** * Create a new resource by POSTing the given object to the URI template, - * and returns the response as {@link ResponseEntity}. + * and return the response as {@link ResponseEntity}. *

    URI Template variables are expanded using the given URI variables, if any. *

    The {@code request} parameter can be a {@link HttpEntity} in order to * add additional HTTP headers to the request. @@ -288,7 +289,7 @@ ResponseEntity postForEntity(String url, @Nullable Object request, Class< /** * Create a new resource by POSTing the given object to the URI template, - * and returns the response as {@link HttpEntity}. + * and return the response as {@link HttpEntity}. *

    URI Template variables are expanded using the given map. *

    The {@code request} parameter can be a {@link HttpEntity} in order to * add additional HTTP headers to the request. @@ -309,7 +310,7 @@ ResponseEntity postForEntity(String url, @Nullable Object request, Class< /** * Create a new resource by POSTing the given object to the URL, - * and returns the response as {@link ResponseEntity}. + * and return the response as {@link ResponseEntity}. *

    The {@code request} parameter can be a {@link HttpEntity} in order to * add additional HTTP headers to the request. *

    The body of the entity, or {@code request} itself, can be a @@ -373,7 +374,7 @@ ResponseEntity postForEntity(URI url, @Nullable Object request, Class *

    The {@code request} parameter can be a {@link HttpEntity} in order to * add additional HTTP headers to the request. *

    NOTE: The standard JDK HTTP library does not support HTTP PATCH. - * You need to use the Apache HttpComponents or OkHttp request factory. + * You need to use e.g. the Apache HttpComponents request factory. * @param url the URL * @param request the object to be PATCHed (may be {@code null}) * @param responseType the type of the return value @@ -382,8 +383,7 @@ ResponseEntity postForEntity(URI url, @Nullable Object request, Class * @since 4.3.5 * @see HttpEntity * @see RestTemplate#setRequestFactory - * @see org.springframework.http.client.HttpComponentsAsyncClientHttpRequestFactory - * @see org.springframework.http.client.OkHttp3ClientHttpRequestFactory + * @see org.springframework.http.client.HttpComponentsClientHttpRequestFactory */ @Nullable T patchForObject(String url, @Nullable Object request, Class responseType, Object... uriVariables) @@ -396,7 +396,7 @@ T patchForObject(String url, @Nullable Object request, Class responseType *

    The {@code request} parameter can be a {@link HttpEntity} in order to * add additional HTTP headers to the request. *

    NOTE: The standard JDK HTTP library does not support HTTP PATCH. - * You need to use the Apache HttpComponents or OkHttp request factory. + * You need to use e.g. the Apache HttpComponents request factory. * @param url the URL * @param request the object to be PATCHed (may be {@code null}) * @param responseType the type of the return value @@ -405,8 +405,7 @@ T patchForObject(String url, @Nullable Object request, Class responseType * @since 4.3.5 * @see HttpEntity * @see RestTemplate#setRequestFactory - * @see org.springframework.http.client.HttpComponentsAsyncClientHttpRequestFactory - * @see org.springframework.http.client.OkHttp3ClientHttpRequestFactory + * @see org.springframework.http.client.HttpComponentsClientHttpRequestFactory */ @Nullable T patchForObject(String url, @Nullable Object request, Class responseType, @@ -418,7 +417,7 @@ T patchForObject(String url, @Nullable Object request, Class responseType *

    The {@code request} parameter can be a {@link HttpEntity} in order to * add additional HTTP headers to the request. *

    NOTE: The standard JDK HTTP library does not support HTTP PATCH. - * You need to use the Apache HttpComponents or OkHttp request factory. + * You need to use e.g. the Apache HttpComponents request factory. * @param url the URL * @param request the object to be PATCHed (may be {@code null}) * @param responseType the type of the return value @@ -426,8 +425,7 @@ T patchForObject(String url, @Nullable Object request, Class responseType * @since 4.3.5 * @see HttpEntity * @see RestTemplate#setRequestFactory - * @see org.springframework.http.client.HttpComponentsAsyncClientHttpRequestFactory - * @see org.springframework.http.client.OkHttp3ClientHttpRequestFactory + * @see org.springframework.http.client.HttpComponentsClientHttpRequestFactory */ @Nullable T patchForObject(URI url, @Nullable Object request, Class responseType) @@ -491,8 +489,8 @@ T patchForObject(URI url, @Nullable Object request, Class responseType) // exchange /** - * Execute the HTTP method to the given URI template, writing the given request entity to the request, and - * returns the response as {@link ResponseEntity}. + * Execute the HTTP method to the given URI template, writing the given request entity to the request, + * and return the response as {@link ResponseEntity}. *

    URI Template variables are expanded using the given URI variables, if any. * @param url the URL * @param method the HTTP method (GET, POST, etc) @@ -507,8 +505,8 @@ ResponseEntity exchange(String url, HttpMethod method, @Nullable HttpEnti Class responseType, Object... uriVariables) throws RestClientException; /** - * Execute the HTTP method to the given URI template, writing the given request entity to the request, and - * returns the response as {@link ResponseEntity}. + * Execute the HTTP method to the given URI template, writing the given request entity to the request, + * and return the response as {@link ResponseEntity}. *

    URI Template variables are expanded using the given URI variables, if any. * @param url the URL * @param method the HTTP method (GET, POST, etc) @@ -523,8 +521,8 @@ ResponseEntity exchange(String url, HttpMethod method, @Nullable HttpEnti Class responseType, Map uriVariables) throws RestClientException; /** - * Execute the HTTP method to the given URI template, writing the given request entity to the request, and - * returns the response as {@link ResponseEntity}. + * Execute the HTTP method to the given URI template, writing the given request entity to the request, + * and return the response as {@link ResponseEntity}. * @param url the URL * @param method the HTTP method (GET, POST, etc) * @param requestEntity the entity (headers and/or body) to write to the request @@ -538,7 +536,7 @@ ResponseEntity exchange(URI url, HttpMethod method, @Nullable HttpEntity< /** * Execute the HTTP method to the given URI template, writing the given - * request entity to the request, and returns the response as {@link ResponseEntity}. + * request entity to the request, and return the response as {@link ResponseEntity}. * The given {@link ParameterizedTypeReference} is used to pass generic type information: *

     	 * ParameterizedTypeReference<List<MyBean>> myBean =
    @@ -561,7 +559,7 @@  ResponseEntity exchange(String url,HttpMethod method, @Nullable HttpEntit
     
     	/**
     	 * Execute the HTTP method to the given URI template, writing the given
    -	 * request entity to the request, and returns the response as {@link ResponseEntity}.
    +	 * request entity to the request, and return the response as {@link ResponseEntity}.
     	 * The given {@link ParameterizedTypeReference} is used to pass generic type information:
     	 * 
     	 * ParameterizedTypeReference<List<MyBean>> myBean =
    @@ -584,7 +582,7 @@  ResponseEntity exchange(String url, HttpMethod method, @Nullable HttpEnti
     
     	/**
     	 * Execute the HTTP method to the given URI template, writing the given
    -	 * request entity to the request, and returns the response as {@link ResponseEntity}.
    +	 * request entity to the request, and return the response as {@link ResponseEntity}.
     	 * The given {@link ParameterizedTypeReference} is used to pass generic type information:
     	 * 
     	 * ParameterizedTypeReference<List<MyBean>> myBean =
    diff --git a/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java b/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java
    index 29e3fb4626ce..ce275c92cb98 100644
    --- a/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java
    +++ b/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright 2002-2022 the original author or authors.
    + * Copyright 2002-2023 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.
    @@ -818,7 +818,7 @@ protected void handleResponse(URI url, HttpMethod method, ClientHttpResponse res
     				logger.debug("Response " + (status != null ? status : code));
     			}
     			catch (IOException ex) {
    -				// ignore
    +				logger.debug("Failed to obtain response status code", ex);
     			}
     		}
     		if (hasError) {
    diff --git a/spring-web/src/main/java/org/springframework/web/client/support/RestGatewaySupport.java b/spring-web/src/main/java/org/springframework/web/client/support/RestGatewaySupport.java
    index a31c4d911a80..adf6b470befa 100644
    --- a/spring-web/src/main/java/org/springframework/web/client/support/RestGatewaySupport.java
    +++ b/spring-web/src/main/java/org/springframework/web/client/support/RestGatewaySupport.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright 2002-2018 the original author or authors.
    + * Copyright 2002-2022 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-web/src/main/java/org/springframework/web/context/ContextLoader.java b/spring-web/src/main/java/org/springframework/web/context/ContextLoader.java
    index dfd4b0196caa..586d8f8c4ed9 100644
    --- a/spring-web/src/main/java/org/springframework/web/context/ContextLoader.java
    +++ b/spring-web/src/main/java/org/springframework/web/context/ContextLoader.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright 2002-2018 the original author or authors.
    + * Copyright 2002-2023 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.
    @@ -133,22 +133,6 @@ public class ContextLoader {
     	private static final String DEFAULT_STRATEGIES_PATH = "ContextLoader.properties";
     
     
    -	private static final Properties defaultStrategies;
    -
    -	static {
    -		// Load default strategy implementations from properties file.
    -		// This is currently strictly internal and not meant to be customized
    -		// by application developers.
    -		try {
    -			ClassPathResource resource = new ClassPathResource(DEFAULT_STRATEGIES_PATH, ContextLoader.class);
    -			defaultStrategies = PropertiesLoaderUtils.loadProperties(resource);
    -		}
    -		catch (IOException ex) {
    -			throw new IllegalStateException("Could not load 'ContextLoader.properties': " + ex.getMessage());
    -		}
    -	}
    -
    -
     	/**
     	 * Map from (thread context) ClassLoader to corresponding 'current' WebApplicationContext.
     	 */
    @@ -162,6 +146,8 @@ public class ContextLoader {
     	@Nullable
     	private static volatile WebApplicationContext currentContext;
     
    +	@Nullable
    +	private static Properties defaultStrategies;
     
     	/**
     	 * The root WebApplicationContext instance that this loader manages.
    @@ -357,6 +343,18 @@ protected Class determineContextClass(ServletContext servletContext) {
     			}
     		}
     		else {
    +			if (defaultStrategies == null) {
    +				// Load default strategy implementations from properties file.
    +				// This is currently strictly internal and not meant to be customized
    +				// by application developers.
    +				try {
    +					ClassPathResource resource = new ClassPathResource(DEFAULT_STRATEGIES_PATH, ContextLoader.class);
    +					defaultStrategies = PropertiesLoaderUtils.loadProperties(resource);
    +				}
    +				catch (IOException ex) {
    +					throw new IllegalStateException("Could not load 'ContextLoader.properties': " + ex.getMessage());
    +				}
    +			}
     			contextClassName = defaultStrategies.getProperty(WebApplicationContext.class.getName());
     			try {
     				return ClassUtils.forName(contextClassName, ContextLoader.class.getClassLoader());
    diff --git a/spring-web/src/main/java/org/springframework/web/context/request/AbstractRequestAttributesScope.java b/spring-web/src/main/java/org/springframework/web/context/request/AbstractRequestAttributesScope.java
    index eb104e23543f..89dc05ac8c3c 100644
    --- a/spring-web/src/main/java/org/springframework/web/context/request/AbstractRequestAttributesScope.java
    +++ b/spring-web/src/main/java/org/springframework/web/context/request/AbstractRequestAttributesScope.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright 2002-2017 the original author or authors.
    + * Copyright 2002-2022 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-web/src/main/java/org/springframework/web/context/request/ServletWebRequest.java b/spring-web/src/main/java/org/springframework/web/context/request/ServletWebRequest.java
    index 581163a86692..8bb3b4817f6b 100644
    --- a/spring-web/src/main/java/org/springframework/web/context/request/ServletWebRequest.java
    +++ b/spring-web/src/main/java/org/springframework/web/context/request/ServletWebRequest.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright 2002-2018 the original author or authors.
    + * Copyright 2002-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,13 +26,12 @@
     import java.util.Locale;
     import java.util.Map;
     import java.util.TimeZone;
    -import java.util.regex.Matcher;
    -import java.util.regex.Pattern;
     
     import javax.servlet.http.HttpServletRequest;
     import javax.servlet.http.HttpServletResponse;
     import javax.servlet.http.HttpSession;
     
    +import org.springframework.http.ETag;
     import org.springframework.http.HttpHeaders;
     import org.springframework.http.HttpMethod;
     import org.springframework.http.HttpStatus;
    @@ -54,12 +53,6 @@ public class ServletWebRequest extends ServletRequestAttributes implements Nativ
     
     	private static final List SAFE_METHODS = Arrays.asList("GET", "HEAD");
     
    -	/**
    -	 * Pattern matching ETag multiple field values in headers such as "If-Match", "If-None-Match".
    -	 * @see Section 2.3 of RFC 7232
    -	 */
    -	private static final Pattern ETAG_HEADER_VALUE_PATTERN = Pattern.compile("\\*|\\s*((W\\/)?(\"[^\"]*\"))\\s*,?");
    -
     	/**
     	 * Date formats as specified in the HTTP RFC.
     	 * @see Section 7.1.1.1 of RFC 7231
    @@ -289,11 +282,10 @@ private boolean validateIfNoneMatch(@Nullable String etag) {
     			etag = etag.substring(2);
     		}
     		while (ifNoneMatch.hasMoreElements()) {
    -			String clientETags = ifNoneMatch.nextElement();
    -			Matcher etagMatcher = ETAG_HEADER_VALUE_PATTERN.matcher(clientETags);
     			// Compare weak/strong ETags as per https://tools.ietf.org/html/rfc7232#section-2.3
    -			while (etagMatcher.find()) {
    -				if (StringUtils.hasLength(etagMatcher.group()) && etag.equals(etagMatcher.group(3))) {
    +			for (ETag requestedETag : ETag.parse(ifNoneMatch.nextElement())) {
    +				String tag = requestedETag.tag();
    +				if (StringUtils.hasLength(tag) && etag.equals(padEtagIfNecessary(tag))) {
     					this.notModified = true;
     					break;
     				}
    diff --git a/spring-web/src/main/java/org/springframework/web/context/request/async/AsyncRequestNotUsableException.java b/spring-web/src/main/java/org/springframework/web/context/request/async/AsyncRequestNotUsableException.java
    new file mode 100644
    index 000000000000..45198fe728d6
    --- /dev/null
    +++ b/spring-web/src/main/java/org/springframework/web/context/request/async/AsyncRequestNotUsableException.java
    @@ -0,0 +1,44 @@
    +/*
    + * Copyright 2002-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.web.context.request.async;
    +
    +import java.io.IOException;
    +
    +/**
    + * Raised when the response for an asynchronous request becomes unusable as
    + * indicated by a write failure, or a Servlet container error notification, or
    + * after the async request has completed.
    + *
    + * 

    The exception relies on response wrapping, and on {@code AsyncListener} + * notifications, managed by {@link StandardServletAsyncWebRequest}. + * + * @author Rossen Stoyanchev + * @since 5.3.33 + */ +@SuppressWarnings("serial") +public class AsyncRequestNotUsableException extends IOException { + + + public AsyncRequestNotUsableException(String message) { + super(message); + } + + public AsyncRequestNotUsableException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/spring-web/src/main/java/org/springframework/web/context/request/async/StandardServletAsyncWebRequest.java b/spring-web/src/main/java/org/springframework/web/context/request/async/StandardServletAsyncWebRequest.java index 06e4721cb97e..b494b62fcbc4 100644 --- a/spring-web/src/main/java/org/springframework/web/context/request/async/StandardServletAsyncWebRequest.java +++ b/spring-web/src/main/java/org/springframework/web/context/request/async/StandardServletAsyncWebRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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,22 +17,28 @@ package org.springframework.web.context.request.async; import java.io.IOException; +import java.io.PrintWriter; import java.util.ArrayList; import java.util.List; -import java.util.concurrent.atomic.AtomicBoolean; +import java.util.Locale; +import java.util.concurrent.locks.ReentrantLock; import java.util.function.Consumer; import javax.servlet.AsyncContext; import javax.servlet.AsyncEvent; import javax.servlet.AsyncListener; +import javax.servlet.ServletOutputStream; +import javax.servlet.WriteListener; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpServletResponseWrapper; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.web.context.request.ServletWebRequest; /** - * A Servlet 3.0 implementation of {@link AsyncWebRequest}. + * A Servlet implementation of {@link AsyncWebRequest}. * *

    The servlet and all filters involved in an async request must have async * support enabled using the Servlet API or by adding an @@ -44,18 +50,22 @@ */ public class StandardServletAsyncWebRequest extends ServletWebRequest implements AsyncWebRequest, AsyncListener { - private Long timeout; - - private AsyncContext asyncContext; - - private AtomicBoolean asyncCompleted = new AtomicBoolean(); - private final List timeoutHandlers = new ArrayList<>(); private final List> exceptionHandlers = new ArrayList<>(); private final List completionHandlers = new ArrayList<>(); + @Nullable + private Long timeout; + + @Nullable + private AsyncContext asyncContext; + + private State state; + + private final ReentrantLock stateLock = new ReentrantLock(); + /** * Create a new instance for the given request/response pair. @@ -63,7 +73,26 @@ public class StandardServletAsyncWebRequest extends ServletWebRequest implements * @param response current HTTP response */ public StandardServletAsyncWebRequest(HttpServletRequest request, HttpServletResponse response) { - super(request, response); + this(request, response, null); + } + + /** + * Constructor to wrap the request and response for the current dispatch that + * also picks up the state of the last (probably the REQUEST) dispatch. + * @param request current HTTP request + * @param response current HTTP response + * @param previousRequest the existing request from the last dispatch + * @since 5.3.33 + */ + StandardServletAsyncWebRequest(HttpServletRequest request, HttpServletResponse response, + @Nullable StandardServletAsyncWebRequest previousRequest) { + + super(request, new LifecycleHttpServletResponse(response)); + + this.state = (previousRequest != null ? previousRequest.state : State.NEW); + + //noinspection DataFlowIssue + ((LifecycleHttpServletResponse) getResponse()).setAsyncWebRequest(this); } @@ -104,7 +133,7 @@ public boolean isAsyncStarted() { */ @Override public boolean isAsyncComplete() { - return this.asyncCompleted.get(); + return (this.state == State.COMPLETED); } @Override @@ -114,11 +143,18 @@ public void startAsync() { "in async request processing. This is done in Java code using the Servlet API " + "or by adding \"true\" to servlet and " + "filter declarations in web.xml."); - Assert.state(!isAsyncComplete(), "Async processing has already completed"); if (isAsyncStarted()) { return; } + + if (this.state == State.NEW) { + this.state = State.ASYNC; + } + else { + Assert.state(this.state == State.ASYNC, "Cannot start async: [" + this.state + "]"); + } + this.asyncContext = getRequest().startAsync(getRequest(), getResponse()); this.asyncContext.addListener(this); if (this.timeout != null) { @@ -128,8 +164,10 @@ public void startAsync() { @Override public void dispatch() { - Assert.notNull(this.asyncContext, "Cannot dispatch without an AsyncContext"); - this.asyncContext.dispatch(); + Assert.state(this.asyncContext != null, "AsyncContext not yet initialized"); + if (!this.isAsyncComplete()) { + this.asyncContext.dispatch(); + } } @@ -142,20 +180,522 @@ public void onStartAsync(AsyncEvent event) throws IOException { } @Override - public void onError(AsyncEvent event) throws IOException { - this.exceptionHandlers.forEach(consumer -> consumer.accept(event.getThrowable())); + public void onTimeout(AsyncEvent event) throws IOException { + this.timeoutHandlers.forEach(Runnable::run); } @Override - public void onTimeout(AsyncEvent event) throws IOException { - this.timeoutHandlers.forEach(Runnable::run); + public void onError(AsyncEvent event) throws IOException { + this.stateLock.lock(); + try { + this.state = State.ERROR; + Throwable ex = event.getThrowable(); + this.exceptionHandlers.forEach(consumer -> consumer.accept(ex)); + } + finally { + this.stateLock.unlock(); + } } @Override public void onComplete(AsyncEvent event) throws IOException { - this.completionHandlers.forEach(Runnable::run); - this.asyncContext = null; - this.asyncCompleted.set(true); + this.stateLock.lock(); + try { + this.completionHandlers.forEach(Runnable::run); + this.asyncContext = null; + this.state = State.COMPLETED; + } + finally { + this.stateLock.unlock(); + } + } + + + /** + * Package private access for testing only. + */ + ReentrantLock stateLock() { + return this.stateLock; + } + + + /** + * Response wrapper to wrap the output stream with {@link LifecycleServletOutputStream}. + * @since 5.3.33 + */ + private static final class LifecycleHttpServletResponse extends HttpServletResponseWrapper { + + @Nullable + private StandardServletAsyncWebRequest asyncWebRequest; + + @Nullable + private ServletOutputStream outputStream; + + @Nullable + private PrintWriter writer; + + public LifecycleHttpServletResponse(HttpServletResponse response) { + super(response); + } + + public void setAsyncWebRequest(StandardServletAsyncWebRequest asyncWebRequest) { + this.asyncWebRequest = asyncWebRequest; + } + + @Override + public ServletOutputStream getOutputStream() throws IOException { + int level = obtainLockAndCheckState(); + try { + if (this.outputStream == null) { + Assert.notNull(this.asyncWebRequest, "Not initialized"); + ServletOutputStream delegate = getResponse().getOutputStream(); + this.outputStream = new LifecycleServletOutputStream(delegate, this); + } + } + catch (IOException ex) { + handleIOException(ex, "Failed to get ServletResponseOutput"); + } + finally { + releaseLock(level); + } + return this.outputStream; + } + + @Override + public PrintWriter getWriter() throws IOException { + int level = obtainLockAndCheckState(); + try { + if (this.writer == null) { + Assert.notNull(this.asyncWebRequest, "Not initialized"); + this.writer = new LifecyclePrintWriter(getResponse().getWriter(), this.asyncWebRequest); + } + } + catch (IOException ex) { + handleIOException(ex, "Failed to get PrintWriter"); + } + finally { + releaseLock(level); + } + return this.writer; + } + + @Override + public void flushBuffer() throws IOException { + int level = obtainLockAndCheckState(); + try { + getResponse().flushBuffer(); + } + catch (IOException ex) { + handleIOException(ex, "ServletResponse failed to flushBuffer"); + } + finally { + releaseLock(level); + } + } + + /** + * Return 0 if checks passed and lock is not needed, 1 if checks passed + * and lock is held, or raise AsyncRequestNotUsableException. + */ + private int obtainLockAndCheckState() throws AsyncRequestNotUsableException { + Assert.notNull(this.asyncWebRequest, "Not initialized"); + if (this.asyncWebRequest.state == State.NEW) { + return 0; + } + + this.asyncWebRequest.stateLock.lock(); + if (this.asyncWebRequest.state == State.ASYNC) { + return 1; + } + + this.asyncWebRequest.stateLock.unlock(); + throw new AsyncRequestNotUsableException("Response not usable after " + + (this.asyncWebRequest.state == State.COMPLETED ? + "async request completion" : "response errors") + "."); + } + + void handleIOException(IOException ex, String msg) throws AsyncRequestNotUsableException { + Assert.notNull(this.asyncWebRequest, "Not initialized"); + this.asyncWebRequest.state = State.ERROR; + throw new AsyncRequestNotUsableException(msg + ": " + ex.getMessage(), ex); + } + + void releaseLock(int level) { + Assert.notNull(this.asyncWebRequest, "Not initialized"); + if (level > 0) { + this.asyncWebRequest.stateLock.unlock(); + } + } + } + + + /** + * Wraps a ServletOutputStream to prevent use after Servlet container onError + * notifications, and after async request completion. + * @since 5.3.33 + */ + private static final class LifecycleServletOutputStream extends ServletOutputStream { + + private final ServletOutputStream delegate; + + private final LifecycleHttpServletResponse response; + + private LifecycleServletOutputStream(ServletOutputStream delegate, LifecycleHttpServletResponse response) { + this.delegate = delegate; + this.response = response; + } + + @Override + public boolean isReady() { + return this.delegate.isReady(); + } + + @Override + public void setWriteListener(WriteListener writeListener) { + this.delegate.setWriteListener(writeListener); + } + + @Override + public void write(int b) throws IOException { + int level = this.response.obtainLockAndCheckState(); + try { + this.delegate.write(b); + } + catch (IOException ex) { + this.response.handleIOException(ex, "ServletOutputStream failed to write"); + } + finally { + this.response.releaseLock(level); + } + } + + public void write(byte[] buf, int offset, int len) throws IOException { + int level = this.response.obtainLockAndCheckState(); + try { + this.delegate.write(buf, offset, len); + } + catch (IOException ex) { + this.response.handleIOException(ex, "ServletOutputStream failed to write"); + } + finally { + this.response.releaseLock(level); + } + } + + @Override + public void flush() throws IOException { + int level = this.response.obtainLockAndCheckState(); + try { + this.delegate.flush(); + } + catch (IOException ex) { + this.response.handleIOException(ex, "ServletOutputStream failed to flush"); + } + finally { + this.response.releaseLock(level); + } + } + + @Override + public void close() throws IOException { + int level = this.response.obtainLockAndCheckState(); + try { + this.delegate.close(); + } + catch (IOException ex) { + this.response.handleIOException(ex, "ServletOutputStream failed to close"); + } + finally { + this.response.releaseLock(level); + } + } + + } + + + /** + * Wraps a PrintWriter to prevent use after Servlet container onError + * notifications, and after async request completion. + * @since 5.3.33 + */ + private static final class LifecyclePrintWriter extends PrintWriter { + + private final PrintWriter delegate; + + private final StandardServletAsyncWebRequest asyncWebRequest; + + private LifecyclePrintWriter(PrintWriter delegate, StandardServletAsyncWebRequest asyncWebRequest) { + super(delegate); + this.delegate = delegate; + this.asyncWebRequest = asyncWebRequest; + } + + @Override + public void flush() { + int level = tryObtainLockAndCheckState(); + if (level > -1) { + try { + this.delegate.flush(); + } + finally { + releaseLock(level); + } + } + } + + @Override + public void close() { + int level = tryObtainLockAndCheckState(); + if (level > -1) { + try { + this.delegate.close(); + } + finally { + releaseLock(level); + } + } + } + + @Override + public boolean checkError() { + return this.delegate.checkError(); + } + + @Override + public void write(int c) { + int level = tryObtainLockAndCheckState(); + if (level > -1) { + try { + this.delegate.write(c); + } + finally { + releaseLock(level); + } + } + } + + @Override + public void write(char[] buf, int off, int len) { + int level = tryObtainLockAndCheckState(); + if (level > -1) { + try { + this.delegate.write(buf, off, len); + } + finally { + releaseLock(level); + } + } + } + + @Override + public void write(char[] buf) { + this.delegate.write(buf); + } + + @Override + public void write(String s, int off, int len) { + int level = tryObtainLockAndCheckState(); + if (level > -1) { + try { + this.delegate.write(s, off, len); + } + finally { + releaseLock(level); + } + } + } + + @Override + public void write(String s) { + this.delegate.write(s); + } + + /** + * Return 0 if checks passed and lock is not needed, 1 if checks passed + * and lock is held, and -1 if checks did not pass. + */ + private int tryObtainLockAndCheckState() { + if (this.asyncWebRequest.state == State.NEW) { + return 0; + } + this.asyncWebRequest.stateLock.lock(); + if (this.asyncWebRequest.state == State.ASYNC) { + return 1; + } + this.asyncWebRequest.stateLock.unlock(); + return -1; + } + + private void releaseLock(int level) { + if (level > 0) { + this.asyncWebRequest.stateLock.unlock(); + } + } + + // Plain delegates + + @Override + public void print(boolean b) { + this.delegate.print(b); + } + + @Override + public void print(char c) { + this.delegate.print(c); + } + + @Override + public void print(int i) { + this.delegate.print(i); + } + + @Override + public void print(long l) { + this.delegate.print(l); + } + + @Override + public void print(float f) { + this.delegate.print(f); + } + + @Override + public void print(double d) { + this.delegate.print(d); + } + + @Override + public void print(char[] s) { + this.delegate.print(s); + } + + @Override + public void print(String s) { + this.delegate.print(s); + } + + @Override + public void print(Object obj) { + this.delegate.print(obj); + } + + @Override + public void println() { + this.delegate.println(); + } + + @Override + public void println(boolean x) { + this.delegate.println(x); + } + + @Override + public void println(char x) { + this.delegate.println(x); + } + + @Override + public void println(int x) { + this.delegate.println(x); + } + + @Override + public void println(long x) { + this.delegate.println(x); + } + + @Override + public void println(float x) { + this.delegate.println(x); + } + + @Override + public void println(double x) { + this.delegate.println(x); + } + + @Override + public void println(char[] x) { + this.delegate.println(x); + } + + @Override + public void println(String x) { + this.delegate.println(x); + } + + @Override + public void println(Object x) { + this.delegate.println(x); + } + + @Override + public PrintWriter printf(String format, Object... args) { + return this.delegate.printf(format, args); + } + + @Override + public PrintWriter printf(Locale l, String format, Object... args) { + return this.delegate.printf(l, format, args); + } + + @Override + public PrintWriter format(String format, Object... args) { + return this.delegate.format(format, args); + } + + @Override + public PrintWriter format(Locale l, String format, Object... args) { + return this.delegate.format(l, format, args); + } + + @Override + public PrintWriter append(CharSequence csq) { + return this.delegate.append(csq); + } + + @Override + public PrintWriter append(CharSequence csq, int start, int end) { + return this.delegate.append(csq, start, end); + } + + @Override + public PrintWriter append(char c) { + return this.delegate.append(c); + } + } + + + /** + * Represents a state for {@link StandardServletAsyncWebRequest} to be in. + *

    +	 *    +------ NEW
    +	 *    |        |
    +	 *    |        v
    +	 *    |      ASYNC ----> +
    +	 *    |        |         |
    +	 *    |        v         |
    +	 *    +----> ERROR       |
    +	 *             |         |
    +	 *             v         |
    +	 *         COMPLETED <---+
    +	 * 
    + * @since 5.3.33 + */ + private enum State { + + /** New request (may not start async handling). */ + NEW, + + /** Async handling has started. */ + ASYNC, + + /** ServletOutputStream failed, or onError notification received. */ + ERROR, + + /** onComplete notification received. */ + COMPLETED + } } diff --git a/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncManager.java b/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncManager.java index 14b17ebe098b..880d680a260f 100644 --- a/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncManager.java +++ b/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncManager.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-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,7 +22,7 @@ import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.Future; -import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.atomic.AtomicReference; import javax.servlet.http.HttpServletRequest; @@ -53,6 +53,7 @@ * * @author Rossen Stoyanchev * @author Juergen Hoeller + * @author Sam Brannen * @since 3.2 * @see org.springframework.web.context.request.AsyncWebRequestInterceptor * @see org.springframework.web.servlet.AsyncHandlerInterceptor @@ -77,20 +78,18 @@ public final class WebAsyncManager { private static Boolean taskExecutorWarning = true; + @Nullable private AsyncWebRequest asyncWebRequest; private AsyncTaskExecutor taskExecutor = DEFAULT_TASK_EXECUTOR; + @Nullable private volatile Object concurrentResult = RESULT_NONE; + @Nullable private volatile Object[] concurrentResultContext; - /* - * Whether the concurrentResult is an error. If such errors remain unhandled, some - * Servlet containers will call AsyncListener#onError at the end, after the ASYNC - * and/or the ERROR dispatch (Boot's case), and we need to ignore those. - */ - private volatile boolean errorHandlingInProgress; + private final AtomicReference state = new AtomicReference<>(State.NOT_STARTED); private final Map callableInterceptors = new LinkedHashMap<>(); @@ -122,6 +121,15 @@ public void setAsyncWebRequest(AsyncWebRequest asyncWebRequest) { WebAsyncUtils.WEB_ASYNC_MANAGER_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST)); } + /** + * Return the current {@link AsyncWebRequest}. + * @since 5.3.33 + */ + @Nullable + public AsyncWebRequest getAsyncWebRequest() { + return this.asyncWebRequest; + } + /** * Configure an AsyncTaskExecutor for use with concurrent processing via * {@link #startCallableProcessing(Callable, Object...)}. @@ -132,8 +140,8 @@ public void setTaskExecutor(AsyncTaskExecutor taskExecutor) { } /** - * Whether the selected handler for the current request chose to handle the - * request asynchronously. A return value of "true" indicates concurrent + * Return whether the selected handler for the current request chose to handle + * the request asynchronously. A return value of "true" indicates concurrent * handling is under way and the response will remain open. A return value * of "false" means concurrent handling was either not started or possibly * that it has completed and the request was dispatched for further @@ -144,27 +152,28 @@ public boolean isConcurrentHandlingStarted() { } /** - * Whether a result value exists as a result of concurrent handling. + * Return whether a result value exists as a result of concurrent handling. */ public boolean hasConcurrentResult() { return (this.concurrentResult != RESULT_NONE); } /** - * Provides access to the result from concurrent handling. + * Get the result from concurrent handling. * @return an Object, possibly an {@code Exception} or {@code Throwable} if - * concurrent handling raised one. + * concurrent handling raised one * @see #clearConcurrentResult() */ + @Nullable public Object getConcurrentResult() { return this.concurrentResult; } /** - * Provides access to additional processing context saved at the start of - * concurrent handling. + * Get the additional processing context saved at the start of concurrent handling. * @see #clearConcurrentResult() */ + @Nullable public Object[] getConcurrentResultContext() { return this.concurrentResultContext; } @@ -202,7 +211,7 @@ public void registerCallableInterceptor(Object key, CallableProcessingIntercepto /** * Register a {@link CallableProcessingInterceptor} without a key. - * The key is derived from the class name and hashcode. + * The key is derived from the class name and hash code. * @param interceptors one or more interceptors to register */ public void registerCallableInterceptors(CallableProcessingInterceptor... interceptors) { @@ -225,8 +234,8 @@ public void registerDeferredResultInterceptor(Object key, DeferredResultProcessi } /** - * Register one or more {@link DeferredResultProcessingInterceptor DeferredResultProcessingInterceptors} without a specified key. - * The default key is derived from the interceptor class name and hash code. + * Register one or more {@link DeferredResultProcessingInterceptor DeferredResultProcessingInterceptors} + * without a specified key. The default key is derived from the interceptor class name and hash code. * @param interceptors one or more interceptors to register */ public void registerDeferredResultInterceptors(DeferredResultProcessingInterceptor... interceptors) { @@ -242,6 +251,12 @@ public void registerDeferredResultInterceptors(DeferredResultProcessingIntercept * {@linkplain #getConcurrentResultContext() concurrentResultContext}. */ public void clearConcurrentResult() { + if (!this.state.compareAndSet(State.RESULT_SET, State.NOT_STARTED)) { + if (logger.isDebugEnabled()) { + logger.debug("Unexpected call to clear: [" + this.state.get() + "]"); + } + return; + } synchronized (WebAsyncManager.this) { this.concurrentResult = RESULT_NONE; this.concurrentResultContext = null; @@ -282,6 +297,11 @@ public void startCallableProcessing(final WebAsyncTask webAsyncTask, Object.. Assert.notNull(webAsyncTask, "WebAsyncTask must not be null"); Assert.state(this.asyncWebRequest != null, "AsyncWebRequest must not be null"); + if (!this.state.compareAndSet(State.NOT_STARTED, State.ASYNC_PROCESSING)) { + throw new IllegalStateException( + "Unexpected call to startCallableProcessing: [" + this.state.get() + "]"); + } + Long timeout = webAsyncTask.getTimeout(); if (timeout != null) { this.asyncWebRequest.setTimeout(timeout); @@ -292,7 +312,7 @@ public void startCallableProcessing(final WebAsyncTask webAsyncTask, Object.. this.taskExecutor = executor; } else { - logExecutorWarning(); + logExecutorWarning(this.asyncWebRequest); } List interceptors = new ArrayList<>(); @@ -305,7 +325,7 @@ public void startCallableProcessing(final WebAsyncTask webAsyncTask, Object.. this.asyncWebRequest.addTimeoutHandler(() -> { if (logger.isDebugEnabled()) { - logger.debug("Async request timeout for " + formatRequestUri()); + logger.debug("Servlet container timeout notification for " + formatUri(this.asyncWebRequest)); } Object result = interceptorChain.triggerAfterTimeout(this.asyncWebRequest, callable); if (result != CallableProcessingInterceptor.RESULT_NONE) { @@ -314,14 +334,12 @@ public void startCallableProcessing(final WebAsyncTask webAsyncTask, Object.. }); this.asyncWebRequest.addErrorHandler(ex -> { - if (!this.errorHandlingInProgress) { - if (logger.isDebugEnabled()) { - logger.debug("Async request error for " + formatRequestUri() + ": " + ex); - } - Object result = interceptorChain.triggerAfterError(this.asyncWebRequest, callable, ex); - result = (result != CallableProcessingInterceptor.RESULT_NONE ? result : ex); - setConcurrentResultAndDispatch(result); + if (logger.isDebugEnabled()) { + logger.debug("Servlet container error notification for " + formatUri(this.asyncWebRequest) + ": " + ex); } + Object result = interceptorChain.triggerAfterError(this.asyncWebRequest, callable, ex); + result = (result != CallableProcessingInterceptor.RESULT_NONE ? result : ex); + setConcurrentResultAndDispatch(result); }); this.asyncWebRequest.addCompletionHandler(() -> @@ -346,14 +364,13 @@ public void startCallableProcessing(final WebAsyncTask webAsyncTask, Object.. }); interceptorChain.setTaskFuture(future); } - catch (RejectedExecutionException ex) { + catch (Throwable ex) { Object result = interceptorChain.applyPostProcess(this.asyncWebRequest, callable, ex); setConcurrentResultAndDispatch(result); - throw ex; } } - private void logExecutorWarning() { + private void logExecutorWarning(AsyncWebRequest asyncWebRequest) { if (taskExecutorWarning && logger.isWarnEnabled()) { synchronized (DEFAULT_TASK_EXECUTOR) { AsyncTaskExecutor executor = this.taskExecutor; @@ -365,7 +382,7 @@ private void logExecutorWarning() { "Please, configure a TaskExecutor in the MVC config under \"async support\".\n" + "The " + executorTypeName + " currently in use is not suitable under load.\n" + "-------------------------------\n" + - "Request URI: '" + formatRequestUri() + "'\n" + + "Request URI: '" + formatUri(asyncWebRequest) + "'\n" + "!!!"); taskExecutorWarning = false; } @@ -373,32 +390,35 @@ private void logExecutorWarning() { } } - private String formatRequestUri() { - HttpServletRequest request = this.asyncWebRequest.getNativeRequest(HttpServletRequest.class); - return request != null ? request.getRequestURI() : "servlet container"; - } - - private void setConcurrentResultAndDispatch(Object result) { + private void setConcurrentResultAndDispatch(@Nullable Object result) { + Assert.state(this.asyncWebRequest != null, "AsyncWebRequest must not be null"); synchronized (WebAsyncManager.this) { - if (this.concurrentResult != RESULT_NONE) { + if (!this.state.compareAndSet(State.ASYNC_PROCESSING, State.RESULT_SET)) { + if (logger.isDebugEnabled()) { + logger.debug("Async result already set: " + + "[" + this.state.get() + "], ignored result: " + result + + " for " + formatUri(this.asyncWebRequest)); + } return; } - this.concurrentResult = result; - this.errorHandlingInProgress = (result instanceof Throwable); - } - if (this.asyncWebRequest.isAsyncComplete()) { + this.concurrentResult = result; if (logger.isDebugEnabled()) { - logger.debug("Async result set but request already complete: " + formatRequestUri()); + logger.debug("Async result set to: " + result + " for " + formatUri(this.asyncWebRequest)); } - return; - } - if (logger.isDebugEnabled()) { - boolean isError = result instanceof Throwable; - logger.debug("Async " + (isError ? "error" : "result set") + ", dispatch to " + formatRequestUri()); + if (this.asyncWebRequest.isAsyncComplete()) { + if (logger.isDebugEnabled()) { + logger.debug("Async request already completed for " + formatUri(this.asyncWebRequest)); + } + return; + } + + if (logger.isDebugEnabled()) { + logger.debug("Performing async dispatch for " + formatUri(this.asyncWebRequest)); + } + this.asyncWebRequest.dispatch(); } - this.asyncWebRequest.dispatch(); } /** @@ -421,6 +441,11 @@ public void startDeferredResultProcessing( Assert.notNull(deferredResult, "DeferredResult must not be null"); Assert.state(this.asyncWebRequest != null, "AsyncWebRequest must not be null"); + if (!this.state.compareAndSet(State.NOT_STARTED, State.ASYNC_PROCESSING)) { + throw new IllegalStateException( + "Unexpected call to startDeferredResultProcessing: [" + this.state.get() + "]"); + } + Long timeout = deferredResult.getTimeoutValue(); if (timeout != null) { this.asyncWebRequest.setTimeout(timeout); @@ -434,6 +459,9 @@ public void startDeferredResultProcessing( final DeferredResultInterceptorChain interceptorChain = new DeferredResultInterceptorChain(interceptors); this.asyncWebRequest.addTimeoutHandler(() -> { + if (logger.isDebugEnabled()) { + logger.debug("Servlet container timeout notification for " + formatUri(this.asyncWebRequest)); + } try { interceptorChain.triggerAfterTimeout(this.asyncWebRequest, deferredResult); } @@ -443,21 +471,22 @@ public void startDeferredResultProcessing( }); this.asyncWebRequest.addErrorHandler(ex -> { - if (!this.errorHandlingInProgress) { - try { - if (!interceptorChain.triggerAfterError(this.asyncWebRequest, deferredResult, ex)) { - return; - } - deferredResult.setErrorResult(ex); - } - catch (Throwable interceptorEx) { - setConcurrentResultAndDispatch(interceptorEx); + if (logger.isDebugEnabled()) { + logger.debug("Servlet container error notification for " + formatUri(this.asyncWebRequest)); + } + try { + if (!interceptorChain.triggerAfterError(this.asyncWebRequest, deferredResult, ex)) { + return; } + deferredResult.setErrorResult(ex); + } + catch (Throwable interceptorEx) { + setConcurrentResultAndDispatch(interceptorEx); } }); - this.asyncWebRequest.addCompletionHandler(() - -> interceptorChain.triggerAfterCompletion(this.asyncWebRequest, deferredResult)); + this.asyncWebRequest.addCompletionHandler(() -> + interceptorChain.triggerAfterCompletion(this.asyncWebRequest, deferredResult)); interceptorChain.applyBeforeConcurrentHandling(this.asyncWebRequest, deferredResult); startAsyncProcessing(processingContext); @@ -478,13 +507,46 @@ private void startAsyncProcessing(Object[] processingContext) { synchronized (WebAsyncManager.this) { this.concurrentResult = RESULT_NONE; this.concurrentResultContext = processingContext; - this.errorHandlingInProgress = false; } - this.asyncWebRequest.startAsync(); + Assert.state(this.asyncWebRequest != null, "AsyncWebRequest must not be null"); if (logger.isDebugEnabled()) { - logger.debug("Started async request"); + logger.debug("Started async request for " + formatUri(this.asyncWebRequest)); } + + this.asyncWebRequest.startAsync(); + } + + private static String formatUri(AsyncWebRequest asyncWebRequest) { + HttpServletRequest request = asyncWebRequest.getNativeRequest(HttpServletRequest.class); + return (request != null ? "\"" + request.getRequestURI() + "\"" : "servlet container"); + } + + + /** + * Represents a state for {@link WebAsyncManager} to be in. + *

    +	 *        NOT_STARTED <------+
    +	 *             |             |
    +	 *             v             |
    +	 *      ASYNC_PROCESSING     |
    +	 *             |             |
    +	 *             v             |
    +	 *         RESULT_SET -------+
    +	 * 
    + * @since 5.3.33 + */ + private enum State { + + /** No async processing in progress. */ + NOT_STARTED, + + /** Async handling has started, but the result hasn't been set yet. */ + ASYNC_PROCESSING, + + /** The result is set, and an async dispatch was performed, unless there is a network error. */ + RESULT_SET + } } diff --git a/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncTask.java b/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncTask.java index 3ca91bf7ef71..5b49b79d466f 100644 --- a/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncTask.java +++ b/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncTask.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-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,6 +30,7 @@ * * @author Rossen Stoyanchev * @author Juergen Hoeller + * @author Sam Brannen * @since 3.2 * @param the value type */ @@ -37,18 +38,25 @@ public class WebAsyncTask implements BeanFactoryAware { private final Callable callable; - private Long timeout; + @Nullable + private final Long timeout; - private AsyncTaskExecutor executor; + @Nullable + private final AsyncTaskExecutor executor; - private String executorName; + @Nullable + private final String executorName; + @Nullable private BeanFactory beanFactory; + @Nullable private Callable timeoutCallback; + @Nullable private Callable errorCallback; + @Nullable private Runnable completionCallback; @@ -59,6 +67,9 @@ public class WebAsyncTask implements BeanFactoryAware { public WebAsyncTask(Callable callable) { Assert.notNull(callable, "Callable must not be null"); this.callable = callable; + this.timeout = null; + this.executor = null; + this.executorName = null; } /** @@ -67,8 +78,11 @@ public WebAsyncTask(Callable callable) { * @param callable the callable for concurrent handling */ public WebAsyncTask(long timeout, Callable callable) { - this(callable); + Assert.notNull(callable, "Callable must not be null"); + this.callable = callable; this.timeout = timeout; + this.executor = null; + this.executorName = null; } /** @@ -78,10 +92,12 @@ public WebAsyncTask(long timeout, Callable callable) { * @param callable the callable for concurrent handling */ public WebAsyncTask(@Nullable Long timeout, String executorName, Callable callable) { - this(callable); + Assert.notNull(callable, "Callable must not be null"); Assert.notNull(executorName, "Executor name must not be null"); - this.executorName = executorName; + this.callable = callable; this.timeout = timeout; + this.executor = null; + this.executorName = executorName; } /** @@ -91,10 +107,12 @@ public WebAsyncTask(@Nullable Long timeout, String executorName, Callable cal * @param callable the callable for concurrent handling */ public WebAsyncTask(@Nullable Long timeout, AsyncTaskExecutor executor, Callable callable) { - this(callable); + Assert.notNull(callable, "Callable must not be null"); Assert.notNull(executor, "Executor must not be null"); - this.executor = executor; + this.callable = callable; this.timeout = timeout; + this.executor = executor; + this.executorName = null; } diff --git a/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncUtils.java b/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncUtils.java index abd2336ab1d9..de3e49c7268d 100644 --- a/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncUtils.java +++ b/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-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. @@ -82,7 +82,10 @@ public static WebAsyncManager getAsyncManager(WebRequest webRequest) { * @return an AsyncWebRequest instance (never {@code null}) */ public static AsyncWebRequest createAsyncWebRequest(HttpServletRequest request, HttpServletResponse response) { - return new StandardServletAsyncWebRequest(request, response); + AsyncWebRequest prev = getAsyncManager(request).getAsyncWebRequest(); + return (prev instanceof StandardServletAsyncWebRequest ? + new StandardServletAsyncWebRequest(request, response, (StandardServletAsyncWebRequest) prev) : + new StandardServletAsyncWebRequest(request, response)); } } diff --git a/spring-web/src/main/java/org/springframework/web/context/support/ServletContextResource.java b/spring-web/src/main/java/org/springframework/web/context/support/ServletContextResource.java index fff20515e3eb..41ba664f100f 100644 --- a/spring-web/src/main/java/org/springframework/web/context/support/ServletContextResource.java +++ b/spring-web/src/main/java/org/springframework/web/context/support/ServletContextResource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -57,7 +57,7 @@ public class ServletContextResource extends AbstractFileResolvingResource implem /** - * Create a new ServletContextResource. + * Create a new {@code ServletContextResource} for the given path. *

    The Servlet spec requires that resource paths start with a slash, * even if many containers accept paths without leading slash too. * Consequently, the given path will be prepended with a slash if it @@ -94,6 +94,7 @@ public final String getPath() { return this.path; } + /** * This implementation checks {@code ServletContext.getResource}. * @see javax.servlet.ServletContext#getResource(String) @@ -250,7 +251,7 @@ public boolean equals(@Nullable Object other) { return false; } ServletContextResource otherRes = (ServletContextResource) other; - return (this.servletContext.equals(otherRes.servletContext) && this.path.equals(otherRes.path)); + return (this.path.equals(otherRes.path) && this.servletContext.equals(otherRes.servletContext)); } /** diff --git a/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java b/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java index f88db838bbb9..bfcad3d4c590 100644 --- a/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java +++ b/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-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. @@ -93,6 +93,9 @@ public class CorsConfiguration { @Nullable private Boolean allowCredentials; + @Nullable + private Boolean allowPrivateNetwork; + @Nullable private Long maxAge; @@ -117,6 +120,7 @@ public CorsConfiguration(CorsConfiguration other) { this.allowedHeaders = other.allowedHeaders; this.exposedHeaders = other.exposedHeaders; this.allowCredentials = other.allowCredentials; + this.allowPrivateNetwork = other.allowPrivateNetwork; this.maxAge = other.maxAge; } @@ -129,9 +133,10 @@ public CorsConfiguration(CorsConfiguration other) { * {@code Access-Control-Allow-Origin} response header is set either to the * matched domain value or to {@code "*"}. Keep in mind however that the * CORS spec does not allow {@code "*"} when {@link #setAllowCredentials - * allowCredentials} is set to {@code true} and as of 5.3 that combination - * is rejected in favor of using {@link #setAllowedOriginPatterns - * allowedOriginPatterns} instead. + * allowCredentials} is set to {@code true}, and does not recommend {@code "*"} + * when {@link #setAllowPrivateNetwork allowPrivateNetwork} is set to {@code true}. + * As a consequence, those combinations are rejected in favor of using + * {@link #setAllowedOriginPatterns allowedOriginPatterns} instead. *

    By default this is not set which means that no origins are allowed. * However, an instance of this class is often initialized further, e.g. for * {@code @CrossOrigin}, via {@link #applyPermitDefaultValues()}. @@ -182,11 +187,13 @@ else if (this.allowedOrigins == DEFAULT_PERMIT_ALL && CollectionUtils.isEmpty(th * domain1.com on any port, including the default port * *

    In contrast to {@link #setAllowedOrigins(List) allowedOrigins} which - * only supports "*" and cannot be used with {@code allowCredentials}, when - * an allowedOriginPattern is matched, the {@code Access-Control-Allow-Origin} - * response header is set to the matched origin and not to {@code "*"} nor - * to the pattern. Therefore allowedOriginPatterns can be used in combination - * with {@link #setAllowCredentials} set to {@code true}. + * only supports "*" and cannot be used with {@code allowCredentials} or + * {@code allowPrivateNetwork}, when an {@code allowedOriginPattern} is matched, + * the {@code Access-Control-Allow-Origin} response header is set to the + * matched origin and not to {@code "*"} nor to the pattern. + * Therefore, {@code allowedOriginPatterns} can be used in combination with + * {@link #setAllowCredentials} and {@link #setAllowPrivateNetwork} set to + * {@code true}. *

    By default this is not set. * @since 5.3 */ @@ -237,8 +244,12 @@ public void addAllowedOriginPattern(@Nullable String originPattern) { /** * Set the HTTP methods to allow, e.g. {@code "GET"}, {@code "POST"}, - * {@code "PUT"}, etc. - *

    The special value {@code "*"} allows all methods. + * {@code "PUT"}, etc. The special value {@code "*"} allows all methods. + *

    {@code Access-Control-Allow-Methods} response header is set either + * to the configured method or to {@code "*"}. Keep in mind however that the + * CORS spec does not allow {@code "*"} when {@link #setAllowCredentials + * allowCredentials} is set to {@code true}, that combination is handled + * by copying the method specified in the CORS preflight request. *

    If not set, only {@code "GET"} and {@code "HEAD"} are allowed. *

    By default this is not set. *

    Note: CORS checks use values from "Forwarded" @@ -269,9 +280,9 @@ public void setAllowedMethods(@Nullable List allowedMethods) { /** * Return the allowed HTTP methods, or {@code null} in which case * only {@code "GET"} and {@code "HEAD"} allowed. + * @see #setAllowedMethods(List) * @see #addAllowedMethod(HttpMethod) * @see #addAllowedMethod(String) - * @see #setAllowedMethods(List) */ @Nullable public List getAllowedMethods() { @@ -279,14 +290,14 @@ public List getAllowedMethods() { } /** - * Add an HTTP method to allow. + * Variant of {@link #setAllowedMethods} for adding one allowed method at a time. */ public void addAllowedMethod(HttpMethod method) { addAllowedMethod(method.name()); } /** - * Add an HTTP method to allow. + * Variant of {@link #setAllowedMethods} for adding one allowed method at a time. */ public void addAllowedMethod(String method) { if (StringUtils.hasText(method)) { @@ -309,9 +320,13 @@ else if (this.resolvedMethods != null) { /** * Set the list of headers that a pre-flight request can list as allowed - * for use during an actual request. - *

    The special value {@code "*"} allows actual requests to send any - * header. + * for use during an actual request. The special value {@code "*"} allows + * actual requests to send any header. + *

    {@code Access-Control-Allow-Headers} response header is set either + * to the configured list of headers or to {@code "*"}. Keep in mind however + * that the CORS spec does not allow {@code "*"} when {@link #setAllowCredentials + * allowCredentials} is set to {@code true}, that combination is handled by + * copying the headers specified in the CORS preflight request. *

    A header name is not required to be listed if it is one of: * {@code Cache-Control}, {@code Content-Language}, {@code Expires}, * {@code Last-Modified}, or {@code Pragma}. @@ -332,7 +347,7 @@ public List getAllowedHeaders() { } /** - * Add an actual request header to allow. + * Variant of {@link #setAllowedHeaders(List)} for adding one allowed header at a time. */ public void addAllowedHeader(String allowedHeader) { if (this.allowedHeaders == null) { @@ -345,12 +360,19 @@ else if (this.allowedHeaders == DEFAULT_PERMIT_ALL) { } /** - * Set the list of response headers other than simple headers (i.e. - * {@code Cache-Control}, {@code Content-Language}, {@code Content-Type}, - * {@code Expires}, {@code Last-Modified}, or {@code Pragma}) that an - * actual response might have and can be exposed. - *

    The special value {@code "*"} allows all headers to be exposed for - * non-credentialed requests. + * Set the list of response headers that an actual response might have + * and can be exposed to the client. The special value {@code "*"} + * allows all headers to be exposed. + *

    {@code Access-Control-Expose-Headers} response header is set either + * to the configured list of headers or to {@code "*"}. While the CORS + * spec does not allow {@code "*"} when {@code Access-Control-Allow-Credentials} + * is set to {@code true}, most browsers support it and + * the response headers are not all available during the CORS processing, + * so as a consequence {@code "*"} is the header value used when specified + * regardless of the value of the `allowCredentials` property. + *

    A header name is not required to be listed if it is one of: + * {@code Cache-Control}, {@code Content-Language}, {@code Expires}, + * {@code Last-Modified}, or {@code Pragma}. *

    By default this is not set. */ public void setExposedHeaders(@Nullable List exposedHeaders) { @@ -368,9 +390,7 @@ public List getExposedHeaders() { } /** - * Add a response header to expose. - *

    The special value {@code "*"} allows all headers to be exposed for - * non-credentialed requests. + * Variant of {@link #setExposedHeaders} for adding one exposed header at a time. */ public void addExposedHeader(String exposedHeader) { if (this.exposedHeaders == null) { @@ -381,6 +401,15 @@ public void addExposedHeader(String exposedHeader) { /** * Whether user credentials are supported. + *

    Setting this property has an impact on how {@link #setAllowedOrigins(List) + * origins}, {@link #setAllowedOriginPatterns(List) originPatterns}, + * {@link #setAllowedMethods(List) allowedMethods} and + * {@link #setAllowedHeaders(List) allowedHeaders} are processed, see related + * API documentation for more details. + *

    NOTE: Be aware that this option establishes a high + * level of trust with the configured domains and also increases the surface + * attack of the web application by exposing sensitive user-specific + * information such as cookies and CSRF tokens. *

    By default this is not set (i.e. user credentials are not supported). */ public void setAllowCredentials(@Nullable Boolean allowCredentials) { @@ -396,6 +425,33 @@ public Boolean getAllowCredentials() { return this.allowCredentials; } + /** + * Whether private network access is supported for user-agents restricting such access by default. + *

    Private network requests are requests whose target server's IP address is more private than + * that from which the request initiator was fetched. For example, a request from a public website + * (https://example.com) to a private website (https://router.local), or a request from a private + * website to localhost. + *

    Setting this property has an impact on how {@link #setAllowedOrigins(List) + * origins} and {@link #setAllowedOriginPatterns(List) originPatterns} are processed, + * see related API documentation for more details. + *

    By default this is not set (i.e. private network access is not supported). + * @since 5.3.32 + * @see Private network access specifications + */ + public void setAllowPrivateNetwork(@Nullable Boolean allowPrivateNetwork) { + this.allowPrivateNetwork = allowPrivateNetwork; + } + + /** + * Return the configured {@code allowPrivateNetwork} flag, or {@code null} if none. + * @since 5.3.32 + * @see #setAllowPrivateNetwork(Boolean) + */ + @Nullable + public Boolean getAllowPrivateNetwork() { + return this.allowPrivateNetwork; + } + /** * Configure how long, as a duration, the response from a pre-flight request * can be cached by clients. @@ -478,6 +534,25 @@ public void validateAllowCredentials() { } } + /** + * Validate that when {@link #setAllowPrivateNetwork allowPrivateNetwork} is {@code true}, + * {@link #setAllowedOrigins allowedOrigins} does not contain the special + * value {@code "*"} since this is insecure. + * @throws IllegalArgumentException if the validation fails + * @since 5.3.32 + */ + public void validateAllowPrivateNetwork() { + if (this.allowPrivateNetwork == Boolean.TRUE && + this.allowedOrigins != null && this.allowedOrigins.contains(ALL)) { + + throw new IllegalArgumentException( + "When allowPrivateNetwork is true, allowedOrigins cannot contain the special value \"*\" " + + "as it is not recommended from a security perspective. " + + "To allow private network access to a set of origins, list them explicitly " + + "or consider using \"allowedOriginPatterns\" instead."); + } + } + /** * Combine the non-null properties of the supplied * {@code CorsConfiguration} with this one. @@ -512,6 +587,10 @@ public CorsConfiguration combine(@Nullable CorsConfiguration other) { if (allowCredentials != null) { config.setAllowCredentials(allowCredentials); } + Boolean allowPrivateNetwork = other.getAllowPrivateNetwork(); + if (allowPrivateNetwork != null) { + config.setAllowPrivateNetwork(allowPrivateNetwork); + } Long maxAge = other.getMaxAge(); if (maxAge != null) { config.setMaxAge(maxAge); @@ -575,6 +654,7 @@ public String checkOrigin(@Nullable String origin) { if (!ObjectUtils.isEmpty(this.allowedOrigins)) { if (this.allowedOrigins.contains(ALL)) { validateAllowCredentials(); + validateAllowPrivateNetwork(); return ALL; } for (String allowedOrigin : this.allowedOrigins) { diff --git a/spring-web/src/main/java/org/springframework/web/cors/CorsUtils.java b/spring-web/src/main/java/org/springframework/web/cors/CorsUtils.java index eec489d6cc27..020729b5c9d0 100644 --- a/spring-web/src/main/java/org/springframework/web/cors/CorsUtils.java +++ b/spring-web/src/main/java/org/springframework/web/cors/CorsUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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. @@ -66,7 +66,7 @@ else if ("https".equals(scheme) || "wss".equals(scheme)) { } /** - * Returns {@code true} if the request is a valid CORS pre-flight one by checking {code OPTIONS} method with + * Returns {@code true} if the request is a valid CORS pre-flight one by checking {@code OPTIONS} method with * {@code Origin} and {@code Access-Control-Request-Method} headers presence. */ public static boolean isPreFlightRequest(HttpServletRequest request) { diff --git a/spring-web/src/main/java/org/springframework/web/cors/DefaultCorsProcessor.java b/spring-web/src/main/java/org/springframework/web/cors/DefaultCorsProcessor.java index 37d3fb8c9dd9..fd6ce5adbc22 100644 --- a/spring-web/src/main/java/org/springframework/web/cors/DefaultCorsProcessor.java +++ b/spring-web/src/main/java/org/springframework/web/cors/DefaultCorsProcessor.java @@ -55,6 +55,18 @@ public class DefaultCorsProcessor implements CorsProcessor { private static final Log logger = LogFactory.getLog(DefaultCorsProcessor.class); + /** + * The {@code Access-Control-Request-Private-Network} request header field name. + * @see Private Network Access specification + */ + static final String ACCESS_CONTROL_REQUEST_PRIVATE_NETWORK = "Access-Control-Request-Private-Network"; + + /** + * The {@code Access-Control-Allow-Private-Network} response header field name. + * @see Private Network Access specification + */ + static final String ACCESS_CONTROL_ALLOW_PRIVATE_NETWORK = "Access-Control-Allow-Private-Network"; + @Override @SuppressWarnings("resource") @@ -156,6 +168,11 @@ protected boolean handleInternal(ServerHttpRequest request, ServerHttpResponse r responseHeaders.setAccessControlAllowCredentials(true); } + if (Boolean.TRUE.equals(config.getAllowPrivateNetwork()) && + Boolean.parseBoolean(request.getHeaders().getFirst(ACCESS_CONTROL_REQUEST_PRIVATE_NETWORK))) { + responseHeaders.set(ACCESS_CONTROL_ALLOW_PRIVATE_NETWORK, Boolean.toString(true)); + } + if (preFlightRequest && config.getMaxAge() != null) { responseHeaders.setAccessControlMaxAge(config.getMaxAge()); } @@ -192,7 +209,7 @@ private HttpMethod getMethodToUse(ServerHttpRequest request, boolean isPreFlight /** * Check the headers and determine the headers for the response of a * pre-flight request. The default implementation simply delegates to - * {@link org.springframework.web.cors.CorsConfiguration#checkOrigin(String)}. + * {@link org.springframework.web.cors.CorsConfiguration#checkHeaders(List)}. */ @Nullable protected List checkHeaders(CorsConfiguration config, List requestHeaders) { diff --git a/spring-web/src/main/java/org/springframework/web/cors/reactive/CorsUtils.java b/spring-web/src/main/java/org/springframework/web/cors/reactive/CorsUtils.java index 1d78ed9aab0c..deeacccb5cc6 100644 --- a/spring-web/src/main/java/org/springframework/web/cors/reactive/CorsUtils.java +++ b/spring-web/src/main/java/org/springframework/web/cors/reactive/CorsUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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. @@ -45,7 +45,7 @@ public static boolean isCorsRequest(ServerHttpRequest request) { } /** - * Returns {@code true} if the request is a valid CORS pre-flight one by checking {code OPTIONS} method with + * Returns {@code true} if the request is a valid CORS pre-flight one by checking {@code OPTIONS} method with * {@code Origin} and {@code Access-Control-Request-Method} headers presence. */ public static boolean isPreFlightRequest(ServerHttpRequest request) { diff --git a/spring-web/src/main/java/org/springframework/web/cors/reactive/DefaultCorsProcessor.java b/spring-web/src/main/java/org/springframework/web/cors/reactive/DefaultCorsProcessor.java index 70796c384808..ccad5f09b552 100644 --- a/spring-web/src/main/java/org/springframework/web/cors/reactive/DefaultCorsProcessor.java +++ b/spring-web/src/main/java/org/springframework/web/cors/reactive/DefaultCorsProcessor.java @@ -53,6 +53,18 @@ public class DefaultCorsProcessor implements CorsProcessor { private static final List VARY_HEADERS = Arrays.asList( HttpHeaders.ORIGIN, HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS); + /** + * The {@code Access-Control-Request-Private-Network} request header field name. + * @see Private Network Access specification + */ + static final String ACCESS_CONTROL_REQUEST_PRIVATE_NETWORK = "Access-Control-Request-Private-Network"; + + /** + * The {@code Access-Control-Allow-Private-Network} response header field name. + * @see Private Network Access specification + */ + static final String ACCESS_CONTROL_ALLOW_PRIVATE_NETWORK = "Access-Control-Allow-Private-Network"; + @Override public boolean process(@Nullable CorsConfiguration config, ServerWebExchange exchange) { @@ -155,6 +167,11 @@ protected boolean handleInternal(ServerWebExchange exchange, responseHeaders.setAccessControlAllowCredentials(true); } + if (Boolean.TRUE.equals(config.getAllowPrivateNetwork()) && + Boolean.parseBoolean(request.getHeaders().getFirst(ACCESS_CONTROL_REQUEST_PRIVATE_NETWORK))) { + responseHeaders.set(ACCESS_CONTROL_ALLOW_PRIVATE_NETWORK, Boolean.toString(true)); + } + if (preFlightRequest && config.getMaxAge() != null) { responseHeaders.setAccessControlMaxAge(config.getMaxAge()); } @@ -190,7 +207,7 @@ private HttpMethod getMethodToUse(ServerHttpRequest request, boolean isPreFlight /** * Check the headers and determine the headers for the response of a * pre-flight request. The default implementation simply delegates to - * {@link CorsConfiguration#checkOrigin(String)}. + * {@link CorsConfiguration#checkHeaders(List)}. */ @Nullable diff --git a/spring-web/src/main/java/org/springframework/web/filter/DelegatingFilterProxy.java b/spring-web/src/main/java/org/springframework/web/filter/DelegatingFilterProxy.java index 0dbc22408240..dccd60228e9c 100644 --- a/spring-web/src/main/java/org/springframework/web/filter/DelegatingFilterProxy.java +++ b/spring-web/src/main/java/org/springframework/web/filter/DelegatingFilterProxy.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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 @@ * be delegated to that bean in the Spring context, which is required to implement * the standard Servlet Filter interface. * - *

    This approach is particularly useful for Filter implementation with complex + *

    This approach is particularly useful for Filter implementations with complex * setup needs, allowing to apply the full Spring bean definition machinery to * Filter instances. Alternatively, consider standard Filter setup in combination * with looking up service beans from the Spring root application context. @@ -52,11 +52,11 @@ * Spring application context to manage the lifecycle of that bean. Specifying * the "targetFilterLifecycle" filter init-param as "true" will enforce invocation * of the {@code Filter.init} and {@code Filter.destroy} lifecycle methods - * on the target bean, letting the servlet container manage the filter lifecycle. + * on the target bean, letting the Servlet container manage the filter lifecycle. * - *

    As of Spring 3.1, {@code DelegatingFilterProxy} has been updated to optionally - * accept constructor parameters when using a Servlet container's instance-based filter - * registration methods, usually in conjunction with Spring's + *

    {@code DelegatingFilterProxy} can optionally accept constructor parameters + * when using a Servlet container's instance-based filter registration methods, + * usually in conjunction with Spring's * {@link org.springframework.web.WebApplicationInitializer} SPI. These constructors allow * for providing the delegate Filter bean directly, or providing the application context * and bean name to fetch, avoiding the need to look up the application context from the @@ -160,10 +160,10 @@ public DelegatingFilterProxy(String targetBeanName) { */ public DelegatingFilterProxy(String targetBeanName, @Nullable WebApplicationContext wac) { Assert.hasText(targetBeanName, "Target Filter bean name must not be null or empty"); - this.setTargetBeanName(targetBeanName); + setTargetBeanName(targetBeanName); this.webApplicationContext = wac; if (wac != null) { - this.setEnvironment(wac.getEnvironment()); + setEnvironment(wac.getEnvironment()); } } diff --git a/spring-web/src/main/java/org/springframework/web/filter/RelativeRedirectResponseWrapper.java b/spring-web/src/main/java/org/springframework/web/filter/RelativeRedirectResponseWrapper.java index 0d418548c59d..b771597943de 100644 --- a/spring-web/src/main/java/org/springframework/web/filter/RelativeRedirectResponseWrapper.java +++ b/spring-web/src/main/java/org/springframework/web/filter/RelativeRedirectResponseWrapper.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2022 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-web/src/main/java/org/springframework/web/method/HandlerMethod.java b/spring-web/src/main/java/org/springframework/web/method/HandlerMethod.java index 97fef625c252..226d156791c7 100644 --- a/spring-web/src/main/java/org/springframework/web/method/HandlerMethod.java +++ b/spring-web/src/main/java/org/springframework/web/method/HandlerMethod.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -218,6 +218,7 @@ private HandlerMethod(HandlerMethod handlerMethod, Object handler) { this.description = handlerMethod.description; } + private MethodParameter[] initMethodParameters() { int count = this.bridgedMethod.getParameterCount(); MethodParameter[] result = new MethodParameter[count]; @@ -248,7 +249,7 @@ private static String initDescription(Class beanType, Method method) { for (Class paramType : method.getParameterTypes()) { joiner.add(paramType.getSimpleName()); } - return beanType.getName() + "#" + method.getName() + joiner.toString(); + return beanType.getName() + "#" + method.getName() + joiner; } diff --git a/spring-web/src/main/java/org/springframework/web/method/HandlerTypePredicate.java b/spring-web/src/main/java/org/springframework/web/method/HandlerTypePredicate.java index d91704195198..c0366067f4df 100644 --- a/spring-web/src/main/java/org/springframework/web/method/HandlerTypePredicate.java +++ b/spring-web/src/main/java/org/springframework/web/method/HandlerTypePredicate.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 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-web/src/main/java/org/springframework/web/multipart/commons/CommonsFileUploadSupport.java b/spring-web/src/main/java/org/springframework/web/multipart/commons/CommonsFileUploadSupport.java index 99fcee5f8c48..3b0f0ec85ba5 100644 --- a/spring-web/src/main/java/org/springframework/web/multipart/commons/CommonsFileUploadSupport.java +++ b/spring-web/src/main/java/org/springframework/web/multipart/commons/CommonsFileUploadSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2023 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. @@ -255,16 +255,21 @@ protected MultipartParsingResult parseFileItems(List fileItems, String for (FileItem fileItem : fileItems) { if (fileItem.isFormField()) { String value; - String partEncoding = determineEncoding(fileItem.getContentType(), encoding); - try { - value = fileItem.getString(partEncoding); - } - catch (UnsupportedEncodingException ex) { - if (logger.isWarnEnabled()) { - logger.warn("Could not decode multipart item '" + fileItem.getFieldName() + - "' with encoding '" + partEncoding + "': using platform default"); + if (fileItem.getSize() > 0) { + String partEncoding = determineEncoding(fileItem.getContentType(), encoding); + try { + value = fileItem.getString(partEncoding); + } + catch (UnsupportedEncodingException ex) { + if (logger.isWarnEnabled()) { + logger.warn("Could not decode multipart item '" + fileItem.getFieldName() + + "' with encoding '" + partEncoding + "': using platform default"); + } + value = fileItem.getString(); } - value = fileItem.getString(); + } + else { + value = ""; } String[] curParam = multipartParameters.get(fileItem.getFieldName()); if (curParam == null) { diff --git a/spring-web/src/main/java/org/springframework/web/server/adapter/DefaultServerWebExchange.java b/spring-web/src/main/java/org/springframework/web/server/adapter/DefaultServerWebExchange.java index 79d3d2db2fa8..4149074d659c 100644 --- a/spring-web/src/main/java/org/springframework/web/server/adapter/DefaultServerWebExchange.java +++ b/spring-web/src/main/java/org/springframework/web/server/adapter/DefaultServerWebExchange.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2022 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-web/src/main/java/org/springframework/web/server/session/InMemoryWebSessionStore.java b/spring-web/src/main/java/org/springframework/web/server/session/InMemoryWebSessionStore.java index 2e12a19a3ed3..bfb416e7a9cb 100644 --- a/spring-web/src/main/java/org/springframework/web/server/session/InMemoryWebSessionStore.java +++ b/spring-web/src/main/java/org/springframework/web/server/session/InMemoryWebSessionStore.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-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.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import reactor.core.publisher.Mono; @@ -310,12 +311,10 @@ private class ExpiredSessionChecker { /** Max time between expiration checks. */ private static final int CHECK_PERIOD = 60 * 1000; - - private final ReentrantLock lock = new ReentrantLock(); + private final Lock lock = new ReentrantLock(); private Instant checkTime = clock.instant().plus(CHECK_PERIOD, ChronoUnit.MILLIS); - public void checkIfNecessary(Instant now) { if (this.checkTime.isBefore(now)) { removeExpiredSessions(now); diff --git a/spring-web/src/main/java/org/springframework/web/util/ContentCachingResponseWrapper.java b/spring-web/src/main/java/org/springframework/web/util/ContentCachingResponseWrapper.java index 1b8d3337d801..dbe602cdfab6 100644 --- a/spring-web/src/main/java/org/springframework/web/util/ContentCachingResponseWrapper.java +++ b/spring-web/src/main/java/org/springframework/web/util/ContentCachingResponseWrapper.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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,6 +21,10 @@ import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.io.UnsupportedEncodingException; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; import javax.servlet.ServletOutputStream; import javax.servlet.WriteListener; @@ -34,12 +38,13 @@ /** * {@link javax.servlet.http.HttpServletResponse} wrapper that caches all content written to * the {@linkplain #getOutputStream() output stream} and {@linkplain #getWriter() writer}, - * and allows this content to be retrieved via a {@link #getContentAsByteArray() byte array}. + * and allows this content to be retrieved via a {@linkplain #getContentAsByteArray() byte array}. * *

    Used e.g. by {@link org.springframework.web.filter.ShallowEtagHeaderFilter}. * Note: As of Spring Framework 5.0, this wrapper is built on the Servlet 3.1 API. * * @author Juergen Hoeller + * @author Sam Brannen * @since 4.1.3 * @see ContentCachingRequestWrapper */ @@ -115,9 +120,16 @@ public PrintWriter getWriter() throws IOException { return this.writer; } + /** + * This method neither flushes content to the client nor commits the underlying + * response, since the content has not yet been copied to the response. + *

    Invoke {@link #copyBodyToResponse()} to copy the cached body content to + * the wrapped response object and flush its buffer. + * @see javax.servlet.ServletResponseWrapper#flushBuffer() + */ @Override public void flushBuffer() throws IOException { - // do not flush the underlying response as the content has not been copied to it yet + // no-op } @Override @@ -135,11 +147,91 @@ public void setContentLengthLong(long len) { throw new IllegalArgumentException("Content-Length exceeds ContentCachingResponseWrapper's maximum (" + Integer.MAX_VALUE + "): " + len); } - int lenInt = (int) len; - if (lenInt > this.content.size()) { - this.content.resize(lenInt); + setContentLength((int) len); + } + + @Override + public boolean containsHeader(String name) { + if (this.contentLength != null && HttpHeaders.CONTENT_LENGTH.equalsIgnoreCase(name)) { + return true; + } + else { + return super.containsHeader(name); + } + } + + @Override + public void setHeader(String name, String value) { + if (HttpHeaders.CONTENT_LENGTH.equalsIgnoreCase(name)) { + this.contentLength = Integer.valueOf(value); + } + else { + super.setHeader(name, value); + } + } + + @Override + public void addHeader(String name, String value) { + if (HttpHeaders.CONTENT_LENGTH.equalsIgnoreCase(name)) { + this.contentLength = Integer.valueOf(value); + } + else { + super.addHeader(name, value); + } + } + + @Override + public void setIntHeader(String name, int value) { + if (HttpHeaders.CONTENT_LENGTH.equalsIgnoreCase(name)) { + this.contentLength = Integer.valueOf(value); + } + else { + super.setIntHeader(name, value); + } + } + + @Override + public void addIntHeader(String name, int value) { + if (HttpHeaders.CONTENT_LENGTH.equalsIgnoreCase(name)) { + this.contentLength = Integer.valueOf(value); + } + else { + super.addIntHeader(name, value); + } + } + + @Override + @Nullable + public String getHeader(String name) { + if (this.contentLength != null && HttpHeaders.CONTENT_LENGTH.equalsIgnoreCase(name)) { + return this.contentLength.toString(); + } + else { + return super.getHeader(name); + } + } + + @Override + public Collection getHeaders(String name) { + if (this.contentLength != null && HttpHeaders.CONTENT_LENGTH.equalsIgnoreCase(name)) { + return Collections.singleton(this.contentLength.toString()); + } + else { + return super.getHeaders(name); + } + } + + @Override + public Collection getHeaderNames() { + Collection headerNames = super.getHeaderNames(); + if (this.contentLength != null) { + Set result = new LinkedHashSet<>(headerNames); + result.add(HttpHeaders.CONTENT_LENGTH); + return result; + } + else { + return headerNames; } - this.contentLength = lenInt; } @Override @@ -209,11 +301,13 @@ public void copyBodyToResponse() throws IOException { protected void copyBodyToResponse(boolean complete) throws IOException { if (this.content.size() > 0) { HttpServletResponse rawResponse = (HttpServletResponse) getResponse(); - if ((complete || this.contentLength != null) && !rawResponse.isCommitted()) { - if (rawResponse.getHeader(HttpHeaders.TRANSFER_ENCODING) == null) { - rawResponse.setContentLength(complete ? this.content.size() : this.contentLength); + if (!rawResponse.isCommitted()) { + if (complete || this.contentLength != null) { + if (rawResponse.getHeader(HttpHeaders.TRANSFER_ENCODING) == null) { + rawResponse.setContentLength(complete ? this.content.size() : this.contentLength); + } + this.contentLength = null; } - this.contentLength = null; } this.content.writeTo(rawResponse.getOutputStream()); this.content.reset(); diff --git a/spring-web/src/main/java/org/springframework/web/util/JavaScriptUtils.java b/spring-web/src/main/java/org/springframework/web/util/JavaScriptUtils.java index 5d85c5dfa57b..8f5964662433 100644 --- a/spring-web/src/main/java/org/springframework/web/util/JavaScriptUtils.java +++ b/spring-web/src/main/java/org/springframework/web/util/JavaScriptUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2022 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-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java b/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java index 68c98562a280..4d1d07c808ad 100644 --- a/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java +++ b/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-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. @@ -73,19 +73,19 @@ public class UriComponentsBuilder implements UriBuilder, Cloneable { private static final Pattern QUERY_PARAM_PATTERN = Pattern.compile("([^&=]+)(=?)([^&]+)?"); - private static final String SCHEME_PATTERN = "([^:/?#]+):"; + private static final String SCHEME_PATTERN = "([^:/?#\\\\]+):"; private static final String HTTP_PATTERN = "(?i)(http|https):"; - private static final String USERINFO_PATTERN = "([^@\\[/?#]*)"; + private static final String USERINFO_PATTERN = "([^/?#\\\\]*)"; - private static final String HOST_IPV4_PATTERN = "[^\\[/?#:]*"; + private static final String HOST_IPV4_PATTERN = "[^/?#:\\\\]*"; private static final String HOST_IPV6_PATTERN = "\\[[\\p{XDigit}:.]*[%\\p{Alnum}]*]"; private static final String HOST_PATTERN = "(" + HOST_IPV6_PATTERN + "|" + HOST_IPV4_PATTERN + ")"; - private static final String PORT_PATTERN = "(\\{[^}]+\\}?|[^/?#]*)"; + private static final String PORT_PATTERN = "(\\{[^}]+\\}?|[^/?#\\\\]*)"; private static final String PATH_PATTERN = "([^?#]*)"; @@ -252,9 +252,7 @@ public static UriComponentsBuilder fromUriString(String uri) { builder.schemeSpecificPart(ssp); } else { - if (StringUtils.hasLength(scheme) && scheme.startsWith("http") && !StringUtils.hasLength(host)) { - throw new IllegalArgumentException("[" + uri + "] is not a valid HTTP URL"); - } + checkSchemeAndHost(uri, scheme, host); builder.userInfo(userInfo); builder.host(host); if (StringUtils.hasLength(port)) { @@ -296,9 +294,7 @@ public static UriComponentsBuilder fromHttpUrl(String httpUrl) { builder.scheme(scheme != null ? scheme.toLowerCase() : null); builder.userInfo(matcher.group(4)); String host = matcher.group(5); - if (StringUtils.hasLength(scheme) && !StringUtils.hasLength(host)) { - throw new IllegalArgumentException("[" + httpUrl + "] is not a valid HTTP URL"); - } + checkSchemeAndHost(httpUrl, scheme, host); builder.host(host); String port = matcher.group(7); if (StringUtils.hasLength(port)) { @@ -317,6 +313,15 @@ public static UriComponentsBuilder fromHttpUrl(String httpUrl) { } } + private static void checkSchemeAndHost(String uri, @Nullable String scheme, @Nullable String host) { + if (StringUtils.hasLength(scheme) && scheme.startsWith("http") && !StringUtils.hasLength(host)) { + throw new IllegalArgumentException("[" + uri + "] is not a valid HTTP URL"); + } + if (StringUtils.hasLength(host) && host.startsWith("[") && !host.endsWith("]")) { + throw new IllegalArgumentException("Invalid IPV6 host in [" + uri + "]"); + } + } + /** * Create a new {@code UriComponents} object from the URI associated with * the given HttpRequest while also overlaying with values from the headers @@ -402,6 +407,7 @@ public static UriComponentsBuilder fromOriginHeader(String origin) { if (StringUtils.hasLength(port)) { builder.port(port); } + checkSchemeAndHost(origin, scheme, host); return builder; } else { diff --git a/spring-web/src/main/java/org/springframework/web/util/UriTemplate.java b/spring-web/src/main/java/org/springframework/web/util/UriTemplate.java index 770c6ad7498e..94dfb9de617b 100644 --- a/spring-web/src/main/java/org/springframework/web/util/UriTemplate.java +++ b/spring-web/src/main/java/org/springframework/web/util/UriTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-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. @@ -66,7 +66,7 @@ public class UriTemplate implements Serializable { * @param uriTemplate the URI template string */ public UriTemplate(String uriTemplate) { - Assert.hasText(uriTemplate, "'uriTemplate' must not be null"); + Assert.notNull(uriTemplate, "'uriTemplate' must not be null"); this.uriTemplate = uriTemplate; this.uriComponents = UriComponentsBuilder.fromUriString(uriTemplate).build(); diff --git a/spring-web/src/main/java/org/springframework/web/util/UrlPathHelper.java b/spring-web/src/main/java/org/springframework/web/util/UrlPathHelper.java index 1c81fbc9971e..d347cd516ae4 100644 --- a/spring-web/src/main/java/org/springframework/web/util/UrlPathHelper.java +++ b/spring-web/src/main/java/org/springframework/web/util/UrlPathHelper.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 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-web/src/main/java/org/springframework/web/util/pattern/LiteralPathElement.java b/spring-web/src/main/java/org/springframework/web/util/pattern/LiteralPathElement.java index 6c6249df1302..80497fe41ee1 100644 --- a/spring-web/src/main/java/org/springframework/web/util/pattern/LiteralPathElement.java +++ b/spring-web/src/main/java/org/springframework/web/util/pattern/LiteralPathElement.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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. @@ -118,6 +118,10 @@ public char[] getChars() { return this.text; } + @Override + public boolean isLiteral() { + return true; + } @Override public String toString() { diff --git a/spring-web/src/main/java/org/springframework/web/util/pattern/PathElement.java b/spring-web/src/main/java/org/springframework/web/util/pattern/PathElement.java index df4330172645..d4db90da88e4 100644 --- a/spring-web/src/main/java/org/springframework/web/util/pattern/PathElement.java +++ b/spring-web/src/main/java/org/springframework/web/util/pattern/PathElement.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2023 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 @@ * Common supertype for the Ast nodes created to represent a path pattern. * * @author Andy Clement + * @author Brian Clozel * @since 5.0 */ abstract class PathElement { @@ -99,6 +100,14 @@ public int getScore() { return 0; } + /** + * Return whether this PathElement can be strictly {@link String#compareTo(String) compared} + * against another element for matching. + */ + public boolean isLiteral() { + return false; + } + /** * Return if the there are no more PathElements in the pattern. * @return {@code true} if the there are no more elements diff --git a/spring-web/src/main/java/org/springframework/web/util/pattern/PathPattern.java b/spring-web/src/main/java/org/springframework/web/util/pattern/PathPattern.java index 8b5aee9223c5..b3d02b6c26b7 100644 --- a/spring-web/src/main/java/org/springframework/web/util/pattern/PathPattern.java +++ b/spring-web/src/main/java/org/springframework/web/util/pattern/PathPattern.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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 org.springframework.http.server.PathContainer; import org.springframework.http.server.PathContainer.Element; +import org.springframework.http.server.PathContainer.PathSegment; import org.springframework.http.server.PathContainer.Separator; import org.springframework.lang.Nullable; import org.springframework.util.CollectionUtils; @@ -303,7 +304,7 @@ public PathContainer extractPathWithinPattern(PathContainer path) { // Find first path element that is not a separator or a literal (i.e. the first pattern based element) PathElement elem = this.head; while (elem != null) { - if (elem.getWildcardCount() != 0 || elem.getCaptureCount() != 0) { + if (!elem.isLiteral()) { break; } elem = elem.next; @@ -430,6 +431,9 @@ else if (!StringUtils.hasLength(pattern2string.patternString)) { @Override public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } if (!(other instanceof PathPattern)) { return false; } @@ -600,13 +604,11 @@ public static class PathRemainingMatchInfo { private final PathMatchInfo pathMatchInfo; - PathRemainingMatchInfo(PathContainer pathMatched, PathContainer pathRemaining) { this(pathMatched, pathRemaining, PathMatchInfo.EMPTY); } - PathRemainingMatchInfo(PathContainer pathMatched, PathContainer pathRemaining, - PathMatchInfo pathMatchInfo) { + PathRemainingMatchInfo(PathContainer pathMatched, PathContainer pathRemaining, PathMatchInfo pathMatchInfo) { this.pathRemaining = pathRemaining; this.pathMatched = pathMatched; this.pathMatchInfo = pathMatchInfo; @@ -726,8 +728,8 @@ boolean isSeparator(int pathIndex) { */ String pathElementValue(int pathIndex) { Element element = (pathIndex < this.pathLength) ? this.pathElements.get(pathIndex) : null; - if (element instanceof PathContainer.PathSegment) { - return ((PathContainer.PathSegment)element).valueToMatch(); + if (element instanceof PathSegment) { + return ((PathSegment) element).valueToMatch(); } return ""; } diff --git a/spring-web/src/main/java/org/springframework/web/util/pattern/PathPatternParser.java b/spring-web/src/main/java/org/springframework/web/util/pattern/PathPatternParser.java index 12488ffa4be9..0d794781f703 100644 --- a/spring-web/src/main/java/org/springframework/web/util/pattern/PathPatternParser.java +++ b/spring-web/src/main/java/org/springframework/web/util/pattern/PathPatternParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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.web.util.pattern; import org.springframework.http.server.PathContainer; +import org.springframework.util.StringUtils; /** * Parser for URI path patterns producing {@link PathPattern} instances that can @@ -96,6 +97,17 @@ public PathContainer.Options getPathOptions() { } + /** + * Prepare the given pattern for use in matching to full URL paths. + *

    By default, prepend a leading slash if needed for non-empty patterns. + * @param pattern the pattern to initialize + * @return the updated pattern + * @since 5.2.25 + */ + public String initFullPathPattern(String pattern) { + return (StringUtils.hasLength(pattern) && !pattern.startsWith("/") ? "/" + pattern : pattern); + } + /** * Process the path pattern content, a character at a time, breaking it into * path elements around separator boundaries and verifying the structure at each diff --git a/spring-web/src/main/java/org/springframework/web/util/pattern/SeparatorPathElement.java b/spring-web/src/main/java/org/springframework/web/util/pattern/SeparatorPathElement.java index b140e88b44e3..ef2249366bd4 100644 --- a/spring-web/src/main/java/org/springframework/web/util/pattern/SeparatorPathElement.java +++ b/spring-web/src/main/java/org/springframework/web/util/pattern/SeparatorPathElement.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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. @@ -67,6 +67,10 @@ public char[] getChars() { return new char[] {this.separator}; } + @Override + public boolean isLiteral() { + return true; + } @Override public String toString() { diff --git a/spring-web/src/test/java/org/springframework/http/ContentDispositionTests.java b/spring-web/src/test/java/org/springframework/http/ContentDispositionTests.java index 690eda7f5500..af8cf0aee73d 100644 --- a/spring-web/src/test/java/org/springframework/http/ContentDispositionTests.java +++ b/spring-web/src/test/java/org/springframework/http/ContentDispositionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 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,20 +25,22 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.springframework.http.ContentDisposition.parse; /** - * Unit tests for {@link ContentDisposition} + * Unit tests for {@link ContentDisposition}. + * * @author Sebastien Deleuze * @author Rossen Stoyanchev */ class ContentDispositionTests { - private static DateTimeFormatter formatter = DateTimeFormatter.RFC_1123_DATE_TIME; + private static final DateTimeFormatter formatter = DateTimeFormatter.RFC_1123_DATE_TIME; - @Test @SuppressWarnings("deprecation") - void parse() { + @Test + void parseFilenameQuoted() { assertThat(parse("form-data; name=\"foo\"; filename=\"foo.txt\"; size=123")) .isEqualTo(ContentDisposition.formData() .name("foo") @@ -72,7 +74,7 @@ void parseEncodedFilename() { .build()); } - @Test // gh-24112 + @Test // gh-24112 void parseEncodedFilenameWithPaddedCharset() { assertThat(parse("attachment; filename*= UTF-8''some-file.zip")) .isEqualTo(ContentDisposition.attachment() @@ -80,13 +82,13 @@ void parseEncodedFilenameWithPaddedCharset() { .build()); } - @Test // gh-26463 + @Test // gh-26463 void parseBase64EncodedFilename() { String input = "attachment; filename=\"=?UTF-8?B?5pel5pys6KqeLmNzdg==?=\""; assertThat(parse(input).getFilename()).isEqualTo("日本語.csv"); } - @Test // gh-26463 + @Test // gh-26463 void parseBase64EncodedShiftJISFilename() { String input = "attachment; filename=\"=?SHIFT_JIS?B?k/qWe4zqLmNzdg==?=\""; assertThat(parse(input).getFilename()).isEqualTo("日本語.csv"); @@ -116,7 +118,7 @@ void parseEncodedFilenameWithInvalidName() { .isThrownBy(() -> parse("form-data; name=\"name\"; filename*=UTF-8''%A.txt")); } - @Test // gh-23077 + @Test // gh-23077 @SuppressWarnings("deprecation") void parseWithEscapedQuote() { BiConsumer tester = (description, filename) -> @@ -137,8 +139,8 @@ void parseWithEscapedQuote() { "The Twilight Zone \\\\\\\\"); } - @Test @SuppressWarnings("deprecation") + @Test void parseWithExtraSemicolons() { assertThat(parse("form-data; name=\"foo\";; ; filename=\"foo.txt\"; size=123")) .isEqualTo(ContentDisposition.formData() @@ -148,8 +150,8 @@ void parseWithExtraSemicolons() { .build()); } - @Test @SuppressWarnings("deprecation") + @Test void parseDates() { ZonedDateTime creationTime = ZonedDateTime.parse("Mon, 12 Feb 2007 10:15:30 -0500", formatter); ZonedDateTime modificationTime = ZonedDateTime.parse("Tue, 13 Feb 2007 10:15:30 -0500", formatter); @@ -167,8 +169,8 @@ void parseDates() { .build()); } - @Test @SuppressWarnings("deprecation") + @Test void parseIgnoresInvalidDates() { ZonedDateTime readTime = ZonedDateTime.parse("Wed, 14 Feb 2007 10:15:30 -0500", formatter); @@ -197,13 +199,8 @@ void parseInvalidParameter() { assertThatIllegalArgumentException().isThrownBy(() -> parse("foo;bar")); } - private static ContentDisposition parse(String input) { - return ContentDisposition.parse(input); - } - - - @Test @SuppressWarnings("deprecation") + @Test void format() { assertThat( ContentDisposition.formData() @@ -235,14 +232,11 @@ void formatWithEncodedFilenameUsingUsAscii() { .isEqualTo("form-data; name=\"name\"; filename=\"test.txt\""); } - @Test // gh-24220 + @Test // gh-24220 void formatWithFilenameWithQuotes() { - BiConsumer tester = (input, output) -> { - assertThat(ContentDisposition.formData().filename(input).build().toString()) .isEqualTo("form-data; filename=\"" + output + "\""); - assertThat(ContentDisposition.formData().filename(input, StandardCharsets.US_ASCII).build().toString()) .isEqualTo("form-data; filename=\"" + output + "\""); }; diff --git a/spring-web/src/test/java/org/springframework/http/HttpHeadersTests.java b/spring-web/src/test/java/org/springframework/http/HttpHeadersTests.java index 0bad35ed16ce..2aa52c5c2d18 100644 --- a/spring-web/src/test/java/org/springframework/http/HttpHeadersTests.java +++ b/spring-web/src/test/java/org/springframework/http/HttpHeadersTests.java @@ -192,11 +192,17 @@ void ipv6Host() { } @Test - void illegalETag() { + void illegalETagWithoutQuotes() { String eTag = "v2.6"; assertThatIllegalArgumentException().isThrownBy(() -> headers.setETag(eTag)); } + @Test + void illegalWeakETagWithoutLeadingQuote() { + String etag = "W/v2.6\""; + assertThatIllegalArgumentException().isThrownBy(() -> headers.setETag(etag)); + } + @Test void ifMatch() { String ifMatch = "\"v2.6\""; diff --git a/spring-web/src/test/java/org/springframework/http/MockHttpOutputMessage.java b/spring-web/src/test/java/org/springframework/http/MockHttpOutputMessage.java index 3d2d5bb2a336..6669b820b5f6 100644 --- a/spring-web/src/test/java/org/springframework/http/MockHttpOutputMessage.java +++ b/spring-web/src/test/java/org/springframework/http/MockHttpOutputMessage.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2022 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,8 +21,6 @@ import java.io.OutputStream; import java.nio.charset.Charset; -import static org.mockito.Mockito.spy; - /** * @author Arjen Poutsma * @author Rossen Stoyanchev @@ -31,7 +29,7 @@ public class MockHttpOutputMessage implements HttpOutputMessage { private final HttpHeaders headers = new HttpHeaders(); - private final ByteArrayOutputStream body = spy(new ByteArrayOutputStream()); + private final ByteArrayOutputStream body = new ByteArrayOutputStream(); private boolean headersWritten = false; diff --git a/spring-web/src/test/java/org/springframework/http/client/SimpleClientHttpRequestFactoryTests.java b/spring-web/src/test/java/org/springframework/http/client/SimpleClientHttpRequestFactoryTests.java index df8f9a58893f..19fb334ba8f9 100644 --- a/spring-web/src/test/java/org/springframework/http/client/SimpleClientHttpRequestFactoryTests.java +++ b/spring-web/src/test/java/org/springframework/http/client/SimpleClientHttpRequestFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 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. @@ -32,14 +32,15 @@ */ public class SimpleClientHttpRequestFactoryTests { - - @Test // SPR-13225 + @Test // SPR-13225 public void headerWithNullValue() { HttpURLConnection urlConnection = mock(HttpURLConnection.class); given(urlConnection.getRequestMethod()).willReturn("GET"); + HttpHeaders headers = new HttpHeaders(); headers.set("foo", null); SimpleBufferingClientHttpRequest.addHeaders(urlConnection, headers); + verify(urlConnection, times(1)).addRequestProperty("foo", ""); } diff --git a/spring-web/src/test/java/org/springframework/http/client/reactive/ClientHttpConnectorTests.java b/spring-web/src/test/java/org/springframework/http/client/reactive/ClientHttpConnectorTests.java index 3082fe0c31fb..b92326e9e40c 100644 --- a/spring-web/src/test/java/org/springframework/http/client/reactive/ClientHttpConnectorTests.java +++ b/spring-web/src/test/java/org/springframework/http/client/reactive/ClientHttpConnectorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -51,7 +51,6 @@ import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.ReactiveHttpOutputMessage; -import org.springframework.lang.NonNull; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; @@ -173,7 +172,6 @@ void cancelResponseBody(ClientHttpConnector connector) { .verify(); } - @NonNull private Buffer randomBody(int size) { Buffer responseBody = new Buffer(); Random rnd = new Random(); diff --git a/spring-web/src/test/java/org/springframework/http/codec/json/JacksonCsvEncoderTests.java b/spring-web/src/test/java/org/springframework/http/codec/json/JacksonCsvEncoderTests.java new file mode 100644 index 000000000000..e886f1bb3599 --- /dev/null +++ b/spring-web/src/test/java/org/springframework/http/codec/json/JacksonCsvEncoderTests.java @@ -0,0 +1,101 @@ +/* + * Copyright 2002-2023 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.http.codec.json; + +import java.util.Arrays; +import java.util.Map; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectWriter; +import com.fasterxml.jackson.dataformat.csv.CsvMapper; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; + +import org.springframework.core.ResolvableType; +import org.springframework.core.testfixture.codec.AbstractEncoderTests; +import org.springframework.http.MediaType; +import org.springframework.util.Assert; +import org.springframework.util.MimeType; +import org.springframework.web.testfixture.xml.Pojo; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link AbstractJackson2Encoder} for the CSV variant and how resources are managed. + * @author Brian Clozel + */ +class JacksonCsvEncoderTests extends AbstractEncoderTests { + + public JacksonCsvEncoderTests() { + super(new JacksonCsvEncoder()); + } + + @Test + @Override + public void canEncode() throws Exception { + ResolvableType pojoType = ResolvableType.forClass(Pojo.class); + assertThat(this.encoder.canEncode(pojoType, JacksonCsvEncoder.TEXT_CSV)).isTrue(); + } + + @Test + @Override + public void encode() throws Exception { + Flux input = Flux.just(new Pojo("spring", "framework"), + new Pojo("spring", "data"), + new Pojo("spring", "boot")); + + testEncode(input, Pojo.class, step -> step + .consumeNextWith(expectString("bar,foo\nframework,spring\n")) + .consumeNextWith(expectString("data,spring\n")) + .consumeNextWith(expectString("boot,spring\n")) + .verifyComplete()); + } + + @Test + // See gh-30493 + // this test did not fail directly but logged a NullPointerException dropped by the reactive pipeline + void encodeEmptyFlux() { + Flux input = Flux.empty(); + testEncode(input, Pojo.class, step -> step.verifyComplete()); + } + + static class JacksonCsvEncoder extends AbstractJackson2Encoder { + public static final MediaType TEXT_CSV = new MediaType("text", "csv"); + + public JacksonCsvEncoder() { + this(CsvMapper.builder().build(), TEXT_CSV); + } + + @Override + protected byte[] getStreamingMediaTypeSeparator(MimeType mimeType) { + // CsvMapper emits newlines + return new byte[0]; + } + + public JacksonCsvEncoder(ObjectMapper mapper, MimeType... mimeTypes) { + super(mapper, mimeTypes); + Assert.isInstanceOf(CsvMapper.class, mapper); + setStreamingMediaTypes(Arrays.asList(TEXT_CSV)); + } + + @Override + protected ObjectWriter customizeWriter(ObjectWriter writer, MimeType mimeType, ResolvableType elementType, Map hints) { + CsvMapper mapper = (CsvMapper) getObjectMapper(); + return writer.with(mapper.schemaFor(elementType.toClass()).withHeader()); + } + } +} diff --git a/spring-web/src/test/java/org/springframework/http/converter/BufferedImageHttpMessageConverterTests.java b/spring-web/src/test/java/org/springframework/http/converter/BufferedImageHttpMessageConverterTests.java index 477cd7138cda..bf2cde849673 100644 --- a/spring-web/src/test/java/org/springframework/http/converter/BufferedImageHttpMessageConverterTests.java +++ b/spring-web/src/test/java/org/springframework/http/converter/BufferedImageHttpMessageConverterTests.java @@ -19,11 +19,9 @@ import java.awt.image.BufferedImage; import java.io.ByteArrayInputStream; import java.io.IOException; -import java.io.InputStream; import javax.imageio.ImageIO; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.core.io.ClassPathResource; @@ -31,56 +29,47 @@ import org.springframework.http.MediaType; import org.springframework.http.MockHttpInputMessage; import org.springframework.http.MockHttpOutputMessage; -import org.springframework.util.FileCopyUtils; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; /** - * Unit tests for BufferedImageHttpMessageConverter. + * Unit tests for {@link BufferedImageHttpMessageConverter}. + * * @author Arjen Poutsma * @author Rossen Stoyanchev + * @author Sam Brannen */ -public class BufferedImageHttpMessageConverterTests { +class BufferedImageHttpMessageConverterTests { - private BufferedImageHttpMessageConverter converter; + private final BufferedImageHttpMessageConverter converter = new BufferedImageHttpMessageConverter(); + + private final Resource logo = new ClassPathResource("logo.jpg", getClass()); - @BeforeEach - public void setUp() { - converter = new BufferedImageHttpMessageConverter(); - } @Test - public void canRead() { + void canRead() { assertThat(converter.canRead(BufferedImage.class, null)).as("Image not supported").isTrue(); assertThat(converter.canRead(BufferedImage.class, new MediaType("image", "png"))).as("Image not supported").isTrue(); } @Test - public void canWrite() { + void canWrite() { assertThat(converter.canWrite(BufferedImage.class, null)).as("Image not supported").isTrue(); assertThat(converter.canWrite(BufferedImage.class, new MediaType("image", "png"))).as("Image not supported").isTrue(); assertThat(converter.canWrite(BufferedImage.class, new MediaType("*", "*"))).as("Image not supported").isTrue(); } @Test - public void read() throws IOException { - Resource logo = new ClassPathResource("logo.jpg", BufferedImageHttpMessageConverterTests.class); - byte[] body = FileCopyUtils.copyToByteArray(logo.getInputStream()); - InputStream inputStream = spy(new ByteArrayInputStream(body)); - MockHttpInputMessage inputMessage = new MockHttpInputMessage(inputStream); + void read() throws IOException { + MockHttpInputMessage inputMessage = new MockHttpInputMessage(logo.getInputStream()); inputMessage.getHeaders().setContentType(new MediaType("image", "jpeg")); BufferedImage result = converter.read(BufferedImage.class, inputMessage); assertThat(result.getHeight()).as("Invalid height").isEqualTo(500); assertThat(result.getWidth()).as("Invalid width").isEqualTo(750); - verify(inputStream, never()).close(); } @Test - public void write() throws IOException { - Resource logo = new ClassPathResource("logo.jpg", BufferedImageHttpMessageConverterTests.class); + void write() throws IOException { BufferedImage body = ImageIO.read(logo.getFile()); MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); MediaType contentType = new MediaType("image", "png"); @@ -90,12 +79,10 @@ public void write() throws IOException { BufferedImage result = ImageIO.read(new ByteArrayInputStream(outputMessage.getBodyAsBytes())); assertThat(result.getHeight()).as("Invalid height").isEqualTo(500); assertThat(result.getWidth()).as("Invalid width").isEqualTo(750); - verify(outputMessage.getBody(), never()).close(); } @Test - public void writeDefaultContentType() throws IOException { - Resource logo = new ClassPathResource("logo.jpg", BufferedImageHttpMessageConverterTests.class); + void writeDefaultContentType() throws IOException { MediaType contentType = new MediaType("image", "png"); converter.setDefaultContentType(contentType); BufferedImage body = ImageIO.read(logo.getFile()); diff --git a/spring-web/src/test/java/org/springframework/http/converter/FormHttpMessageConverterTests.java b/spring-web/src/test/java/org/springframework/http/converter/FormHttpMessageConverterTests.java index 1c3b8729abd8..f4e8feb345ef 100644 --- a/spring-web/src/test/java/org/springframework/http/converter/FormHttpMessageConverterTests.java +++ b/spring-web/src/test/java/org/springframework/http/converter/FormHttpMessageConverterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2022 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. @@ -48,8 +48,6 @@ import org.springframework.util.MultiValueMap; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; import static org.springframework.http.MediaType.APPLICATION_FORM_URLENCODED; import static org.springframework.http.MediaType.MULTIPART_FORM_DATA; import static org.springframework.http.MediaType.MULTIPART_MIXED; @@ -229,7 +227,6 @@ public String getFilename() { item = items.get(5); assertThat(item.getFieldName()).isEqualTo("xml"); assertThat(item.getContentType()).isEqualTo("text/xml"); - verify(outputMessage.getBody(), never()).close(); } @Test // SPR-13309 diff --git a/spring-web/src/test/java/org/springframework/http/converter/feed/AtomFeedHttpMessageConverterTests.java b/spring-web/src/test/java/org/springframework/http/converter/feed/AtomFeedHttpMessageConverterTests.java index cd71fa9f3976..05e09b3700b3 100644 --- a/spring-web/src/test/java/org/springframework/http/converter/feed/AtomFeedHttpMessageConverterTests.java +++ b/spring-web/src/test/java/org/springframework/http/converter/feed/AtomFeedHttpMessageConverterTests.java @@ -38,9 +38,6 @@ import static java.util.Collections.singletonMap; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; /** * @author Arjen Poutsma @@ -74,7 +71,7 @@ public void canWrite() { @Test public void read() throws IOException { - InputStream inputStream = spy(getClass().getResourceAsStream("atom.xml")); + InputStream inputStream = getClass().getResourceAsStream("atom.xml"); MockHttpInputMessage inputMessage = new MockHttpInputMessage(inputStream); inputMessage.getHeaders().setContentType(ATOM_XML_UTF8); Feed result = converter.read(Feed.class, inputMessage); @@ -90,7 +87,6 @@ public void read() throws IOException { Entry entry2 = (Entry) entries.get(1); assertThat(entry2.getId()).isEqualTo("id2"); assertThat(entry2.getTitle()).isEqualTo("title2"); - verify(inputStream, never()).close(); } @Test @@ -123,7 +119,6 @@ public void write() throws IOException { NodeMatcher nm = new DefaultNodeMatcher(ElementSelectors.byName); assertThat(XmlContent.of(outputMessage.getBodyAsString(StandardCharsets.UTF_8))) .isSimilarToIgnoringWhitespace(expected, nm); - verify(outputMessage.getBody(), never()).close(); } @Test diff --git a/spring-web/src/test/java/org/springframework/http/converter/feed/RssChannelHttpMessageConverterTests.java b/spring-web/src/test/java/org/springframework/http/converter/feed/RssChannelHttpMessageConverterTests.java index b537604ad3e6..72dda284efd2 100644 --- a/spring-web/src/test/java/org/springframework/http/converter/feed/RssChannelHttpMessageConverterTests.java +++ b/spring-web/src/test/java/org/springframework/http/converter/feed/RssChannelHttpMessageConverterTests.java @@ -34,9 +34,6 @@ import static java.util.Collections.singletonMap; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; /** * @author Arjen Poutsma @@ -59,7 +56,7 @@ public void canReadAndWrite() { @Test public void read() throws IOException { - InputStream inputStream = spy(getClass().getResourceAsStream("rss.xml")); + InputStream inputStream = getClass().getResourceAsStream("rss.xml"); MockHttpInputMessage inputMessage = new MockHttpInputMessage(inputStream); inputMessage.getHeaders().setContentType(RSS_XML_UTF8); Channel result = converter.read(Channel.class, inputMessage); @@ -75,7 +72,6 @@ public void read() throws IOException { Item item2 = (Item) items.get(1); assertThat(item2.getTitle()).isEqualTo("title2"); - verify(inputStream, never()).close(); } @Test @@ -109,7 +105,6 @@ public void write() throws IOException { ""; assertThat(XmlContent.of(outputMessage.getBodyAsString(StandardCharsets.UTF_8))) .isSimilarToIgnoringWhitespace(expected); - verify(outputMessage.getBody(), never()).close(); } @Test diff --git a/spring-web/src/test/java/org/springframework/http/converter/json/GsonHttpMessageConverterTests.java b/spring-web/src/test/java/org/springframework/http/converter/json/GsonHttpMessageConverterTests.java index 6d5cf1453dce..5fe76e2c9afb 100644 --- a/spring-web/src/test/java/org/springframework/http/converter/json/GsonHttpMessageConverterTests.java +++ b/spring-web/src/test/java/org/springframework/http/converter/json/GsonHttpMessageConverterTests.java @@ -16,9 +16,7 @@ package org.springframework.http.converter.json; -import java.io.ByteArrayInputStream; import java.io.IOException; -import java.io.InputStream; import java.lang.reflect.Field; import java.lang.reflect.Type; import java.nio.charset.Charset; @@ -40,9 +38,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.within; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; /** * Gson 2.x converter tests. @@ -50,35 +45,34 @@ * @author Roy Clarkson * @author Juergen Hoeller */ -public class GsonHttpMessageConverterTests { +class GsonHttpMessageConverterTests { private final GsonHttpMessageConverter converter = new GsonHttpMessageConverter(); @Test - public void canRead() { + void canRead() { assertThat(this.converter.canRead(MyBean.class, new MediaType("application", "json"))).isTrue(); assertThat(this.converter.canRead(Map.class, new MediaType("application", "json"))).isTrue(); } @Test - public void canWrite() { + void canWrite() { assertThat(this.converter.canWrite(MyBean.class, new MediaType("application", "json"))).isTrue(); assertThat(this.converter.canWrite(Map.class, new MediaType("application", "json"))).isTrue(); } @Test - public void canReadAndWriteMicroformats() { + void canReadAndWriteMicroformats() { assertThat(this.converter.canRead(MyBean.class, new MediaType("application", "vnd.test-micro-type+json"))).isTrue(); assertThat(this.converter.canWrite(MyBean.class, new MediaType("application", "vnd.test-micro-type+json"))).isTrue(); } @Test - public void readTyped() throws IOException { + void readTyped() throws IOException { String body = "{\"bytes\":[1,2],\"array\":[\"Foo\",\"Bar\"]," + "\"number\":42,\"string\":\"Foo\",\"bool\":true,\"fraction\":42.0}"; - InputStream inputStream = spy(new ByteArrayInputStream(body.getBytes(StandardCharsets.UTF_8))); - MockHttpInputMessage inputMessage = new MockHttpInputMessage(inputStream); + MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(StandardCharsets.UTF_8)); inputMessage.getHeaders().setContentType(new MediaType("application", "json")); MyBean result = (MyBean) this.converter.read(MyBean.class, inputMessage); @@ -89,12 +83,11 @@ public void readTyped() throws IOException { assertThat(result.getArray()).isEqualTo(new String[] {"Foo", "Bar"}); assertThat(result.isBool()).isTrue(); assertThat(result.getBytes()).isEqualTo(new byte[] {0x1, 0x2}); - verify(inputStream, never()).close(); } @Test @SuppressWarnings("unchecked") - public void readUntyped() throws IOException { + void readUntyped() throws IOException { String body = "{\"bytes\":[1,2],\"array\":[\"Foo\",\"Bar\"]," + "\"number\":42,\"string\":\"Foo\",\"bool\":true,\"fraction\":42.0}"; MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes("UTF-8")); @@ -120,7 +113,7 @@ public void readUntyped() throws IOException { } @Test - public void write() throws IOException { + void write() throws IOException { MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); MyBean body = new MyBean(); body.setString("Foo"); @@ -140,11 +133,10 @@ public void write() throws IOException { assertThat(result.contains("\"bytes\":[1,2]")).isTrue(); assertThat(outputMessage.getHeaders().getContentType()) .as("Invalid content-type").isEqualTo(new MediaType("application", "json", utf8)); - verify(outputMessage.getBody(), never()).close(); } @Test - public void writeWithBaseType() throws IOException { + void writeWithBaseType() throws IOException { MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); MyBean body = new MyBean(); body.setString("Foo"); @@ -167,7 +159,7 @@ public void writeWithBaseType() throws IOException { } @Test - public void writeUTF16() throws IOException { + void writeUTF16() throws IOException { MediaType contentType = new MediaType("application", "json", StandardCharsets.UTF_16BE); MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); String body = "H\u00e9llo W\u00f6rld"; @@ -177,7 +169,7 @@ public void writeUTF16() throws IOException { } @Test - public void readInvalidJson() throws IOException { + void readInvalidJson() throws IOException { String body = "FooBar"; MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(StandardCharsets.UTF_8)); inputMessage.getHeaders().setContentType(new MediaType("application", "json")); @@ -187,7 +179,7 @@ public void readInvalidJson() throws IOException { @Test @SuppressWarnings("unchecked") - public void readAndWriteGenerics() throws Exception { + void readAndWriteGenerics() throws Exception { Field beansList = ListHolder.class.getField("listField"); String body = "[{\"bytes\":[1,2],\"array\":[\"Foo\",\"Bar\"]," + @@ -214,7 +206,7 @@ public void readAndWriteGenerics() throws Exception { @Test @SuppressWarnings("unchecked") - public void readAndWriteParameterizedType() throws Exception { + void readAndWriteParameterizedType() throws Exception { ParameterizedTypeReference> beansList = new ParameterizedTypeReference>() { }; @@ -241,7 +233,7 @@ public void readAndWriteParameterizedType() throws Exception { @Test @SuppressWarnings("unchecked") - public void writeParameterizedBaseType() throws Exception { + void writeParameterizedBaseType() throws Exception { ParameterizedTypeReference> beansList = new ParameterizedTypeReference>() {}; ParameterizedTypeReference> baseList = new ParameterizedTypeReference>() {}; @@ -267,7 +259,7 @@ public void writeParameterizedBaseType() throws Exception { } @Test - public void prefixJson() throws IOException { + void prefixJson() throws IOException { MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); this.converter.setPrefixJson(true); this.converter.writeInternal("foo", null, outputMessage); @@ -275,7 +267,7 @@ public void prefixJson() throws IOException { } @Test - public void prefixJsonCustom() throws IOException { + void prefixJsonCustom() throws IOException { MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); this.converter.setJsonPrefix(")))"); this.converter.writeInternal("foo", null, outputMessage); diff --git a/spring-web/src/test/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilderTests.java b/spring-web/src/test/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilderTests.java index f1a5748d2725..13f789161f2b 100644 --- a/spring-web/src/test/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilderTests.java +++ b/spring-web/src/test/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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,11 +83,9 @@ import org.joda.time.DateTimeZone; import org.junit.jupiter.api.Test; -import org.springframework.beans.FatalBeanException; import org.springframework.util.StringUtils; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; /** @@ -106,7 +104,7 @@ class Jackson2ObjectMapperBuilderTests { @Test void unknownFeature() { - assertThatExceptionOfType(FatalBeanException.class).isThrownBy(() -> + assertThatIllegalArgumentException().isThrownBy(() -> Jackson2ObjectMapperBuilder.json().featuresToEnable(Boolean.TRUE).build()); } @@ -231,26 +229,6 @@ void wrongTimeZoneStringSetter() { Jackson2ObjectMapperBuilder.json().timeZone(zoneId).build()); } - @Test - void modules() { - NumberSerializer serializer1 = new NumberSerializer(Integer.class); - SimpleModule module = new SimpleModule(); - module.addSerializer(Integer.class, serializer1); - ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.json().modules(module).build(); - Serializers serializers = getSerializerFactoryConfig(objectMapper).serializers().iterator().next(); - assertThat(serializers.findSerializer(null, SimpleType.construct(Integer.class), null)).isSameAs(serializer1); - } - - @Test - void modulesWithConsumer() { - NumberSerializer serializer1 = new NumberSerializer(Integer.class); - SimpleModule module = new SimpleModule(); - module.addSerializer(Integer.class, serializer1); - ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.json().modules(list -> list.add(module) ).build(); - Serializers serializers = getSerializerFactoryConfig(objectMapper).serializers().iterator().next(); - assertThat(serializers.findSerializer(null, SimpleType.construct(Integer.class), null)).isSameAs(serializer1); - } - @Test void modulesToInstallByClass() { ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.json() @@ -353,14 +331,15 @@ void registerMultipleModulesWithNullTypeId() { barModule.addSerializer(new BarSerializer()); builder.modulesToInstall(fooModule, barModule); ObjectMapper objectMapper = builder.build(); + assertThat(StreamSupport - .stream(getSerializerFactoryConfig(objectMapper).serializers().spliterator(), false) - .filter(s -> s.findSerializer(null, SimpleType.construct(Foo.class), null) != null) - .count()).isEqualTo(1); + .stream(getSerializerFactoryConfig(objectMapper).serializers().spliterator(), false) + .filter(s -> s.findSerializer(null, SimpleType.construct(Foo.class), null) != null) + .count()).isEqualTo(1); assertThat(StreamSupport - .stream(getSerializerFactoryConfig(objectMapper).serializers().spliterator(), false) - .filter(s -> s.findSerializer(null, SimpleType.construct(Bar.class), null) != null) - .count()).isEqualTo(1); + .stream(getSerializerFactoryConfig(objectMapper).serializers().spliterator(), false) + .filter(s -> s.findSerializer(null, SimpleType.construct(Bar.class), null) != null) + .count()).isEqualTo(1); } private static SerializerFactoryConfig getSerializerFactoryConfig(ObjectMapper objectMapper) { @@ -371,6 +350,38 @@ private static DeserializerFactoryConfig getDeserializerFactoryConfig(ObjectMapp return ((BasicDeserializerFactory) objectMapper.getDeserializationContext().getFactory()).getFactoryConfig(); } + @Test + void modules() { + NumberSerializer serializer1 = new NumberSerializer(Integer.class); + SimpleModule module = new SimpleModule(); + module.addSerializer(Integer.class, serializer1); + ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.json().modules(module).build(); + Serializers serializers = getSerializerFactoryConfig(objectMapper).serializers().iterator().next(); + assertThat(serializers.findSerializer(null, SimpleType.construct(Integer.class), null)).isSameAs(serializer1); + } + + @Test + void modulesWithConsumer() { + NumberSerializer serializer1 = new NumberSerializer(Integer.class); + SimpleModule module = new SimpleModule(); + module.addSerializer(Integer.class, serializer1); + ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.json().modules(list -> list.add(module) ).build(); + Serializers serializers = getSerializerFactoryConfig(objectMapper).serializers().iterator().next(); + assertThat(serializers.findSerializer(null, SimpleType.construct(Integer.class), null)).isSameAs(serializer1); + } + + @Test + void modulesWithConsumerAfterModulesToInstall() { + NumberSerializer serializer1 = new NumberSerializer(Integer.class); + SimpleModule module = new SimpleModule(); + module.addSerializer(Integer.class, serializer1); + ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.json() + .modulesToInstall(new JavaTimeModule()) + .modules(list -> list.add(module) ).build(); + Serializers serializers = getSerializerFactoryConfig(objectMapper).serializers().iterator().next(); + assertThat(serializers.findSerializer(null, SimpleType.construct(Integer.class), null)).isSameAs(serializer1); + } + @Test void propertyNamingStrategy() { PropertyNamingStrategy strategy = new PropertyNamingStrategy.SnakeCaseStrategy(); @@ -383,7 +394,7 @@ void propertyNamingStrategy() { void serializerByType() { JsonSerializer serializer = new NumberSerializer(Integer.class); ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.json() - .modules(new ArrayList<>()) // Disable well-known modules detection + .modules(new ArrayList<>()) // disable well-known modules detection .serializerByType(Boolean.class, serializer) .build(); assertThat(getSerializerFactoryConfig(objectMapper).hasSerializers()).isTrue(); @@ -395,7 +406,7 @@ void serializerByType() { void deserializerByType() throws JsonMappingException { JsonDeserializer deserializer = new DateDeserializers.DateDeserializer(); ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.json() - .modules(new ArrayList<>()) // Disable well-known modules detection + .modules(new ArrayList<>()) // disable well-known modules detection .deserializerByType(Date.class, deserializer) .build(); assertThat(getDeserializerFactoryConfig(objectMapper).hasDeserializers()).isTrue(); @@ -474,7 +485,7 @@ void completeSetup() throws JsonMappingException { JsonSerializer serializer2 = new NumberSerializer(Integer.class); Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.json() - .modules(new ArrayList<>()) // Disable well-known modules detection + .modules(new ArrayList<>()) // disable well-known modules detection .serializers(serializer1) .serializersByType(Collections.singletonMap(Boolean.class, serializer2)) .deserializersByType(deserializerMap) diff --git a/spring-web/src/test/java/org/springframework/http/converter/json/Jackson2ObjectMapperFactoryBeanTests.java b/spring-web/src/test/java/org/springframework/http/converter/json/Jackson2ObjectMapperFactoryBeanTests.java index 716abf906f06..fd27a3879e00 100644 --- a/spring-web/src/test/java/org/springframework/http/converter/json/Jackson2ObjectMapperFactoryBeanTests.java +++ b/spring-web/src/test/java/org/springframework/http/converter/json/Jackson2ObjectMapperFactoryBeanTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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. @@ -62,10 +62,8 @@ import org.joda.time.DateTimeZone; import org.junit.jupiter.api.Test; -import org.springframework.beans.FatalBeanException; - import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; /** * Test cases for {@link Jackson2ObjectMapperFactoryBean}. @@ -88,8 +86,7 @@ public class Jackson2ObjectMapperFactoryBeanTests { @Test public void unknownFeature() { this.factory.setFeaturesToEnable(Boolean.TRUE); - assertThatExceptionOfType(FatalBeanException.class).isThrownBy( - this.factory::afterPropertiesSet); + assertThatIllegalArgumentException().isThrownBy(this.factory::afterPropertiesSet); } @Test diff --git a/spring-web/src/test/java/org/springframework/http/converter/json/JsonbHttpMessageConverterTests.java b/spring-web/src/test/java/org/springframework/http/converter/json/JsonbHttpMessageConverterTests.java index 2519005f6672..20ab4f77e76e 100644 --- a/spring-web/src/test/java/org/springframework/http/converter/json/JsonbHttpMessageConverterTests.java +++ b/spring-web/src/test/java/org/springframework/http/converter/json/JsonbHttpMessageConverterTests.java @@ -40,9 +40,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.within; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; /** * Integration tests for the JSON Binding API, running against Apache Johnzon. @@ -77,7 +74,7 @@ public void canReadAndWriteMicroformats() { public void readTyped() throws IOException { String body = "{\"bytes\":[1,2],\"array\":[\"Foo\",\"Bar\"]," + "\"number\":42,\"string\":\"Foo\",\"bool\":true,\"fraction\":42.0}"; - InputStream inputStream = spy(new ByteArrayInputStream(body.getBytes(StandardCharsets.UTF_8))); + InputStream inputStream = new ByteArrayInputStream(body.getBytes(StandardCharsets.UTF_8)); MockHttpInputMessage inputMessage = new MockHttpInputMessage(inputStream); inputMessage.getHeaders().setContentType(new MediaType("application", "json")); MyBean result = (MyBean) this.converter.read(MyBean.class, inputMessage); @@ -89,7 +86,6 @@ public void readTyped() throws IOException { assertThat(result.getArray()).isEqualTo(new String[] {"Foo", "Bar"}); assertThat(result.isBool()).isTrue(); assertThat(result.getBytes()).isEqualTo(new byte[] {0x1, 0x2}); - verify(inputStream, never()).close(); } @Test @@ -140,7 +136,6 @@ public void write() throws IOException { assertThat(result.contains("\"bytes\":[1,2]")).isTrue(); assertThat(outputMessage.getHeaders().getContentType()) .as("Invalid content-type").isEqualTo(new MediaType("application", "json", utf8)); - verify(outputMessage.getBody(), never()).close(); } @Test diff --git a/spring-web/src/test/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverterTests.java b/spring-web/src/test/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverterTests.java index da5dcd4281d3..f598e3c1fc73 100644 --- a/spring-web/src/test/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverterTests.java +++ b/spring-web/src/test/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverterTests.java @@ -16,9 +16,7 @@ package org.springframework.http.converter.json; -import java.io.ByteArrayInputStream; import java.io.IOException; -import java.io.InputStream; import java.lang.reflect.Type; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; @@ -49,9 +47,6 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.entry; import static org.assertj.core.api.Assertions.within; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; /** * Jackson 2.x converter tests. @@ -60,7 +55,7 @@ * @author Sebastien Deleuze * @author Juergen Hoeller */ -public class MappingJackson2HttpMessageConverterTests { +class MappingJackson2HttpMessageConverterTests { protected static final String NEWLINE_SYSTEM_PROPERTY = System.lineSeparator(); @@ -68,7 +63,7 @@ public class MappingJackson2HttpMessageConverterTests { @Test - public void canRead() { + void canRead() { assertThat(converter.canRead(MyBean.class, new MediaType("application", "json"))).isTrue(); assertThat(converter.canRead(Map.class, new MediaType("application", "json"))).isTrue(); assertThat(converter.canRead(MyBean.class, new MediaType("application", "json", StandardCharsets.UTF_8))).isTrue(); @@ -77,7 +72,7 @@ public void canRead() { } @Test - public void canReadWithObjectMapperRegistrationForType() { + void canReadWithObjectMapperRegistrationForType() { MediaType halJsonMediaType = MediaType.parseMediaType("application/hal+json"); MediaType halFormsJsonMediaType = MediaType.parseMediaType("application/prs.hal-forms+json"); @@ -98,7 +93,7 @@ public void canReadWithObjectMapperRegistrationForType() { } @Test - public void canWrite() { + void canWrite() { assertThat(converter.canWrite(MyBean.class, new MediaType("application", "json"))).isTrue(); assertThat(converter.canWrite(Map.class, new MediaType("application", "json"))).isTrue(); assertThat(converter.canWrite(MyBean.class, new MediaType("application", "json", StandardCharsets.UTF_8))).isTrue(); @@ -107,13 +102,13 @@ public void canWrite() { } @Test // SPR-7905 - public void canReadAndWriteMicroformats() { + void canReadAndWriteMicroformats() { assertThat(converter.canRead(MyBean.class, new MediaType("application", "vnd.test-micro-type+json"))).isTrue(); assertThat(converter.canWrite(MyBean.class, new MediaType("application", "vnd.test-micro-type+json"))).isTrue(); } @Test - public void getSupportedMediaTypes() { + void getSupportedMediaTypes() { MediaType[] defaultMediaTypes = {MediaType.APPLICATION_JSON, MediaType.parseMediaType("application/*+json")}; assertThat(converter.getSupportedMediaTypes()).containsExactly(defaultMediaTypes); assertThat(converter.getSupportedMediaTypes(MyBean.class)).containsExactly(defaultMediaTypes); @@ -129,7 +124,7 @@ public void getSupportedMediaTypes() { } @Test - public void readTyped() throws IOException { + void readTyped() throws IOException { String body = "{" + "\"bytes\":\"AQI=\"," + "\"array\":[\"Foo\",\"Bar\"]," + @@ -137,8 +132,7 @@ public void readTyped() throws IOException { "\"string\":\"Foo\"," + "\"bool\":true," + "\"fraction\":42.0}"; - InputStream inputStream = spy(new ByteArrayInputStream(body.getBytes(StandardCharsets.UTF_8))); - MockHttpInputMessage inputMessage = new MockHttpInputMessage(inputStream); + MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(StandardCharsets.UTF_8)); inputMessage.getHeaders().setContentType(new MediaType("application", "json")); MyBean result = (MyBean) converter.read(MyBean.class, inputMessage); assertThat(result.getString()).isEqualTo("Foo"); @@ -147,12 +141,11 @@ public void readTyped() throws IOException { assertThat(result.getArray()).isEqualTo(new String[] {"Foo", "Bar"}); assertThat(result.isBool()).isTrue(); assertThat(result.getBytes()).isEqualTo(new byte[] {0x1, 0x2}); - verify(inputStream, never()).close(); } @Test @SuppressWarnings("unchecked") - public void readUntyped() throws IOException { + void readUntyped() throws IOException { String body = "{" + "\"bytes\":\"AQI=\"," + "\"array\":[\"Foo\",\"Bar\"]," + @@ -175,7 +168,7 @@ public void readUntyped() throws IOException { } @Test - public void write() throws IOException { + void write() throws IOException { MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); MyBean body = new MyBean(); body.setString("Foo"); @@ -194,11 +187,10 @@ public void write() throws IOException { assertThat(result.contains("\"bytes\":\"AQI=\"")).isTrue(); assertThat(outputMessage.getHeaders().getContentType()) .as("Invalid content-type").isEqualTo(MediaType.APPLICATION_JSON); - verify(outputMessage.getBody(), never()).close(); } @Test - public void writeWithBaseType() throws IOException { + void writeWithBaseType() throws IOException { MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); MyBean body = new MyBean(); body.setString("Foo"); @@ -220,7 +212,7 @@ public void writeWithBaseType() throws IOException { } @Test - public void writeUTF16() throws IOException { + void writeUTF16() throws IOException { MediaType contentType = new MediaType("application", "json", StandardCharsets.UTF_16BE); MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); String body = "H\u00e9llo W\u00f6rld"; @@ -230,7 +222,7 @@ public void writeUTF16() throws IOException { } @Test - public void readInvalidJson() throws IOException { + void readInvalidJson() throws IOException { String body = "FooBar"; MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(StandardCharsets.UTF_8)); inputMessage.getHeaders().setContentType(new MediaType("application", "json")); @@ -239,7 +231,7 @@ public void readInvalidJson() throws IOException { } @Test - public void readValidJsonWithUnknownProperty() throws IOException { + void readValidJsonWithUnknownProperty() throws IOException { String body = "{\"string\":\"string\",\"unknownProperty\":\"value\"}"; MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(StandardCharsets.UTF_8)); inputMessage.getHeaders().setContentType(new MediaType("application", "json")); @@ -249,7 +241,7 @@ public void readValidJsonWithUnknownProperty() throws IOException { @Test @SuppressWarnings("unchecked") - public void readAndWriteGenerics() throws Exception { + void readAndWriteGenerics() throws Exception { MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter() { @Override protected JavaType getJavaType(Type type, @Nullable Class contextClass) { @@ -288,7 +280,7 @@ protected JavaType getJavaType(Type type, @Nullable Class contextClass) { @Test @SuppressWarnings("unchecked") - public void readAndWriteParameterizedType() throws Exception { + void readAndWriteParameterizedType() throws Exception { ParameterizedTypeReference> beansList = new ParameterizedTypeReference>() {}; String body = "[{" + @@ -319,7 +311,7 @@ public void readAndWriteParameterizedType() throws Exception { @Test @SuppressWarnings("unchecked") - public void writeParameterizedBaseType() throws Exception { + void writeParameterizedBaseType() throws Exception { ParameterizedTypeReference> beansList = new ParameterizedTypeReference>() {}; ParameterizedTypeReference> baseList = new ParameterizedTypeReference>() {}; @@ -350,7 +342,7 @@ public void writeParameterizedBaseType() throws Exception { } @Test - public void prettyPrint() throws Exception { + void prettyPrint() throws Exception { MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); PrettyPrintBean bean = new PrettyPrintBean(); bean.setName("Jason"); @@ -364,7 +356,7 @@ public void prettyPrint() throws Exception { } @Test - public void prettyPrintWithSse() throws Exception { + void prettyPrintWithSse() throws Exception { MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); outputMessage.getHeaders().setContentType(MediaType.TEXT_EVENT_STREAM); PrettyPrintBean bean = new PrettyPrintBean(); @@ -378,7 +370,7 @@ public void prettyPrintWithSse() throws Exception { } @Test - public void prefixJson() throws Exception { + void prefixJson() throws Exception { MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); this.converter.setPrefixJson(true); this.converter.writeInternal("foo", null, outputMessage); @@ -387,7 +379,7 @@ public void prefixJson() throws Exception { } @Test - public void prefixJsonCustom() throws Exception { + void prefixJsonCustom() throws Exception { MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); this.converter.setJsonPrefix(")))"); this.converter.writeInternal("foo", null, outputMessage); @@ -396,7 +388,7 @@ public void prefixJsonCustom() throws Exception { } @Test - public void fieldLevelJsonView() throws Exception { + void fieldLevelJsonView() throws Exception { MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); JacksonViewBean bean = new JacksonViewBean(); bean.setWithView1("with"); @@ -414,7 +406,7 @@ public void fieldLevelJsonView() throws Exception { } @Test - public void classLevelJsonView() throws Exception { + void classLevelJsonView() throws Exception { MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); JacksonViewBean bean = new JacksonViewBean(); bean.setWithView1("with"); @@ -432,7 +424,7 @@ public void classLevelJsonView() throws Exception { } @Test - public void filters() throws Exception { + void filters() throws Exception { MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); JacksonFilteredBean bean = new JacksonFilteredBean(); bean.setProperty1("value"); @@ -450,7 +442,7 @@ public void filters() throws Exception { } @Test // SPR-13318 - public void writeSubType() throws Exception { + void writeSubType() throws Exception { MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); MyBean bean = new MyBean(); bean.setString("Foo"); @@ -464,7 +456,7 @@ public void writeSubType() throws Exception { } @Test // SPR-13318 - public void writeSubTypeList() throws Exception { + void writeSubTypeList() throws Exception { MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); List beans = new ArrayList<>(); MyBean foo = new MyBean(); @@ -488,7 +480,7 @@ public void writeSubTypeList() throws Exception { } @Test - public void readWithNoDefaultConstructor() throws Exception { + void readWithNoDefaultConstructor() throws Exception { String body = "{\"property1\":\"foo\",\"property2\":\"bar\"}"; MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(StandardCharsets.UTF_8)); inputMessage.getHeaders().setContentType(MediaType.APPLICATION_JSON); @@ -499,7 +491,7 @@ public void readWithNoDefaultConstructor() throws Exception { @Test @SuppressWarnings("unchecked") - public void readNonUnicode() throws Exception { + void readNonUnicode() throws Exception { String body = "{\"føø\":\"bår\"}"; Charset charset = StandardCharsets.ISO_8859_1; MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(charset)); @@ -511,7 +503,7 @@ public void readNonUnicode() throws Exception { @Test @SuppressWarnings("unchecked") - public void readAscii() throws Exception { + void readAscii() throws Exception { String body = "{\"foo\":\"bar\"}"; Charset charset = StandardCharsets.US_ASCII; MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(charset)); @@ -523,7 +515,7 @@ public void readAscii() throws Exception { @Test @SuppressWarnings("unchecked") - public void writeAscii() throws Exception { + void writeAscii() throws Exception { MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); Map body = new HashMap<>(); body.put("foo", "bar"); diff --git a/spring-web/src/test/java/org/springframework/http/converter/protobuf/ProtobufHttpMessageConverterTests.java b/spring-web/src/test/java/org/springframework/http/converter/protobuf/ProtobufHttpMessageConverterTests.java index 01dc97d84f94..b9bef0aad54f 100644 --- a/spring-web/src/test/java/org/springframework/http/converter/protobuf/ProtobufHttpMessageConverterTests.java +++ b/spring-web/src/test/java/org/springframework/http/converter/protobuf/ProtobufHttpMessageConverterTests.java @@ -16,15 +16,12 @@ package org.springframework.http.converter.protobuf; -import java.io.ByteArrayInputStream; import java.io.IOException; -import java.io.InputStream; import java.nio.charset.StandardCharsets; import com.google.protobuf.ExtensionRegistry; import com.google.protobuf.Message; import com.google.protobuf.util.JsonFormat; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.http.MediaType; @@ -36,8 +33,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -48,47 +43,39 @@ * @author Juergen Hoeller * @author Andreas Ahlenstorf * @author Sebastien Deleuze + * @author Sam Brannen */ @SuppressWarnings("deprecation") -public class ProtobufHttpMessageConverterTests { +class ProtobufHttpMessageConverterTests { - private ProtobufHttpMessageConverter converter; + private ExtensionRegistry extensionRegistry = mock(ExtensionRegistry.class); - private ExtensionRegistry extensionRegistry; + private ExtensionRegistryInitializer registryInitializer = mock(ExtensionRegistryInitializer.class); - private ExtensionRegistryInitializer registryInitializer; + private ProtobufHttpMessageConverter converter = new ProtobufHttpMessageConverter(this.registryInitializer); - private Msg testMsg; - - - @BeforeEach - public void setup() { - this.registryInitializer = mock(ExtensionRegistryInitializer.class); - this.extensionRegistry = mock(ExtensionRegistry.class); - this.converter = new ProtobufHttpMessageConverter(this.registryInitializer); - this.testMsg = Msg.newBuilder().setFoo("Foo").setBlah(SecondMsg.newBuilder().setBlah(123).build()).build(); - } + private Msg testMsg = Msg.newBuilder().setFoo("Foo").setBlah(SecondMsg.newBuilder().setBlah(123).build()).build(); @Test - public void extensionRegistryInitialized() { + void extensionRegistryInitialized() { verify(this.registryInitializer, times(1)).initializeExtensionRegistry(any()); } @Test - public void extensionRegistryInitializerNull() { + void extensionRegistryInitializerNull() { ProtobufHttpMessageConverter converter = new ProtobufHttpMessageConverter((ExtensionRegistryInitializer)null); assertThat(converter.extensionRegistry).isNotNull(); } @Test - public void extensionRegistryNull() { + void extensionRegistryNull() { ProtobufHttpMessageConverter converter = new ProtobufHttpMessageConverter((ExtensionRegistry)null); assertThat(converter.extensionRegistry).isNotNull(); } @Test - public void canRead() { + void canRead() { assertThat(this.converter.canRead(Msg.class, null)).isTrue(); assertThat(this.converter.canRead(Msg.class, ProtobufHttpMessageConverter.PROTOBUF)).isTrue(); assertThat(this.converter.canRead(Msg.class, MediaType.APPLICATION_JSON)).isTrue(); @@ -100,7 +87,7 @@ public void canRead() { } @Test - public void canWrite() { + void canWrite() { assertThat(this.converter.canWrite(Msg.class, null)).isTrue(); assertThat(this.converter.canWrite(Msg.class, ProtobufHttpMessageConverter.PROTOBUF)).isTrue(); assertThat(this.converter.canWrite(Msg.class, MediaType.APPLICATION_JSON)).isTrue(); @@ -110,18 +97,16 @@ public void canWrite() { } @Test - public void read() throws IOException { + void read() throws IOException { byte[] body = this.testMsg.toByteArray(); - InputStream inputStream = spy(new ByteArrayInputStream(body)); MockHttpInputMessage inputMessage = new MockHttpInputMessage(body); inputMessage.getHeaders().setContentType(ProtobufHttpMessageConverter.PROTOBUF); Message result = this.converter.read(Msg.class, inputMessage); assertThat(result).isEqualTo(this.testMsg); - verify(inputStream, never()).close(); } @Test - public void readNoContentType() throws IOException { + void readNoContentType() throws IOException { byte[] body = this.testMsg.toByteArray(); MockHttpInputMessage inputMessage = new MockHttpInputMessage(body); Message result = this.converter.read(Msg.class, inputMessage); @@ -129,7 +114,7 @@ public void readNoContentType() throws IOException { } @Test - public void writeProtobuf() throws IOException { + void writeProtobuf() throws IOException { MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); MediaType contentType = ProtobufHttpMessageConverter.PROTOBUF; this.converter.write(this.testMsg, contentType, outputMessage); @@ -144,11 +129,10 @@ public void writeProtobuf() throws IOException { String schemaHeader = outputMessage.getHeaders().getFirst(ProtobufHttpMessageConverter.X_PROTOBUF_SCHEMA_HEADER); assertThat(schemaHeader).isEqualTo("sample.proto"); - verify(outputMessage.getBody(), never()).close(); } @Test - public void writeJsonWithGoogleProtobuf() throws IOException { + void writeJsonWithGoogleProtobuf() throws IOException { this.converter = new ProtobufHttpMessageConverter( new ProtobufHttpMessageConverter.ProtobufJavaUtilSupport(null, null), this.extensionRegistry); @@ -172,7 +156,7 @@ public void writeJsonWithGoogleProtobuf() throws IOException { } @Test - public void writeJsonWithJavaFormat() throws IOException { + void writeJsonWithJavaFormat() throws IOException { this.converter = new ProtobufHttpMessageConverter( new ProtobufHttpMessageConverter.ProtobufJavaFormatSupport(), this.extensionRegistry); @@ -196,13 +180,13 @@ public void writeJsonWithJavaFormat() throws IOException { } @Test - public void defaultContentType() throws Exception { + void defaultContentType() throws Exception { assertThat(this.converter.getDefaultContentType(this.testMsg)) .isEqualTo(ProtobufHttpMessageConverter.PROTOBUF); } @Test - public void getContentLength() throws Exception { + void getContentLength() throws Exception { MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); MediaType contentType = ProtobufHttpMessageConverter.PROTOBUF; this.converter.write(this.testMsg, contentType, outputMessage); diff --git a/spring-web/src/test/java/org/springframework/http/converter/protobuf/ProtobufJsonFormatHttpMessageConverterTests.java b/spring-web/src/test/java/org/springframework/http/converter/protobuf/ProtobufJsonFormatHttpMessageConverterTests.java index c933c4262286..87b06f560777 100644 --- a/spring-web/src/test/java/org/springframework/http/converter/protobuf/ProtobufJsonFormatHttpMessageConverterTests.java +++ b/spring-web/src/test/java/org/springframework/http/converter/protobuf/ProtobufJsonFormatHttpMessageConverterTests.java @@ -16,9 +16,7 @@ package org.springframework.http.converter.protobuf; -import java.io.ByteArrayInputStream; import java.io.IOException; -import java.io.InputStream; import com.google.protobuf.ExtensionRegistry; import com.google.protobuf.Message; @@ -34,8 +32,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -92,12 +88,10 @@ public void canWrite() { @Test public void read() throws IOException { byte[] body = this.testMsg.toByteArray(); - InputStream inputStream = spy(new ByteArrayInputStream(body)); MockHttpInputMessage inputMessage = new MockHttpInputMessage(body); inputMessage.getHeaders().setContentType(ProtobufHttpMessageConverter.PROTOBUF); Message result = this.converter.read(Msg.class, inputMessage); assertThat(result).isEqualTo(this.testMsg); - verify(inputStream, never()).close(); } @Test @@ -124,7 +118,6 @@ public void write() throws IOException { String schemaHeader = outputMessage.getHeaders().getFirst(ProtobufHttpMessageConverter.X_PROTOBUF_SCHEMA_HEADER); assertThat(schemaHeader).isEqualTo("sample.proto"); - verify(outputMessage.getBody(), never()).close(); } @Test diff --git a/spring-web/src/test/java/org/springframework/http/converter/smile/MappingJackson2SmileHttpMessageConverterTests.java b/spring-web/src/test/java/org/springframework/http/converter/smile/MappingJackson2SmileHttpMessageConverterTests.java index 231d00306060..7cea8efe3a9d 100644 --- a/spring-web/src/test/java/org/springframework/http/converter/smile/MappingJackson2SmileHttpMessageConverterTests.java +++ b/spring-web/src/test/java/org/springframework/http/converter/smile/MappingJackson2SmileHttpMessageConverterTests.java @@ -30,37 +30,34 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.within; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; /** * Jackson 2.x Smile converter tests. * * @author Sebastien Deleuze */ -public class MappingJackson2SmileHttpMessageConverterTests { +class MappingJackson2SmileHttpMessageConverterTests { private final MappingJackson2SmileHttpMessageConverter converter = new MappingJackson2SmileHttpMessageConverter(); private final ObjectMapper mapper = new ObjectMapper(new SmileFactory()); @Test - public void canRead() { + void canRead() { assertThat(converter.canRead(MyBean.class, new MediaType("application", "x-jackson-smile"))).isTrue(); assertThat(converter.canRead(MyBean.class, new MediaType("application", "json"))).isFalse(); assertThat(converter.canRead(MyBean.class, new MediaType("application", "xml"))).isFalse(); } @Test - public void canWrite() { + void canWrite() { assertThat(converter.canWrite(MyBean.class, new MediaType("application", "x-jackson-smile"))).isTrue(); assertThat(converter.canWrite(MyBean.class, new MediaType("application", "json"))).isFalse(); assertThat(converter.canWrite(MyBean.class, new MediaType("application", "xml"))).isFalse(); } @Test - public void read() throws IOException { + void read() throws IOException { MyBean body = new MyBean(); body.setString("Foo"); body.setNumber(42); @@ -68,7 +65,7 @@ public void read() throws IOException { body.setArray(new String[]{"Foo", "Bar"}); body.setBool(true); body.setBytes(new byte[]{0x1, 0x2}); - InputStream inputStream = spy(new ByteArrayInputStream(mapper.writeValueAsBytes(body))); + InputStream inputStream = new ByteArrayInputStream(mapper.writeValueAsBytes(body)); MockHttpInputMessage inputMessage = new MockHttpInputMessage(inputStream); inputMessage.getHeaders().setContentType(new MediaType("application", "x-jackson-smile")); MyBean result = (MyBean) converter.read(MyBean.class, inputMessage); @@ -79,11 +76,10 @@ public void read() throws IOException { assertThat(result.getArray()).isEqualTo(new String[]{"Foo", "Bar"}); assertThat(result.isBool()).isTrue(); assertThat(result.getBytes()).isEqualTo(new byte[]{0x1, 0x2}); - verify(inputStream, never()).close(); } @Test - public void write() throws IOException { + void write() throws IOException { MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); MyBean body = new MyBean(); body.setString("Foo"); @@ -96,7 +92,6 @@ public void write() throws IOException { assertThat(outputMessage.getBodyAsBytes()).isEqualTo(mapper.writeValueAsBytes(body)); assertThat(outputMessage.getHeaders().getContentType()) .as("Invalid content-type").isEqualTo(new MediaType("application", "x-jackson-smile")); - verify(outputMessage.getBody(), never()).close(); } diff --git a/spring-web/src/test/java/org/springframework/http/converter/xml/Jaxb2CollectionHttpMessageConverterTests.java b/spring-web/src/test/java/org/springframework/http/converter/xml/Jaxb2CollectionHttpMessageConverterTests.java index 399e2e0b4bc4..968793270260 100644 --- a/spring-web/src/test/java/org/springframework/http/converter/xml/Jaxb2CollectionHttpMessageConverterTests.java +++ b/spring-web/src/test/java/org/springframework/http/converter/xml/Jaxb2CollectionHttpMessageConverterTests.java @@ -16,8 +16,6 @@ package org.springframework.http.converter.xml; -import java.io.ByteArrayInputStream; -import java.io.InputStream; import java.lang.reflect.Type; import java.nio.charset.StandardCharsets; import java.util.Collection; @@ -41,9 +39,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; /** * Test fixture for {@link Jaxb2CollectionHttpMessageConverter}. @@ -51,7 +46,7 @@ * @author Arjen Poutsma * @author Rossen Stoyanchev */ -public class Jaxb2CollectionHttpMessageConverterTests { +class Jaxb2CollectionHttpMessageConverterTests { private Jaxb2CollectionHttpMessageConverter converter; @@ -65,7 +60,7 @@ public class Jaxb2CollectionHttpMessageConverterTests { @BeforeEach - public void setup() { + void setup() { converter = new Jaxb2CollectionHttpMessageConverter>(); rootElementListType = new ParameterizedTypeReference>() {}.getType(); rootElementSetType = new ParameterizedTypeReference>() {}.getType(); @@ -75,7 +70,7 @@ public void setup() { @Test - public void canRead() { + void canRead() { assertThat(converter.canRead(rootElementListType, null, null)).isTrue(); assertThat(converter.canRead(rootElementSetType, null, null)).isTrue(); assertThat(converter.canRead(typeSetType, null, null)).isTrue(); @@ -83,21 +78,19 @@ public void canRead() { @Test @SuppressWarnings("unchecked") - public void readXmlRootElementList() throws Exception { + void readXmlRootElementList() throws Exception { String content = ""; - InputStream inputStream = spy(new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8))); - MockHttpInputMessage inputMessage = new MockHttpInputMessage(inputStream); + MockHttpInputMessage inputMessage = new MockHttpInputMessage(content.getBytes(StandardCharsets.UTF_8)); List result = (List) converter.read(rootElementListType, null, inputMessage); assertThat(result.size()).as("Invalid result").isEqualTo(2); assertThat(result.get(0).type.s).as("Invalid result").isEqualTo("1"); assertThat(result.get(1).type.s).as("Invalid result").isEqualTo("2"); - verify(inputStream, never()).close(); } @Test @SuppressWarnings("unchecked") - public void readXmlRootElementSet() throws Exception { + void readXmlRootElementSet() throws Exception { String content = ""; MockHttpInputMessage inputMessage = new MockHttpInputMessage(content.getBytes(StandardCharsets.UTF_8)); Set result = (Set) converter.read(rootElementSetType, null, inputMessage); @@ -109,7 +102,7 @@ public void readXmlRootElementSet() throws Exception { @Test @SuppressWarnings("unchecked") - public void readXmlTypeList() throws Exception { + void readXmlTypeList() throws Exception { String content = ""; MockHttpInputMessage inputMessage = new MockHttpInputMessage(content.getBytes(StandardCharsets.UTF_8)); List result = (List) converter.read(typeListType, null, inputMessage); @@ -121,7 +114,7 @@ public void readXmlTypeList() throws Exception { @Test @SuppressWarnings("unchecked") - public void readXmlTypeSet() throws Exception { + void readXmlTypeSet() throws Exception { String content = ""; MockHttpInputMessage inputMessage = new MockHttpInputMessage(content.getBytes(StandardCharsets.UTF_8)); Set result = (Set) converter.read(typeSetType, null, inputMessage); @@ -133,7 +126,7 @@ public void readXmlTypeSet() throws Exception { @Test @SuppressWarnings("unchecked") - public void readXmlRootElementExternalEntityDisabled() throws Exception { + void readXmlRootElementExternalEntityDisabled() throws Exception { Resource external = new ClassPathResource("external.txt", getClass()); String content = "\n" + @@ -162,7 +155,7 @@ protected XMLInputFactory createXmlInputFactory() { @Test @SuppressWarnings("unchecked") - public void readXmlRootElementExternalEntityEnabled() throws Exception { + void readXmlRootElementExternalEntityEnabled() throws Exception { Resource external = new ClassPathResource("external.txt", getClass()); String content = "\n" + @@ -185,7 +178,7 @@ protected XMLInputFactory createXmlInputFactory() { } @Test - public void testXmlBomb() throws Exception { + void testXmlBomb() throws Exception { // https://en.wikipedia.org/wiki/Billion_laughs // https://msdn.microsoft.com/en-us/magazine/ee335713.aspx String content = "\n" + diff --git a/spring-web/src/test/java/org/springframework/http/converter/xml/Jaxb2RootElementHttpMessageConverterTests.java b/spring-web/src/test/java/org/springframework/http/converter/xml/Jaxb2RootElementHttpMessageConverterTests.java index 0324edcc10c5..7642dbd73fca 100644 --- a/spring-web/src/test/java/org/springframework/http/converter/xml/Jaxb2RootElementHttpMessageConverterTests.java +++ b/spring-web/src/test/java/org/springframework/http/converter/xml/Jaxb2RootElementHttpMessageConverterTests.java @@ -16,8 +16,6 @@ package org.springframework.http.converter.xml; -import java.io.ByteArrayInputStream; -import java.io.InputStream; import java.nio.charset.StandardCharsets; import javax.xml.bind.Marshaller; @@ -29,7 +27,6 @@ import javax.xml.bind.annotation.adapters.XmlAdapter; import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.xmlunit.diff.DifferenceEvaluator; @@ -46,9 +43,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; import static org.xmlunit.diff.ComparisonType.XML_STANDALONE; import static org.xmlunit.diff.DifferenceEvaluators.Default; import static org.xmlunit.diff.DifferenceEvaluators.chain; @@ -60,31 +54,17 @@ * @author Arjen Poutsma * @author Sebastien Deleuze * @author Rossen Stoyanchev + * @author Sam Brannen */ -public class Jaxb2RootElementHttpMessageConverterTests { +class Jaxb2RootElementHttpMessageConverterTests { - private Jaxb2RootElementHttpMessageConverter converter; + private final Jaxb2RootElementHttpMessageConverter converter = new Jaxb2RootElementHttpMessageConverter(); - private RootElement rootElement; - - private RootElement rootElementCglib; - - - @BeforeEach - public void setup() { - converter = new Jaxb2RootElementHttpMessageConverter(); - rootElement = new RootElement(); - DefaultAopProxyFactory proxyFactory = new DefaultAopProxyFactory(); - AdvisedSupport advisedSupport = new AdvisedSupport(); - advisedSupport.setTarget(rootElement); - advisedSupport.setProxyTargetClass(true); - AopProxy proxy = proxyFactory.createAopProxy(advisedSupport); - rootElementCglib = (RootElement) proxy.getProxy(); - } + private final RootElement rootElement = new RootElement(); @Test - public void canRead() { + void canRead() { assertThat(converter.canRead(RootElement.class, null)) .as("Converter does not support reading @XmlRootElement").isTrue(); assertThat(converter.canRead(Type.class, null)) @@ -92,29 +72,27 @@ public void canRead() { } @Test - public void canWrite() { + void canWrite() { assertThat(converter.canWrite(RootElement.class, null)) .as("Converter does not support writing @XmlRootElement").isTrue(); assertThat(converter.canWrite(RootElementSubclass.class, null)) .as("Converter does not support writing @XmlRootElement subclass").isTrue(); - assertThat(converter.canWrite(rootElementCglib.getClass(), null)) + assertThat(converter.canWrite(createRootElementCglib().getClass(), null)) .as("Converter does not support writing @XmlRootElement subclass").isTrue(); assertThat(converter.canWrite(Type.class, null)) .as("Converter supports writing @XmlType").isFalse(); } @Test - public void readXmlRootElement() throws Exception { + void readXmlRootElement() throws Exception { byte[] body = "".getBytes(StandardCharsets.UTF_8); - InputStream inputStream = spy(new ByteArrayInputStream(body)); - MockHttpInputMessage inputMessage = new MockHttpInputMessage(inputStream); + MockHttpInputMessage inputMessage = new MockHttpInputMessage(body); RootElement result = (RootElement) converter.read(RootElement.class, inputMessage); assertThat(result.type.s).as("Invalid result").isEqualTo("Hello World"); - verify(inputStream, never()).close(); } @Test - public void readXmlRootElementSubclass() throws Exception { + void readXmlRootElementSubclass() throws Exception { byte[] body = "".getBytes(StandardCharsets.UTF_8); MockHttpInputMessage inputMessage = new MockHttpInputMessage(body); RootElementSubclass result = (RootElementSubclass) converter.read(RootElementSubclass.class, inputMessage); @@ -122,7 +100,7 @@ public void readXmlRootElementSubclass() throws Exception { } @Test - public void readXmlType() throws Exception { + void readXmlType() throws Exception { byte[] body = "".getBytes(StandardCharsets.UTF_8); MockHttpInputMessage inputMessage = new MockHttpInputMessage(body); Type result = (Type) converter.read(Type.class, inputMessage); @@ -130,7 +108,7 @@ public void readXmlType() throws Exception { } @Test - public void readXmlRootElementExternalEntityDisabled() throws Exception { + void readXmlRootElementExternalEntityDisabled() throws Exception { Resource external = new ClassPathResource("external.txt", getClass()); String content = "\n" + @@ -144,7 +122,7 @@ public void readXmlRootElementExternalEntityDisabled() throws Exception { } @Test - public void readXmlRootElementExternalEntityEnabled() throws Exception { + void readXmlRootElementExternalEntityEnabled() throws Exception { Resource external = new ClassPathResource("external.txt", getClass()); String content = "\n" + @@ -158,7 +136,7 @@ public void readXmlRootElementExternalEntityEnabled() throws Exception { } @Test - public void testXmlBomb() throws Exception { + void testXmlBomb() throws Exception { // https://en.wikipedia.org/wiki/Billion_laughs // https://msdn.microsoft.com/en-us/magazine/ee335713.aspx String content = "\n" + @@ -183,7 +161,7 @@ public void testXmlBomb() throws Exception { } @Test - public void writeXmlRootElement() throws Exception { + void writeXmlRootElement() throws Exception { MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); converter.write(rootElement, null, outputMessage); assertThat(outputMessage.getHeaders().getContentType()) @@ -191,13 +169,12 @@ public void writeXmlRootElement() throws Exception { DifferenceEvaluator ev = chain(Default, downgradeDifferencesToEqual(XML_STANDALONE)); assertThat(XmlContent.of(outputMessage.getBodyAsString(StandardCharsets.UTF_8))) .isSimilarTo("", ev); - verify(outputMessage.getBody(), never()).close(); } @Test - public void writeXmlRootElementSubclass() throws Exception { + void writeXmlRootElementSubclass() throws Exception { MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); - converter.write(rootElementCglib, null, outputMessage); + converter.write(createRootElementCglib(), null, outputMessage); assertThat(outputMessage.getHeaders().getContentType()) .as("Invalid content-type").isEqualTo(MediaType.APPLICATION_XML); DifferenceEvaluator ev = chain(Default, downgradeDifferencesToEqual(XML_STANDALONE)); @@ -208,7 +185,7 @@ public void writeXmlRootElementSubclass() throws Exception { // SPR-11488 @Test - public void customizeMarshaller() throws Exception { + void customizeMarshaller() throws Exception { MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); MyJaxb2RootElementHttpMessageConverter myConverter = new MyJaxb2RootElementHttpMessageConverter(); myConverter.write(new MyRootElement(new MyCustomElement("a", "b")), null, outputMessage); @@ -218,7 +195,7 @@ public void customizeMarshaller() throws Exception { } @Test - public void customizeUnmarshaller() throws Exception { + void customizeUnmarshaller() throws Exception { byte[] body = "a|||b".getBytes(StandardCharsets.UTF_8); MyJaxb2RootElementHttpMessageConverter myConverter = new MyJaxb2RootElementHttpMessageConverter(); MockHttpInputMessage inputMessage = new MockHttpInputMessage(body); @@ -227,6 +204,15 @@ public void customizeUnmarshaller() throws Exception { assertThat(result.getElement().getField2()).isEqualTo("b"); } + private RootElement createRootElementCglib() { + DefaultAopProxyFactory proxyFactory = new DefaultAopProxyFactory(); + AdvisedSupport advisedSupport = new AdvisedSupport(); + advisedSupport.setTarget(this.rootElement); + advisedSupport.setProxyTargetClass(true); + AopProxy proxy = proxyFactory.createAopProxy(advisedSupport); + return (RootElement) proxy.getProxy(); + } + @XmlRootElement public static class RootElement { diff --git a/spring-web/src/test/java/org/springframework/http/converter/xml/MappingJackson2XmlHttpMessageConverterTests.java b/spring-web/src/test/java/org/springframework/http/converter/xml/MappingJackson2XmlHttpMessageConverterTests.java index dcd4504e6f0e..57771e906847 100644 --- a/spring-web/src/test/java/org/springframework/http/converter/xml/MappingJackson2XmlHttpMessageConverterTests.java +++ b/spring-web/src/test/java/org/springframework/http/converter/xml/MappingJackson2XmlHttpMessageConverterTests.java @@ -16,9 +16,7 @@ package org.springframework.http.converter.xml; -import java.io.ByteArrayInputStream; import java.io.IOException; -import java.io.InputStream; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; @@ -36,9 +34,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.within; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; /** * Jackson 2.x XML converter tests. @@ -46,13 +41,13 @@ * @author Sebastien Deleuze * @author Rossen Stoyanchev */ -public class MappingJackson2XmlHttpMessageConverterTests { +class MappingJackson2XmlHttpMessageConverterTests { private final MappingJackson2XmlHttpMessageConverter converter = new MappingJackson2XmlHttpMessageConverter(); @Test - public void canRead() { + void canRead() { assertThat(converter.canRead(MyBean.class, new MediaType("application", "xml"))).isTrue(); assertThat(converter.canRead(MyBean.class, new MediaType("text", "xml"))).isTrue(); assertThat(converter.canRead(MyBean.class, new MediaType("application", "soap+xml"))).isTrue(); @@ -61,7 +56,7 @@ public void canRead() { } @Test - public void canWrite() { + void canWrite() { assertThat(converter.canWrite(MyBean.class, new MediaType("application", "xml"))).isTrue(); assertThat(converter.canWrite(MyBean.class, new MediaType("text", "xml"))).isTrue(); assertThat(converter.canWrite(MyBean.class, new MediaType("application", "soap+xml"))).isTrue(); @@ -70,7 +65,7 @@ public void canWrite() { } @Test - public void read() throws IOException { + void read() throws IOException { String body = "" + "Foo" + "42" + @@ -79,8 +74,7 @@ public void read() throws IOException { "Bar" + "true" + "AQI="; - InputStream inputStream = spy(new ByteArrayInputStream(body.getBytes(StandardCharsets.UTF_8))); - MockHttpInputMessage inputMessage = new MockHttpInputMessage(inputStream); + MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(StandardCharsets.UTF_8)); inputMessage.getHeaders().setContentType(new MediaType("application", "xml")); MyBean result = (MyBean) converter.read(MyBean.class, inputMessage); assertThat(result.getString()).isEqualTo("Foo"); @@ -89,11 +83,10 @@ public void read() throws IOException { assertThat(result.getArray()).isEqualTo(new String[]{"Foo", "Bar"}); assertThat(result.isBool()).isTrue(); assertThat(result.getBytes()).isEqualTo(new byte[]{0x1, 0x2}); - verify(inputStream, never()).close(); } @Test - public void write() throws IOException { + void write() throws IOException { MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); MyBean body = new MyBean(); body.setString("Foo"); @@ -112,11 +105,10 @@ public void write() throws IOException { assertThat(result.contains("AQI=")).isTrue(); assertThat(outputMessage.getHeaders().getContentType()) .as("Invalid content-type").isEqualTo(new MediaType("application", "xml", StandardCharsets.UTF_8)); - verify(outputMessage.getBody(), never()).close(); } @Test - public void readInvalidXml() throws IOException { + void readInvalidXml() throws IOException { String body = "FooBar"; MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(StandardCharsets.UTF_8)); inputMessage.getHeaders().setContentType(MediaType.APPLICATION_XML); @@ -125,7 +117,7 @@ public void readInvalidXml() throws IOException { } @Test - public void readValidXmlWithUnknownProperty() throws IOException { + void readValidXmlWithUnknownProperty() throws IOException { String body = "stringvalue"; MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(StandardCharsets.UTF_8)); inputMessage.getHeaders().setContentType(MediaType.APPLICATION_XML); @@ -134,7 +126,7 @@ public void readValidXmlWithUnknownProperty() throws IOException { } @Test - public void jsonView() throws Exception { + void jsonView() throws Exception { MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); JacksonViewBean bean = new JacksonViewBean(); bean.setWithView1("with"); @@ -152,13 +144,13 @@ public void jsonView() throws Exception { } @Test - public void customXmlMapper() { + void customXmlMapper() { new MappingJackson2XmlHttpMessageConverter(new MyXmlMapper()); // Assert no exception is thrown } @Test - public void readWithExternalReference() throws IOException { + void readWithExternalReference() throws IOException { String body = "\n" + " \n" + @@ -201,7 +193,7 @@ public void readWithXmlBomb() throws IOException { @Test @SuppressWarnings("unchecked") - public void readNonUnicode() throws Exception { + void readNonUnicode() throws Exception { String body = "" + "føø bår" + ""; diff --git a/spring-web/src/test/java/org/springframework/http/converter/xml/MarshallingHttpMessageConverterTests.java b/spring-web/src/test/java/org/springframework/http/converter/xml/MarshallingHttpMessageConverterTests.java index 3bb9c6f8877b..ddd80d5d74ac 100644 --- a/spring-web/src/test/java/org/springframework/http/converter/xml/MarshallingHttpMessageConverterTests.java +++ b/spring-web/src/test/java/org/springframework/http/converter/xml/MarshallingHttpMessageConverterTests.java @@ -16,8 +16,6 @@ package org.springframework.http.converter.xml; -import java.io.ByteArrayInputStream; -import java.io.InputStream; import java.nio.charset.StandardCharsets; import javax.xml.transform.Result; @@ -44,19 +42,16 @@ import static org.mockito.BDDMockito.willDoNothing; import static org.mockito.BDDMockito.willThrow; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; /** * Tests for {@link MarshallingHttpMessageConverter}. * * @author Arjen Poutsma */ -public class MarshallingHttpMessageConverterTests { +class MarshallingHttpMessageConverterTests { @Test - public void canRead() { + void canRead() { Unmarshaller unmarshaller = mock(Unmarshaller.class); given(unmarshaller.supports(Integer.class)).willReturn(false); @@ -71,7 +66,7 @@ public void canRead() { } @Test - public void canWrite() { + void canWrite() { Marshaller marshaller = mock(Marshaller.class); given(marshaller.supports(Integer.class)).willReturn(false); @@ -86,10 +81,9 @@ public void canWrite() { } @Test - public void read() throws Exception { + void read() throws Exception { String body = "Hello World"; - InputStream inputStream = spy(new ByteArrayInputStream(body.getBytes(StandardCharsets.UTF_8))); - MockHttpInputMessage inputMessage = new MockHttpInputMessage(inputStream); + MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(StandardCharsets.UTF_8)); Unmarshaller unmarshaller = mock(Unmarshaller.class); given(unmarshaller.unmarshal(isA(StreamSource.class))).willReturn(body); @@ -99,11 +93,10 @@ public void read() throws Exception { String result = (String) converter.read(Object.class, inputMessage); assertThat(result).as("Invalid result").isEqualTo(body); - verify(inputStream, never()).close(); } @Test - public void readWithTypeMismatchException() throws Exception { + void readWithTypeMismatchException() throws Exception { MockHttpInputMessage inputMessage = new MockHttpInputMessage(new byte[0]); Marshaller marshaller = mock(Marshaller.class); @@ -117,7 +110,7 @@ public void readWithTypeMismatchException() throws Exception { } @Test - public void readWithMarshallingFailureException() throws Exception { + void readWithMarshallingFailureException() throws Exception { MockHttpInputMessage inputMessage = new MockHttpInputMessage(new byte[0]); UnmarshallingFailureException ex = new UnmarshallingFailureException("forced"); @@ -132,7 +125,7 @@ public void readWithMarshallingFailureException() throws Exception { } @Test - public void write() throws Exception { + void write() throws Exception { String body = "Hello World"; MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); @@ -144,11 +137,10 @@ public void write() throws Exception { assertThat(outputMessage.getHeaders().getContentType()) .as("Invalid content-type").isEqualTo(new MediaType("application", "xml")); - verify(outputMessage.getBody(), never()).close(); } @Test - public void writeWithMarshallingFailureException() throws Exception { + void writeWithMarshallingFailureException() throws Exception { String body = "Hello World"; MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); MarshallingFailureException ex = new MarshallingFailureException("forced"); @@ -162,8 +154,9 @@ public void writeWithMarshallingFailureException() throws Exception { } @Test - public void supports() { + void supports() { assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> new MarshallingHttpMessageConverter().supports(Object.class)); } + } diff --git a/spring-web/src/test/java/org/springframework/http/converter/xml/SourceHttpMessageConverterTests.java b/spring-web/src/test/java/org/springframework/http/converter/xml/SourceHttpMessageConverterTests.java index 54f2fd1322cb..9568d53854e8 100644 --- a/spring-web/src/test/java/org/springframework/http/converter/xml/SourceHttpMessageConverterTests.java +++ b/spring-web/src/test/java/org/springframework/http/converter/xml/SourceHttpMessageConverterTests.java @@ -16,9 +16,7 @@ package org.springframework.http.converter.xml; -import java.io.ByteArrayInputStream; import java.io.IOException; -import java.io.InputStream; import java.io.InputStreamReader; import java.io.StringReader; import java.nio.charset.StandardCharsets; @@ -32,7 +30,6 @@ import javax.xml.transform.stax.StAXSource; import javax.xml.transform.stream.StreamSource; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.w3c.dom.Document; import org.w3c.dom.Element; @@ -52,60 +49,53 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; /** * @author Arjen Poutsma * @author Rossen Stoyanchev + * @author Sam Brannen */ -public class SourceHttpMessageConverterTests { +class SourceHttpMessageConverterTests { private static final String BODY = "Hello World"; - private SourceHttpMessageConverter converter; + private final SourceHttpMessageConverter converter = new SourceHttpMessageConverter<>(); - private String bodyExternal; + private final String bodyExternal; - @BeforeEach - public void setup() throws IOException { - converter = new SourceHttpMessageConverter<>(); + SourceHttpMessageConverterTests() throws IOException { Resource external = new ClassPathResource("external.txt", getClass()); - - bodyExternal = "\n" + " ]>&ext;"; } @Test - public void canRead() { + void canRead() { assertThat(converter.canRead(Source.class, new MediaType("application", "xml"))).isTrue(); assertThat(converter.canRead(Source.class, new MediaType("application", "soap+xml"))).isTrue(); } @Test - public void canWrite() { + void canWrite() { assertThat(converter.canWrite(Source.class, new MediaType("application", "xml"))).isTrue(); assertThat(converter.canWrite(Source.class, new MediaType("application", "soap+xml"))).isTrue(); assertThat(converter.canWrite(Source.class, MediaType.ALL)).isTrue(); } @Test - public void readDOMSource() throws Exception { - InputStream inputStream = spy(new ByteArrayInputStream(BODY.getBytes(StandardCharsets.UTF_8))); - MockHttpInputMessage inputMessage = new MockHttpInputMessage(inputStream); + void readDOMSource() throws Exception { + MockHttpInputMessage inputMessage = new MockHttpInputMessage(BODY.getBytes(StandardCharsets.UTF_8)); inputMessage.getHeaders().setContentType(MediaType.APPLICATION_XML); DOMSource result = (DOMSource) converter.read(DOMSource.class, inputMessage); Document document = (Document) result.getNode(); assertThat(document.getDocumentElement().getLocalName()).as("Invalid result").isEqualTo("root"); - verify(inputStream, never()).close(); } @Test - public void readDOMSourceExternal() throws Exception { + void readDOMSourceExternal() throws Exception { MockHttpInputMessage inputMessage = new MockHttpInputMessage(bodyExternal.getBytes(StandardCharsets.UTF_8)); inputMessage.getHeaders().setContentType(MediaType.APPLICATION_XML); converter.setSupportDtd(true); @@ -116,7 +106,7 @@ public void readDOMSourceExternal() throws Exception { } @Test - public void readDomSourceWithXmlBomb() throws Exception { + void readDomSourceWithXmlBomb() throws Exception { // https://en.wikipedia.org/wiki/Billion_laughs // https://msdn.microsoft.com/en-us/magazine/ee335713.aspx String content = "\n" + @@ -142,7 +132,7 @@ public void readDomSourceWithXmlBomb() throws Exception { } @Test - public void readSAXSource() throws Exception { + void readSAXSource() throws Exception { MockHttpInputMessage inputMessage = new MockHttpInputMessage(BODY.getBytes(StandardCharsets.UTF_8)); inputMessage.getHeaders().setContentType(MediaType.APPLICATION_XML); SAXSource result = (SAXSource) converter.read(SAXSource.class, inputMessage); @@ -152,7 +142,7 @@ public void readSAXSource() throws Exception { } @Test - public void readSAXSourceExternal() throws Exception { + void readSAXSourceExternal() throws Exception { MockHttpInputMessage inputMessage = new MockHttpInputMessage(bodyExternal.getBytes(StandardCharsets.UTF_8)); inputMessage.getHeaders().setContentType(MediaType.APPLICATION_XML); converter.setSupportDtd(true); @@ -170,7 +160,7 @@ public void characters(char[] ch, int start, int length) { } @Test - public void readSAXSourceWithXmlBomb() throws Exception { + void readSAXSourceWithXmlBomb() throws Exception { // https://en.wikipedia.org/wiki/Billion_laughs // https://msdn.microsoft.com/en-us/magazine/ee335713.aspx String content = "\n" + @@ -199,7 +189,7 @@ public void readSAXSourceWithXmlBomb() throws Exception { } @Test - public void readStAXSource() throws Exception { + void readStAXSource() throws Exception { MockHttpInputMessage inputMessage = new MockHttpInputMessage(BODY.getBytes(StandardCharsets.UTF_8)); inputMessage.getHeaders().setContentType(MediaType.APPLICATION_XML); StAXSource result = (StAXSource) converter.read(StAXSource.class, inputMessage); @@ -214,7 +204,7 @@ public void readStAXSource() throws Exception { } @Test - public void readStAXSourceExternal() throws Exception { + void readStAXSourceExternal() throws Exception { MockHttpInputMessage inputMessage = new MockHttpInputMessage(bodyExternal.getBytes(StandardCharsets.UTF_8)); inputMessage.getHeaders().setContentType(MediaType.APPLICATION_XML); converter.setSupportDtd(true); @@ -236,7 +226,7 @@ public void readStAXSourceExternal() throws Exception { } @Test - public void readStAXSourceWithXmlBomb() throws Exception { + void readStAXSourceWithXmlBomb() throws Exception { // https://en.wikipedia.org/wiki/Billion_laughs // https://msdn.microsoft.com/en-us/magazine/ee335713.aspx String content = "\n" + @@ -268,7 +258,7 @@ public void readStAXSourceWithXmlBomb() throws Exception { } @Test - public void readStreamSource() throws Exception { + void readStreamSource() throws Exception { MockHttpInputMessage inputMessage = new MockHttpInputMessage(BODY.getBytes(StandardCharsets.UTF_8)); inputMessage.getHeaders().setContentType(MediaType.APPLICATION_XML); StreamSource result = (StreamSource) converter.read(StreamSource.class, inputMessage); @@ -277,14 +267,14 @@ public void readStreamSource() throws Exception { } @Test - public void readSource() throws Exception { + void readSource() throws Exception { MockHttpInputMessage inputMessage = new MockHttpInputMessage(BODY.getBytes(StandardCharsets.UTF_8)); inputMessage.getHeaders().setContentType(MediaType.APPLICATION_XML); converter.read(Source.class, inputMessage); } @Test - public void writeDOMSource() throws Exception { + void writeDOMSource() throws Exception { DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); documentBuilderFactory.setNamespaceAware(true); Document document = documentBuilderFactory.newDocumentBuilder().newDocument(); @@ -301,11 +291,10 @@ public void writeDOMSource() throws Exception { .as("Invalid content-type").isEqualTo(MediaType.APPLICATION_XML); assertThat(outputMessage.getHeaders().getContentLength()) .as("Invalid content-length").isEqualTo(outputMessage.getBodyAsBytes().length); - verify(outputMessage.getBody(), never()).close(); } @Test - public void writeSAXSource() throws Exception { + void writeSAXSource() throws Exception { String xml = "Hello World"; SAXSource saxSource = new SAXSource(new InputSource(new StringReader(xml))); @@ -318,7 +307,7 @@ public void writeSAXSource() throws Exception { } @Test - public void writeStreamSource() throws Exception { + void writeStreamSource() throws Exception { String xml = "Hello World"; StreamSource streamSource = new StreamSource(new StringReader(xml)); diff --git a/spring-web/src/test/java/org/springframework/web/client/RestTemplateTests.java b/spring-web/src/test/java/org/springframework/web/client/RestTemplateTests.java index 3df1ce1f28a7..bcc5c34a94c3 100644 --- a/spring-web/src/test/java/org/springframework/web/client/RestTemplateTests.java +++ b/spring-web/src/test/java/org/springframework/web/client/RestTemplateTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 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-web/src/test/java/org/springframework/web/context/request/ServletRequestAttributesTests.java b/spring-web/src/test/java/org/springframework/web/context/request/ServletRequestAttributesTests.java index b24979a9548e..d37c21668f6b 100644 --- a/spring-web/src/test/java/org/springframework/web/context/request/ServletRequestAttributesTests.java +++ b/spring-web/src/test/java/org/springframework/web/context/request/ServletRequestAttributesTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2022 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. @@ -153,7 +153,7 @@ public void skipImmutableString() { @Test public void skipImmutableCharacter() { - doSkipImmutableValue(new Character('x')); + doSkipImmutableValue('x'); } @Test diff --git a/spring-web/src/test/java/org/springframework/web/context/request/async/AsyncRequestNotUsableTests.java b/spring-web/src/test/java/org/springframework/web/context/request/async/AsyncRequestNotUsableTests.java new file mode 100644 index 000000000000..f96da6ca80d2 --- /dev/null +++ b/spring-web/src/test/java/org/springframework/web/context/request/async/AsyncRequestNotUsableTests.java @@ -0,0 +1,358 @@ +/* + * Copyright 2002-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.web.context.request.async; + +import java.io.IOException; +import java.io.PrintWriter; +import java.util.concurrent.atomic.AtomicInteger; + +import javax.servlet.AsyncEvent; +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServletResponse; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.stubbing.Answer; + +import org.springframework.web.testfixture.servlet.MockAsyncContext; +import org.springframework.web.testfixture.servlet.MockHttpServletRequest; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.BDDMockito.doAnswer; +import static org.mockito.BDDMockito.doThrow; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.mock; +import static org.mockito.BDDMockito.verify; +import static org.mockito.BDDMockito.verifyNoInteractions; + +/** + * {@link StandardServletAsyncWebRequest} tests related to response wrapping in + * order to enforce thread safety and prevent use after errors. + * + * @author Rossen Stoyanchev + */ +public class AsyncRequestNotUsableTests { + + private final MockHttpServletRequest request = new MockHttpServletRequest(); + + private final HttpServletResponse response = mock(HttpServletResponse.class); + + private final ServletOutputStream outputStream = mock(ServletOutputStream.class); + + private final PrintWriter writer = mock(PrintWriter.class); + + private StandardServletAsyncWebRequest asyncRequest; + + + @BeforeEach + void setup() throws IOException { + this.request.setAsyncSupported(true); + given(this.response.getOutputStream()).willReturn(this.outputStream); + given(this.response.getWriter()).willReturn(this.writer); + + this.asyncRequest = new StandardServletAsyncWebRequest(this.request, this.response); + } + + @AfterEach + void tearDown() { + assertThat(this.asyncRequest.stateLock().isLocked()).isFalse(); + } + + + @SuppressWarnings("DataFlowIssue") + private ServletOutputStream getWrappedOutputStream() throws IOException { + return this.asyncRequest.getResponse().getOutputStream(); + } + + @SuppressWarnings("DataFlowIssue") + private PrintWriter getWrappedWriter() throws IOException { + return this.asyncRequest.getResponse().getWriter(); + } + + + @Nested + class ResponseTests { + + @Test + void notUsableAfterError() throws IOException { + asyncRequest.startAsync(); + asyncRequest.onError(new AsyncEvent(new MockAsyncContext(request, response), new Exception())); + + HttpServletResponse wrapped = asyncRequest.getResponse(); + assertThat(wrapped).isNotNull(); + assertThatThrownBy(wrapped::getOutputStream).hasMessage("Response not usable after response errors."); + assertThatThrownBy(wrapped::getWriter).hasMessage("Response not usable after response errors."); + assertThatThrownBy(wrapped::flushBuffer).hasMessage("Response not usable after response errors."); + } + + @Test + void notUsableAfterCompletion() throws IOException { + asyncRequest.startAsync(); + asyncRequest.onComplete(new AsyncEvent(new MockAsyncContext(request, response))); + + HttpServletResponse wrapped = asyncRequest.getResponse(); + assertThat(wrapped).isNotNull(); + assertThatThrownBy(wrapped::getOutputStream).hasMessage("Response not usable after async request completion."); + assertThatThrownBy(wrapped::getWriter).hasMessage("Response not usable after async request completion."); + assertThatThrownBy(wrapped::flushBuffer).hasMessage("Response not usable after async request completion."); + } + + @Test + void notUsableWhenRecreatedAfterCompletion() throws IOException { + asyncRequest.startAsync(); + asyncRequest.onComplete(new AsyncEvent(new MockAsyncContext(request, response))); + + StandardServletAsyncWebRequest newWebRequest = + new StandardServletAsyncWebRequest(request, response, asyncRequest); + + HttpServletResponse wrapped = newWebRequest.getResponse(); + assertThat(wrapped).isNotNull(); + assertThatThrownBy(wrapped::getOutputStream).hasMessage("Response not usable after async request completion."); + assertThatThrownBy(wrapped::getWriter).hasMessage("Response not usable after async request completion."); + assertThatThrownBy(wrapped::flushBuffer).hasMessage("Response not usable after async request completion."); + } + } + + + @Nested + class OutputStreamTests { + + @Test + void use() throws IOException { + testUseOutputStream(); + } + + @Test + void useInAsyncState() throws IOException { + asyncRequest.startAsync(); + testUseOutputStream(); + } + + private void testUseOutputStream() throws IOException { + ServletOutputStream wrapped = getWrappedOutputStream(); + + wrapped.write('a'); + wrapped.write(new byte[0], 1, 2); + wrapped.flush(); + wrapped.close(); + + verify(outputStream).write('a'); + verify(outputStream).write(new byte[0], 1, 2); + verify(outputStream).flush(); + verify(outputStream).close(); + } + + @Test + void notUsableAfterCompletion() throws IOException { + asyncRequest.startAsync(); + ServletOutputStream wrapped = getWrappedOutputStream(); + + asyncRequest.onComplete(new AsyncEvent(new MockAsyncContext(request, response))); + + assertThatThrownBy(() -> wrapped.write('a')).hasMessage("Response not usable after async request completion."); + assertThatThrownBy(() -> wrapped.write(new byte[0])).hasMessage("Response not usable after async request completion."); + assertThatThrownBy(() -> wrapped.write(new byte[0], 0, 0)).hasMessage("Response not usable after async request completion."); + assertThatThrownBy(wrapped::flush).hasMessage("Response not usable after async request completion."); + assertThatThrownBy(wrapped::close).hasMessage("Response not usable after async request completion."); + } + + @Test + void lockingNotUsed() throws IOException { + AtomicInteger count = new AtomicInteger(-1); + doAnswer((Answer) invocation -> { + count.set(asyncRequest.stateLock().getHoldCount()); + return null; + }).when(outputStream).write('a'); + + // Access ServletOutputStream in NEW state (no async handling) without locking + getWrappedOutputStream().write('a'); + + assertThat(count.get()).isEqualTo(0); + } + + @Test + void lockingUsedInAsyncState() throws IOException { + AtomicInteger count = new AtomicInteger(-1); + doAnswer((Answer) invocation -> { + count.set(asyncRequest.stateLock().getHoldCount()); + return null; + }).when(outputStream).write('a'); + + // Access ServletOutputStream in ASYNC state with locking + asyncRequest.startAsync(); + getWrappedOutputStream().write('a'); + + assertThat(count.get()).isEqualTo(1); + } + } + + + @Nested + class OutputStreamErrorTests { + + @Test + void writeInt() throws IOException { + asyncRequest.startAsync(); + ServletOutputStream wrapped = getWrappedOutputStream(); + + doThrow(new IOException("Broken pipe")).when(outputStream).write('a'); + assertThatThrownBy(() -> wrapped.write('a')).hasMessage("ServletOutputStream failed to write: Broken pipe"); + } + + @Test + void writeBytesFull() throws IOException { + asyncRequest.startAsync(); + ServletOutputStream wrapped = getWrappedOutputStream(); + + byte[] bytes = new byte[0]; + doThrow(new IOException("Broken pipe")).when(outputStream).write(bytes, 0, 0); + assertThatThrownBy(() -> wrapped.write(bytes)).hasMessage("ServletOutputStream failed to write: Broken pipe"); + } + + @Test + void writeBytes() throws IOException { + asyncRequest.startAsync(); + ServletOutputStream wrapped = getWrappedOutputStream(); + + byte[] bytes = new byte[0]; + doThrow(new IOException("Broken pipe")).when(outputStream).write(bytes, 0, 0); + assertThatThrownBy(() -> wrapped.write(bytes, 0, 0)).hasMessage("ServletOutputStream failed to write: Broken pipe"); + } + + @Test + void flush() throws IOException { + asyncRequest.startAsync(); + ServletOutputStream wrapped = getWrappedOutputStream(); + + doThrow(new IOException("Broken pipe")).when(outputStream).flush(); + assertThatThrownBy(wrapped::flush).hasMessage("ServletOutputStream failed to flush: Broken pipe"); + } + + @Test + void close() throws IOException { + asyncRequest.startAsync(); + ServletOutputStream wrapped = getWrappedOutputStream(); + + doThrow(new IOException("Broken pipe")).when(outputStream).close(); + assertThatThrownBy(wrapped::close).hasMessage("ServletOutputStream failed to close: Broken pipe"); + } + + @Test + void writeErrorPreventsFurtherWriting() throws IOException { + ServletOutputStream wrapped = getWrappedOutputStream(); + + doThrow(new IOException("Broken pipe")).when(outputStream).write('a'); + assertThatThrownBy(() -> wrapped.write('a')).hasMessage("ServletOutputStream failed to write: Broken pipe"); + assertThatThrownBy(() -> wrapped.write('a')).hasMessage("Response not usable after response errors."); + } + + @Test + void writeErrorInAsyncStatePreventsFurtherWriting() throws IOException { + asyncRequest.startAsync(); + ServletOutputStream wrapped = getWrappedOutputStream(); + + doThrow(new IOException("Broken pipe")).when(outputStream).write('a'); + assertThatThrownBy(() -> wrapped.write('a')).hasMessage("ServletOutputStream failed to write: Broken pipe"); + assertThatThrownBy(() -> wrapped.write('a')).hasMessage("Response not usable after response errors."); + } + } + + + @Nested + class WriterTests { + + @Test + void useWriter() throws IOException { + testUseWriter(); + } + + @Test + void useWriterInAsyncState() throws IOException { + asyncRequest.startAsync(); + testUseWriter(); + } + + private void testUseWriter() throws IOException { + PrintWriter wrapped = getWrappedWriter(); + + wrapped.write('a'); + wrapped.write(new char[0], 1, 2); + wrapped.write("abc", 1, 2); + wrapped.flush(); + wrapped.close(); + + verify(writer).write('a'); + verify(writer).write(new char[0], 1, 2); + verify(writer).write("abc", 1, 2); + verify(writer).flush(); + verify(writer).close(); + } + + @Test + void writerNotUsableAfterCompletion() throws IOException { + asyncRequest.startAsync(); + PrintWriter wrapped = getWrappedWriter(); + + asyncRequest.onComplete(new AsyncEvent(new MockAsyncContext(request, response))); + + char[] chars = new char[0]; + wrapped.write('a'); + wrapped.write(chars, 1, 2); + wrapped.flush(); + wrapped.close(); + + verifyNoInteractions(writer); + } + + @Test + void lockingNotUsed() throws IOException { + AtomicInteger count = new AtomicInteger(-1); + + doAnswer((Answer) invocation -> { + count.set(asyncRequest.stateLock().getHoldCount()); + return null; + }).when(writer).write('a'); + + // Use Writer in NEW state (no async handling) without locking + PrintWriter wrapped = getWrappedWriter(); + wrapped.write('a'); + + assertThat(count.get()).isEqualTo(0); + } + + @Test + void lockingUsedInAsyncState() throws IOException { + AtomicInteger count = new AtomicInteger(-1); + + doAnswer((Answer) invocation -> { + count.set(asyncRequest.stateLock().getHoldCount()); + return null; + }).when(writer).write('a'); + + // Use Writer in ASYNC state with locking + asyncRequest.startAsync(); + PrintWriter wrapped = getWrappedWriter(); + wrapped.write('a'); + + assertThat(count.get()).isEqualTo(1); + } + } + +} diff --git a/spring-web/src/test/java/org/springframework/web/context/request/async/StandardServletAsyncWebRequestTests.java b/spring-web/src/test/java/org/springframework/web/context/request/async/StandardServletAsyncWebRequestTests.java index 6a71cf98582f..9bc204d157e8 100644 --- a/spring-web/src/test/java/org/springframework/web/context/request/async/StandardServletAsyncWebRequestTests.java +++ b/spring-web/src/test/java/org/springframework/web/context/request/async/StandardServletAsyncWebRequestTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -96,9 +96,8 @@ public void startAsyncNotSupported() throws Exception { @Test public void startAsyncAfterCompleted() throws Exception { this.asyncRequest.onComplete(new AsyncEvent(new MockAsyncContext(this.request, this.response))); - assertThatIllegalStateException().isThrownBy( - this.asyncRequest::startAsync) - .withMessage("Async processing has already completed"); + assertThatIllegalStateException().isThrownBy(this.asyncRequest::startAsync) + .withMessage("Cannot start async: [COMPLETED]"); } @Test diff --git a/spring-web/src/test/java/org/springframework/web/cors/CorsConfigurationTests.java b/spring-web/src/test/java/org/springframework/web/cors/CorsConfigurationTests.java index 9549902149ec..545008ef7456 100644 --- a/spring-web/src/test/java/org/springframework/web/cors/CorsConfigurationTests.java +++ b/spring-web/src/test/java/org/springframework/web/cors/CorsConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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,6 +50,8 @@ void setNullValues() { assertThat(config.getExposedHeaders()).isNull(); config.setAllowCredentials(null); assertThat(config.getAllowCredentials()).isNull(); + config.setAllowPrivateNetwork(null); + assertThat(config.getAllowPrivateNetwork()).isNull(); config.setMaxAge((Long) null); assertThat(config.getMaxAge()).isNull(); } @@ -63,6 +65,7 @@ void setValues() { config.addAllowedMethod("*"); config.addExposedHeader("*"); config.setAllowCredentials(true); + config.setAllowPrivateNetwork(true); config.setMaxAge(123L); assertThat(config.getAllowedOrigins()).containsExactly("*"); @@ -71,6 +74,7 @@ void setValues() { assertThat(config.getAllowedMethods()).containsExactly("*"); assertThat(config.getExposedHeaders()).containsExactly("*"); assertThat(config.getAllowCredentials()).isTrue(); + assertThat(config.getAllowPrivateNetwork()).isTrue(); assertThat(config.getMaxAge()).isEqualTo(Long.valueOf(123)); } @@ -93,6 +97,7 @@ void combineWithNullProperties() { config.addAllowedMethod(HttpMethod.GET.name()); config.setMaxAge(123L); config.setAllowCredentials(true); + config.setAllowPrivateNetwork(true); CorsConfiguration other = new CorsConfiguration(); config = config.combine(other); @@ -105,6 +110,7 @@ void combineWithNullProperties() { assertThat(config.getAllowedMethods()).containsExactly(HttpMethod.GET.name()); assertThat(config.getMaxAge()).isEqualTo(Long.valueOf(123)); assertThat(config.getAllowCredentials()).isTrue(); + assertThat(config.getAllowPrivateNetwork()).isTrue(); } @Test // SPR-15772 @@ -258,6 +264,7 @@ void combine() { config.addAllowedMethod(HttpMethod.GET.name()); config.setMaxAge(123L); config.setAllowCredentials(true); + config.setAllowPrivateNetwork(true); CorsConfiguration other = new CorsConfiguration(); other.addAllowedOrigin("https://domain2.com"); @@ -267,6 +274,7 @@ void combine() { other.addAllowedMethod(HttpMethod.PUT.name()); other.setMaxAge(456L); other.setAllowCredentials(false); + other.setAllowPrivateNetwork(false); config = config.combine(other); assertThat(config).isNotNull(); @@ -277,6 +285,7 @@ void combine() { assertThat(config.getMaxAge()).isEqualTo(Long.valueOf(456)); assertThat(config).isNotNull(); assertThat(config.getAllowCredentials()).isFalse(); + assertThat(config.getAllowPrivateNetwork()).isFalse(); assertThat(config.getAllowedOriginPatterns()).containsExactly("http://*.domain1.com", "http://*.domain2.com"); } diff --git a/spring-web/src/test/java/org/springframework/web/cors/DefaultCorsProcessorTests.java b/spring-web/src/test/java/org/springframework/web/cors/DefaultCorsProcessorTests.java index c57aeffeadab..c0e780089ffb 100644 --- a/spring-web/src/test/java/org/springframework/web/cors/DefaultCorsProcessorTests.java +++ b/spring-web/src/test/java/org/springframework/web/cors/DefaultCorsProcessorTests.java @@ -352,6 +352,32 @@ public void preflightRequestCredentialsWithWildcardOrigin() throws Exception { assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); } + @Test + public void preflightRequestPrivateNetworkWithWildcardOrigin() throws Exception { + this.request.setMethod(HttpMethod.OPTIONS.name()); + this.request.addHeader(HttpHeaders.ORIGIN, "https://domain2.com"); + this.request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET"); + this.request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS, "Header1"); + this.request.addHeader(DefaultCorsProcessor.ACCESS_CONTROL_REQUEST_PRIVATE_NETWORK, "true"); + this.conf.setAllowedOrigins(Arrays.asList("https://domain1.com", "*", "http://domain3.example")); + this.conf.addAllowedHeader("Header1"); + this.conf.setAllowPrivateNetwork(true); + + assertThatIllegalArgumentException().isThrownBy(() -> + this.processor.processRequest(this.conf, this.request, this.response)); + + this.conf.setAllowedOrigins(null); + this.conf.addAllowedOriginPattern("*"); + + this.processor.processRequest(this.conf, this.request, this.response); + assertThat(this.response.containsHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue(); + assertThat(this.response.containsHeader(DefaultCorsProcessor.ACCESS_CONTROL_ALLOW_PRIVATE_NETWORK)).isTrue(); + assertThat(this.response.getHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)).isEqualTo("https://domain2.com"); + assertThat(this.response.getHeaders(HttpHeaders.VARY)).contains(HttpHeaders.ORIGIN, + HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS); + assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); + } + @Test public void preflightRequestAllowedHeaders() throws Exception { this.request.setMethod(HttpMethod.OPTIONS.name()); @@ -435,4 +461,49 @@ public void preventDuplicatedVaryHeaders() throws Exception { HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS); } + @Test + public void preflightRequestWithoutAccessControlRequestPrivateNetwork() throws Exception { + this.request.setMethod(HttpMethod.OPTIONS.name()); + this.request.addHeader(HttpHeaders.ORIGIN, "https://domain2.com"); + this.request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET"); + this.conf.addAllowedHeader("*"); + this.conf.addAllowedOrigin("https://domain2.com"); + + this.processor.processRequest(this.conf, this.request, this.response); + assertThat(this.response.containsHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue(); + assertThat(this.response.containsHeader(DefaultCorsProcessor.ACCESS_CONTROL_ALLOW_PRIVATE_NETWORK)).isFalse(); + assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); + } + + @Test + public void preflightRequestWithAccessControlRequestPrivateNetworkNotAllowed() throws Exception { + this.request.setMethod(HttpMethod.OPTIONS.name()); + this.request.addHeader(HttpHeaders.ORIGIN, "https://domain2.com"); + this.request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET"); + this.request.addHeader(DefaultCorsProcessor.ACCESS_CONTROL_REQUEST_PRIVATE_NETWORK, "true"); + this.conf.addAllowedHeader("*"); + this.conf.addAllowedOrigin("https://domain2.com"); + + this.processor.processRequest(this.conf, this.request, this.response); + assertThat(this.response.containsHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue(); + assertThat(this.response.containsHeader(DefaultCorsProcessor.ACCESS_CONTROL_ALLOW_PRIVATE_NETWORK)).isFalse(); + assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); + } + + @Test + public void preflightRequestWithAccessControlRequestPrivateNetworkAllowed() throws Exception { + this.request.setMethod(HttpMethod.OPTIONS.name()); + this.request.addHeader(HttpHeaders.ORIGIN, "https://domain2.com"); + this.request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET"); + this.request.addHeader(DefaultCorsProcessor.ACCESS_CONTROL_REQUEST_PRIVATE_NETWORK, "true"); + this.conf.addAllowedHeader("*"); + this.conf.addAllowedOrigin("https://domain2.com"); + this.conf.setAllowPrivateNetwork(true); + + this.processor.processRequest(this.conf, this.request, this.response); + assertThat(this.response.containsHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue(); + assertThat(this.response.containsHeader(DefaultCorsProcessor.ACCESS_CONTROL_ALLOW_PRIVATE_NETWORK)).isTrue(); + assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); + } + } diff --git a/spring-web/src/test/java/org/springframework/web/cors/reactive/DefaultCorsProcessorTests.java b/spring-web/src/test/java/org/springframework/web/cors/reactive/DefaultCorsProcessorTests.java index 36b5a4787e95..954904d05335 100644 --- a/spring-web/src/test/java/org/springframework/web/cors/reactive/DefaultCorsProcessorTests.java +++ b/spring-web/src/test/java/org/springframework/web/cors/reactive/DefaultCorsProcessorTests.java @@ -364,6 +364,33 @@ public void preflightRequestCredentialsWithWildcardOrigin() { assertThat((Object) response.getStatusCode()).isNull(); } + @Test + public void preflightRequestPrivateNetworkWithWildcardOrigin() { + ServerWebExchange exchange = MockServerWebExchange.from(preFlightRequest() + .header(ACCESS_CONTROL_REQUEST_METHOD, "GET") + .header(ACCESS_CONTROL_REQUEST_HEADERS, "Header1") + .header(DefaultCorsProcessor.ACCESS_CONTROL_REQUEST_PRIVATE_NETWORK, "true")); + + this.conf.addAllowedOrigin("https://domain1.com"); + this.conf.addAllowedOrigin("*"); + this.conf.addAllowedOrigin("http://domain3.example"); + this.conf.addAllowedHeader("Header1"); + this.conf.setAllowPrivateNetwork(true); + assertThatIllegalArgumentException().isThrownBy(() -> this.processor.process(this.conf, exchange)); + + this.conf.setAllowedOrigins(null); + this.conf.addAllowedOriginPattern("*"); + this.processor.process(this.conf, exchange); + + ServerHttpResponse response = exchange.getResponse(); + assertThat(response.getHeaders().containsKey(ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue(); + assertThat(response.getHeaders().containsKey(DefaultCorsProcessor.ACCESS_CONTROL_ALLOW_PRIVATE_NETWORK)).isTrue(); + assertThat(response.getHeaders().getFirst(ACCESS_CONTROL_ALLOW_ORIGIN)).isEqualTo("https://domain2.com"); + assertThat(response.getHeaders().get(VARY)).contains(ORIGIN, + ACCESS_CONTROL_REQUEST_METHOD, ACCESS_CONTROL_REQUEST_HEADERS); + assertThat(response.getStatusCode()).isNull(); + } + @Test public void preflightRequestAllowedHeaders() { ServerWebExchange exchange = MockServerWebExchange.from(preFlightRequest() @@ -460,6 +487,57 @@ public void preventDuplicatedVaryHeaders() { ACCESS_CONTROL_REQUEST_METHOD, ACCESS_CONTROL_REQUEST_HEADERS); } + @Test + public void preflightRequestWithoutAccessControlRequestPrivateNetwork() { + ServerWebExchange exchange = MockServerWebExchange.from(preFlightRequest() + .header(ACCESS_CONTROL_REQUEST_METHOD, "GET")); + + this.conf.addAllowedHeader("*"); + this.conf.addAllowedOrigin("https://domain2.com"); + + this.processor.process(this.conf, exchange); + + ServerHttpResponse response = exchange.getResponse(); + assertThat(response.getHeaders().containsKey(ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue(); + assertThat(response.getHeaders().containsKey(DefaultCorsProcessor.ACCESS_CONTROL_ALLOW_PRIVATE_NETWORK)).isFalse(); + assertThat(response.getStatusCode()).isNull(); + } + + @Test + public void preflightRequestWithAccessControlRequestPrivateNetworkNotAllowed() { + ServerWebExchange exchange = MockServerWebExchange.from(preFlightRequest() + .header(ACCESS_CONTROL_REQUEST_METHOD, "GET") + .header(DefaultCorsProcessor.ACCESS_CONTROL_REQUEST_PRIVATE_NETWORK, "true")); + + this.conf.addAllowedHeader("*"); + this.conf.addAllowedOrigin("https://domain2.com"); + + this.processor.process(this.conf, exchange); + + ServerHttpResponse response = exchange.getResponse(); + assertThat(response.getHeaders().containsKey(ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue(); + assertThat(response.getHeaders().containsKey(DefaultCorsProcessor.ACCESS_CONTROL_ALLOW_PRIVATE_NETWORK)).isFalse(); + assertThat(response.getStatusCode()).isNull(); + } + + @Test + public void preflightRequestWithAccessControlRequestPrivateNetworkAllowed() { + ServerWebExchange exchange = MockServerWebExchange.from(preFlightRequest() + .header(ACCESS_CONTROL_REQUEST_METHOD, "GET") + .header(DefaultCorsProcessor.ACCESS_CONTROL_REQUEST_PRIVATE_NETWORK, "true")); + + this.conf.addAllowedHeader("*"); + this.conf.addAllowedOrigin("https://domain2.com"); + this.conf.setAllowPrivateNetwork(true); + + this.processor.process(this.conf, exchange); + + ServerHttpResponse response = exchange.getResponse(); + assertThat(response.getHeaders().containsKey(ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue(); + assertThat(response.getHeaders().containsKey(DefaultCorsProcessor.ACCESS_CONTROL_ALLOW_PRIVATE_NETWORK)).isTrue(); + assertThat(response.getStatusCode()).isNull(); + } + private ServerWebExchange actualRequest() { return MockServerWebExchange.from(corsRequest(HttpMethod.GET)); diff --git a/spring-web/src/test/java/org/springframework/web/filter/ContentCachingResponseWrapperTests.java b/spring-web/src/test/java/org/springframework/web/filter/ContentCachingResponseWrapperTests.java index 464198916a07..a8b516809998 100644 --- a/spring-web/src/test/java/org/springframework/web/filter/ContentCachingResponseWrapperTests.java +++ b/spring-web/src/test/java/org/springframework/web/filter/ContentCachingResponseWrapperTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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,55 +16,255 @@ package org.springframework.web.filter; -import java.nio.charset.StandardCharsets; +import java.util.stream.Stream; import javax.servlet.http.HttpServletResponse; +import org.junit.jupiter.api.Named; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; -import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; import org.springframework.util.FileCopyUtils; import org.springframework.web.testfixture.servlet.MockHttpServletResponse; import org.springframework.web.util.ContentCachingResponseWrapper; +import static java.nio.charset.StandardCharsets.UTF_8; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Named.named; +import static org.springframework.http.HttpHeaders.CONTENT_LENGTH; +import static org.springframework.http.HttpHeaders.CONTENT_TYPE; +import static org.springframework.http.HttpHeaders.TRANSFER_ENCODING; /** * Unit tests for {@link ContentCachingResponseWrapper}. + * * @author Rossen Stoyanchev + * @author Sam Brannen */ -public class ContentCachingResponseWrapperTests { +class ContentCachingResponseWrapperTests { @Test void copyBodyToResponse() throws Exception { - byte[] responseBody = "Hello World".getBytes(StandardCharsets.UTF_8); + byte[] responseBody = "Hello World".getBytes(UTF_8); + MockHttpServletResponse response = new MockHttpServletResponse(); + + ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response); + responseWrapper.setStatus(HttpServletResponse.SC_CREATED); + FileCopyUtils.copy(responseBody, responseWrapper.getOutputStream()); + responseWrapper.copyBodyToResponse(); + + assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_CREATED); + assertThat(response.getContentLength()).isGreaterThan(0); + assertThat(response.getContentAsByteArray()).isEqualTo(responseBody); + } + + @Test + void copyBodyToResponseWithPresetHeaders() throws Exception { + String PUZZLE = "puzzle"; + String ENIGMA = "enigma"; + String NUMBER = "number"; + String MAGIC = "42"; + + byte[] responseBody = "Hello World".getBytes(UTF_8); + int responseLength = responseBody.length; + int originalContentLength = 999; + String contentType = MediaType.APPLICATION_JSON_VALUE; + + MockHttpServletResponse response = new MockHttpServletResponse(); + response.setContentType(contentType); + response.setContentLength(originalContentLength); + response.setHeader(PUZZLE, ENIGMA); + response.setIntHeader(NUMBER, 42); + + ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response); + responseWrapper.setStatus(HttpServletResponse.SC_CREATED); + + assertThat(responseWrapper.getStatus()).isEqualTo(HttpServletResponse.SC_CREATED); + assertThat(responseWrapper.getContentSize()).isZero(); + assertThat(responseWrapper.getHeaderNames()) + .containsExactlyInAnyOrder(PUZZLE, NUMBER, CONTENT_TYPE, CONTENT_LENGTH); + + assertHeader(responseWrapper, PUZZLE, ENIGMA); + assertHeader(responseWrapper, NUMBER, MAGIC); + assertHeader(responseWrapper, CONTENT_LENGTH, originalContentLength); + assertContentTypeHeader(responseWrapper, contentType); + + FileCopyUtils.copy(responseBody, responseWrapper.getOutputStream()); + assertThat(responseWrapper.getContentSize()).isEqualTo(responseLength); + + responseWrapper.copyBodyToResponse(); + + assertThat(responseWrapper.getStatus()).isEqualTo(HttpServletResponse.SC_CREATED); + assertThat(responseWrapper.getContentSize()).isZero(); + assertThat(responseWrapper.getHeaderNames()) + .containsExactlyInAnyOrder(PUZZLE, NUMBER, CONTENT_TYPE, CONTENT_LENGTH); + + assertHeader(responseWrapper, PUZZLE, ENIGMA); + assertHeader(responseWrapper, NUMBER, MAGIC); + assertHeader(responseWrapper, CONTENT_LENGTH, responseLength); + assertContentTypeHeader(responseWrapper, contentType); + + assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_CREATED); + assertThat(response.getContentLength()).isEqualTo(responseLength); + assertThat(response.getContentAsByteArray()).isEqualTo(responseBody); + assertThat(response.getHeaderNames()) + .containsExactlyInAnyOrder(PUZZLE, NUMBER, CONTENT_TYPE, CONTENT_LENGTH); + + assertHeader(response, PUZZLE, ENIGMA); + assertHeader(response, NUMBER, MAGIC); + assertHeader(response, CONTENT_LENGTH, responseLength); + assertContentTypeHeader(response, contentType); + } + + @ParameterizedTest(name = "[{index}] {0}") + @MethodSource("setContentLengthFunctions") + void copyBodyToResponseWithOverridingContentLength(SetContentLength setContentLength) throws Exception { + byte[] responseBody = "Hello World".getBytes(UTF_8); + int responseLength = responseBody.length; + int originalContentLength = 11; + int overridingContentLength = 22; + + MockHttpServletResponse response = new MockHttpServletResponse(); + response.setContentLength(originalContentLength); + + ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response); + responseWrapper.setContentLength(overridingContentLength); + + setContentLength.invoke(responseWrapper, overridingContentLength); + + assertThat(responseWrapper.getContentSize()).isZero(); + assertThat(responseWrapper.getHeaderNames()).containsExactlyInAnyOrder(CONTENT_LENGTH); + + assertHeader(response, CONTENT_LENGTH, originalContentLength); + assertHeader(responseWrapper, CONTENT_LENGTH, overridingContentLength); + + FileCopyUtils.copy(responseBody, responseWrapper.getOutputStream()); + assertThat(responseWrapper.getContentSize()).isEqualTo(responseLength); + + responseWrapper.copyBodyToResponse(); + + assertThat(responseWrapper.getContentSize()).isZero(); + assertThat(responseWrapper.getHeaderNames()).containsExactlyInAnyOrder(CONTENT_LENGTH); + + assertHeader(response, CONTENT_LENGTH, responseLength); + assertHeader(responseWrapper, CONTENT_LENGTH, responseLength); + + assertThat(response.getContentLength()).isEqualTo(responseLength); + assertThat(response.getContentAsByteArray()).isEqualTo(responseBody); + assertThat(response.getHeaderNames()).containsExactlyInAnyOrder(CONTENT_LENGTH); + } + + private static Stream> setContentLengthFunctions() { + return Stream.of( + named("setContentLength()", HttpServletResponse::setContentLength), + named("setContentLengthLong()", HttpServletResponse::setContentLengthLong), + named("setIntHeader()", (response, contentLength) -> response.setIntHeader(CONTENT_LENGTH, contentLength)), + named("addIntHeader()", (response, contentLength) -> response.addIntHeader(CONTENT_LENGTH, contentLength)), + named("setHeader()", (response, contentLength) -> response.setHeader(CONTENT_LENGTH, "" + contentLength)), + named("addHeader()", (response, contentLength) -> response.addHeader(CONTENT_LENGTH, "" + contentLength)) + ); + } + + @ParameterizedTest(name = "[{index}] {0}") + @MethodSource("setContentTypeFunctions") + void copyBodyToResponseWithOverridingContentType(SetContentType setContentType) throws Exception { + byte[] responseBody = "Hello World".getBytes(UTF_8); + int responseLength = responseBody.length; + String originalContentType = MediaType.TEXT_PLAIN_VALUE; + String overridingContentType = MediaType.APPLICATION_JSON_VALUE; + MockHttpServletResponse response = new MockHttpServletResponse(); + response.setContentType(originalContentType); ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response); - responseWrapper.setStatus(HttpServletResponse.SC_OK); + + assertContentTypeHeader(response, originalContentType); + assertContentTypeHeader(responseWrapper, originalContentType); + + setContentType.invoke(responseWrapper, overridingContentType); + + assertThat(responseWrapper.getContentSize()).isZero(); + assertThat(responseWrapper.getHeaderNames()).containsExactlyInAnyOrder(CONTENT_TYPE); + + assertContentTypeHeader(response, overridingContentType); + assertContentTypeHeader(responseWrapper, overridingContentType); + FileCopyUtils.copy(responseBody, responseWrapper.getOutputStream()); + assertThat(responseWrapper.getContentSize()).isEqualTo(responseLength); + responseWrapper.copyBodyToResponse(); - assertThat(response.getStatus()).isEqualTo(200); - assertThat(response.getContentLength() > 0).isTrue(); + assertThat(responseWrapper.getContentSize()).isZero(); + assertThat(responseWrapper.getHeaderNames()).containsExactlyInAnyOrder(CONTENT_TYPE, CONTENT_LENGTH); + + assertHeader(response, CONTENT_LENGTH, responseLength); + assertHeader(responseWrapper, CONTENT_LENGTH, responseLength); + assertContentTypeHeader(response, overridingContentType); + assertContentTypeHeader(responseWrapper, overridingContentType); + + assertThat(response.getContentLength()).isEqualTo(responseLength); assertThat(response.getContentAsByteArray()).isEqualTo(responseBody); + assertThat(response.getHeaderNames()).containsExactlyInAnyOrder(CONTENT_TYPE, CONTENT_LENGTH); + } + + private static Stream> setContentTypeFunctions() { + return Stream.of( + named("setContentType()", HttpServletResponse::setContentType), + named("setHeader()", (response, contentType) -> response.setHeader(CONTENT_TYPE, contentType)), + named("addHeader()", (response, contentType) -> response.addHeader(CONTENT_TYPE, contentType)) + ); } @Test void copyBodyToResponseWithTransferEncoding() throws Exception { - byte[] responseBody = "6\r\nHello 5\r\nWorld0\r\n\r\n".getBytes(StandardCharsets.UTF_8); + byte[] responseBody = "6\r\nHello 5\r\nWorld0\r\n\r\n".getBytes(UTF_8); MockHttpServletResponse response = new MockHttpServletResponse(); ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response); - responseWrapper.setStatus(HttpServletResponse.SC_OK); - responseWrapper.setHeader(HttpHeaders.TRANSFER_ENCODING, "chunked"); + responseWrapper.setStatus(HttpServletResponse.SC_CREATED); + responseWrapper.setHeader(TRANSFER_ENCODING, "chunked"); FileCopyUtils.copy(responseBody, responseWrapper.getOutputStream()); responseWrapper.copyBodyToResponse(); - assertThat(response.getStatus()).isEqualTo(200); - assertThat(response.getHeader(HttpHeaders.TRANSFER_ENCODING)).isEqualTo("chunked"); - assertThat(response.getHeader(HttpHeaders.CONTENT_LENGTH)).isNull(); + assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_CREATED); + assertHeader(response, TRANSFER_ENCODING, "chunked"); + assertHeader(response, CONTENT_LENGTH, null); assertThat(response.getContentAsByteArray()).isEqualTo(responseBody); } + private void assertHeader(HttpServletResponse response, String header, int value) { + assertHeader(response, header, Integer.toString(value)); + } + + private void assertHeader(HttpServletResponse response, String header, String value) { + if (value == null) { + assertThat(response.containsHeader(header)).as(header).isFalse(); + assertThat(response.getHeader(header)).as(header).isNull(); + assertThat(response.getHeaders(header)).as(header).isEmpty(); + } + else { + assertThat(response.containsHeader(header)).as(header).isTrue(); + assertThat(response.getHeader(header)).as(header).isEqualTo(value); + assertThat(response.getHeaders(header)).as(header).containsExactly(value); + } + } + + private void assertContentTypeHeader(HttpServletResponse response, String contentType) { + assertHeader(response, CONTENT_TYPE, contentType); + assertThat(response.getContentType()).as(CONTENT_TYPE).isEqualTo(contentType); + } + + + @FunctionalInterface + private interface SetContentLength { + void invoke(HttpServletResponse response, int contentLength); + } + + @FunctionalInterface + private interface SetContentType { + void invoke(HttpServletResponse response, String contentType); + } + } diff --git a/spring-web/src/test/java/org/springframework/web/filter/ShallowEtagHeaderFilterTests.java b/spring-web/src/test/java/org/springframework/web/filter/ShallowEtagHeaderFilterTests.java index 1dccc8e02abc..e1e5df056310 100644 --- a/spring-web/src/test/java/org/springframework/web/filter/ShallowEtagHeaderFilterTests.java +++ b/spring-web/src/test/java/org/springframework/web/filter/ShallowEtagHeaderFilterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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,8 +16,6 @@ package org.springframework.web.filter; -import java.nio.charset.StandardCharsets; - import javax.servlet.FilterChain; import javax.servlet.http.HttpServletResponse; @@ -28,20 +26,26 @@ import org.springframework.web.testfixture.servlet.MockHttpServletRequest; import org.springframework.web.testfixture.servlet.MockHttpServletResponse; +import static java.nio.charset.StandardCharsets.UTF_8; import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; +import static org.springframework.http.MediaType.TEXT_PLAIN_VALUE; /** + * Tests for {@link ShallowEtagHeaderFilter}. + * * @author Arjen Poutsma * @author Brian Clozel * @author Juergen Hoeller + * @author Sam Brannen */ -public class ShallowEtagHeaderFilterTests { +class ShallowEtagHeaderFilterTests { private final ShallowEtagHeaderFilter filter = new ShallowEtagHeaderFilter(); @Test - public void isEligibleForEtag() { + void isEligibleForEtag() { MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels"); MockHttpServletResponse response = new MockHttpServletResponse(); @@ -60,76 +64,81 @@ public void isEligibleForEtag() { } @Test - public void filterNoMatch() throws Exception { - final MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels"); + void filterNoMatch() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels"); MockHttpServletResponse response = new MockHttpServletResponse(); - final byte[] responseBody = "Hello World".getBytes(StandardCharsets.UTF_8); + byte[] responseBody = "Hello World".getBytes(UTF_8); FilterChain filterChain = (filterRequest, filterResponse) -> { assertThat(filterRequest).as("Invalid request passed").isEqualTo(request); ((HttpServletResponse) filterResponse).setStatus(HttpServletResponse.SC_OK); + filterResponse.setContentType(TEXT_PLAIN_VALUE); FileCopyUtils.copy(responseBody, filterResponse.getOutputStream()); }; filter.doFilter(request, response, filterChain); assertThat(response.getStatus()).as("Invalid status").isEqualTo(200); assertThat(response.getHeader("ETag")).as("Invalid ETag").isEqualTo("\"0b10a8db164e0754105b7a99be72e3fe5\""); - assertThat(response.getContentLength() > 0).as("Invalid Content-Length header").isTrue(); + assertThat(response.getContentLength()).as("Invalid Content-Length header").isGreaterThan(0); + assertThat(response.getContentType()).as("Invalid Content-Type header").isEqualTo(TEXT_PLAIN_VALUE); assertThat(response.getContentAsByteArray()).as("Invalid content").isEqualTo(responseBody); } @Test - public void filterNoMatchWeakETag() throws Exception { + void filterNoMatchWeakETag() throws Exception { this.filter.setWriteWeakETag(true); - final MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels"); + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels"); MockHttpServletResponse response = new MockHttpServletResponse(); - final byte[] responseBody = "Hello World".getBytes(StandardCharsets.UTF_8); + byte[] responseBody = "Hello World".getBytes(UTF_8); FilterChain filterChain = (filterRequest, filterResponse) -> { assertThat(filterRequest).as("Invalid request passed").isEqualTo(request); ((HttpServletResponse) filterResponse).setStatus(HttpServletResponse.SC_OK); + filterResponse.setContentType(TEXT_PLAIN_VALUE); FileCopyUtils.copy(responseBody, filterResponse.getOutputStream()); }; filter.doFilter(request, response, filterChain); assertThat(response.getStatus()).as("Invalid status").isEqualTo(200); assertThat(response.getHeader("ETag")).as("Invalid ETag").isEqualTo("W/\"0b10a8db164e0754105b7a99be72e3fe5\""); - assertThat(response.getContentLength() > 0).as("Invalid Content-Length header").isTrue(); + assertThat(response.getContentLength()).as("Invalid Content-Length header").isGreaterThan(0); + assertThat(response.getContentType()).as("Invalid Content-Type header").isEqualTo(TEXT_PLAIN_VALUE); assertThat(response.getContentAsByteArray()).as("Invalid content").isEqualTo(responseBody); } @Test - public void filterMatch() throws Exception { - final MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels"); + void filterMatch() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels"); String etag = "\"0b10a8db164e0754105b7a99be72e3fe5\""; request.addHeader("If-None-Match", etag); MockHttpServletResponse response = new MockHttpServletResponse(); FilterChain filterChain = (filterRequest, filterResponse) -> { assertThat(filterRequest).as("Invalid request passed").isEqualTo(request); - byte[] responseBody = "Hello World".getBytes(StandardCharsets.UTF_8); - FileCopyUtils.copy(responseBody, filterResponse.getOutputStream()); + byte[] responseBody = "Hello World".getBytes(UTF_8); filterResponse.setContentLength(responseBody.length); + filterResponse.setContentType(TEXT_PLAIN_VALUE); + FileCopyUtils.copy(responseBody, filterResponse.getOutputStream()); }; filter.doFilter(request, response, filterChain); assertThat(response.getStatus()).as("Invalid status").isEqualTo(304); assertThat(response.getHeader("ETag")).as("Invalid ETag").isEqualTo("\"0b10a8db164e0754105b7a99be72e3fe5\""); assertThat(response.containsHeader("Content-Length")).as("Response has Content-Length header").isFalse(); - byte[] expecteds = new byte[0]; - assertThat(response.getContentAsByteArray()).as("Invalid content").isEqualTo(expecteds); + assertThat(response.getContentType()).as("Invalid Content-Type header").isEqualTo(TEXT_PLAIN_VALUE); + assertThat(response.getContentAsByteArray()).as("Invalid content").isEmpty(); } @Test - public void filterMatchWeakEtag() throws Exception { - final MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels"); + void filterMatchWeakEtag() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels"); String etag = "\"0b10a8db164e0754105b7a99be72e3fe5\""; request.addHeader("If-None-Match", "W/" + etag); MockHttpServletResponse response = new MockHttpServletResponse(); FilterChain filterChain = (filterRequest, filterResponse) -> { assertThat(filterRequest).as("Invalid request passed").isEqualTo(request); - byte[] responseBody = "Hello World".getBytes(StandardCharsets.UTF_8); + byte[] responseBody = "Hello World".getBytes(UTF_8); FileCopyUtils.copy(responseBody, filterResponse.getOutputStream()); filterResponse.setContentLength(responseBody.length); }; @@ -138,13 +147,12 @@ public void filterMatchWeakEtag() throws Exception { assertThat(response.getStatus()).as("Invalid status").isEqualTo(304); assertThat(response.getHeader("ETag")).as("Invalid ETag").isEqualTo("\"0b10a8db164e0754105b7a99be72e3fe5\""); assertThat(response.containsHeader("Content-Length")).as("Response has Content-Length header").isFalse(); - byte[] expecteds = new byte[0]; - assertThat(response.getContentAsByteArray()).as("Invalid content").isEqualTo(expecteds); + assertThat(response.getContentAsByteArray()).as("Invalid content").isEmpty(); } @Test - public void filterWriter() throws Exception { - final MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels"); + void filterWriter() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels"); String etag = "\"0b10a8db164e0754105b7a99be72e3fe5\""; request.addHeader("If-None-Match", etag); MockHttpServletResponse response = new MockHttpServletResponse(); @@ -160,19 +168,20 @@ public void filterWriter() throws Exception { assertThat(response.getStatus()).as("Invalid status").isEqualTo(304); assertThat(response.getHeader("ETag")).as("Invalid ETag").isEqualTo("\"0b10a8db164e0754105b7a99be72e3fe5\""); assertThat(response.containsHeader("Content-Length")).as("Response has Content-Length header").isFalse(); - byte[] expecteds = new byte[0]; - assertThat(response.getContentAsByteArray()).as("Invalid content").isEqualTo(expecteds); + assertThat(response.getContentAsByteArray()).as("Invalid content").isEmpty(); } @Test // SPR-12960 - public void filterWriterWithDisabledCaching() throws Exception { - final MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels"); + void filterWriterWithDisabledCaching() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels"); MockHttpServletResponse response = new MockHttpServletResponse(); + response.setContentType(TEXT_PLAIN_VALUE); - final byte[] responseBody = "Hello World".getBytes(StandardCharsets.UTF_8); + byte[] responseBody = "Hello World".getBytes(UTF_8); FilterChain filterChain = (filterRequest, filterResponse) -> { assertThat(filterRequest).as("Invalid request passed").isEqualTo(request); ((HttpServletResponse) filterResponse).setStatus(HttpServletResponse.SC_OK); + filterResponse.setContentType(APPLICATION_JSON_VALUE); FileCopyUtils.copy(responseBody, filterResponse.getOutputStream()); }; @@ -181,15 +190,16 @@ public void filterWriterWithDisabledCaching() throws Exception { assertThat(response.getStatus()).isEqualTo(200); assertThat(response.getHeader("ETag")).isNull(); + assertThat(response.getContentType()).as("Invalid Content-Type header").isEqualTo(APPLICATION_JSON_VALUE); assertThat(response.getContentAsByteArray()).isEqualTo(responseBody); } @Test - public void filterSendError() throws Exception { - final MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels"); + void filterSendError() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels"); MockHttpServletResponse response = new MockHttpServletResponse(); - final byte[] responseBody = "Hello World".getBytes(StandardCharsets.UTF_8); + byte[] responseBody = "Hello World".getBytes(UTF_8); FilterChain filterChain = (filterRequest, filterResponse) -> { assertThat(filterRequest).as("Invalid request passed").isEqualTo(request); response.setContentLength(100); @@ -205,11 +215,11 @@ public void filterSendError() throws Exception { } @Test - public void filterSendErrorMessage() throws Exception { - final MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels"); + void filterSendErrorMessage() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels"); MockHttpServletResponse response = new MockHttpServletResponse(); - final byte[] responseBody = "Hello World".getBytes(StandardCharsets.UTF_8); + byte[] responseBody = "Hello World".getBytes(UTF_8); FilterChain filterChain = (filterRequest, filterResponse) -> { assertThat(filterRequest).as("Invalid request passed").isEqualTo(request); response.setContentLength(100); @@ -226,11 +236,11 @@ public void filterSendErrorMessage() throws Exception { } @Test - public void filterSendRedirect() throws Exception { - final MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels"); + void filterSendRedirect() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels"); MockHttpServletResponse response = new MockHttpServletResponse(); - final byte[] responseBody = "Hello World".getBytes(StandardCharsets.UTF_8); + byte[] responseBody = "Hello World".getBytes(UTF_8); FilterChain filterChain = (filterRequest, filterResponse) -> { assertThat(filterRequest).as("Invalid request passed").isEqualTo(request); response.setContentLength(100); @@ -247,11 +257,11 @@ public void filterSendRedirect() throws Exception { } @Test // SPR-13717 - public void filterFlushResponse() throws Exception { - final MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels"); + void filterFlushResponse() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels"); MockHttpServletResponse response = new MockHttpServletResponse(); - final byte[] responseBody = "Hello World".getBytes(StandardCharsets.UTF_8); + byte[] responseBody = "Hello World".getBytes(UTF_8); FilterChain filterChain = (filterRequest, filterResponse) -> { assertThat(filterRequest).as("Invalid request passed").isEqualTo(request); ((HttpServletResponse) filterResponse).setStatus(HttpServletResponse.SC_OK); diff --git a/spring-web/src/test/java/org/springframework/web/method/annotation/InitBinderDataBinderFactoryTests.java b/spring-web/src/test/java/org/springframework/web/method/annotation/InitBinderDataBinderFactoryTests.java index ce284f935e71..08fd411307c4 100644 --- a/spring-web/src/test/java/org/springframework/web/method/annotation/InitBinderDataBinderFactoryTests.java +++ b/spring-web/src/test/java/org/springframework/web/method/annotation/InitBinderDataBinderFactoryTests.java @@ -21,7 +21,7 @@ import org.junit.jupiter.api.Test; -import org.springframework.core.LocalVariableTableParameterNameDiscoverer; +import org.springframework.core.DefaultParameterNameDiscoverer; import org.springframework.core.convert.ConversionService; import org.springframework.format.support.DefaultFormattingConversionService; import org.springframework.web.bind.WebDataBinder; @@ -128,7 +128,7 @@ private WebDataBinderFactory createFactory(String methodName, Class... parame InvocableHandlerMethod handlerMethod = new InvocableHandlerMethod(handler, method); handlerMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers); handlerMethod.setDataBinderFactory(new DefaultDataBinderFactory(null)); - handlerMethod.setParameterNameDiscoverer(new LocalVariableTableParameterNameDiscoverer()); + handlerMethod.setParameterNameDiscoverer(new DefaultParameterNameDiscoverer()); return new InitBinderDataBinderFactory( Collections.singletonList(handlerMethod), this.bindingInitializer); diff --git a/spring-web/src/test/java/org/springframework/web/method/annotation/ModelFactoryTests.java b/spring-web/src/test/java/org/springframework/web/method/annotation/ModelFactoryTests.java index 1275c08b6950..f859cb880f06 100644 --- a/spring-web/src/test/java/org/springframework/web/method/annotation/ModelFactoryTests.java +++ b/spring-web/src/test/java/org/springframework/web/method/annotation/ModelFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2022 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,7 +22,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.springframework.core.LocalVariableTableParameterNameDiscoverer; +import org.springframework.core.DefaultParameterNameDiscoverer; import org.springframework.ui.Model; import org.springframework.ui.ModelMap; import org.springframework.validation.BindingResult; @@ -177,7 +177,7 @@ public void updateModelBindingResult() throws Exception { assertThat(container.getModel().get(commandName)).isEqualTo(command); String bindingResultKey = BindingResult.MODEL_KEY_PREFIX + commandName; assertThat(container.getModel().get(bindingResultKey)).isSameAs(dataBinder.getBindingResult()); - assertThat(container.getModel().size()).isEqualTo(2); + assertThat(container.getModel()).hasSize(2); } @Test @@ -240,7 +240,7 @@ public void updateModelWhenRedirecting() throws Exception { modelFactory.updateModel(this.webRequest, container); assertThat(container.getModel().get(queryParamName)).isEqualTo(queryParam); - assertThat(container.getModel().size()).isEqualTo(1); + assertThat(container.getModel()).hasSize(1); assertThat(this.attributeStore.retrieveAttribute(this.webRequest, attributeName)).isEqualTo(attribute); } @@ -252,7 +252,7 @@ private ModelFactory createModelFactory(String methodName, Class... parameter InvocableHandlerMethod modelMethod = createHandlerMethod(methodName, parameterTypes); modelMethod.setHandlerMethodArgumentResolvers(resolvers); modelMethod.setDataBinderFactory(null); - modelMethod.setParameterNameDiscoverer(new LocalVariableTableParameterNameDiscoverer()); + modelMethod.setParameterNameDiscoverer(new DefaultParameterNameDiscoverer()); return new ModelFactory(Collections.singletonList(modelMethod), null, this.attributeHandler); } diff --git a/spring-web/src/test/java/org/springframework/web/server/adapter/DefaultServerWebExchangeCheckNotModifiedTests.java b/spring-web/src/test/java/org/springframework/web/server/adapter/DefaultServerWebExchangeCheckNotModifiedTests.java index cbfd8840d1f6..4c47dd01e562 100644 --- a/spring-web/src/test/java/org/springframework/web/server/adapter/DefaultServerWebExchangeCheckNotModifiedTests.java +++ b/spring-web/src/test/java/org/springframework/web/server/adapter/DefaultServerWebExchangeCheckNotModifiedTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2022 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-web/src/test/java/org/springframework/web/server/handler/ResponseStatusExceptionHandlerTests.java b/spring-web/src/test/java/org/springframework/web/server/handler/ResponseStatusExceptionHandlerTests.java index 61288d9f6cd1..4d97e02c696e 100644 --- a/spring-web/src/test/java/org/springframework/web/server/handler/ResponseStatusExceptionHandlerTests.java +++ b/spring-web/src/test/java/org/springframework/web/server/handler/ResponseStatusExceptionHandlerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2022 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-web/src/test/java/org/springframework/web/util/UriTemplateTests.java b/spring-web/src/test/java/org/springframework/web/util/UriTemplateTests.java index 6405e5cabfea..54c0640bfc07 100644 --- a/spring-web/src/test/java/org/springframework/web/util/UriTemplateTests.java +++ b/spring-web/src/test/java/org/springframework/web/util/UriTemplateTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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,79 +27,104 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatNoException; /** * @author Arjen Poutsma * @author Juergen Hoeller * @author Rossen Stoyanchev */ -public class UriTemplateTests { +class UriTemplateTests { @Test - public void getVariableNames() throws Exception { + void emptyPathDoesNotThrowException() { + assertThatNoException().isThrownBy(() -> new UriTemplate("")); + } + + @Test + void nullPathThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> new UriTemplate(null)); + } + + @Test + void getVariableNames() { UriTemplate template = new UriTemplate("/hotels/{hotel}/bookings/{booking}"); List variableNames = template.getVariableNames(); assertThat(variableNames).as("Invalid variable names").isEqualTo(Arrays.asList("hotel", "booking")); } @Test - public void expandVarArgs() throws Exception { + void getVariableNamesFromEmpty() { + UriTemplate template = new UriTemplate(""); + List variableNames = template.getVariableNames(); + assertThat(variableNames).isEmpty(); + } + + @Test + void expandVarArgs() { UriTemplate template = new UriTemplate("/hotels/{hotel}/bookings/{booking}"); URI result = template.expand("1", "42"); - assertThat(result).as("Invalid expanded template").isEqualTo(new URI("/hotels/1/bookings/42")); + assertThat(result).as("Invalid expanded template").isEqualTo(URI.create("/hotels/1/bookings/42")); + } + + @Test + void expandVarArgsFromEmpty() { + UriTemplate template = new UriTemplate(""); + URI result = template.expand(); + assertThat(result).as("Invalid expanded template").isEqualTo(URI.create("")); } @Test // SPR-9712 - public void expandVarArgsWithArrayValue() throws Exception { + void expandVarArgsWithArrayValue() { UriTemplate template = new UriTemplate("/sum?numbers={numbers}"); URI result = template.expand(new int[] {1, 2, 3}); - assertThat(result).isEqualTo(new URI("/sum?numbers=1,2,3")); + assertThat(result).isEqualTo(URI.create("/sum?numbers=1,2,3")); } @Test - public void expandVarArgsNotEnoughVariables() throws Exception { + void expandVarArgsNotEnoughVariables() { UriTemplate template = new UriTemplate("/hotels/{hotel}/bookings/{booking}"); assertThatIllegalArgumentException().isThrownBy(() -> template.expand("1")); } @Test - public void expandMap() throws Exception { + void expandMap() { Map uriVariables = new HashMap<>(2); uriVariables.put("booking", "42"); uriVariables.put("hotel", "1"); UriTemplate template = new UriTemplate("/hotels/{hotel}/bookings/{booking}"); URI result = template.expand(uriVariables); - assertThat(result).as("Invalid expanded template").isEqualTo(new URI("/hotels/1/bookings/42")); + assertThat(result).as("Invalid expanded template").isEqualTo(URI.create("/hotels/1/bookings/42")); } @Test - public void expandMapDuplicateVariables() throws Exception { + void expandMapDuplicateVariables() { UriTemplate template = new UriTemplate("/order/{c}/{c}/{c}"); assertThat(template.getVariableNames()).isEqualTo(Arrays.asList("c", "c", "c")); URI result = template.expand(Collections.singletonMap("c", "cheeseburger")); - assertThat(result).isEqualTo(new URI("/order/cheeseburger/cheeseburger/cheeseburger")); + assertThat(result).isEqualTo(URI.create("/order/cheeseburger/cheeseburger/cheeseburger")); } @Test - public void expandMapNonString() throws Exception { + void expandMapNonString() { Map uriVariables = new HashMap<>(2); uriVariables.put("booking", 42); uriVariables.put("hotel", 1); UriTemplate template = new UriTemplate("/hotels/{hotel}/bookings/{booking}"); URI result = template.expand(uriVariables); - assertThat(result).as("Invalid expanded template").isEqualTo(new URI("/hotels/1/bookings/42")); + assertThat(result).as("Invalid expanded template").isEqualTo(URI.create("/hotels/1/bookings/42")); } @Test - public void expandMapEncoded() throws Exception { + void expandMapEncoded() { Map uriVariables = Collections.singletonMap("hotel", "Z\u00fcrich"); UriTemplate template = new UriTemplate("/hotel list/{hotel}"); URI result = template.expand(uriVariables); - assertThat(result).as("Invalid expanded template").isEqualTo(new URI("/hotel%20list/Z%C3%BCrich")); + assertThat(result).as("Invalid expanded template").isEqualTo(URI.create("/hotel%20list/Z%C3%BCrich")); } @Test - public void expandMapUnboundVariables() throws Exception { + void expandMapUnboundVariables() { Map uriVariables = new HashMap<>(2); uriVariables.put("booking", "42"); uriVariables.put("bar", "1"); @@ -109,14 +134,14 @@ public void expandMapUnboundVariables() throws Exception { } @Test - public void expandEncoded() throws Exception { + void expandEncoded() { UriTemplate template = new UriTemplate("/hotel list/{hotel}"); URI result = template.expand("Z\u00fcrich"); - assertThat(result).as("Invalid expanded template").isEqualTo(new URI("/hotel%20list/Z%C3%BCrich")); + assertThat(result).as("Invalid expanded template").isEqualTo(URI.create("/hotel%20list/Z%C3%BCrich")); } @Test - public void matches() throws Exception { + void matches() { UriTemplate template = new UriTemplate("/hotels/{hotel}/bookings/{booking}"); assertThat(template.matches("/hotels/1/bookings/42")).as("UriTemplate does not match").isTrue(); assertThat(template.matches("/hotels/bookings")).as("UriTemplate matches").isFalse(); @@ -125,14 +150,23 @@ public void matches() throws Exception { } @Test - public void matchesCustomRegex() throws Exception { + void matchesAgainstEmpty() { + UriTemplate template = new UriTemplate(""); + assertThat(template.matches("/hotels/1/bookings/42")).as("UriTemplate matches").isFalse(); + assertThat(template.matches("/hotels/bookings")).as("UriTemplate matches").isFalse(); + assertThat(template.matches("")).as("UriTemplate does not match").isTrue(); + assertThat(template.matches(null)).as("UriTemplate matches").isFalse(); + } + + @Test + void matchesCustomRegex() { UriTemplate template = new UriTemplate("/hotels/{hotel:\\d+}"); assertThat(template.matches("/hotels/42")).as("UriTemplate does not match").isTrue(); assertThat(template.matches("/hotels/foo")).as("UriTemplate matches").isFalse(); } @Test - public void match() throws Exception { + void match() { Map expected = new HashMap<>(2); expected.put("booking", "42"); expected.put("hotel", "1"); @@ -143,7 +177,14 @@ public void match() throws Exception { } @Test - public void matchCustomRegex() throws Exception { + void matchAgainstEmpty() { + UriTemplate template = new UriTemplate(""); + Map result = template.match("/hotels/1/bookings/42"); + assertThat(result).as("Invalid match").isEmpty(); + } + + @Test + void matchCustomRegex() { Map expected = new HashMap<>(2); expected.put("booking", "42"); expected.put("hotel", "1"); @@ -154,14 +195,14 @@ public void matchCustomRegex() throws Exception { } @Test // SPR-13627 - public void matchCustomRegexWithNestedCurlyBraces() throws Exception { + void matchCustomRegexWithNestedCurlyBraces() { UriTemplate template = new UriTemplate("/site.{domain:co.[a-z]{2}}"); Map result = template.match("/site.co.eu"); assertThat(result).as("Invalid match").isEqualTo(Collections.singletonMap("domain", "co.eu")); } @Test - public void matchDuplicate() throws Exception { + void matchDuplicate() { UriTemplate template = new UriTemplate("/order/{c}/{c}/{c}"); Map result = template.match("/order/cheeseburger/cheeseburger/cheeseburger"); Map expected = Collections.singletonMap("c", "cheeseburger"); @@ -169,7 +210,7 @@ public void matchDuplicate() throws Exception { } @Test - public void matchMultipleInOneSegment() throws Exception { + void matchMultipleInOneSegment() { UriTemplate template = new UriTemplate("/{foo}-{bar}"); Map result = template.match("/12-34"); Map expected = new HashMap<>(2); @@ -179,19 +220,19 @@ public void matchMultipleInOneSegment() throws Exception { } @Test // SPR-16169 - public void matchWithMultipleSegmentsAtTheEnd() throws Exception { + void matchWithMultipleSegmentsAtTheEnd() { UriTemplate template = new UriTemplate("/account/{accountId}"); assertThat(template.matches("/account/15/alias/5")).isFalse(); } @Test - public void queryVariables() throws Exception { + void queryVariables() { UriTemplate template = new UriTemplate("/search?q={query}"); assertThat(template.matches("/search?q=foo")).isTrue(); } @Test - public void fragments() throws Exception { + void fragments() { UriTemplate template = new UriTemplate("/search#{fragment}"); assertThat(template.matches("/search#foo")).isTrue(); @@ -200,19 +241,19 @@ public void fragments() throws Exception { } @Test // SPR-13705 - public void matchesWithSlashAtTheEnd() throws Exception { + void matchesWithSlashAtTheEnd() { assertThat(new UriTemplate("/test/").matches("/test/")).isTrue(); } @Test - public void expandWithDollar() throws Exception { + void expandWithDollar() { UriTemplate template = new UriTemplate("/{a}"); URI uri = template.expand("$replacement"); assertThat(uri.toString()).isEqualTo("/$replacement"); } @Test - public void expandWithAtSign() throws Exception { + void expandWithAtSign() { UriTemplate template = new UriTemplate("http://localhost/query={query}"); URI uri = template.expand("foo@bar"); assertThat(uri.toString()).isEqualTo("http://localhost/query=foo@bar"); diff --git a/spring-web/src/test/java/org/springframework/web/util/pattern/PathPatternTests.java b/spring-web/src/test/java/org/springframework/web/util/pattern/PathPatternTests.java index 97aad62e6229..dd5eb08eb6b7 100644 --- a/spring-web/src/test/java/org/springframework/web/util/pattern/PathPatternTests.java +++ b/spring-web/src/test/java/org/springframework/web/util/pattern/PathPatternTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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. @@ -684,6 +684,7 @@ public void extractPathWithinPattern() throws Exception { checkExtractPathWithinPattern("/docs/commit.html", "/docs/commit.html", ""); checkExtractPathWithinPattern("/docs/*", "/docs/cvs/commit", "cvs/commit"); checkExtractPathWithinPattern("/docs/cvs/*.html", "/docs/cvs/commit.html", "commit.html"); + checkExtractPathWithinPattern("/docs/cvs/file.*.html", "/docs/cvs/file.sha.html", "file.sha.html"); checkExtractPathWithinPattern("/docs/**", "/docs/cvs/commit", "cvs/commit"); checkExtractPathWithinPattern("/doo/{*foobar}", "/doo/customer.html", "customer.html"); checkExtractPathWithinPattern("/doo/{*foobar}", "/doo/daa/customer.html", "daa/customer.html"); diff --git a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/method/ResolvableMethod.java b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/method/ResolvableMethod.java index 6ccef862b459..52a48d98b9c4 100644 --- a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/method/ResolvableMethod.java +++ b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/method/ResolvableMethod.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 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.web.testfixture.method; import java.lang.annotation.Annotation; +import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Parameter; +import java.lang.reflect.Proxy; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -28,18 +30,16 @@ import java.util.function.Predicate; import java.util.function.Supplier; -import org.aopalliance.intercept.MethodInterceptor; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.springframework.aop.framework.ProxyFactory; -import org.springframework.aop.target.EmptyTargetSource; import org.springframework.cglib.core.SpringNamingPolicy; import org.springframework.cglib.proxy.Callback; import org.springframework.cglib.proxy.Enhancer; import org.springframework.cglib.proxy.Factory; +import org.springframework.cglib.proxy.MethodInterceptor; import org.springframework.cglib.proxy.MethodProxy; -import org.springframework.core.LocalVariableTableParameterNameDiscoverer; +import org.springframework.core.DefaultParameterNameDiscoverer; import org.springframework.core.MethodIntrospector; import org.springframework.core.MethodParameter; import org.springframework.core.ParameterNameDiscoverer; @@ -122,6 +122,7 @@ * * * @author Rossen Stoyanchev + * @author Juergen Hoeller * @since 5.0 */ public class ResolvableMethod { @@ -130,7 +131,7 @@ public class ResolvableMethod { private static final SpringObjenesis objenesis = new SpringObjenesis(); - private static final ParameterNameDiscoverer nameDiscoverer = new LocalVariableTableParameterNameDiscoverer(); + private static final ParameterNameDiscoverer nameDiscoverer = new DefaultParameterNameDiscoverer(); // Matches ValueConstants.DEFAULT_NONE (spring-web and spring-messaging) private static final String DEFAULT_VALUE_NONE = "\n\t\t\n\t\t\n\uE000\uE001\uE002\n\t\t\t\t\n"; @@ -614,19 +615,14 @@ private List applyFilters() { } - private static class MethodInvocationInterceptor - implements org.springframework.cglib.proxy.MethodInterceptor, MethodInterceptor { + private static class MethodInvocationInterceptor implements MethodInterceptor, InvocationHandler { + @Nullable private Method invokedMethod; - - Method getInvokedMethod() { - return this.invokedMethod; - } - @Override @Nullable - public Object intercept(Object object, Method method, Object[] args, MethodProxy proxy) { + public Object intercept(Object object, Method method, @Nullable Object[] args, @Nullable MethodProxy proxy) { if (ReflectionUtils.isObjectMethod(method)) { return ReflectionUtils.invokeMethod(method, object, args); } @@ -638,20 +634,24 @@ public Object intercept(Object object, Method method, Object[] args, MethodProxy @Override @Nullable - public Object invoke(org.aopalliance.intercept.MethodInvocation inv) throws Throwable { - return intercept(inv.getThis(), inv.getMethod(), inv.getArguments(), null); + public Object invoke(Object proxy, Method method, @Nullable Object[] args) { + return intercept(proxy, method, args, null); + } + + @Nullable + Method getInvokedMethod() { + return this.invokedMethod; } } + @SuppressWarnings("unchecked") private static T initProxy(Class type, MethodInvocationInterceptor interceptor) { Assert.notNull(type, "'type' must not be null"); if (type.isInterface()) { - ProxyFactory factory = new ProxyFactory(EmptyTargetSource.INSTANCE); - factory.addInterface(type); - factory.addInterface(Supplier.class); - factory.addAdvice(interceptor); - return (T) factory.getProxy(); + return (T) Proxy.newProxyInstance(type.getClassLoader(), + new Class[] {type, Supplier.class}, + interceptor); } else { diff --git a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockHttpServletRequest.java b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockHttpServletRequest.java index cd262536d6fd..1df60c636efe 100644 --- a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockHttpServletRequest.java +++ b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockHttpServletRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-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. @@ -61,7 +61,6 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; -import org.springframework.lang.NonNull; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.LinkedCaseInsensitiveMap; @@ -993,7 +992,7 @@ public void setCookies(@Nullable Cookie... cookies) { } } - private static String encodeCookies(@NonNull Cookie... cookies) { + private static String encodeCookies(Cookie... cookies) { return Arrays.stream(cookies) .map(c -> c.getName() + '=' + (c.getValue() == null ? "" : c.getValue())) .collect(Collectors.joining("; ")); diff --git a/spring-webflux/spring-webflux.gradle b/spring-webflux/spring-webflux.gradle index b762e09c5cb6..5d466280097b 100644 --- a/spring-webflux/spring-webflux.gradle +++ b/spring-webflux/spring-webflux.gradle @@ -1,6 +1,7 @@ description = "Spring WebFlux" apply plugin: "kotlin" +apply plugin: "kotlinx-serialization" dependencies { api(project(":spring-beans")) @@ -45,6 +46,7 @@ dependencies { testImplementation('org.apache.httpcomponents.core5:httpcore5-reactive') testImplementation("com.squareup.okhttp3:mockwebserver") testImplementation("org.jetbrains.kotlin:kotlin-script-runtime") + testImplementation("org.jetbrains.kotlinx:kotlinx-serialization-json") testRuntimeOnly("org.jetbrains.kotlin:kotlin-scripting-jsr223") testRuntimeOnly("org.jruby:jruby") testRuntimeOnly("org.python:jython-standalone") diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/accept/RequestedContentTypeResolverBuilder.java b/spring-webflux/src/main/java/org/springframework/web/reactive/accept/RequestedContentTypeResolverBuilder.java index e82ce8a6bcf2..150adddf01c5 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/accept/RequestedContentTypeResolverBuilder.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/accept/RequestedContentTypeResolverBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2023 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. @@ -94,7 +94,7 @@ public RequestedContentTypeResolver build() { return exchange -> { for (RequestedContentTypeResolver resolver : resolvers) { List mediaTypes = resolver.resolveMediaTypes(exchange); - if (mediaTypes.equals(RequestedContentTypeResolver.MEDIA_TYPE_ALL_LIST)) { + if (isMediaTypeAll(mediaTypes)) { continue; } return mediaTypes; @@ -103,6 +103,11 @@ public RequestedContentTypeResolver build() { }; } + private boolean isMediaTypeAll(List mediaTypes) { + return mediaTypes.size() == 1 + && mediaTypes.get(0).removeQualityValue().equals(MediaType.ALL); + } + /** * Helper to create and configure {@link ParameterContentTypeResolver}. diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/config/CorsRegistration.java b/spring-webflux/src/main/java/org/springframework/web/reactive/config/CorsRegistration.java index 327c83ff8177..bb7dccf290b1 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/config/CorsRegistration.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/config/CorsRegistration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-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. @@ -76,9 +76,11 @@ public CorsRegistration allowedOriginPatterns(String... patterns) { /** * Set the HTTP methods to allow, e.g. {@code "GET"}, {@code "POST"}, etc. - *

    The special value {@code "*"} allows all methods. - *

    By default "simple" methods {@code GET}, {@code HEAD}, and {@code POST} + * The special value {@code "*"} allows all methods. By default, + * "simple" methods {@code GET}, {@code HEAD}, and {@code POST} * are allowed. + *

    Please, see {@link CorsConfiguration#setAllowedMethods(List)} for + * details. */ public CorsRegistration allowedMethods(String... methods) { this.config.setAllowedMethods(Arrays.asList(methods)); @@ -87,11 +89,10 @@ public CorsRegistration allowedMethods(String... methods) { /** * Set the list of headers that a pre-flight request can list as allowed - * for use during an actual request. - *

    The special value {@code "*"} may be used to allow all headers. - *

    A header name is not required to be listed if it is one of: - * {@code Cache-Control}, {@code Content-Language}, {@code Expires}, - * {@code Last-Modified}, or {@code Pragma} as per the CORS spec. + * for use during an actual request. The special value {@code "*"} + * may be used to allow all headers. + *

    Please, see {@link CorsConfiguration#setAllowedHeaders(List)} for + * details. *

    By default all headers are allowed. */ public CorsRegistration allowedHeaders(String... headers) { @@ -100,12 +101,11 @@ public CorsRegistration allowedHeaders(String... headers) { } /** - * Set the list of response headers other than "simple" headers, i.e. - * {@code Cache-Control}, {@code Content-Language}, {@code Content-Type}, - * {@code Expires}, {@code Last-Modified}, or {@code Pragma}, that an - * actual response might have and can be exposed. - *

    The special value {@code "*"} allows all headers to be exposed for - * non-credentialed requests. + * Set the list of response headers that an actual response might have and + * can be exposed. The special value {@code "*"} allows all headers to be + * exposed. + *

    Please, see {@link CorsConfiguration#setExposedHeaders(List)} for + * details. *

    By default this is not set. */ public CorsRegistration exposedHeaders(String... headers) { @@ -131,6 +131,17 @@ public CorsRegistration allowCredentials(boolean allowCredentials) { return this; } + /** + * Whether private network access is supported. + *

    Please, see {@link CorsConfiguration#setAllowPrivateNetwork(Boolean)} for details. + *

    By default this is not set (i.e. private network access is not supported). + * @since 5.3.32 + */ + public CorsRegistration allowPrivateNetwork(boolean allowPrivateNetwork) { + this.config.setAllowPrivateNetwork(allowPrivateNetwork); + return this; + } + /** * Configure how long in seconds the response from a pre-flight request * can be cached by clients. diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFilterFunctions.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFilterFunctions.java index 485d5d9e8da3..c8be890e40c9 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFilterFunctions.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFilterFunctions.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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. @@ -183,7 +183,7 @@ public boolean equals(@Nullable Object other) { @Override public int hashCode() { - return 31 * this.username.hashCode() + this.password.hashCode(); + return this.username.hashCode() * 31 + this.password.hashCode(); } } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFunction.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFunction.java index c5325284a64d..661310b769df 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFunction.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFunction.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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. @@ -43,12 +43,11 @@ public interface ExchangeFunction { /** * Exchange the given request for a {@link ClientResponse} promise. - * *

    Note: When calling this method from an * {@link ExchangeFilterFunction} that handles the response in some way, * extra care must be taken to always consume its content or otherwise * propagate it downstream for further handling, for example by the - * {@link WebClient}. Please, see the reference documentation for more + * {@link WebClient}. Please see the reference documentation for more * details on this. * @param request the request to exchange * @return the delayed response diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java index 724ce4c7a8ad..444f119083ae 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 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-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultRenderingResponseBuilder.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultRenderingResponseBuilder.java index 7b8439f4f138..b2337d0f8f64 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultRenderingResponseBuilder.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultRenderingResponseBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2022 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-webflux/src/main/java/org/springframework/web/reactive/function/server/RequestPredicates.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RequestPredicates.java index 005ca67b272c..ae721fef2b9c 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RequestPredicates.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RequestPredicates.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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. @@ -109,10 +109,9 @@ public static RequestPredicate methods(HttpMethod... httpMethods) { */ public static RequestPredicate path(String pattern) { Assert.notNull(pattern, "'pattern' must not be null"); - if (!pattern.isEmpty() && !pattern.startsWith("/")) { - pattern = "/" + pattern; - } - return pathPredicates(PathPatternParser.defaultInstance).apply(pattern); + PathPatternParser parser = PathPatternParser.defaultInstance; + pattern = parser.initFullPathPattern(pattern); + return pathPredicates(parser).apply(pattern); } /** diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/handler/AbstractHandlerMapping.java b/spring-webflux/src/main/java/org/springframework/web/reactive/handler/AbstractHandlerMapping.java index 2e368e86f0e8..078595d87be1 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/handler/AbstractHandlerMapping.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/handler/AbstractHandlerMapping.java @@ -196,6 +196,7 @@ public Mono getHandler(ServerWebExchange exchange) { config = (config != null ? config.combine(handlerConfig) : handlerConfig); if (config != null) { config.validateAllowCredentials(); + config.validateAllowPrivateNetwork(); } if (!this.corsProcessor.process(config, exchange) || CorsUtils.isPreFlightRequest(request)) { return NO_OP_HANDLER; diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/handler/AbstractUrlHandlerMapping.java b/spring-webflux/src/main/java/org/springframework/web/reactive/handler/AbstractUrlHandlerMapping.java index b60c49998097..b4e722787caa 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/handler/AbstractUrlHandlerMapping.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/handler/AbstractUrlHandlerMapping.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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,9 +29,9 @@ import org.springframework.http.server.PathContainer; import org.springframework.lang.Nullable; import org.springframework.util.Assert; -import org.springframework.util.StringUtils; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.util.pattern.PathPattern; +import org.springframework.web.util.pattern.PathPatternParser; /** * Abstract base class for URL-mapped @@ -211,8 +211,9 @@ protected void registerHandler(String urlPath, Object handler) throws BeansExcep Object resolvedHandler = handler; // Parse path pattern - urlPath = prependLeadingSlash(urlPath); - PathPattern pattern = getPathPatternParser().parse(urlPath); + PathPatternParser parser = getPathPatternParser(); + urlPath = parser.initFullPathPattern(urlPath); + PathPattern pattern = parser.parse(urlPath); if (this.handlerMap.containsKey(pattern)) { Object existingHandler = this.handlerMap.get(pattern); if (existingHandler != null && existingHandler != resolvedHandler) { @@ -241,14 +242,4 @@ private String getHandlerDescription(Object handler) { return (handler instanceof String ? "'" + handler + "'" : handler.toString()); } - - private static String prependLeadingSlash(String pattern) { - if (StringUtils.hasLength(pattern) && !pattern.startsWith("/")) { - return "/" + pattern; - } - else { - return pattern; - } - } - } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/resource/EncodedResourceResolver.java b/spring-webflux/src/main/java/org/springframework/web/reactive/resource/EncodedResourceResolver.java index e56af089603a..1bbdb443011d 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/resource/EncodedResourceResolver.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/resource/EncodedResourceResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 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,6 +21,7 @@ import java.io.InputStream; import java.net.URI; import java.net.URL; +import java.nio.channels.ReadableByteChannel; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -203,11 +204,6 @@ static final class EncodedResource extends AbstractResource implements HttpResou this.encoded = original.createRelative(original.getFilename() + extension); } - @Override - public InputStream getInputStream() throws IOException { - return this.encoded.getInputStream(); - } - @Override public boolean exists() { return this.encoded.exists(); @@ -243,6 +239,16 @@ public File getFile() throws IOException { return this.encoded.getFile(); } + @Override + public InputStream getInputStream() throws IOException { + return this.encoded.getInputStream(); + } + + @Override + public ReadableByteChannel readableChannel() throws IOException { + return this.encoded.readableChannel(); + } + @Override public long contentLength() throws IOException { return this.encoded.contentLength(); diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/resource/ResourceUrlProvider.java b/spring-webflux/src/main/java/org/springframework/web/reactive/resource/ResourceUrlProvider.java index b78d85320085..211b5da503e4 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/resource/ResourceUrlProvider.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/resource/ResourceUrlProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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. @@ -35,7 +35,6 @@ import org.springframework.http.server.PathContainer; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.lang.Nullable; -import org.springframework.util.StringUtils; import org.springframework.web.reactive.handler.SimpleUrlHandlerMapping; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.util.pattern.PathPattern; @@ -86,8 +85,9 @@ public Map getHandlerMap() { public void registerHandlers(Map handlerMap) { this.handlerMap.clear(); handlerMap.forEach((rawPattern, resourceWebHandler) -> { - rawPattern = prependLeadingSlash(rawPattern); - PathPattern pattern = PathPatternParser.defaultInstance.parse(rawPattern); + PathPatternParser parser = PathPatternParser.defaultInstance; + rawPattern = parser.initFullPathPattern(rawPattern); + PathPattern pattern = parser.parse(rawPattern); this.handlerMap.put(pattern, resourceWebHandler); }); } @@ -173,14 +173,4 @@ private Mono resolveResourceUrl(ServerWebExchange exchange, PathContaine }); } - - private static String prependLeadingSlash(String pattern) { - if (StringUtils.hasLength(pattern) && !pattern.startsWith("/")) { - return "/" + pattern; - } - else { - return pattern; - } - } - } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/resource/VersionResourceResolver.java b/spring-webflux/src/main/java/org/springframework/web/reactive/resource/VersionResourceResolver.java index 9d625b78e03e..05a4a2516cf4 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/resource/VersionResourceResolver.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/resource/VersionResourceResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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,6 +21,7 @@ import java.io.InputStream; import java.net.URI; import java.net.URL; +import java.nio.channels.ReadableByteChannel; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; @@ -65,7 +66,7 @@ */ public class VersionResourceResolver extends AbstractResourceResolver { - private AntPathMatcher pathMatcher = new AntPathMatcher(); + private final AntPathMatcher pathMatcher = new AntPathMatcher(); /** Map from path pattern -> VersionStrategy. */ private final Map versionStrategyMap = new LinkedHashMap<>(); @@ -283,9 +284,13 @@ public File getFile() throws IOException { } @Override - @Nullable - public String getFilename() { - return this.original.getFilename(); + public InputStream getInputStream() throws IOException { + return this.original.getInputStream(); + } + + @Override + public ReadableByteChannel readableChannel() throws IOException { + return this.original.readableChannel(); } @Override @@ -304,13 +309,14 @@ public Resource createRelative(String relativePath) throws IOException { } @Override - public String getDescription() { - return this.original.getDescription(); + @Nullable + public String getFilename() { + return this.original.getFilename(); } @Override - public InputStream getInputStream() throws IOException { - return this.original.getInputStream(); + public String getDescription() { + return this.original.getDescription(); } @Override diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/resource/WebJarsResourceResolver.java b/spring-webflux/src/main/java/org/springframework/web/reactive/resource/WebJarsResourceResolver.java index 461d72dec4a2..af58c274d91c 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/resource/WebJarsResourceResolver.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/resource/WebJarsResourceResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2022 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-webflux/src/main/java/org/springframework/web/reactive/result/condition/ProducesRequestCondition.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/condition/ProducesRequestCondition.java index 6fa70307ad73..59d2ab895fbe 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/condition/ProducesRequestCondition.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/condition/ProducesRequestCondition.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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. @@ -247,6 +247,9 @@ private List getMatchingExpressions(ServerWebExchang */ @Override public int compareTo(ProducesRequestCondition other, ServerWebExchange exchange) { + if (this.expressions.isEmpty() && other.expressions.isEmpty()) { + return 0; + } try { List acceptedMediaTypes = getAcceptedMediaTypes(exchange); for (MediaType acceptedMediaType : acceptedMediaTypes) { diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/AbstractHandlerMethodMapping.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/AbstractHandlerMethodMapping.java index 4e531ad088d0..85213611aff5 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/AbstractHandlerMethodMapping.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/AbstractHandlerMethodMapping.java @@ -534,6 +534,7 @@ public void register(T mapping, Object handler, Method method) { CorsConfiguration corsConfig = initCorsConfiguration(handler, method, mapping); if (corsConfig != null) { corsConfig.validateAllowCredentials(); + corsConfig.validateAllowPrivateNetwork(); this.corsLookup.put(handlerMethod, corsConfig); } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/RequestMappingInfo.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/RequestMappingInfo.java index 8782b9a5b831..59f398fc5909 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/RequestMappingInfo.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/RequestMappingInfo.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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. @@ -602,11 +602,9 @@ static List parse(String[] patterns, PathPatternParser parser) { return Collections.emptyList(); } List result = new ArrayList<>(patterns.length); - for (String path : patterns) { - if (StringUtils.hasText(path) && !path.startsWith("/")) { - path = "/" + path; - } - result.add(parser.parse(path)); + for (String pattern : patterns) { + pattern = parser.initFullPathPattern(pattern); + result.add(parser.parse(pattern)); } return result; } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMapping.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMapping.java index 1716b33eef59..d7b287a3472e 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMapping.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMapping.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 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. @@ -36,6 +36,7 @@ import org.springframework.http.server.PathContainer; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; import org.springframework.util.MultiValueMap; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.method.HandlerMethod; @@ -165,8 +166,11 @@ protected void handleMatch(RequestMappingInfo info, HandlerMethod handlerMethod, protected HandlerMethod handleNoMatch(Set infos, ServerWebExchange exchange) throws Exception { - PartialMatchHelper helper = new PartialMatchHelper(infos, exchange); + if (CollectionUtils.isEmpty(infos)) { + return null; + } + PartialMatchHelper helper = new PartialMatchHelper(infos, exchange); if (helper.isEmpty()) { return null; } @@ -218,15 +222,14 @@ private static class PartialMatchHelper { private final List partialMatches = new ArrayList<>(); - - public PartialMatchHelper(Set infos, ServerWebExchange exchange) { - this.partialMatches.addAll(infos.stream(). - filter(info -> info.getPatternsCondition().getMatchingCondition(exchange) != null). - map(info -> new PartialMatch(info, exchange)). - collect(Collectors.toList())); + PartialMatchHelper(Set infos, ServerWebExchange exchange) { + for (RequestMappingInfo info : infos) { + if (info.getPatternsCondition().getMatchingCondition(exchange) != null) { + this.partialMatches.add(new PartialMatch(info, exchange)); + } + } } - /** * Whether there are any partial matches. */ diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageReaderArgumentResolver.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageReaderArgumentResolver.java index 5d7129b8fa22..cbc793eb9ebb 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageReaderArgumentResolver.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageReaderArgumentResolver.java @@ -35,8 +35,10 @@ import org.springframework.core.codec.Hints; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferUtils; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; +import org.springframework.http.InvalidMediaTypeException; import org.springframework.http.MediaType; import org.springframework.http.codec.HttpMessageReader; import org.springframework.http.server.reactive.ServerHttpRequest; @@ -146,7 +148,16 @@ protected Mono readBody(MethodParameter bodyParam, @Nullable MethodParam ServerHttpRequest request = exchange.getRequest(); ServerHttpResponse response = exchange.getResponse(); - MediaType contentType = request.getHeaders().getContentType(); + MediaType contentType; + HttpHeaders headers = request.getHeaders(); + try { + contentType = headers.getContentType(); + } + catch (InvalidMediaTypeException ex) { + throw new UnsupportedMediaTypeStatusException( + "Can't parse Content-Type [" + headers.getFirst("Content-Type") + "]: " + ex.getMessage()); + } + MediaType mediaType = (contentType != null ? contentType : MediaType.APPLICATION_OCTET_STREAM); Object[] hints = extractValidationHints(bodyParam); diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageWriterResultHandler.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageWriterResultHandler.java index fd20d1eec387..d65ea838c341 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageWriterResultHandler.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageWriterResultHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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. @@ -139,8 +139,15 @@ protected Mono writeBody(@Nullable Object body, MethodParameter bodyParame } else { publisher = Mono.justOrEmpty(body); - actualElementType = body != null ? ResolvableType.forInstance(body) : bodyType; - elementType = (bodyType.toClass() == Object.class && body != null ? actualElementType : bodyType); + ResolvableType bodyInstanceType = ResolvableType.forInstance(body); + if (bodyType.toClass() == Object.class && body != null) { + actualElementType = bodyInstanceType; + elementType = bodyInstanceType; + } + else { + actualElementType = (body == null || bodyInstanceType.hasUnresolvableGenerics()) ? bodyType : bodyInstanceType; + elementType = bodyType; + } } if (elementType.resolve() == void.class || elementType.resolve() == Void.class) { diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/MatrixVariableMapMethodArgumentResolver.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/MatrixVariableMapMethodArgumentResolver.java index bd2ff501bdb6..a9d179d0aa7b 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/MatrixVariableMapMethodArgumentResolver.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/MatrixVariableMapMethodArgumentResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2023 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.web.reactive.result.method.annotation; -import java.util.Collections; import java.util.List; import java.util.Map; @@ -71,12 +70,17 @@ public Object resolveArgumentValue(MethodParameter parameter, BindingContext bin Map> matrixVariables = exchange.getAttribute(HandlerMapping.MATRIX_VARIABLES_ATTRIBUTE); + MultiValueMap map = mapMatrixVariables(parameter, matrixVariables); + return (isSingleValueMap(parameter) ? map.toSingleValueMap() : map); + } - if (CollectionUtils.isEmpty(matrixVariables)) { - return Collections.emptyMap(); - } + private MultiValueMap mapMatrixVariables(MethodParameter parameter, + @Nullable Map> matrixVariables) { MultiValueMap map = new LinkedMultiValueMap<>(); + if (CollectionUtils.isEmpty(matrixVariables)) { + return map; + } MatrixVariable annotation = parameter.getParameterAnnotation(MatrixVariable.class); Assert.state(annotation != null, "No MatrixVariable annotation"); String pathVariable = annotation.pathVar(); @@ -84,7 +88,7 @@ public Object resolveArgumentValue(MethodParameter parameter, BindingContext bin if (!pathVariable.equals(ValueConstants.DEFAULT_NONE)) { MultiValueMap mapForPathVariable = matrixVariables.get(pathVariable); if (mapForPathVariable == null) { - return Collections.emptyMap(); + return map; } map.putAll(mapForPathVariable); } @@ -97,8 +101,7 @@ public Object resolveArgumentValue(MethodParameter parameter, BindingContext bin }); } } - - return (isSingleValueMap(parameter) ? map.toSingleValueMap() : map); + return map; } private boolean isSingleValueMap(MethodParameter parameter) { diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMapping.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMapping.java index e58948a04863..a0b8badaa5b5 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMapping.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMapping.java @@ -341,6 +341,18 @@ else if (!allowCredentials.isEmpty()) { "or an empty string (\"\"): current value is [" + allowCredentials + "]"); } + String allowPrivateNetwork = resolveCorsAnnotationValue(annotation.allowPrivateNetwork()); + if ("true".equalsIgnoreCase(allowPrivateNetwork)) { + config.setAllowPrivateNetwork(true); + } + else if ("false".equalsIgnoreCase(allowPrivateNetwork)) { + config.setAllowPrivateNetwork(false); + } + else if (!allowPrivateNetwork.isEmpty()) { + throw new IllegalStateException("@CrossOrigin's allowPrivateNetwork value must be \"true\", \"false\", " + + "or an empty string (\"\"): current value is [" + allowPrivateNetwork + "]"); + } + if (annotation.maxAge() >= 0) { config.setMaxAge(annotation.maxAge()); } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/AbstractListenerWebSocketSession.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/AbstractListenerWebSocketSession.java index 9bf0246b4ec4..5d9200816602 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/AbstractListenerWebSocketSession.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/AbstractListenerWebSocketSession.java @@ -42,7 +42,7 @@ /** * Base class for {@link WebSocketSession} implementations that bridge between - * event-listener WebSocket APIs (e.g. Java WebSocket API JSR-356, Jetty, + * event-listener WebSocket APIs (e.g. Java WebSocket API (JSR-356), Jetty, * Undertow) and Reactive Streams. * *

    Also implements {@code Subscriber} so it can be used to subscribe to diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/TomcatWebSocketClient.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/TomcatWebSocketClient.java index 8310268db4a6..3304c80c945c 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/TomcatWebSocketClient.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/TomcatWebSocketClient.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2022 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,14 +27,15 @@ import org.springframework.web.reactive.socket.adapter.TomcatWebSocketSession; /** - * {@link WebSocketClient} implementation for use with the Java WebSocket API. + * {@link WebSocketClient} implementation for use with Tomcat, + * based on the Java WebSocket API. * * @author Violeta Georgieva * @since 5.0 + * @see StandardWebSocketClient */ public class TomcatWebSocketClient extends StandardWebSocketClient { - public TomcatWebSocketClient() { this(new WsWebSocketContainer()); } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/upgrade/Jetty10RequestUpgradeStrategy.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/upgrade/Jetty10RequestUpgradeStrategy.java index 4629c969802e..e2422a19c347 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/upgrade/Jetty10RequestUpgradeStrategy.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/upgrade/Jetty10RequestUpgradeStrategy.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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,24 +17,20 @@ package org.springframework.web.reactive.socket.server.upgrade; import java.lang.reflect.Method; +import java.lang.reflect.Proxy; import java.util.function.Supplier; import javax.servlet.ServletContext; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import org.aopalliance.intercept.MethodInterceptor; -import org.aopalliance.intercept.MethodInvocation; import reactor.core.publisher.Mono; -import org.springframework.aop.framework.ProxyFactory; -import org.springframework.aop.target.EmptyTargetSource; import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpRequestDecorator; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.http.server.reactive.ServerHttpResponseDecorator; -import org.springframework.lang.NonNull; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ReflectionUtils; @@ -119,40 +115,14 @@ public Mono upgrade( private static Object createJettyWebSocketCreator( Jetty10WebSocketHandlerAdapter adapter, @Nullable String protocol) { - ProxyFactory factory = new ProxyFactory(EmptyTargetSource.INSTANCE); - factory.addInterface(webSocketCreatorClass); - factory.addAdvice(new WebSocketCreatorInterceptor(adapter, protocol)); - return factory.getProxy(); - } - - - /** - * Proxy for a JettyWebSocketCreator to supply the WebSocket handler and set the sub-protocol. - */ - private static class WebSocketCreatorInterceptor implements MethodInterceptor { - - private final Jetty10WebSocketHandlerAdapter adapter; - - @Nullable - private final String protocol; - - - public WebSocketCreatorInterceptor( - Jetty10WebSocketHandlerAdapter adapter, @Nullable String protocol) { - - this.adapter = adapter; - this.protocol = protocol; - } - - @Nullable - @Override - public Object invoke(@NonNull MethodInvocation invocation) { - if (this.protocol != null) { - ReflectionUtils.invokeMethod( - setAcceptedSubProtocol, invocation.getArguments()[1], this.protocol); - } - return this.adapter; - } + return Proxy.newProxyInstance( + webSocketCreatorClass.getClassLoader(), new Class[] {webSocketCreatorClass}, + (proxy, method, args) -> { + if (protocol != null) { + ReflectionUtils.invokeMethod(setAcceptedSubProtocol, args[1], protocol); + } + return adapter; + }); } } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/accept/RequestedContentTypeResolverBuilderTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/accept/RequestedContentTypeResolverBuilderTests.java index f4b7dc36fab5..f0dccf848b6d 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/accept/RequestedContentTypeResolverBuilderTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/accept/RequestedContentTypeResolverBuilderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 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. @@ -34,8 +34,7 @@ public class RequestedContentTypeResolverBuilderTests { @Test - public void defaultSettings() throws Exception { - + void defaultSettings() { RequestedContentTypeResolver resolver = new RequestedContentTypeResolverBuilder().build(); MockServerWebExchange exchange = MockServerWebExchange.from( MockServerHttpRequest.get("/flower").accept(MediaType.IMAGE_GIF)); @@ -45,8 +44,7 @@ public void defaultSettings() throws Exception { } @Test - public void parameterResolver() throws Exception { - + void parameterResolver() { RequestedContentTypeResolverBuilder builder = new RequestedContentTypeResolverBuilder(); builder.parameterResolver().mediaType("json", MediaType.APPLICATION_JSON); RequestedContentTypeResolver resolver = builder.build(); @@ -58,8 +56,7 @@ public void parameterResolver() throws Exception { } @Test - public void parameterResolverWithCustomParamName() throws Exception { - + void parameterResolverWithCustomParamName() { RequestedContentTypeResolverBuilder builder = new RequestedContentTypeResolverBuilder(); builder.parameterResolver().mediaType("json", MediaType.APPLICATION_JSON).parameterName("s"); RequestedContentTypeResolver resolver = builder.build(); @@ -71,8 +68,7 @@ public void parameterResolverWithCustomParamName() throws Exception { } @Test // SPR-10513 - public void fixedResolver() throws Exception { - + void fixedResolver() { RequestedContentTypeResolverBuilder builder = new RequestedContentTypeResolverBuilder(); builder.fixedResolver(MediaType.APPLICATION_JSON); RequestedContentTypeResolver resolver = builder.build(); @@ -84,8 +80,7 @@ public void fixedResolver() throws Exception { } @Test // SPR-12286 - public void resolver() throws Exception { - + void resolver() { RequestedContentTypeResolverBuilder builder = new RequestedContentTypeResolverBuilder(); builder.resolver(new FixedContentTypeResolver(MediaType.APPLICATION_JSON)); RequestedContentTypeResolver resolver = builder.build(); @@ -99,4 +94,17 @@ public void resolver() throws Exception { assertThat(mediaTypes).isEqualTo(Collections.singletonList(MediaType.APPLICATION_JSON)); } + @Test + void removeQualityFactorForMediaTypeAllChecks() { + RequestedContentTypeResolverBuilder builder = new RequestedContentTypeResolverBuilder(); + builder.resolver(new HeaderContentTypeResolver()); + builder.resolver(new FixedContentTypeResolver(MediaType.APPLICATION_JSON)); + RequestedContentTypeResolver resolver = builder.build(); + + MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/") + .accept(MediaType.valueOf("*/*;q=0.8"))); + List mediaTypes = resolver.resolveMediaTypes(exchange); + assertThat(mediaTypes).isEqualTo(Collections.singletonList(MediaType.APPLICATION_JSON)); + } + } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/config/CorsRegistryTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/config/CorsRegistryTests.java index 92cbe9a4f9a8..9e4cfe31a6c3 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/config/CorsRegistryTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/config/CorsRegistryTests.java @@ -51,8 +51,8 @@ public void multipleMappings() { @Test public void customizedMapping() { this.registry.addMapping("/foo").allowedOrigins("https://domain2.com", "https://domain2.com") - .allowedMethods("DELETE").allowCredentials(false).allowedHeaders("header1", "header2") - .exposedHeaders("header3", "header4").maxAge(3600); + .allowedMethods("DELETE").allowCredentials(true).allowPrivateNetwork(true) + .allowedHeaders("header1", "header2").exposedHeaders("header3", "header4").maxAge(3600); Map configs = this.registry.getCorsConfigurations(); assertThat(configs.size()).isEqualTo(1); CorsConfiguration config = configs.get("/foo"); @@ -60,7 +60,8 @@ public void customizedMapping() { assertThat(config.getAllowedMethods()).isEqualTo(Collections.singletonList("DELETE")); assertThat(config.getAllowedHeaders()).isEqualTo(Arrays.asList("header1", "header2")); assertThat(config.getExposedHeaders()).isEqualTo(Arrays.asList("header3", "header4")); - assertThat(config.getAllowCredentials()).isFalse(); + assertThat(config.getAllowCredentials()).isTrue(); + assertThat(config.getAllowPrivateNetwork()).isTrue(); assertThat(config.getMaxAge()).isEqualTo(Long.valueOf(3600)); } @@ -90,6 +91,7 @@ void combine() { assertThat(config.getAllowedHeaders()).isEqualTo(Collections.singletonList("*")); assertThat(config.getExposedHeaders()).isEmpty(); assertThat(config.getAllowCredentials()).isNull(); + assertThat(config.getAllowPrivateNetwork()).isNull(); assertThat(config.getMaxAge()).isEqualTo(Long.valueOf(1800)); } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/config/DelegatingWebFluxConfigurationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/config/DelegatingWebFluxConfigurationTests.java index 6caa8c26f4b3..21b8fbf3f7a4 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/config/DelegatingWebFluxConfigurationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/config/DelegatingWebFluxConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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. @@ -112,7 +112,7 @@ public void requestMappingHandlerAdapter() { boolean condition = initializer.getValidator() instanceof LocalValidatorFactoryBean; assertThat(condition).isTrue(); assertThat(initializer.getConversionService()).isSameAs(formatterRegistry.getValue()); - assertThat(codecsConfigurer.getValue().getReaders().size()).isEqualTo(14); + assertThat(codecsConfigurer.getValue().getReaders()).hasSize(15); } @Test diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/config/WebFluxConfigurationSupportTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/config/WebFluxConfigurationSupportTests.java index 059b7779e740..881f86c20b69 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/config/WebFluxConfigurationSupportTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/config/WebFluxConfigurationSupportTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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. @@ -152,7 +152,7 @@ public void requestMappingHandlerAdapter() { assertThat(adapter).isNotNull(); List> readers = adapter.getMessageReaders(); - assertThat(readers.size()).isEqualTo(14); + assertThat(readers).hasSize(15); ResolvableType multiValueMapType = forClassWithGenerics(MultiValueMap.class, String.class, String.class); @@ -207,7 +207,7 @@ public void responseEntityResultHandler() { assertThat(handler.getOrder()).isEqualTo(0); List> writers = handler.getMessageWriters(); - assertThat(writers.size()).isEqualTo(13); + assertThat(writers).hasSize(14); assertHasMessageWriter(writers, forClass(byte[].class), APPLICATION_OCTET_STREAM); assertHasMessageWriter(writers, forClass(ByteBuffer.class), APPLICATION_OCTET_STREAM); @@ -235,7 +235,7 @@ public void responseBodyResultHandler() { assertThat(handler.getOrder()).isEqualTo(100); List> writers = handler.getMessageWriters(); - assertThat(writers.size()).isEqualTo(13); + assertThat(writers).hasSize(14); assertHasMessageWriter(writers, forClass(byte[].class), APPLICATION_OCTET_STREAM); assertHasMessageWriter(writers, forClass(ByteBuffer.class), APPLICATION_OCTET_STREAM); diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java index 4a3e21645241..722c45bdf0d2 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -475,7 +475,7 @@ void retrieveJsonNull(ClientHttpConnector connector) { .retrieve() .bodyToMono(Map.class); - StepVerifier.create(result).verifyComplete(); + StepVerifier.create(result).expectComplete().verify(Duration.ofSeconds(3)); } @ParameterizedWebClientTest // SPR-15946 @@ -800,7 +800,7 @@ void statusHandlerWithErrorBodyTransformation(ClientHttpConnector connector) { MyException error = (MyException) throwable; assertThat(error.getMessage()).isEqualTo("foofoo"); }) - .verify(); + .verify(Duration.ofSeconds(3)); } @ParameterizedWebClientTest @@ -842,7 +842,7 @@ void statusHandlerSuppressedErrorSignal(ClientHttpConnector connector) { StepVerifier.create(result) .expectNext("Internal Server error") - .verifyComplete(); + .expectComplete().verify(Duration.ofSeconds(3)); expectRequestCount(1); expectRequest(request -> { @@ -866,7 +866,7 @@ void statusHandlerSuppressedErrorSignalWithFlux(ClientHttpConnector connector) { StepVerifier.create(result) .expectNext("Internal Server error") - .verifyComplete(); + .expectComplete().verify(Duration.ofSeconds(3)); expectRequestCount(1); expectRequest(request -> { @@ -1024,7 +1024,7 @@ void exchangeForEmptyBodyAsVoidEntity(ClientHttpConnector connector) { StepVerifier.create(result) .assertNext(r -> assertThat(r.getStatusCode().is2xxSuccessful()).isTrue()) - .verifyComplete(); + .expectComplete().verify(Duration.ofSeconds(3)); } @ParameterizedWebClientTest @@ -1075,24 +1075,6 @@ void exchangeForUnknownStatusCode(ClientHttpConnector connector) { }); } - @ParameterizedWebClientTest // SPR-15782 - void exchangeWithRelativeUrl(ClientHttpConnector connector) { - startServer(connector); - - String uri = "/api/v4/groups/1"; - Mono> responseMono = WebClient.builder().build().get().uri(uri) - .retrieve().toBodilessEntity(); - - StepVerifier.create(responseMono) - .expectErrorSatisfies(throwable -> { - assertThat(throwable).isInstanceOf(WebClientRequestException.class); - WebClientRequestException ex = (WebClientRequestException) throwable; - assertThat(ex.getMethod()).isEqualTo(HttpMethod.GET); - assertThat(ex.getUri()).isEqualTo(URI.create(uri)); - }) - .verify(Duration.ofSeconds(5)); - } - @ParameterizedWebClientTest void filter(ClientHttpConnector connector) { startServer(connector); @@ -1227,7 +1209,7 @@ void malformedResponseChunksOnBodilessEntity(ClientHttpConnector connector) { WebClientException ex = (WebClientException) throwable; assertThat(ex.getCause()).isInstanceOf(IOException.class); }) - .verify(); + .verify(Duration.ofSeconds(3)); } @ParameterizedWebClientTest @@ -1239,7 +1221,7 @@ void malformedResponseChunksOnEntityWithBody(ClientHttpConnector connector) { WebClientException ex = (WebClientException) throwable; assertThat(ex.getCause()).isInstanceOf(IOException.class); }) - .verify(); + .verify(Duration.ofSeconds(3)); } private Mono doMalformedChunkedResponseTest( diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/DefaultRenderingResponseTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/DefaultRenderingResponseTests.java index 987a2a855881..b4df3715aa58 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/DefaultRenderingResponseTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/DefaultRenderingResponseTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2022 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-webflux/src/test/java/org/springframework/web/reactive/result/condition/ProducesRequestConditionTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/condition/ProducesRequestConditionTests.java index 3b9e7e97534e..62c682199904 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/condition/ProducesRequestConditionTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/condition/ProducesRequestConditionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 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. @@ -297,6 +297,18 @@ public void compareToEqualMatch() { assertThat(result > 0).as("Should have used MediaType.equals(Object) to break the match").isTrue(); } + @Test + public void compareEmptyInvalidAccept() { + MockServerWebExchange exchange = MockServerWebExchange.from(get("/").header("Accept", "foo")); + + ProducesRequestCondition condition1 = new ProducesRequestCondition(); + ProducesRequestCondition condition2 = new ProducesRequestCondition(); + + int result = condition1.compareTo(condition2, exchange); + assertThat(result).isEqualTo(0); + } + + @Test public void combine() { ProducesRequestCondition condition1 = new ProducesRequestCondition("text/plain"); diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMappingTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMappingTests.java index cbc923abe1b2..628c9dc407ed 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMappingTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMappingTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 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,6 +22,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.EnumSet; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -334,9 +335,18 @@ public void handlePatchUnsupportedMediaType() { assertThat(umtse.getResponseHeaders().getAcceptPatch()).containsExactly(mediaType); }) .verify(); - } + @Test // gh-29611 + public void handleNoMatchWithoutPartialMatches() throws Exception { + ServerWebExchange exchange = MockServerWebExchange.from(post("/non-existent")); + + HandlerMethod handlerMethod = this.handlerMapping.handleNoMatch(new HashSet<>(), exchange); + assertThat(handlerMethod).isNull(); + + handlerMethod = this.handlerMapping.handleNoMatch(null, exchange); + assertThat(handlerMethod).isNull(); + } @SuppressWarnings("unchecked") private void assertError(Mono mono, final Class exceptionClass, final Consumer consumer) { diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/InitBinderBindingContextTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/InitBinderBindingContextTests.java index d695a3f750c6..5233a4a52ec6 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/InitBinderBindingContextTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/InitBinderBindingContextTests.java @@ -23,7 +23,7 @@ import org.junit.jupiter.api.Test; -import org.springframework.core.LocalVariableTableParameterNameDiscoverer; +import org.springframework.core.DefaultParameterNameDiscoverer; import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.core.convert.ConversionService; import org.springframework.format.support.DefaultFormattingConversionService; @@ -131,7 +131,7 @@ private BindingContext createBindingContext(String methodName, Class... param SyncInvocableHandlerMethod handlerMethod = new SyncInvocableHandlerMethod(handler, method); handlerMethod.setArgumentResolvers(new ArrayList<>(this.argumentResolvers)); - handlerMethod.setParameterNameDiscoverer(new LocalVariableTableParameterNameDiscoverer()); + handlerMethod.setParameterNameDiscoverer(new DefaultParameterNameDiscoverer()); return new InitBinderBindingContext(this.bindingInitializer, Collections.singletonList(handlerMethod)); } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/MatrixVariablesMapMethodArgumentResolverTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/MatrixVariablesMapMethodArgumentResolverTests.java index a6dfd8554b80..5cc897ce32b5 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/MatrixVariablesMapMethodArgumentResolverTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/MatrixVariablesMapMethodArgumentResolverTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 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,6 +22,7 @@ import java.util.LinkedHashMap; import java.util.Map; +import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -139,6 +140,19 @@ public void resolveArgumentPathVariable() throws Exception { assertThat(mapAll.get("colors")).isEqualTo("red"); } + @Test + public void resolveMultiValueMapArgumentNoParams() { + + MethodParameter param = this.testMethod.annot(matrixAttribute().noPathVar()) + .arg(MultiValueMap.class, String.class, String.class); + + Object result = this.resolver.resolveArgument(param, + new BindingContext(), this.exchange).block(Duration.ZERO); + + assertThat(result).isInstanceOf(MultiValueMap.class) + .asInstanceOf(InstanceOfAssertFactories.MAP).isEmpty(); + } + @Test public void resolveArgumentNoParams() throws Exception { diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/MessageWriterResultHandlerTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/MessageWriterResultHandlerTests.java index 49d12463154c..c561f87a7945 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/MessageWriterResultHandlerTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/MessageWriterResultHandlerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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. @@ -62,6 +62,7 @@ * Unit tests for {@link AbstractMessageWriterResultHandler}. * * @author Rossen Stoyanchev + * @author Sebastien Deleuze */ public class MessageWriterResultHandlerTests { @@ -180,6 +181,17 @@ public void jacksonTypeWithSubTypeOfListElement() throws Exception { assertResponseBody("[{\"id\":123,\"name\":\"foo\"},{\"id\":456,\"name\":\"bar\"}]"); } + @Test + public void jacksonTypeWithSubTypeAndObjectReturnValue() { + MethodParameter returnType = on(TestController.class).resolveReturnType(Object.class); + + SimpleBean body = new SimpleBean(123L, "foo"); + this.resultHandler.writeBody(body, returnType, this.exchange).block(Duration.ofSeconds(5)); + + assertThat(this.exchange.getResponse().getHeaders().getContentType()).isEqualTo(APPLICATION_JSON); + assertResponseBody("{\"id\":123,\"name\":\"foo\"}"); + } + private void assertResponseBody(String responseBody) { StepVerifier.create(this.exchange.getResponse().getBody()) @@ -287,6 +299,8 @@ void voidReturn() { } Identifiable identifiable() { return null; } List listIdentifiable() { return null; } + + Object object() { return null; } } } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestBodyMethodArgumentResolverTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestBodyMethodArgumentResolverTests.java index 49f78fa70e5a..2992dac9d6a7 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestBodyMethodArgumentResolverTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestBodyMethodArgumentResolverTests.java @@ -41,12 +41,14 @@ import org.springframework.web.reactive.BindingContext; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.ServerWebInputException; +import org.springframework.web.server.UnsupportedMediaTypeStatusException; import org.springframework.web.testfixture.http.server.reactive.MockServerHttpRequest; import org.springframework.web.testfixture.method.ResolvableMethod; import org.springframework.web.testfixture.server.MockServerWebExchange; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.springframework.web.testfixture.method.MvcAnnotationPredicates.requestBody; /** @@ -214,6 +216,17 @@ public void emptyBodyWithCompletableFuture() { }); } + @Test // gh-29565 + public void invalidContentType() { + MethodParameter parameter = this.testMethod.annot(requestBody()).arg(String.class); + + ServerWebExchange exchange = MockServerWebExchange.from( + MockServerHttpRequest.post("/path").header("Content-Type", "invalid").build()); + + assertThatThrownBy(() -> this.resolver.readBody(parameter, true, new BindingContext(), exchange)) + .isInstanceOf(UnsupportedMediaTypeStatusException.class); + } + @SuppressWarnings("unchecked") private T resolveValue(MethodParameter param, String body) { ServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.post("/path").body(body)); @@ -221,7 +234,8 @@ private T resolveValue(MethodParameter param, String body) { Object value = result.block(Duration.ofSeconds(5)); assertThat(value).isNotNull(); - assertThat(param.getParameterType().isAssignableFrom(value.getClass())).as("Unexpected return value type: " + value).isTrue(); + assertThat(param.getParameterType().isAssignableFrom(value.getClass())) + .as("Unexpected return value type: " + value).isTrue(); //no inspection unchecked return (T) value; @@ -234,7 +248,8 @@ private T resolveValueWithEmptyBody(MethodParameter param) { Object value = result.block(Duration.ofSeconds(5)); if (value != null) { - assertThat(param.getParameterType().isAssignableFrom(value.getClass())).as("Unexpected parameter type: " + value).isTrue(); + assertThat(param.getParameterType().isAssignableFrom(value.getClass())) + .as("Unexpected parameter type: " + value).isTrue(); } //no inspection unchecked diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java index f3191dbf00f9..9203e3025b15 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 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. @@ -47,6 +47,7 @@ * * @author Rossen Stoyanchev * @author Stephane Maldini + * @author Sebastien Deleuze * @since 5.0 */ class RequestMappingIntegrationTests extends AbstractRequestMappingIntegrationTests { @@ -91,8 +92,8 @@ void forwardedHeaders(HttpServer httpServer) throws Exception { void stream(HttpServer httpServer) throws Exception { startServer(httpServer); - String[] expected = {"0", "1", "2", "3", "4"}; - assertThat(performGet("/stream", new HttpHeaders(), String[].class).getBody()).isEqualTo(expected); + Integer[] expected = {0, 1, 2, 3, 4}; + assertThat(performGet("/stream", new HttpHeaders(), Integer[].class).getBody()).isEqualTo(expected); } diff --git a/spring-webflux/src/test/kotlin/org/springframework/web/reactive/result/method/annotation/KotlinMessageWriterResultHandlerTests.kt b/spring-webflux/src/test/kotlin/org/springframework/web/reactive/result/method/annotation/KotlinMessageWriterResultHandlerTests.kt new file mode 100644 index 000000000000..6599d145d9cf --- /dev/null +++ b/spring-webflux/src/test/kotlin/org/springframework/web/reactive/result/method/annotation/KotlinMessageWriterResultHandlerTests.kt @@ -0,0 +1,161 @@ +/* + * Copyright 2002-2023 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.web.reactive.result.method.annotation + +import kotlinx.serialization.Serializable +import org.assertj.core.api.Assertions +import org.junit.jupiter.api.Test +import org.springframework.core.ResolvableType +import org.springframework.core.io.buffer.DataBuffer +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity +import org.springframework.http.codec.EncoderHttpMessageWriter +import org.springframework.http.codec.HttpMessageWriter +import org.springframework.http.codec.json.Jackson2JsonEncoder +import org.springframework.http.codec.json.KotlinSerializationJsonEncoder +import org.springframework.util.ObjectUtils +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RestController +import org.springframework.web.reactive.accept.RequestedContentTypeResolverBuilder +import org.springframework.web.testfixture.http.server.reactive.MockServerHttpRequest +import org.springframework.web.testfixture.method.ResolvableMethod +import org.springframework.web.testfixture.server.MockServerWebExchange +import reactor.test.StepVerifier +import java.nio.charset.StandardCharsets +import java.time.Duration +import java.util.* + +/** + * Kotlin unit tests for {@link AbstractMessageWriterResultHandler}. + * + * @author Sebastien Deleuze + */ +class KotlinMessageWriterResultHandlerTests { + + private val resultHandler = initResultHandler() + + private val exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/path")) + + + private fun initResultHandler(vararg writers: HttpMessageWriter<*>): AbstractMessageWriterResultHandler { + val writerList = if (ObjectUtils.isEmpty(writers)) { + listOf( + EncoderHttpMessageWriter(KotlinSerializationJsonEncoder()), + EncoderHttpMessageWriter(Jackson2JsonEncoder()) + ) + } else { + listOf(*writers) + } + val resolver = RequestedContentTypeResolverBuilder().build() + return object : AbstractMessageWriterResultHandler(writerList, resolver) {} + } + + @Test + fun nonSuspendWithoutResponseEntity() { + val returnType = ResolvableMethod.on(SampleController::class.java) + .resolveReturnType(List::class.java, Person::class.java) + val body = listOf(Person(UserId(1), "John")) + resultHandler.writeBody(body, returnType, exchange).block(Duration.ofSeconds(5)) + + Assertions.assertThat(exchange.response.headers.contentType).isEqualTo(MediaType.APPLICATION_JSON) + assertResponseBody("[{\"userId\":1,\"name\":\"John\"}]") + } + + @Test + fun nonSuspendWithResponseEntity() { + val returnType = ResolvableMethod.on(SampleController::class.java) + .returning(ResolvableType.forClassWithGenerics(ResponseEntity::class.java, + ResolvableType.forClassWithGenerics(List::class.java, Person::class.java))) + .build().returnType() + val body = ResponseEntity.ok(listOf(Person(UserId(1), "John"))) + resultHandler.writeBody(body.body, returnType.nested(), returnType, exchange).block(Duration.ofSeconds(5)) + + Assertions.assertThat(exchange.response.headers.contentType).isEqualTo(MediaType.APPLICATION_JSON) + assertResponseBody("[{\"userId\":1,\"name\":\"John\"}]") + } + + @Test + fun suspendWithoutResponseEntity() { + val returnType = ResolvableMethod.on(CoroutinesSampleController::class.java) + .resolveReturnType(List::class.java, Person::class.java) + val body = listOf(Person(UserId(1), "John")) + resultHandler.writeBody(body, returnType, exchange).block(Duration.ofSeconds(5)) + + Assertions.assertThat(exchange.response.headers.contentType).isEqualTo(MediaType.APPLICATION_JSON) + assertResponseBody("[{\"userId\":1,\"name\":\"John\"}]") + } + + @Test + fun suspendWithResponseEntity() { + val returnType = ResolvableMethod.on(CoroutinesSampleController::class.java) + .returning(ResolvableType.forClassWithGenerics(ResponseEntity::class.java, + ResolvableType.forClassWithGenerics(List::class.java, Person::class.java))) + .build().returnType() + val body = ResponseEntity.ok(listOf(Person(UserId(1), "John"))) + resultHandler.writeBody(body.body, returnType.nested(), returnType, exchange).block(Duration.ofSeconds(5)) + + Assertions.assertThat(exchange.response.headers.contentType).isEqualTo(MediaType.APPLICATION_JSON) + assertResponseBody("[{\"userId\":1,\"name\":\"John\"}]") + } + + private fun assertResponseBody(responseBody: String) { + StepVerifier.create(exchange.response.body) + .consumeNextWith { buf: DataBuffer -> + Assertions.assertThat( + buf.toString(StandardCharsets.UTF_8) + ).isEqualTo(responseBody) + } + .expectComplete() + .verify() + } + + @RestController + class SampleController { + + @GetMapping("/non-suspend-with-response-entity") + fun withResponseEntity(): ResponseEntity> = + TODO() + + @GetMapping("/non-suspend-without-response-entity") + fun withoutResponseEntity(): List = + TODO() + } + + @RestController + class CoroutinesSampleController { + + @GetMapping("/suspend-with-response-entity") + suspend fun suspendAndResponseEntity(): ResponseEntity> = + TODO() + + @GetMapping("/suspend-without-response-entity") + suspend fun suspendWithoutResponseEntity(): List = + TODO() + + } + + @Serializable + data class Person( + val userId: UserId, + val name: String, + ) + + @JvmInline + @Serializable + value class UserId(val id: Int) + +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java index 603f23940f59..bc30ed990c4e 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -279,6 +279,7 @@ public class DispatcherServlet extends FrameworkServlet { */ private static final String DEFAULT_STRATEGIES_PREFIX = "org.springframework.web.servlet"; + /** Additional logger to use when no mapped handler is found for a request. */ protected static final Log pageNotFoundLogger = LogFactory.getLog(PAGE_NOT_FOUND_LOG_CATEGORY); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/CorsBeanDefinitionParser.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/CorsBeanDefinitionParser.java index 20997963fe6f..62e4b553f592 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/CorsBeanDefinitionParser.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/CorsBeanDefinitionParser.java @@ -84,6 +84,7 @@ public BeanDefinition parse(Element element, ParserContext parserContext) { } config.applyPermitDefaultValues(); config.validateAllowCredentials(); + config.validateAllowPrivateNetwork(); corsConfigurations.put(mapping.getAttribute("path"), config); } } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/CorsRegistration.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/CorsRegistration.java index 523f5dcc0c5c..55a8b0fc9bb8 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/CorsRegistration.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/CorsRegistration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-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. @@ -77,9 +77,11 @@ public CorsRegistration allowedOriginPatterns(String... patterns) { /** * Set the HTTP methods to allow, e.g. {@code "GET"}, {@code "POST"}, etc. - *

    The special value {@code "*"} allows all methods. - *

    By default "simple" methods {@code GET}, {@code HEAD}, and {@code POST} + * The special value {@code "*"} allows all methods. By default, + * "simple" methods {@code GET}, {@code HEAD}, and {@code POST} * are allowed. + *

    Please, see {@link CorsConfiguration#setAllowedMethods(List)} for + * details. */ public CorsRegistration allowedMethods(String... methods) { this.config.setAllowedMethods(Arrays.asList(methods)); @@ -88,11 +90,10 @@ public CorsRegistration allowedMethods(String... methods) { /** * Set the list of headers that a pre-flight request can list as allowed - * for use during an actual request. - *

    The special value {@code "*"} may be used to allow all headers. - *

    A header name is not required to be listed if it is one of: - * {@code Cache-Control}, {@code Content-Language}, {@code Expires}, - * {@code Last-Modified}, or {@code Pragma} as per the CORS spec. + * for use during an actual request. The special value {@code "*"} + * may be used to allow all headers. + *

    Please, see {@link CorsConfiguration#setAllowedHeaders(List)} for + * details. *

    By default all headers are allowed. */ public CorsRegistration allowedHeaders(String... headers) { @@ -101,12 +102,11 @@ public CorsRegistration allowedHeaders(String... headers) { } /** - * Set the list of response headers other than "simple" headers, i.e. - * {@code Cache-Control}, {@code Content-Language}, {@code Content-Type}, - * {@code Expires}, {@code Last-Modified}, or {@code Pragma}, that an - * actual response might have and can be exposed. - *

    The special value {@code "*"} allows all headers to be exposed for - * non-credentialed requests. + * Set the list of response headers that an actual response might have and + * can be exposed. The special value {@code "*"} allows all headers to be + * exposed. + *

    Please, see {@link CorsConfiguration#setExposedHeaders(List)} for + * details. *

    By default this is not set. */ public CorsRegistration exposedHeaders(String... headers) { @@ -132,6 +132,17 @@ public CorsRegistration allowCredentials(boolean allowCredentials) { return this; } + /** + * Whether private network access is supported. + *

    By default this is not set (i.e. private network access is not supported). + * @since 5.3.32 + * @see Private network access specifications + */ + public CorsRegistration allowPrivateNetwork(boolean allowPrivateNetwork) { + this.config.setAllowPrivateNetwork(allowPrivateNetwork); + return this; + } + /** * Configure how long in seconds the response from a pre-flight request * can be cached by clients. diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java index 262e9d9a3037..18c44d615117 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 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. @@ -907,8 +907,8 @@ protected final void addDefaultHttpMessageConverters(List()); } - catch (Throwable ex) { - // Ignore when no TransformerFactory implementation is available... + catch (Error err) { + // Ignore when no TransformerFactory implementation is available } } messageConverters.add(new AllEncompassingFormHttpMessageConverter()); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/RequestPredicates.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/RequestPredicates.java index 66b1249d9bad..6846982272ed 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/RequestPredicates.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/RequestPredicates.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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. @@ -108,10 +108,9 @@ public static RequestPredicate methods(HttpMethod... httpMethods) { */ public static RequestPredicate path(String pattern) { Assert.notNull(pattern, "'pattern' must not be null"); - if (!pattern.isEmpty() && !pattern.startsWith("/")) { - pattern = "/" + pattern; - } - return pathPredicates(PathPatternParser.defaultInstance).apply(pattern); + PathPatternParser parser = PathPatternParser.defaultInstance; + pattern = parser.initFullPathPattern(pattern); + return pathPredicates(parser).apply(pattern); } /** diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/SseServerResponse.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/SseServerResponse.java index e078a66d110a..3e4d5be9dcbd 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/SseServerResponse.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/SseServerResponse.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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. @@ -48,6 +48,7 @@ * Server-Sent Events. * * @author Arjen Poutsma + * @author Sebastien Deleuze * @since 5.3.2 */ final class SseServerResponse extends AbstractServerResponse { @@ -90,7 +91,7 @@ protected ModelAndView writeToInternal(HttpServletRequest request, HttpServletRe } DefaultAsyncServerResponse.writeAsync(request, response, result); - this.sseConsumer.accept(new DefaultSseBuilder(response, context, result)); + this.sseConsumer.accept(new DefaultSseBuilder(response, context, result, this.headers())); return null; } @@ -113,15 +114,19 @@ private static final class DefaultSseBuilder implements SseBuilder { private final List> messageConverters; + private final HttpHeaders httpHeaders; + private final StringBuilder builder = new StringBuilder(); private boolean sendFailed; - public DefaultSseBuilder(HttpServletResponse response, Context context, DeferredResult deferredResult) { + public DefaultSseBuilder(HttpServletResponse response, Context context, DeferredResult deferredResult, + HttpHeaders httpHeaders) { this.outputMessage = new ServletServerHttpResponse(response); this.deferredResult = deferredResult; this.messageConverters = context.messageConverters(); + this.httpHeaders = httpHeaders; } @Override @@ -206,7 +211,7 @@ private void writeObject(Object data) throws IOException { for (HttpMessageConverter converter : this.messageConverters) { if (converter.canWrite(dataClass, MediaType.APPLICATION_JSON)) { HttpMessageConverter objectConverter = (HttpMessageConverter) converter; - ServerHttpResponse response = new MutableHeadersServerHttpResponse(this.outputMessage); + ServerHttpResponse response = new MutableHeadersServerHttpResponse(this.outputMessage, this.httpHeaders); objectConverter.write(data, MediaType.APPLICATION_JSON, response); this.outputMessage.getBody().write(NL_NL); this.outputMessage.flush(); @@ -276,9 +281,10 @@ private static final class MutableHeadersServerHttpResponse extends DelegatingSe private final HttpHeaders mutableHeaders = new HttpHeaders(); - public MutableHeadersServerHttpResponse(ServerHttpResponse delegate) { + public MutableHeadersServerHttpResponse(ServerHttpResponse delegate, HttpHeaders headers) { super(delegate); this.mutableHeaders.putAll(delegate.getHeaders()); + this.mutableHeaders.putAll(headers); } @Override diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/support/HandlerFunctionAdapter.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/support/HandlerFunctionAdapter.java index 67fac910b781..9d6111680636 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/support/HandlerFunctionAdapter.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/support/HandlerFunctionAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-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. @@ -94,6 +94,7 @@ public ModelAndView handle(HttpServletRequest servletRequest, Object handler) throws Exception { WebAsyncManager asyncManager = getWebAsyncManager(servletRequest, servletResponse); + servletResponse = getWrappedResponse(asyncManager); ServerRequest serverRequest = getServerRequest(servletRequest); ServerResponse serverResponse; @@ -123,6 +124,22 @@ private WebAsyncManager getWebAsyncManager(HttpServletRequest servletRequest, Ht return asyncManager; } + /** + * Obtain response wrapped by + * {@link org.springframework.web.context.request.async.StandardServletAsyncWebRequest} + * to enforce lifecycle rules from Servlet spec (section 2.3.3.4) + * in case of async handling. + */ + private static HttpServletResponse getWrappedResponse(WebAsyncManager asyncManager) { + AsyncWebRequest asyncRequest = asyncManager.getAsyncWebRequest(); + Assert.notNull(asyncRequest, "No AsyncWebRequest"); + + HttpServletResponse servletResponse = asyncRequest.getNativeResponse(HttpServletResponse.class); + Assert.notNull(servletResponse, "No HttpServletResponse"); + + return servletResponse; + } + private ServerRequest getServerRequest(HttpServletRequest servletRequest) { ServerRequest serverRequest = (ServerRequest) servletRequest.getAttribute(RouterFunctions.REQUEST_ATTRIBUTE); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMapping.java index d3336710f0c4..8b4fdd7aa790 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMapping.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMapping.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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. @@ -157,13 +157,14 @@ public Object getDefaultHandler() { * @param patternParser the parser to use * @since 5.3 */ - public void setPatternParser(PathPatternParser patternParser) { + public void setPatternParser(@Nullable PathPatternParser patternParser) { this.patternParser = patternParser; } /** * Return the {@link #setPatternParser(PathPatternParser) configured} - * {@code PathPatternParser}, or {@code null}. + * {@code PathPatternParser}, or {@code null} otherwise which indicates that + * String pattern matching with {@link AntPathMatcher} is enabled instead. * @since 5.3 */ @Nullable @@ -530,6 +531,7 @@ else if (logger.isDebugEnabled() && !DispatcherType.ASYNC.equals(request.getDisp } if (config != null) { config.validateAllowCredentials(); + config.validateAllowPrivateNetwork(); } executionChain = getCorsHandlerExecutionChain(request, executionChain, config); } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMethodMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMethodMapping.java index 0eedee0d878e..5df50148fa70 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMethodMapping.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMethodMapping.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-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. @@ -102,7 +102,7 @@ public abstract class AbstractHandlerMethodMapping extends AbstractHandlerMap @Override - public void setPatternParser(PathPatternParser patternParser) { + public void setPatternParser(@Nullable PathPatternParser patternParser) { Assert.state(this.mappingRegistry.getRegistrations().isEmpty(), "PathPatternParser must be set before the initialization of " + "request mappings through InitializingBean#afterPropertiesSet."); @@ -567,6 +567,7 @@ protected Set getDirectPaths(T mapping) { /** * A registry that maintains all mappings to handler methods, exposing methods * to perform lookups and providing concurrent access. + * *

    Package-private for testing purposes. */ class MappingRegistry { @@ -648,6 +649,7 @@ public void register(T mapping, Object handler, Method method) { CorsConfiguration corsConfig = initCorsConfiguration(handler, method, mapping); if (corsConfig != null) { corsConfig.validateAllowCredentials(); + corsConfig.validateAllowPrivateNetwork(); this.corsLookup.put(handlerMethod, corsConfig); } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/HandlerMappingIntrospector.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/HandlerMappingIntrospector.java index 7ee392b3514a..1ad369191655 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/HandlerMappingIntrospector.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/HandlerMappingIntrospector.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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. @@ -51,6 +51,7 @@ import org.springframework.web.servlet.HandlerMapping; import org.springframework.web.util.ServletRequestPathUtils; import org.springframework.web.util.UrlPathHelper; +import org.springframework.web.util.pattern.PathPatternParser; /** * Helper class to get information from the {@code HandlerMapping} that would @@ -82,7 +83,7 @@ public class HandlerMappingIntrospector @Nullable private List handlerMappings; - private Map pathPatternHandlerMappings = Collections.emptyMap(); + private Map pathPatternMappings = Collections.emptyMap(); /** @@ -113,10 +114,55 @@ public void afterPropertiesSet() { if (this.handlerMappings == null) { Assert.notNull(this.applicationContext, "No ApplicationContext"); this.handlerMappings = initHandlerMappings(this.applicationContext); - this.pathPatternHandlerMappings = initPathPatternMatchableHandlerMappings(this.handlerMappings); + + this.pathPatternMappings = this.handlerMappings.stream() + .filter(m -> m instanceof MatchableHandlerMapping && ((MatchableHandlerMapping) m).getPatternParser() != null) + .map(mapping -> (MatchableHandlerMapping) mapping) + .collect(Collectors.toMap(mapping -> mapping, PathPatternMatchableHandlerMapping::new)); + } + } + + private static List initHandlerMappings(ApplicationContext context) { + + Map beans = + BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false); + + if (!beans.isEmpty()) { + List mappings = new ArrayList<>(beans.values()); + AnnotationAwareOrderComparator.sort(mappings); + return Collections.unmodifiableList(mappings); + } + + return Collections.unmodifiableList(initFallback(context)); + } + + private static List initFallback(ApplicationContext applicationContext) { + Properties properties; + try { + Resource resource = new ClassPathResource("DispatcherServlet.properties", DispatcherServlet.class); + properties = PropertiesLoaderUtils.loadProperties(resource); + } + catch (IOException ex) { + throw new IllegalStateException("Could not load DispatcherServlet.properties: " + ex.getMessage()); } + + String value = properties.getProperty(HandlerMapping.class.getName()); + String[] names = StringUtils.commaDelimitedListToStringArray(value); + List result = new ArrayList<>(names.length); + for (String name : names) { + try { + Class clazz = ClassUtils.forName(name, DispatcherServlet.class.getClassLoader()); + Object mapping = applicationContext.getAutowireCapableBeanFactory().createBean(clazz); + result.add((HandlerMapping) mapping); + } + catch (ClassNotFoundException ex) { + throw new IllegalStateException("Could not find default HandlerMapping [" + name + "]"); + } + } + return result; } + /** * Return the configured or detected {@code HandlerMapping}s. */ @@ -127,27 +173,27 @@ public List getHandlerMappings() { /** * Find the {@link HandlerMapping} that would handle the given request and - * return it as a {@link MatchableHandlerMapping} that can be used to test - * request-matching criteria. - *

    If the matching HandlerMapping is not an instance of - * {@link MatchableHandlerMapping}, an IllegalStateException is raised. + * return a {@link MatchableHandlerMapping} to use for path matching. * @param request the current request - * @return the resolved matcher, or {@code null} + * @return the resolved {@code MatchableHandlerMapping}, or {@code null} + * @throws IllegalStateException if the matching HandlerMapping is not an + * instance of {@link MatchableHandlerMapping} * @throws Exception if any of the HandlerMapping's raise an exception */ @Nullable public MatchableHandlerMapping getMatchableHandlerMapping(HttpServletRequest request) throws Exception { HttpServletRequest wrappedRequest = new AttributesPreservingRequest(request); - return doWithMatchingMapping(wrappedRequest, false, (matchedMapping, executionChain) -> { - if (matchedMapping instanceof MatchableHandlerMapping) { - PathPatternMatchableHandlerMapping mapping = this.pathPatternHandlerMappings.get(matchedMapping); - if (mapping != null) { + + return doWithHandlerMapping(wrappedRequest, false, (mapping, executionChain) -> { + if (mapping instanceof MatchableHandlerMapping) { + PathPatternMatchableHandlerMapping pathPatternMapping = this.pathPatternMappings.get(mapping); + if (pathPatternMapping != null) { RequestPath requestPath = ServletRequestPathUtils.getParsedRequestPath(wrappedRequest); - return new PathSettingHandlerMapping(mapping, requestPath); + return new LookupPathMatchableHandlerMapping(pathPatternMapping, requestPath); } else { String lookupPath = (String) wrappedRequest.getAttribute(UrlPathHelper.PATH_ATTRIBUTE); - return new PathSettingHandlerMapping((MatchableHandlerMapping) matchedMapping, lookupPath); + return new LookupPathMatchableHandlerMapping((MatchableHandlerMapping) mapping, lookupPath); } } throw new IllegalStateException("HandlerMapping is not a MatchableHandlerMapping"); @@ -158,7 +204,7 @@ public MatchableHandlerMapping getMatchableHandlerMapping(HttpServletRequest req @Nullable public CorsConfiguration getCorsConfiguration(HttpServletRequest request) { AttributesPreservingRequest wrappedRequest = new AttributesPreservingRequest(request); - return doWithMatchingMappingIgnoringException(wrappedRequest, (handlerMapping, executionChain) -> { + return doWithHandlerMappingIgnoringException(wrappedRequest, (handlerMapping, executionChain) -> { for (HandlerInterceptor interceptor : executionChain.getInterceptorList()) { if (interceptor instanceof CorsConfigurationSource) { return ((CorsConfigurationSource) interceptor).getCorsConfiguration(wrappedRequest); @@ -172,15 +218,15 @@ public CorsConfiguration getCorsConfiguration(HttpServletRequest request) { } @Nullable - private T doWithMatchingMapping( + private T doWithHandlerMapping( HttpServletRequest request, boolean ignoreException, - BiFunction matchHandler) throws Exception { + BiFunction extractor) throws Exception { - Assert.notNull(this.handlerMappings, "Handler mappings not initialized"); + Assert.state(this.handlerMappings != null, "HandlerMapping's not initialized"); - boolean parseRequestPath = !this.pathPatternHandlerMappings.isEmpty(); + boolean parsePath = !this.pathPatternMappings.isEmpty(); RequestPath previousPath = null; - if (parseRequestPath) { + if (parsePath) { previousPath = (RequestPath) request.getAttribute(ServletRequestPathUtils.PATH_ATTRIBUTE); ServletRequestPathUtils.parseAndCache(request); } @@ -198,11 +244,11 @@ private T doWithMatchingMapping( if (chain == null) { continue; } - return matchHandler.apply(handlerMapping, chain); + return extractor.apply(handlerMapping, chain); } } finally { - if (parseRequestPath) { + if (parsePath) { ServletRequestPathUtils.setParsedRequestPath(previousPath, request); } } @@ -210,11 +256,11 @@ private T doWithMatchingMapping( } @Nullable - private T doWithMatchingMappingIgnoringException( + private T doWithHandlerMappingIgnoringException( HttpServletRequest request, BiFunction matchHandler) { try { - return doWithMatchingMapping(request, true, matchHandler); + return doWithHandlerMapping(request, true, matchHandler); } catch (Exception ex) { throw new IllegalStateException("HandlerMapping exception not suppressed", ex); @@ -222,55 +268,6 @@ private T doWithMatchingMappingIgnoringException( } - private static List initHandlerMappings(ApplicationContext applicationContext) { - Map beans = BeanFactoryUtils.beansOfTypeIncludingAncestors( - applicationContext, HandlerMapping.class, true, false); - if (!beans.isEmpty()) { - List mappings = new ArrayList<>(beans.values()); - AnnotationAwareOrderComparator.sort(mappings); - return Collections.unmodifiableList(mappings); - } - return Collections.unmodifiableList(initFallback(applicationContext)); - } - - private static List initFallback(ApplicationContext applicationContext) { - Properties props; - String path = "DispatcherServlet.properties"; - try { - Resource resource = new ClassPathResource(path, DispatcherServlet.class); - props = PropertiesLoaderUtils.loadProperties(resource); - } - catch (IOException ex) { - throw new IllegalStateException("Could not load '" + path + "': " + ex.getMessage()); - } - - String value = props.getProperty(HandlerMapping.class.getName()); - String[] names = StringUtils.commaDelimitedListToStringArray(value); - List result = new ArrayList<>(names.length); - for (String name : names) { - try { - Class clazz = ClassUtils.forName(name, DispatcherServlet.class.getClassLoader()); - Object mapping = applicationContext.getAutowireCapableBeanFactory().createBean(clazz); - result.add((HandlerMapping) mapping); - } - catch (ClassNotFoundException ex) { - throw new IllegalStateException("Could not find default HandlerMapping [" + name + "]"); - } - } - return result; - } - - private static Map initPathPatternMatchableHandlerMappings( - List mappings) { - - return mappings.stream() - .filter(mapping -> mapping instanceof MatchableHandlerMapping) - .map(mapping -> (MatchableHandlerMapping) mapping) - .filter(mapping -> mapping.getPatternParser() != null) - .collect(Collectors.toMap(mapping -> mapping, PathPatternMatchableHandlerMapping::new)); - } - - /** * Request wrapper that buffers request attributes in order protect the * underlying request from attribute changes. @@ -316,26 +313,32 @@ public void removeAttribute(String name) { } - private static class PathSettingHandlerMapping implements MatchableHandlerMapping { + private static class LookupPathMatchableHandlerMapping implements MatchableHandlerMapping { private final MatchableHandlerMapping delegate; - private final Object path; + private final Object lookupPath; private final String pathAttributeName; - PathSettingHandlerMapping(MatchableHandlerMapping delegate, Object path) { + LookupPathMatchableHandlerMapping(MatchableHandlerMapping delegate, Object lookupPath) { this.delegate = delegate; - this.path = path; - this.pathAttributeName = (path instanceof RequestPath ? + this.lookupPath = lookupPath; + this.pathAttributeName = (lookupPath instanceof RequestPath ? ServletRequestPathUtils.PATH_ATTRIBUTE : UrlPathHelper.PATH_ATTRIBUTE); } + @Override + public PathPatternParser getPatternParser() { + return this.delegate.getPatternParser(); + } + @Nullable @Override public RequestMatchResult match(HttpServletRequest request, String pattern) { + pattern = initFullPathPattern(pattern); Object previousPath = request.getAttribute(this.pathAttributeName); - request.setAttribute(this.pathAttributeName, this.path); + request.setAttribute(this.pathAttributeName, this.lookupPath); try { return this.delegate.match(request, pattern); } @@ -344,6 +347,11 @@ public RequestMatchResult match(HttpServletRequest request, String pattern) { } } + private String initFullPathPattern(String pattern) { + PathPatternParser parser = (getPatternParser() != null ? getPatternParser() : PathPatternParser.defaultInstance); + return parser.initFullPathPattern(pattern); + } + @Nullable @Override public HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/MappedInterceptor.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/MappedInterceptor.java index 2467605c77f7..d7abc59b19da 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/MappedInterceptor.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/MappedInterceptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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. @@ -61,7 +61,7 @@ */ public final class MappedInterceptor implements HandlerInterceptor { - private static PathMatcher defaultPathMatcher = new AntPathMatcher(); + private static final PathMatcher defaultPathMatcher = new AntPathMatcher(); @Nullable diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/PathPatternMatchableHandlerMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/PathPatternMatchableHandlerMapping.java index 91a3c43908a7..a9205d021def 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/PathPatternMatchableHandlerMapping.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/PathPatternMatchableHandlerMapping.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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,8 +30,9 @@ import org.springframework.web.util.pattern.PathPatternParser; /** - * Wraps {@link MatchableHandlerMapping}s configured with a {@link PathPatternParser} - * in order to parse patterns lazily and cache them for re-ues. + * Decorate another {@link MatchableHandlerMapping} that's configured with a + * {@link PathPatternParser} in order to parse and cache String patterns passed + * into the {@code match} method. * * @author Rossen Stoyanchev * @since 5.3 @@ -49,8 +50,8 @@ class PathPatternMatchableHandlerMapping implements MatchableHandlerMapping { public PathPatternMatchableHandlerMapping(MatchableHandlerMapping delegate) { - Assert.notNull(delegate, "Delegate MatchableHandlerMapping is required."); - Assert.notNull(delegate.getPatternParser(), "PatternParser is required."); + Assert.notNull(delegate, "HandlerMapping to delegate to is required."); + Assert.notNull(delegate.getPatternParser(), "Expected HandlerMapping configured to use PatternParser."); this.delegate = delegate; this.parser = delegate.getPatternParser(); } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/PathPatternsRequestCondition.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/PathPatternsRequestCondition.java index a89b4ed3cb6b..3c1c0e00298a 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/PathPatternsRequestCondition.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/PathPatternsRequestCondition.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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. @@ -75,11 +75,9 @@ private static SortedSet parse(PathPatternParser parser, String... return EMPTY_PATH_PATTERN; } SortedSet result = new TreeSet<>(); - for (String path : patterns) { - if (StringUtils.hasText(path) && !path.startsWith("/")) { - path = "/" + path; - } - result.add(parser.parse(path)); + for (String pattern : patterns) { + pattern = parser.initFullPathPattern(pattern); + result.add(parser.parse(pattern)); } return result; } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/PatternsRequestCondition.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/PatternsRequestCondition.java index 9539415ae388..335e62cbcb0f 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/PatternsRequestCondition.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/PatternsRequestCondition.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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. @@ -35,6 +35,7 @@ import org.springframework.util.StringUtils; import org.springframework.web.util.UrlPathHelper; import org.springframework.web.util.pattern.PathPattern; +import org.springframework.web.util.pattern.PathPatternParser; /** * A logical disjunction (' || ') request condition that matches a request @@ -156,9 +157,7 @@ private static Set initPatterns(String[] patterns) { } Set result = new LinkedHashSet<>(patterns.length); for (String pattern : patterns) { - if (StringUtils.hasLength(pattern) && !pattern.startsWith("/")) { - pattern = "/" + pattern; - } + pattern = PathPatternParser.defaultInstance.initFullPathPattern(pattern); result.add(pattern); } return result; diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/ProducesRequestCondition.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/ProducesRequestCondition.java index 7a6af2272599..412bd1885f50 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/ProducesRequestCondition.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/ProducesRequestCondition.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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. @@ -253,6 +253,9 @@ private List getMatchingExpressions(List */ @Override public int compareTo(ProducesRequestCondition other, HttpServletRequest request) { + if (this.expressions.isEmpty() && other.expressions.isEmpty()) { + return 0; + } try { List acceptedMediaTypes = getAcceptedMediaTypes(request); for (MediaType acceptedMediaType : acceptedMediaTypes) { diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoHandlerMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoHandlerMapping.java index 3f2a099f0003..99131d2bd34d 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoHandlerMapping.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoHandlerMapping.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 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. @@ -241,6 +241,10 @@ private Map> extractMatrixVariables( protected HandlerMethod handleNoMatch( Set infos, String lookupPath, HttpServletRequest request) throws ServletException { + if (CollectionUtils.isEmpty(infos)) { + return null; + } + PartialMatchHelper helper = new PartialMatchHelper(infos, request); if (helper.isEmpty()) { return null; @@ -291,7 +295,7 @@ private static class PartialMatchHelper { private final List partialMatches = new ArrayList<>(); - public PartialMatchHelper(Set infos, HttpServletRequest request) { + PartialMatchHelper(Set infos, HttpServletRequest request) { for (RequestMappingInfo info : infos) { if (info.getActivePatternsCondition().getMatchingCondition(request) != null) { this.partialMatches.add(new PartialMatch(info, request)); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MatrixVariableMapMethodArgumentResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MatrixVariableMapMethodArgumentResolver.java index 313e1b9cd1f9..c60de4a42547 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MatrixVariableMapMethodArgumentResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MatrixVariableMapMethodArgumentResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2023 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.web.servlet.mvc.method.annotation; -import java.util.Collections; import java.util.List; import java.util.Map; @@ -68,11 +67,17 @@ public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewC (Map>) request.getAttribute( HandlerMapping.MATRIX_VARIABLES_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST); - if (CollectionUtils.isEmpty(matrixVariables)) { - return Collections.emptyMap(); - } + MultiValueMap map = mapMatrixVariables(parameter, matrixVariables); + return (isSingleValueMap(parameter) ? map.toSingleValueMap() : map); + } + + private MultiValueMap mapMatrixVariables(MethodParameter parameter, + @Nullable Map> matrixVariables) { MultiValueMap map = new LinkedMultiValueMap<>(); + if (CollectionUtils.isEmpty(matrixVariables)) { + return map; + } MatrixVariable ann = parameter.getParameterAnnotation(MatrixVariable.class); Assert.state(ann != null, "No MatrixVariable annotation"); String pathVariable = ann.pathVar(); @@ -80,7 +85,7 @@ public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewC if (!pathVariable.equals(ValueConstants.DEFAULT_NONE)) { MultiValueMap mapForPathVariable = matrixVariables.get(pathVariable); if (mapForPathVariable == null) { - return Collections.emptyMap(); + return map; } map.putAll(mapForPathVariable); } @@ -93,8 +98,7 @@ public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewC }); } } - - return (isSingleValueMap(parameter) ? map.toSingleValueMap() : map); + return map; } private boolean isSingleValueMap(MethodParameter parameter) { diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MvcUriComponentsBuilder.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MvcUriComponentsBuilder.java index 80dcdd41105d..04bda78c45a7 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MvcUriComponentsBuilder.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MvcUriComponentsBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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.web.servlet.mvc.method.annotation; +import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; +import java.lang.reflect.Proxy; import java.util.Arrays; import java.util.HashMap; import java.util.List; @@ -25,17 +27,15 @@ import javax.servlet.http.HttpServletRequest; -import org.aopalliance.intercept.MethodInterceptor; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.springframework.aop.framework.ProxyFactory; -import org.springframework.aop.target.EmptyTargetSource; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.cglib.core.SpringNamingPolicy; import org.springframework.cglib.proxy.Callback; import org.springframework.cglib.proxy.Enhancer; import org.springframework.cglib.proxy.Factory; +import org.springframework.cglib.proxy.MethodInterceptor; import org.springframework.cglib.proxy.MethodProxy; import org.springframework.core.DefaultParameterNameDiscoverer; import org.springframework.core.MethodIntrospector; @@ -65,6 +65,7 @@ import org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping; import org.springframework.web.servlet.support.ServletUriComponentsBuilder; import org.springframework.web.util.UriComponentsBuilder; +import org.springframework.web.util.pattern.PathPatternParser; /** * Creates instances of {@link org.springframework.web.util.UriComponentsBuilder} @@ -93,6 +94,7 @@ * @author Oliver Gierke * @author Rossen Stoyanchev * @author Sam Brannen + * @author Juergen Hoeller * @since 4.0 */ public class MvcUriComponentsBuilder { @@ -543,9 +545,7 @@ private static UriComponentsBuilder fromMethodInternal(@Nullable UriComponentsBu String typePath = getClassMapping(controllerType); String methodPath = getMethodMapping(method); String path = pathMatcher.combine(typePath, methodPath); - if (StringUtils.hasLength(path) && !path.startsWith("/")) { - path = "/" + path; - } + path = PathPatternParser.defaultInstance.initFullPathPattern(path); builder.path(path); return applyContributors(builder, method, args); @@ -700,7 +700,7 @@ public interface MethodInvocationInfo { private static class ControllerMethodInvocationInterceptor - implements org.springframework.cglib.proxy.MethodInterceptor, MethodInterceptor, MethodInvocationInfo { + implements MethodInterceptor, InvocationHandler, MethodInvocationInfo { private final Class controllerType; @@ -741,8 +741,8 @@ public Object intercept(@Nullable Object obj, Method method, Object[] args, @Nul @Override @Nullable - public Object invoke(org.aopalliance.intercept.MethodInvocation inv) throws Throwable { - return intercept(inv.getThis(), inv.getMethod(), inv.getArguments(), null); + public Object invoke(Object proxy, Method method, @Nullable Object[] args) { + return intercept(proxy, method, (args != null ? args : new Object[0]), null); } @Override @@ -767,19 +767,35 @@ public Object[] getArgumentValues() { private static T initProxy( Class controllerType, @Nullable ControllerMethodInvocationInterceptor interceptor) { - interceptor = interceptor != null ? - interceptor : new ControllerMethodInvocationInterceptor(controllerType); + interceptor = (interceptor != null ? + interceptor : new ControllerMethodInvocationInterceptor(controllerType)); if (controllerType == Object.class) { return (T) interceptor; } else if (controllerType.isInterface()) { - ProxyFactory factory = new ProxyFactory(EmptyTargetSource.INSTANCE); - factory.addInterface(controllerType); - factory.addInterface(MethodInvocationInfo.class); - factory.addAdvice(interceptor); - return (T) factory.getProxy(); + ClassLoader classLoader = controllerType.getClassLoader(); + if (classLoader == null) { + // JDK bootstrap loader -> use MethodInvocationInfo ClassLoader instead. + classLoader = MethodInvocationInfo.class.getClassLoader(); + } + else if (classLoader.getParent() == null) { + // Potentially the JDK platform loader on JDK 9+ + ClassLoader miiClassLoader = MethodInvocationInfo.class.getClassLoader(); + ClassLoader miiParent = miiClassLoader.getParent(); + while (miiParent != null) { + if (classLoader == miiParent) { + // Suggested ClassLoader is ancestor of MethodInvocationInfo ClassLoader + // -> use MethodInvocationInfo ClassLoader itself instead. + classLoader = miiClassLoader; + break; + } + miiParent = miiParent.getParent(); + } + } + Class[] ifcs = new Class[] {controllerType, MethodInvocationInfo.class}; + return (T) Proxy.newProxyInstance(classLoader, ifcs, interceptor); } else { @@ -787,7 +803,7 @@ else if (controllerType.isInterface()) { enhancer.setSuperclass(controllerType); enhancer.setInterfaces(new Class[] {MethodInvocationInfo.class}); enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE); - enhancer.setCallbackType(org.springframework.cglib.proxy.MethodInterceptor.class); + enhancer.setCallbackType(MethodInterceptor.class); Class proxyClass = enhancer.createClass(); Object proxy = null; diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java index 6005b22e247b..fe780a3e6936 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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,6 +50,7 @@ import org.springframework.http.converter.xml.SourceHttpMessageConverter; import org.springframework.lang.Nullable; import org.springframework.ui.ModelMap; +import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import org.springframework.util.ReflectionUtils.MethodFilter; import org.springframework.web.accept.ContentNegotiationManager; @@ -465,7 +466,7 @@ public ReactiveAdapterRegistry getReactiveAdapterRegistry() { } /** - * By default the content of the "default" model is used both during + * By default, the content of the "default" model is used both during * rendering and redirect scenarios. Alternatively a controller method * can declare a {@link RedirectAttributes} argument and use it to provide * attributes for a redirect. @@ -852,7 +853,21 @@ private SessionAttributesHandler getSessionAttributesHandler(HandlerMethod handl protected ModelAndView invokeHandlerMethod(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception { - ServletWebRequest webRequest = new ServletWebRequest(request, response); + WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); + AsyncWebRequest asyncWebRequest = WebAsyncUtils.createAsyncWebRequest(request, response); + asyncWebRequest.setTimeout(this.asyncRequestTimeout); + + asyncManager.setTaskExecutor(this.taskExecutor); + asyncManager.setAsyncWebRequest(asyncWebRequest); + asyncManager.registerCallableInterceptors(this.callableInterceptors); + asyncManager.registerDeferredResultInterceptors(this.deferredResultInterceptors); + + // Obtain wrapped response to enforce lifecycle rule from Servlet spec, section 2.3.3.4 + response = asyncWebRequest.getNativeResponse(HttpServletResponse.class); + + ServletWebRequest webRequest = (asyncWebRequest instanceof ServletWebRequest ? + (ServletWebRequest) asyncWebRequest : new ServletWebRequest(request, response)); + try { WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod); ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory); @@ -872,18 +887,11 @@ protected ModelAndView invokeHandlerMethod(HttpServletRequest request, modelFactory.initModel(webRequest, mavContainer, invocableMethod); mavContainer.setIgnoreDefaultModelOnRedirect(this.ignoreDefaultModelOnRedirect); - AsyncWebRequest asyncWebRequest = WebAsyncUtils.createAsyncWebRequest(request, response); - asyncWebRequest.setTimeout(this.asyncRequestTimeout); - - WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); - asyncManager.setTaskExecutor(this.taskExecutor); - asyncManager.setAsyncWebRequest(asyncWebRequest); - asyncManager.registerCallableInterceptors(this.callableInterceptors); - asyncManager.registerDeferredResultInterceptors(this.deferredResultInterceptors); - if (asyncManager.hasConcurrentResult()) { Object result = asyncManager.getConcurrentResult(); - mavContainer = (ModelAndViewContainer) asyncManager.getConcurrentResultContext()[0]; + Object[] resultContext = asyncManager.getConcurrentResultContext(); + Assert.state(resultContext != null && resultContext.length > 0, "Missing result context"); + mavContainer = (ModelAndViewContainer) resultContext[0]; asyncManager.clearConcurrentResult(); LogFormatUtils.traceDebug(logger, traceOn -> { String formatted = LogFormatUtils.formatValue(result, !traceOn); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java index d550f9a17941..924181e0f772 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java @@ -501,6 +501,18 @@ else if (!allowCredentials.isEmpty()) { "or an empty string (\"\"): current value is [" + allowCredentials + "]"); } + String allowPrivateNetwork = resolveCorsAnnotationValue(annotation.allowPrivateNetwork()); + if ("true".equalsIgnoreCase(allowPrivateNetwork)) { + config.setAllowPrivateNetwork(true); + } + else if ("false".equalsIgnoreCase(allowPrivateNetwork)) { + config.setAllowPrivateNetwork(false); + } + else if (!allowPrivateNetwork.isEmpty()) { + throw new IllegalStateException("@CrossOrigin's allowPrivateNetwork value must be \"true\", \"false\", " + + "or an empty string (\"\"): current value is [" + allowPrivateNetwork + "]"); + } + if (annotation.maxAge() >= 0 ) { config.setMaxAge(annotation.maxAge()); } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletInvocableHandlerMethod.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletInvocableHandlerMethod.java index ae3473873dee..23f19a329205 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletInvocableHandlerMethod.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletInvocableHandlerMethod.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-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,7 @@ private String formatErrorForReturnValue(@Nullable Object returnValue) { * actually invoking the controller method. This is useful when processing * async return values (e.g. Callable, DeferredResult, ListenableFuture). */ - ServletInvocableHandlerMethod wrapConcurrentResult(Object result) { + ServletInvocableHandlerMethod wrapConcurrentResult(@Nullable Object result) { return new ConcurrentResultHandlerMethod(result, new ConcurrentResultMethodParameter(result)); } @@ -214,7 +214,7 @@ private class ConcurrentResultHandlerMethod extends ServletInvocableHandlerMetho private final MethodParameter returnType; - public ConcurrentResultHandlerMethod(final Object result, ConcurrentResultMethodParameter returnType) { + public ConcurrentResultHandlerMethod(@Nullable Object result, ConcurrentResultMethodParameter returnType) { super((Callable) () -> { if (result instanceof Exception) { throw (Exception) result; @@ -278,7 +278,7 @@ private class ConcurrentResultMethodParameter extends HandlerMethodParameter { private final ResolvableType returnType; - public ConcurrentResultMethodParameter(Object returnValue) { + public ConcurrentResultMethodParameter(@Nullable Object returnValue) { super(-1); this.returnValue = returnValue; this.returnType = (returnValue instanceof ReactiveTypeHandler.CollectedValuesList ? diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletRequestMethodArgumentResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletRequestMethodArgumentResolver.java index 45ca54595efa..0377612b57f1 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletRequestMethodArgumentResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletRequestMethodArgumentResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -203,7 +203,6 @@ public static Object resolvePushBuilder(HttpServletRequest request, Class par "Current push builder is not of type [" + paramType.getName() + "]: " + pushBuilder); } return pushBuilder; - } } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/StreamingResponseBody.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/StreamingResponseBody.java index 3c6fe1575ce8..34e78e1b2aee 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/StreamingResponseBody.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/StreamingResponseBody.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2022 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-webmvc/src/main/java/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolver.java index 72f539b83359..6b44e562ad91 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-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. @@ -46,6 +46,7 @@ import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.context.request.async.AsyncRequestNotUsableException; import org.springframework.web.context.request.async.AsyncRequestTimeoutException; import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.support.MissingServletRequestPartException; @@ -228,6 +229,10 @@ else if (ex instanceof AsyncRequestTimeoutException) { return handleAsyncRequestTimeoutException( (AsyncRequestTimeoutException) ex, request, response, handler); } + else if (ex instanceof AsyncRequestNotUsableException) { + return handleAsyncRequestNotUsableException( + (AsyncRequestNotUsableException) ex, request, response, handler); + } } catch (Exception handlerEx) { if (logger.isWarnEnabled()) { @@ -541,6 +546,23 @@ protected ModelAndView handleAsyncRequestTimeoutException(AsyncRequestTimeoutExc return new ModelAndView(); } + /** + * Handle the case of an I/O failure from the ServletOutputStream. + *

    By default, do nothing since the response is not usable. + * @param ex the {@link AsyncRequestTimeoutException} to be handled + * @param request current HTTP request + * @param response current HTTP response + * @param handler the executed handler, or {@code null} if none chosen + * at the time of the exception (for example, if multipart resolution failed) + * @return an empty ModelAndView indicating the exception was handled + * @since 5.3.33 + */ + protected ModelAndView handleAsyncRequestNotUsableException(AsyncRequestNotUsableException ex, + HttpServletRequest request, HttpServletResponse response, @Nullable Object handler) { + + return new ModelAndView(); + } + /** * Invoked to send a server error. Sets the status to 500 and also sets the * request attribute "javax.servlet.error.exception" to the Exception. diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/EncodedResourceResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/EncodedResourceResolver.java index 846938eea344..c4dbaa038df3 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/EncodedResourceResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/EncodedResourceResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 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,6 +21,7 @@ import java.io.InputStream; import java.net.URI; import java.net.URL; +import java.nio.channels.ReadableByteChannel; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -199,12 +200,6 @@ static final class EncodedResource extends AbstractResource implements HttpResou this.encoded = original.createRelative(original.getFilename() + extension); } - - @Override - public InputStream getInputStream() throws IOException { - return this.encoded.getInputStream(); - } - @Override public boolean exists() { return this.encoded.exists(); @@ -240,6 +235,16 @@ public File getFile() throws IOException { return this.encoded.getFile(); } + @Override + public InputStream getInputStream() throws IOException { + return this.encoded.getInputStream(); + } + + @Override + public ReadableByteChannel readableChannel() throws IOException { + return this.encoded.readableChannel(); + } + @Override public long contentLength() throws IOException { return this.encoded.contentLength(); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/VersionResourceResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/VersionResourceResolver.java index f1f91ef42ea1..6d00d570d9e5 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/VersionResourceResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/VersionResourceResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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,6 +21,7 @@ import java.io.InputStream; import java.net.URI; import java.net.URL; +import java.nio.channels.ReadableByteChannel; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; @@ -65,7 +66,7 @@ */ public class VersionResourceResolver extends AbstractResourceResolver { - private AntPathMatcher pathMatcher = new AntPathMatcher(); + private final AntPathMatcher pathMatcher = new AntPathMatcher(); /** Map from path pattern -> VersionStrategy. */ private final Map versionStrategyMap = new LinkedHashMap<>(); @@ -279,9 +280,13 @@ public File getFile() throws IOException { } @Override - @Nullable - public String getFilename() { - return this.original.getFilename(); + public InputStream getInputStream() throws IOException { + return this.original.getInputStream(); + } + + @Override + public ReadableByteChannel readableChannel() throws IOException { + return this.original.readableChannel(); } @Override @@ -300,13 +305,14 @@ public Resource createRelative(String relativePath) throws IOException { } @Override - public String getDescription() { - return this.original.getDescription(); + @Nullable + public String getFilename() { + return this.original.getFilename(); } @Override - public InputStream getInputStream() throws IOException { - return this.original.getInputStream(); + public String getDescription() { + return this.original.getDescription(); } @Override diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/WebJarsResourceResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/WebJarsResourceResolver.java index 2d945bca28de..a8c4f1298ae5 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/WebJarsResourceResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/WebJarsResourceResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2022 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-webmvc/src/main/java/org/springframework/web/servlet/support/AbstractFlashMapManager.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/support/AbstractFlashMapManager.java index d740a3c2cc65..8a81cce0d95f 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/support/AbstractFlashMapManager.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/support/AbstractFlashMapManager.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -221,7 +221,7 @@ public final void saveOutputFlashMap(FlashMap flashMap, HttpServletRequest reque @Nullable private String decodeAndNormalizePath(@Nullable String path, HttpServletRequest request) { - if (path != null && !path.isEmpty()) { + if (StringUtils.hasLength(path)) { path = getUrlPathHelper().decodeRequestString(request, path); if (path.charAt(0) != '/') { String requestUri = getUrlPathHelper().getRequestUri(request); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/AbstractHtmlElementBodyTag.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/AbstractHtmlElementBodyTag.java index 0101735bb1e6..f42fc937cc6c 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/AbstractHtmlElementBodyTag.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/AbstractHtmlElementBodyTag.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2022 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-webmvc/src/main/java/org/springframework/web/servlet/view/AbstractTemplateView.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/AbstractTemplateView.java index 7a1a24cd7568..c6050a5d2fea 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/AbstractTemplateView.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/AbstractTemplateView.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2023 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. @@ -66,6 +66,8 @@ public abstract class AbstractTemplateView extends AbstractUrlBasedView { /** * Set whether all request attributes should be added to the * model prior to merging with the template. Default is "false". + *

    Note that some templates may make request attributes visible + * on their own, e.g. FreeMarker, without exposure in the MVC model. */ public void setExposeRequestAttributes(boolean exposeRequestAttributes) { this.exposeRequestAttributes = exposeRequestAttributes; @@ -73,7 +75,7 @@ public void setExposeRequestAttributes(boolean exposeRequestAttributes) { /** * Set whether HttpServletRequest attributes are allowed to override (hide) - * controller generated model attributes of the same name. Default is "false", + * controller generated model attributes of the same name. Default is "false" * which causes an exception to be thrown if request attributes of the same * name as model attributes are found. */ @@ -122,11 +124,11 @@ protected final void renderMergedOutputModel( String attribute = en.nextElement(); if (model.containsKey(attribute) && !this.allowRequestOverride) { throw new ServletException("Cannot expose request attribute '" + attribute + - "' because of an existing model object of the same name"); + "' because of an existing model object of the same name"); } Object attributeValue = request.getAttribute(attribute); if (logger.isDebugEnabled()) { - exposed = exposed != null ? exposed : new LinkedHashMap<>(); + exposed = (exposed != null ? exposed : new LinkedHashMap<>()); exposed.put(attribute, attributeValue); } model.put(attribute, attributeValue); @@ -144,11 +146,11 @@ protected final void renderMergedOutputModel( String attribute = en.nextElement(); if (model.containsKey(attribute) && !this.allowSessionOverride) { throw new ServletException("Cannot expose session attribute '" + attribute + - "' because of an existing model object of the same name"); + "' because of an existing model object of the same name"); } Object attributeValue = session.getAttribute(attribute); if (logger.isDebugEnabled()) { - exposed = exposed != null ? exposed : new LinkedHashMap<>(); + exposed = (exposed != null ? exposed : new LinkedHashMap<>()); exposed.put(attribute, attributeValue); } model.put(attribute, attributeValue); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/groovy/GroovyMarkupConfigurer.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/groovy/GroovyMarkupConfigurer.java index a00e2510bcf0..ecf91a940c66 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/groovy/GroovyMarkupConfigurer.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/groovy/GroovyMarkupConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2022 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-webmvc/src/main/java/org/springframework/web/servlet/view/groovy/GroovyMarkupView.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/groovy/GroovyMarkupView.java index 8e89390ac6c5..bc257e07e60e 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/groovy/GroovyMarkupView.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/groovy/GroovyMarkupView.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2022 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-webmvc/src/main/kotlin/org/springframework/web/servlet/function/RouterFunctionDsl.kt b/spring-webmvc/src/main/kotlin/org/springframework/web/servlet/function/RouterFunctionDsl.kt index 88381315df0d..5355f5ecb31f 100644 --- a/spring-webmvc/src/main/kotlin/org/springframework/web/servlet/function/RouterFunctionDsl.kt +++ b/spring-webmvc/src/main/kotlin/org/springframework/web/servlet/function/RouterFunctionDsl.kt @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 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,7 +25,7 @@ import java.util.* import java.util.function.Supplier /** - * Allow to create easily a WebMvc.fn [RouterFunction] with a [Reactive router Kotlin DSL][RouterFunctionDsl]. + * Allow to create easily a WebMvc.fn [RouterFunction] with a [router Kotlin DSL][RouterFunctionDsl]. * * Example: * @@ -54,7 +54,7 @@ import java.util.function.Supplier fun router(routes: (RouterFunctionDsl.() -> Unit)) = RouterFunctionDsl(routes).build() /** - * Provide a WebMvc.fn [RouterFunction] Reactive Kotlin DSL created by [`router { }`][router] in order to be able to write idiomatic Kotlin code. + * Provide a WebMvc.fn [RouterFunction] Kotlin DSL created by [`router { }`][router] in order to be able to write idiomatic Kotlin code. * * @author Sebastien Deleuze * @since 5.2 diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/CorsRegistryTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/CorsRegistryTests.java index 9b64d9c26df4..8182aadfd2ea 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/CorsRegistryTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/CorsRegistryTests.java @@ -56,8 +56,8 @@ public void multipleMappings() { @Test public void customizedMapping() { this.registry.addMapping("/foo").allowedOrigins("https://domain2.com", "https://domain2.com") - .allowedMethods("DELETE").allowCredentials(false).allowedHeaders("header1", "header2") - .exposedHeaders("header3", "header4").maxAge(3600); + .allowedMethods("DELETE").allowCredentials(true).allowPrivateNetwork(true) + .allowedHeaders("header1", "header2").exposedHeaders("header3", "header4").maxAge(3600); Map configs = this.registry.getCorsConfigurations(); assertThat(configs.size()).isEqualTo(1); CorsConfiguration config = configs.get("/foo"); @@ -65,7 +65,8 @@ public void customizedMapping() { assertThat(config.getAllowedMethods()).isEqualTo(Collections.singletonList("DELETE")); assertThat(config.getAllowedHeaders()).isEqualTo(Arrays.asList("header1", "header2")); assertThat(config.getExposedHeaders()).isEqualTo(Arrays.asList("header3", "header4")); - assertThat(config.getAllowCredentials()).isFalse(); + assertThat(config.getAllowCredentials()).isTrue(); + assertThat(config.getAllowPrivateNetwork()).isTrue(); assertThat(config.getMaxAge()).isEqualTo(Long.valueOf(3600)); } @@ -95,6 +96,7 @@ void combine() { assertThat(config.getAllowedHeaders()).isEqualTo(Collections.singletonList("*")); assertThat(config.getExposedHeaders()).isEmpty(); assertThat(config.getAllowCredentials()).isNull(); + assertThat(config.getAllowPrivateNetwork()).isNull(); assertThat(config.getMaxAge()).isEqualTo(Long.valueOf(1800)); } } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/function/SseServerResponseTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/function/SseServerResponseTests.java index 0b9aff9d27d6..b889951cfefd 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/function/SseServerResponseTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/function/SseServerResponseTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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. @@ -33,6 +33,7 @@ /** * @author Arjen Poutsma + * @author Sebastien Deleuze */ class SseServerResponseTests { @@ -89,6 +90,33 @@ void sendObject() throws Exception { assertThat(this.mockResponse.getContentAsString()).isEqualTo(expected); } + @Test + void sendObjectWithPrettyPrint() throws Exception { + Person person = new Person("John Doe", 42); + ServerResponse response = ServerResponse.sse(sse -> { + try { + sse.send(person); + } + catch (IOException ex) { + throw new UncheckedIOException(ex); + } + }); + + MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(); + converter.setPrettyPrint(true); + ServerResponse.Context context = () -> Collections.singletonList(converter); + + ModelAndView mav = response.writeTo(this.mockRequest, this.mockResponse, context); + assertThat(mav).isNull(); + + String expected = "data:{\n" + + "data: \"name\" : \"John Doe\",\n" + + "data: \"age\" : 42\n" + + "data:}\n" + + "\n"; + assertThat(this.mockResponse.getContentAsString()).isEqualTo(expected); + } + @Test void builder() throws Exception { ServerResponse response = ServerResponse.sse(sse -> { diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/function/support/HandlerFunctionAdapterTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/function/support/HandlerFunctionAdapterTests.java new file mode 100644 index 000000000000..5b40ddba5b54 --- /dev/null +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/function/support/HandlerFunctionAdapterTests.java @@ -0,0 +1,108 @@ +/* + * Copyright 2002-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.web.servlet.function.support; + +import java.io.IOException; +import java.util.Collections; + +import javax.servlet.AsyncEvent; +import javax.servlet.http.HttpServletResponse; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.http.converter.StringHttpMessageConverter; +import org.springframework.web.context.request.async.AsyncRequestNotUsableException; +import org.springframework.web.context.request.async.StandardServletAsyncWebRequest; +import org.springframework.web.context.request.async.WebAsyncManager; +import org.springframework.web.context.request.async.WebAsyncUtils; +import org.springframework.web.servlet.function.HandlerFunction; +import org.springframework.web.servlet.function.RouterFunctions; +import org.springframework.web.servlet.function.ServerRequest; +import org.springframework.web.servlet.function.ServerResponse; +import org.springframework.web.testfixture.servlet.MockAsyncContext; +import org.springframework.web.testfixture.servlet.MockHttpServletRequest; +import org.springframework.web.testfixture.servlet.MockHttpServletResponse; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.BDDMockito.doThrow; +import static org.mockito.BDDMockito.mock; + +/** + * Unit tests for {@link HandlerFunctionAdapter}. + * + * @author Rossen Stoyanchev + */ +public class HandlerFunctionAdapterTests { + + private final MockHttpServletRequest servletRequest = new MockHttpServletRequest("GET", "/"); + + private final MockHttpServletResponse servletResponse = new MockHttpServletResponse(); + + private final HandlerFunctionAdapter adapter = new HandlerFunctionAdapter(); + + + @BeforeEach + void setUp() { + this.servletRequest.setAttribute(RouterFunctions.REQUEST_ATTRIBUTE, + ServerRequest.create(this.servletRequest, Collections.singletonList(new StringHttpMessageConverter()))); + } + + + @Test + void asyncRequestNotUsable() throws Exception { + + HandlerFunction handler = request -> ServerResponse.sse(sseBuilder -> { + try { + sseBuilder.data("data 1"); + sseBuilder.data("data 2"); + } + catch (IOException ex) { + throw new RuntimeException(ex); + } + }); + + this.servletRequest.setAsyncSupported(true); + + HttpServletResponse mockServletResponse = mock(HttpServletResponse.class); + doThrow(new IOException("Broken pipe")).when(mockServletResponse).getOutputStream(); + + // Use of response should be rejected + assertThatThrownBy(() -> adapter.handle(servletRequest, mockServletResponse, handler)) + .hasRootCauseInstanceOf(IOException.class) + .hasRootCauseMessage("Broken pipe"); + } + + @Test + void asyncRequestNotUsableOnAsyncDispatch() throws Exception { + + HandlerFunction handler = request -> ServerResponse.ok().body("body"); + + // Put AsyncWebRequest in ERROR state + StandardServletAsyncWebRequest asyncRequest = new StandardServletAsyncWebRequest(servletRequest, servletResponse); + asyncRequest.onError(new AsyncEvent(new MockAsyncContext(servletRequest, servletResponse), new Exception())); + + // Set it as the current AsyncWebRequest, from the initial REQUEST dispatch + WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(servletRequest); + asyncManager.setAsyncWebRequest(asyncRequest); + + // Use of response should be rejected + assertThatThrownBy(() -> adapter.handle(servletRequest, servletResponse, handler)) + .isInstanceOf(AsyncRequestNotUsableException.class); + } + +} diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/condition/ProducesRequestConditionTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/condition/ProducesRequestConditionTests.java index fc1d77166acb..feee0152aba2 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/condition/ProducesRequestConditionTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/condition/ProducesRequestConditionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 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. @@ -310,6 +310,17 @@ public void compareToEqualMatch() { assertThat(result > 0).as("Should have used MediaType.equals(Object) to break the match").isTrue(); } + @Test + public void compareEmptyInvalidAccept() { + HttpServletRequest request = createRequest("foo"); + + ProducesRequestCondition condition1 = new ProducesRequestCondition(); + ProducesRequestCondition condition2 = new ProducesRequestCondition(); + + int result = condition1.compareTo(condition2, request); + assertThat(result).isEqualTo(0); + } + @Test public void combine() { ProducesRequestCondition condition1 = new ProducesRequestCondition("text/plain"); diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoHandlerMappingTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoHandlerMappingTests.java index aa31dc21b818..f0f8eddbdd17 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoHandlerMappingTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoHandlerMappingTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 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. @@ -389,6 +389,18 @@ void handleMatchMatrixVariablesDecoding(TestRequestMappingInfoHandlerMapping map assertThat(uriVariables.get("cars")).isEqualTo("cars"); } + @PathPatternsParameterizedTest // gh-29611 + void handleNoMatchWithoutPartialMatches(TestRequestMappingInfoHandlerMapping mapping) throws Exception { + String path = "/non-existent"; + MockHttpServletRequest request = new MockHttpServletRequest("GET", path); + + HandlerMethod handlerMethod = mapping.handleNoMatch(new HashSet<>(), path, request); + assertThat(handlerMethod).isNull(); + + handlerMethod = mapping.handleNoMatch(null, path, request); + assertThat(handlerMethod).isNull(); + } + private HandlerMethod getHandler( TestRequestMappingInfoHandlerMapping mapping, MockHttpServletRequest request) throws Exception { diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/MatrixVariablesMapMethodArgumentResolverTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/MatrixVariablesMapMethodArgumentResolverTests.java index b33e15cf1c4a..555512125648 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/MatrixVariablesMapMethodArgumentResolverTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/MatrixVariablesMapMethodArgumentResolverTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 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,6 +21,7 @@ import java.util.LinkedHashMap; import java.util.Map; +import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -155,6 +156,18 @@ public void resolveArgumentNoParams() throws Exception { assertThat(map).isEqualTo(Collections.emptyMap()); } + @Test + public void resolveMultiValueMapArgumentNoParams() throws Exception { + + MethodParameter param = this.testMethod.annot(matrixAttribute().noPathVar()) + .arg(MultiValueMap.class, String.class, String.class); + + Object result = this.resolver.resolveArgument(param, this.mavContainer, this.webRequest, null); + + assertThat(result).isInstanceOf(MultiValueMap.class) + .asInstanceOf(InstanceOfAssertFactories.MAP).isEmpty(); + } + @Test public void resolveArgumentNoMatch() throws Exception { MultiValueMap params2 = getVariablesFor("planes"); diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/MvcUriComponentsBuilderTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/MvcUriComponentsBuilderTests.java index 99abb24debdf..2b5da88e0d5e 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/MvcUriComponentsBuilderTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/MvcUriComponentsBuilderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2023 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,6 +21,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import java.sql.Savepoint; import java.util.Arrays; import java.util.List; import java.util.Optional; @@ -286,6 +287,14 @@ public void fromMethodNameWithMetaAnnotation() { assertThat(uriComponents.toUriString()).isEqualTo("http://localhost/input"); } + @Test + public void fromMethodCallOnSubclass() { + UriComponents uriComponents = fromMethodCall(on(ExtendedController.class).myMethod(null)).build(); + + assertThat(uriComponents.toUriString()).startsWith("http://localhost"); + assertThat(uriComponents.toUriString()).endsWith("/extended/else"); + } + @Test public void fromMethodCallPlain() { UriComponents uriComponents = fromMethodCall(on(ControllerWithMethods.class).myMethod(null)).build(); @@ -295,11 +304,27 @@ public void fromMethodCallPlain() { } @Test - public void fromMethodCallOnSubclass() { - UriComponents uriComponents = fromMethodCall(on(ExtendedController.class).myMethod(null)).build(); + public void fromMethodCallPlainWithNoArguments() { + UriComponents uriComponents = fromMethodCall(on(ControllerWithMethods.class).myMethod()).build(); assertThat(uriComponents.toUriString()).startsWith("http://localhost"); - assertThat(uriComponents.toUriString()).endsWith("/extended/else"); + assertThat(uriComponents.toUriString()).endsWith("/something/noarg"); + } + + @Test + public void fromMethodCallPlainOnInterface() { + UriComponents uriComponents = fromMethodCall(on(ControllerInterface.class).myMethod(null)).build(); + + assertThat(uriComponents.toUriString()).startsWith("http://localhost"); + assertThat(uriComponents.toUriString()).endsWith("/something/else"); + } + + @Test + public void fromMethodCallPlainWithNoArgumentsOnInterface() { + UriComponents uriComponents = fromMethodCall(on(ControllerInterface.class).myMethod()).build(); + + assertThat(uriComponents.toUriString()).startsWith("http://localhost"); + assertThat(uriComponents.toUriString()).endsWith("/something/noarg"); } @Test @@ -378,7 +403,7 @@ public void fromMethodCallWithObjectReturnType() { assertThat(uriComponents.encode().toUri().toString()).isEqualTo("http://localhost/hotels/42/bookings/21"); } - @Test // SPR-16710 + @Test // SPR-16710 public void fromMethodCallWithStringReturnType() { assertThatIllegalStateException().isThrownBy(() -> { UriComponents uriComponents = fromMethodCall( @@ -395,9 +420,24 @@ public void fromMethodNameWithStringReturnType() { assertThat(uriComponents.encode().toUri().toString()).isEqualTo("http://localhost/hotels/42/bookings/21"); } + @Test // gh-30210 + public void fromMethodCallWithCharSequenceReturnType() { + UriComponents uriComponents = fromMethodCall( + on(BookingControllerWithCharSequence.class).getBooking(21L)).buildAndExpand(42); + + assertThat(uriComponents.encode().toUri().toString()).isEqualTo("http://localhost/hotels/42/bookings/21"); + } + + @Test // gh-30210 + public void fromMethodCallWithJdbc30115ReturnType() { + UriComponents uriComponents = fromMethodCall( + on(BookingControllerWithJdbcSavepoint.class).getBooking(21L)).buildAndExpand(42); + + assertThat(uriComponents.encode().toUri().toString()).isEqualTo("http://localhost/hotels/42/bookings/21"); + } + @Test public void fromMappingNamePlain() { - initWebApplicationContext(WebConfig.class); this.request.setServerName("example.org"); @@ -411,7 +451,6 @@ public void fromMappingNamePlain() { @Test public void fromMappingNameWithCustomBaseUrl() { - initWebApplicationContext(WebConfig.class); UriComponentsBuilder baseUrl = UriComponentsBuilder.fromUriString("https://example.org:9999/base"); @@ -420,9 +459,8 @@ public void fromMappingNameWithCustomBaseUrl() { assertThat(url).isEqualTo("https://example.org:9999/base/people/123/addresses/DE"); } - @Test // SPR-17027 + @Test // SPR-17027 public void fromMappingNameWithEncoding() { - initWebApplicationContext(WebConfig.class); this.request.setServerName("example.org"); @@ -436,7 +474,6 @@ public void fromMappingNameWithEncoding() { @Test public void fromMappingNameWithPathWithoutLeadingSlash() { - initWebApplicationContext(PathWithoutLeadingSlashConfig.class); this.request.setServerName("example.org"); @@ -450,7 +487,6 @@ public void fromMappingNameWithPathWithoutLeadingSlash() { @Test public void fromControllerWithPrefix() { - initWebApplicationContext(PathPrefixWebConfig.class); this.request.setScheme("https"); @@ -463,7 +499,6 @@ public void fromControllerWithPrefix() { @Test public void fromMethodWithPrefix() { - initWebApplicationContext(PathPrefixWebConfig.class); this.request.setScheme("https"); @@ -542,6 +577,11 @@ HttpEntity myMethod(@RequestBody Object payload) { return null; } + @RequestMapping("/noarg") + HttpEntity myMethod() { + return null; + } + @RequestMapping("/{id}/foo") HttpEntity methodWithPathVariable(@PathVariable String id) { return null; @@ -583,6 +623,17 @@ static class ExtendedController extends ControllerWithMethods { } + @RequestMapping("/something") + public interface ControllerInterface { + + @RequestMapping("/else") + HttpEntity myMethod(@RequestBody Object payload); + + @RequestMapping("/noarg") + HttpEntity myMethod(); + } + + @RequestMapping("/user/{userId}/contacts") static class UserContactController { @@ -701,4 +752,26 @@ public String getBooking(@PathVariable Long booking) { } } + + @Controller + @RequestMapping("/hotels/{hotel}") + static class BookingControllerWithCharSequence { + + @GetMapping("/bookings/{booking}") + public CharSequence getBooking(@PathVariable Long booking) { + return "url"; + } + } + + + @Controller + @RequestMapping("/hotels/{hotel}") + static class BookingControllerWithJdbcSavepoint { + + @GetMapping("/bookings/{booking}") + public Savepoint getBooking(@PathVariable Long booking) { + return null; + } + } + } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapterTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapterTests.java index 32ecd653d704..13a61b602078 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapterTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-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,8 +16,11 @@ package org.springframework.web.servlet.mvc.method.annotation; +import java.io.IOException; +import java.io.OutputStream; import java.lang.reflect.Method; import java.lang.reflect.Type; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -25,6 +28,8 @@ import java.util.List; import java.util.Map; +import javax.servlet.AsyncEvent; + import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -46,6 +51,10 @@ import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.SessionAttributes; +import org.springframework.web.context.request.async.AsyncRequestNotUsableException; +import org.springframework.web.context.request.async.StandardServletAsyncWebRequest; +import org.springframework.web.context.request.async.WebAsyncManager; +import org.springframework.web.context.request.async.WebAsyncUtils; import org.springframework.web.context.support.StaticWebApplicationContext; import org.springframework.web.method.HandlerMethod; import org.springframework.web.method.annotation.ModelMethodProcessor; @@ -55,13 +64,15 @@ import org.springframework.web.servlet.DispatcherServlet; import org.springframework.web.servlet.FlashMap; import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.testfixture.servlet.MockAsyncContext; import org.springframework.web.testfixture.servlet.MockHttpServletRequest; import org.springframework.web.testfixture.servlet.MockHttpServletResponse; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; /** - * Unit tests for {@link RequestMappingHandlerAdapter}. + * Tests for {@link RequestMappingHandlerAdapter}. * * @author Rossen Stoyanchev * @author Sam Brannen @@ -250,9 +261,7 @@ public void modelAttributePackageNameAdvice() throws Exception { assertThat(mav.getModel().get("attr3")).isNull(); } - // SPR-10859 - - @Test + @Test // gh-15486 public void responseBodyAdvice() throws Exception { List> converters = new ArrayList<>(); converters.add(new MappingJackson2HttpMessageConverter()); @@ -272,6 +281,26 @@ public void responseBodyAdvice() throws Exception { assertThat(this.response.getContentAsString()).isEqualTo("{\"status\":400,\"message\":\"body\"}"); } + @Test + void asyncRequestNotUsable() throws Exception { + + // Put AsyncWebRequest in ERROR state + StandardServletAsyncWebRequest asyncRequest = new StandardServletAsyncWebRequest(this.request, this.response); + asyncRequest.onError(new AsyncEvent(new MockAsyncContext(this.request, this.response), new Exception())); + + // Set it as the current AsyncWebRequest, from the initial REQUEST dispatch + WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(this.request); + asyncManager.setAsyncWebRequest(asyncRequest); + + // AsyncWebRequest created for current dispatch should inherit state + HandlerMethod handlerMethod = handlerMethod(new SimpleController(), "handleOutputStream", OutputStream.class); + this.handlerAdapter.afterPropertiesSet(); + + // Use of response should be rejected + assertThatThrownBy(() -> this.handlerAdapter.handle(this.request, this.response, handlerMethod)) + .isInstanceOf(AsyncRequestNotUsableException.class); + } + private HandlerMethod handlerMethod(Object handler, String methodName, Class... paramTypes) throws Exception { Method method = handler.getClass().getDeclaredMethod(methodName, paramTypes); return new InvocableHandlerMethod(handler, method); @@ -297,14 +326,16 @@ public String handle() { } public ResponseEntity> handleWithResponseEntity() { - return new ResponseEntity<>(Collections.singletonMap( - "foo", "bar"), HttpStatus.OK); + return new ResponseEntity<>(Collections.singletonMap("foo", "bar"), HttpStatus.OK); } public ResponseEntity handleBadRequest() { return new ResponseEntity<>("body", HttpStatus.BAD_REQUEST); } + public void handleOutputStream(OutputStream outputStream) throws IOException { + outputStream.write("body".getBytes(StandardCharsets.UTF_8)); + } } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestPartMethodArgumentResolverTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestPartMethodArgumentResolverTests.java index cbde3c20b23f..bcf9fe69d559 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestPartMethodArgumentResolverTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestPartMethodArgumentResolverTests.java @@ -33,7 +33,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.springframework.core.LocalVariableTableParameterNameDiscoverer; +import org.springframework.core.DefaultParameterNameDiscoverer; import org.springframework.core.MethodParameter; import org.springframework.core.annotation.SynthesizingMethodParameter; import org.springframework.http.HttpInputMessage; @@ -138,7 +138,7 @@ public InputStream getInputStream() throws IOException { Method method = ReflectionUtils.findMethod(getClass(), "handle", (Class[]) null); paramRequestPart = new SynthesizingMethodParameter(method, 0); - paramRequestPart.initParameterNameDiscovery(new LocalVariableTableParameterNameDiscoverer()); + paramRequestPart.initParameterNameDiscovery(new DefaultParameterNameDiscoverer()); paramNamedRequestPart = new SynthesizingMethodParameter(method, 1); paramValidRequestPart = new SynthesizingMethodParameter(method, 2); paramMultipartFile = new SynthesizingMethodParameter(method, 3); @@ -146,20 +146,20 @@ public InputStream getInputStream() throws IOException { paramMultipartFileArray = new SynthesizingMethodParameter(method, 5); paramInt = new SynthesizingMethodParameter(method, 6); paramMultipartFileNotAnnot = new SynthesizingMethodParameter(method, 7); - paramMultipartFileNotAnnot.initParameterNameDiscovery(new LocalVariableTableParameterNameDiscoverer()); + paramMultipartFileNotAnnot.initParameterNameDiscovery(new DefaultParameterNameDiscoverer()); paramPart = new SynthesizingMethodParameter(method, 8); - paramPart.initParameterNameDiscovery(new LocalVariableTableParameterNameDiscoverer()); + paramPart.initParameterNameDiscovery(new DefaultParameterNameDiscoverer()); paramPartList = new SynthesizingMethodParameter(method, 9); paramPartArray = new SynthesizingMethodParameter(method, 10); paramRequestParamAnnot = new SynthesizingMethodParameter(method, 11); optionalMultipartFile = new SynthesizingMethodParameter(method, 12); - optionalMultipartFile.initParameterNameDiscovery(new LocalVariableTableParameterNameDiscoverer()); + optionalMultipartFile.initParameterNameDiscovery(new DefaultParameterNameDiscoverer()); optionalMultipartFileList = new SynthesizingMethodParameter(method, 13); - optionalMultipartFileList.initParameterNameDiscovery(new LocalVariableTableParameterNameDiscoverer()); + optionalMultipartFileList.initParameterNameDiscovery(new DefaultParameterNameDiscoverer()); optionalPart = new SynthesizingMethodParameter(method, 14); - optionalPart.initParameterNameDiscovery(new LocalVariableTableParameterNameDiscoverer()); + optionalPart.initParameterNameDiscovery(new DefaultParameterNameDiscoverer()); optionalPartList = new SynthesizingMethodParameter(method, 15); - optionalPartList.initParameterNameDiscovery(new LocalVariableTableParameterNameDiscoverer()); + optionalPartList.initParameterNameDiscovery(new DefaultParameterNameDiscoverer()); optionalRequestPart = new SynthesizingMethodParameter(method, 16); } @@ -204,7 +204,7 @@ public void resolveMultipartFileArray() throws Exception { assertThat(actual).isNotNull(); assertThat(actual instanceof MultipartFile[]).isTrue(); MultipartFile[] parts = (MultipartFile[]) actual; - assertThat(parts.length).isEqualTo(2); + assertThat(parts).hasSize(2); assertThat(multipartFile1).isEqualTo(parts[0]); assertThat(multipartFile2).isEqualTo(parts[1]); } @@ -270,7 +270,7 @@ public void resolvePartArrayArgument() throws Exception { Object result = resolver.resolveArgument(paramPartArray, null, webRequest, null); assertThat(result instanceof Part[]).isTrue(); Part[] parts = (Part[]) result; - assertThat(parts.length).isEqualTo(2); + assertThat(parts).hasSize(2); assertThat(part1).isEqualTo(parts[0]); assertThat(part2).isEqualTo(parts[1]); } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandlerTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandlerTests.java index e5fa5c684733..29d2a54f65ce 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandlerTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandlerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-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. @@ -92,6 +92,9 @@ public void supportsAllDefaultHandlerExceptionResolverExceptionTypes() throws Ex Class[] paramTypes = method.getParameterTypes(); if (method.getName().startsWith("handle") && (paramTypes.length == 4)) { String name = paramTypes[0].getSimpleName(); + if (name.equals("AsyncRequestNotUsableException")) { + continue; + } assertThat(exceptionTypes.contains(paramTypes[0])).as("@ExceptionHandler is missing " + name).isTrue(); } } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/tags/form/ItemPet.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/tags/form/ItemPet.java index 18d833cbb421..7d03434599f7 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/tags/form/ItemPet.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/tags/form/ItemPet.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2023 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,8 @@ import java.beans.PropertyEditorSupport; +import org.springframework.util.ObjectUtils; + /** * @author Juergen Hoeller */ @@ -50,7 +52,7 @@ public boolean equals(Object other) { return false; } ItemPet otherPet = (ItemPet) other; - return (this.name != null && this.name.equals(otherPet.getName())); + return ObjectUtils.nullSafeEquals(this.name, otherPet.getName()); } @Override diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/view/freemarker/FreeMarkerViewTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/view/freemarker/FreeMarkerViewTests.java index d9fd65dce96b..a7a79aa16593 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/view/freemarker/FreeMarkerViewTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/view/freemarker/FreeMarkerViewTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 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. @@ -145,6 +145,34 @@ public void keepExistingContentType() throws Exception { assertThat(response.getContentType()).isEqualTo("myContentType"); } + @Test + public void requestAttributeVisible() throws Exception { + FreeMarkerView fv = new FreeMarkerView(); + + WebApplicationContext wac = mock(WebApplicationContext.class); + MockServletContext sc = new MockServletContext(); + + Map configs = new HashMap<>(); + FreeMarkerConfigurer configurer = new FreeMarkerConfigurer(); + configurer.setConfiguration(new TestConfiguration()); + configurer.setServletContext(sc); + configs.put("configurer", configurer); + given(wac.getBeansOfType(FreeMarkerConfig.class, true, false)).willReturn(configs); + given(wac.getServletContext()).willReturn(sc); + + fv.setUrl("templateName"); + fv.setApplicationContext(wac); + + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addPreferredLocale(Locale.US); + request.setAttribute(DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE, wac); + request.setAttribute(DispatcherServlet.LOCALE_RESOLVER_ATTRIBUTE, new AcceptHeaderLocaleResolver()); + HttpServletResponse response = new MockHttpServletResponse(); + + request.setAttribute("myattr", "myvalue"); + fv.render(null, request, response); + } + @Test public void freeMarkerViewResolver() throws Exception { MockServletContext sc = new MockServletContext(); @@ -191,10 +219,9 @@ public Template getTemplate(String name, final Locale locale) throws IOException @Override public void process(Object model, Writer writer) throws TemplateException, IOException { assertThat(locale).isEqualTo(Locale.US); - boolean condition = model instanceof AllHttpScopesHashModel; - assertThat(condition).isTrue(); + assertThat(model instanceof AllHttpScopesHashModel).isTrue(); AllHttpScopesHashModel fmModel = (AllHttpScopesHashModel) model; - assertThat(fmModel.get("myattr").toString()).isEqualTo("myvalue"); + assertThat(String.valueOf(fmModel.get("myattr"))).isEqualTo("myvalue"); } }; } diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/config/WebSocketMessageBrokerStats.java b/spring-websocket/src/main/java/org/springframework/web/socket/config/WebSocketMessageBrokerStats.java index 16e1b2f51393..7bb3cfc828fc 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/config/WebSocketMessageBrokerStats.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/config/WebSocketMessageBrokerStats.java @@ -26,6 +26,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.SmartInitializingSingleton; import org.springframework.core.task.TaskExecutor; import org.springframework.lang.Nullable; import org.springframework.messaging.simp.stomp.StompBrokerRelayMessageHandler; @@ -53,7 +54,7 @@ * @author Sam Brannen * @since 4.1 */ -public class WebSocketMessageBrokerStats { +public class WebSocketMessageBrokerStats implements SmartInitializingSingleton { private static final Log logger = LogFactory.getLog(WebSocketMessageBrokerStats.class); @@ -84,24 +85,6 @@ public class WebSocketMessageBrokerStats { public void setSubProtocolWebSocketHandler(SubProtocolWebSocketHandler webSocketHandler) { this.webSocketHandler = webSocketHandler; - this.stompSubProtocolHandler = initStompSubProtocolHandler(); - } - - @Nullable - private StompSubProtocolHandler initStompSubProtocolHandler() { - if (this.webSocketHandler == null) { - return null; - } - for (SubProtocolHandler handler : this.webSocketHandler.getProtocolHandlers()) { - if (handler instanceof StompSubProtocolHandler) { - return (StompSubProtocolHandler) handler; - } - } - SubProtocolHandler defaultHandler = this.webSocketHandler.getDefaultProtocolHandler(); - if (defaultHandler instanceof StompSubProtocolHandler) { - return (StompSubProtocolHandler) defaultHandler; - } - return null; } public void setStompBrokerRelay(StompBrokerRelayMessageHandler stompBrokerRelay) { @@ -118,17 +101,6 @@ public void setOutboundChannelExecutor(TaskExecutor outboundChannelExecutor) { public void setSockJsTaskScheduler(TaskScheduler sockJsTaskScheduler) { this.sockJsTaskScheduler = sockJsTaskScheduler; - this.loggingTask = initLoggingTask(TimeUnit.MINUTES.toMillis(1)); - } - - @Nullable - private ScheduledFuture initLoggingTask(long initialDelay) { - if (this.sockJsTaskScheduler != null && this.loggingPeriod > 0 && logger.isInfoEnabled()) { - return this.sockJsTaskScheduler.scheduleWithFixedDelay( - () -> logger.info(WebSocketMessageBrokerStats.this.toString()), - Instant.now().plusMillis(initialDelay), Duration.ofMillis(this.loggingPeriod)); - } - return null; } /** @@ -137,11 +109,11 @@ private ScheduledFuture initLoggingTask(long initialDelay) { *

    By default this property is set to 30 minutes (30 * 60 * 1000). */ public void setLoggingPeriod(long period) { + this.loggingPeriod = period; if (this.loggingTask != null) { this.loggingTask.cancel(true); + this.loggingTask = initLoggingTask(0); } - this.loggingPeriod = period; - this.loggingTask = initLoggingTask(0); } /** @@ -151,6 +123,41 @@ public long getLoggingPeriod() { return this.loggingPeriod; } + + @Override + public void afterSingletonsInstantiated() { + this.stompSubProtocolHandler = initStompSubProtocolHandler(); + this.loggingTask = initLoggingTask(TimeUnit.MINUTES.toMillis(1)); + } + + @Nullable + private StompSubProtocolHandler initStompSubProtocolHandler() { + if (this.webSocketHandler == null) { + return null; + } + for (SubProtocolHandler handler : this.webSocketHandler.getProtocolHandlers()) { + if (handler instanceof StompSubProtocolHandler) { + return (StompSubProtocolHandler) handler; + } + } + SubProtocolHandler defaultHandler = this.webSocketHandler.getDefaultProtocolHandler(); + if (defaultHandler instanceof StompSubProtocolHandler) { + return (StompSubProtocolHandler) defaultHandler; + } + return null; + } + + @Nullable + private ScheduledFuture initLoggingTask(long initialDelay) { + if (this.sockJsTaskScheduler != null && this.loggingPeriod > 0 && logger.isInfoEnabled()) { + return this.sockJsTaskScheduler.scheduleWithFixedDelay( + () -> logger.info(WebSocketMessageBrokerStats.this.toString()), + Instant.now().plusMillis(initialDelay), Duration.ofMillis(this.loggingPeriod)); + } + return null; + } + + /** * Get stats about WebSocket sessions. */ diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/EnableWebSocket.java b/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/EnableWebSocket.java index 85a0f781eb95..2c406ea8a5d3 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/EnableWebSocket.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/EnableWebSocket.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 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. @@ -49,7 +49,7 @@ * registry.addHandler(echoWebSocketHandler(), "/echo").withSockJS(); * } * - * @Override + * @Bean * public WebSocketHandler echoWebSocketHandler() { * return new EchoWebSocketHandler(); * } diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/handler/ConcurrentWebSocketSessionDecorator.java b/spring-websocket/src/main/java/org/springframework/web/socket/handler/ConcurrentWebSocketSessionDecorator.java index 2e072035e42a..8e18c60f373a 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/handler/ConcurrentWebSocketSessionDecorator.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/handler/ConcurrentWebSocketSessionDecorator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-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. @@ -248,30 +248,31 @@ private void limitExceeded(String reason) { @Override public void close(CloseStatus status) throws IOException { - this.closeLock.lock(); - try { - if (this.closeInProgress) { - return; - } - if (!CloseStatus.SESSION_NOT_RELIABLE.equals(status)) { - try { - checkSessionLimits(); - } - catch (SessionLimitExceededException ex) { - // Ignore + if (this.closeLock.tryLock()) { + try { + if (this.closeInProgress) { + return; } - if (this.limitExceeded) { - if (logger.isDebugEnabled()) { - logger.debug("Changing close status " + status + " to SESSION_NOT_RELIABLE."); + if (!CloseStatus.SESSION_NOT_RELIABLE.equals(status)) { + try { + checkSessionLimits(); + } + catch (SessionLimitExceededException ex) { + // Ignore + } + if (this.limitExceeded) { + if (logger.isDebugEnabled()) { + logger.debug("Changing close status " + status + " to SESSION_NOT_RELIABLE."); + } + status = CloseStatus.SESSION_NOT_RELIABLE; } - status = CloseStatus.SESSION_NOT_RELIABLE; } + this.closeInProgress = true; + super.close(status); + } + finally { + this.closeLock.unlock(); } - this.closeInProgress = true; - super.close(status); - } - finally { - this.closeLock.unlock(); } } diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/messaging/SubProtocolWebSocketHandler.java b/spring-websocket/src/main/java/org/springframework/web/socket/messaging/SubProtocolWebSocketHandler.java index d629a9a92c64..4e1cd54e01db 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/messaging/SubProtocolWebSocketHandler.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/messaging/SubProtocolWebSocketHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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.TreeMap; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import org.apache.commons.logging.Log; @@ -47,6 +48,7 @@ import org.springframework.web.socket.WebSocketSession; import org.springframework.web.socket.handler.ConcurrentWebSocketSessionDecorator; import org.springframework.web.socket.handler.SessionLimitExceededException; +import org.springframework.web.socket.handler.WebSocketSessionDecorator; import org.springframework.web.socket.sockjs.transport.session.PollingSockJsSession; import org.springframework.web.socket.sockjs.transport.session.StreamingSockJsSession; @@ -97,7 +99,7 @@ public class SubProtocolWebSocketHandler private volatile long lastSessionCheckTime = System.currentTimeMillis(); - private final ReentrantLock sessionCheckLock = new ReentrantLock(); + private final Lock sessionCheckLock = new ReentrantLock(); private final DefaultStats stats = new DefaultStats(); @@ -267,7 +269,7 @@ public Stats getStats() { @Override public final void start() { - Assert.isTrue(this.defaultProtocolHandler != null || !this.protocolHandlers.isEmpty(), "No handlers"); + Assert.state(this.defaultProtocolHandler != null || !this.protocolHandlers.isEmpty(), "No handlers"); synchronized (this.lifecycleMonitor) { this.clientOutboundChannel.subscribe(this); @@ -669,6 +671,7 @@ void incrementTransportError() { } AtomicInteger getCountFor(WebSocketSession session) { + session = WebSocketSessionDecorator.unwrap(session); if (session instanceof PollingSockJsSession) { return this.httpPolling; } diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/server/jetty/Jetty10RequestUpgradeStrategy.java b/spring-websocket/src/main/java/org/springframework/web/socket/server/jetty/Jetty10RequestUpgradeStrategy.java index 0391bbf5f949..c4ac70007423 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/server/jetty/Jetty10RequestUpgradeStrategy.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/server/jetty/Jetty10RequestUpgradeStrategy.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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.web.socket.server.jetty; import java.lang.reflect.Method; +import java.lang.reflect.Proxy; import java.lang.reflect.UndeclaredThrowableException; import java.security.Principal; import java.util.Collections; @@ -27,16 +28,10 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import org.aopalliance.intercept.MethodInterceptor; -import org.aopalliance.intercept.MethodInvocation; - -import org.springframework.aop.framework.ProxyFactory; -import org.springframework.aop.target.EmptyTargetSource; import org.springframework.http.server.ServerHttpRequest; import org.springframework.http.server.ServerHttpResponse; import org.springframework.http.server.ServletServerHttpRequest; import org.springframework.http.server.ServletServerHttpResponse; -import org.springframework.lang.NonNull; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ReflectionUtils; @@ -128,40 +123,14 @@ public void upgrade(ServerHttpRequest request, ServerHttpResponse response, private static Object createJettyWebSocketCreator( Jetty10WebSocketHandlerAdapter adapter, @Nullable String protocol) { - ProxyFactory factory = new ProxyFactory(EmptyTargetSource.INSTANCE); - factory.addInterface(webSocketCreatorClass); - factory.addAdvice(new WebSocketCreatorInterceptor(adapter, protocol)); - return factory.getProxy(); - } - - - /** - * Proxy for a JettyWebSocketCreator to supply the WebSocket handler and set the sub-protocol. - */ - private static class WebSocketCreatorInterceptor implements MethodInterceptor { - - private final Jetty10WebSocketHandlerAdapter adapter; - - @Nullable - private final String protocol; - - - public WebSocketCreatorInterceptor( - Jetty10WebSocketHandlerAdapter adapter, @Nullable String protocol) { - - this.adapter = adapter; - this.protocol = protocol; - } - - @Nullable - @Override - public Object invoke(@NonNull MethodInvocation invocation) { - if (this.protocol != null) { - ReflectionUtils.invokeMethod( - setAcceptedSubProtocol, invocation.getArguments()[1], this.protocol); - } - return this.adapter; - } + return Proxy.newProxyInstance( + webSocketCreatorClass.getClassLoader(), new Class[] {webSocketCreatorClass}, + (proxy, method, args) -> { + if (protocol != null) { + ReflectionUtils.invokeMethod(setAcceptedSubProtocol, args[1], protocol); + } + return adapter; + }); } } diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/AbstractSockJsService.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/AbstractSockJsService.java index 39f6d2436b19..3b9fcfe0637e 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/AbstractSockJsService.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/AbstractSockJsService.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 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. @@ -59,7 +59,7 @@ * path resolution and handling of static SockJS requests (e.g. "/info", "/iframe.html", * etc). Sub-classes must handle session URLs (i.e. transport-specific requests). * - * By default, only same origin requests are allowed. Use {@link #setAllowedOrigins} + *

    By default, only same origin requests are allowed. Use {@link #setAllowedOrigins} * to specify a list of allowed origins (a list containing "*" will allow all origins). * * @author Rossen Stoyanchev @@ -620,6 +620,7 @@ private class IframeHandler implements SockJsRequestHandler { "\n" + " \n" + " \n" + + " SockJS iframe\n" + "