mirror of
https://github.com/spring-projects/spring-framework
synced 2026-06-08 17:33:33 +00:00
Compare commits
67 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9b59345547 | |||
| 611d3e5551 | |||
| fa2a58b9db | |||
| 83ca2c0cff | |||
| dc16f3cffb | |||
| f2b3263fff | |||
| 1d890a8952 | |||
| 3ccaefe38f | |||
| 300f4585be | |||
| a0f5c16627 | |||
| b5a86dec92 | |||
| c55c64427c | |||
| a4fcd301f2 | |||
| ec4558dc14 | |||
| d6c623be40 | |||
| 86da45ba15 | |||
| 6a60298024 | |||
| f8b448e949 | |||
| 52a0e6f900 | |||
| 3d2fb3af3d | |||
| 4f38079042 | |||
| 3008d97f93 | |||
| f0c758498b | |||
| bc2deef951 | |||
| 7322a0d3c5 | |||
| 7227c30917 | |||
| 7e7f55cb23 | |||
| 84f20cd0a7 | |||
| 3d8f1fb00f | |||
| e06115c06f | |||
| 69850cad27 | |||
| bbbc95f773 | |||
| 83f7996c79 | |||
| 0544dfe090 | |||
| eacfd77d6a | |||
| 89338c91a9 | |||
| 51641ece72 | |||
| 06d267f04e | |||
| dfa6b4bd42 | |||
| 099a97740e | |||
| daea3f0eae | |||
| 8974da2a5a | |||
| ab236c7741 | |||
| 61adf2dd25 | |||
| 61894af0bd | |||
| c74666a883 | |||
| 976b4f3533 | |||
| 100da83913 | |||
| e881c70a93 | |||
| fea237c065 | |||
| 8b11ee9ee2 | |||
| 1cf5264163 | |||
| c68c6faa03 | |||
| 6dd5c85ed0 | |||
| d133ab60ee | |||
| 5d6e143ff4 | |||
| 4e2fb308f6 | |||
| 5f765fc8ce | |||
| 6b456b6157 | |||
| 000b563e83 | |||
| c571ee1f95 | |||
| 899f6308d9 | |||
| a580d6d6fc | |||
| 66eddf99af | |||
| e94ec80df4 | |||
| 2861e570fd | |||
| ee7a1e8b7e |
@@ -0,0 +1,56 @@
|
||||
name: 'Build'
|
||||
description: 'Builds the project, optionally publishing it to a local deployment repository'
|
||||
inputs:
|
||||
java-version:
|
||||
required: false
|
||||
default: '17'
|
||||
description: 'The Java version to compile and test with'
|
||||
java-distribution:
|
||||
required: false
|
||||
default: 'liberica'
|
||||
description: 'The Java distribution to use for the build'
|
||||
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-distribution: ${{ inputs.java-distribution }}
|
||||
java-toolchain: ${{ inputs.java-toolchain }}
|
||||
- name: Build
|
||||
id: build
|
||||
if: ${{ inputs.publish == 'false' }}
|
||||
shell: bash
|
||||
run: ./gradlew check antora
|
||||
- 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
|
||||
@@ -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
|
||||
@@ -0,0 +1,28 @@
|
||||
changelog:
|
||||
repository: spring-projects/spring-framework
|
||||
sections:
|
||||
- title: ":star: New Features"
|
||||
labels:
|
||||
- "type: enhancement"
|
||||
- title: ":lady_beetle: Bug Fixes"
|
||||
labels:
|
||||
- "type: bug"
|
||||
- "type: regression"
|
||||
- title: ":notebook_with_decorative_cover: Documentation"
|
||||
labels:
|
||||
- "type: documentation"
|
||||
- title: ":hammer: Dependency Upgrades"
|
||||
sort: "title"
|
||||
labels:
|
||||
- "type: dependency-upgrade"
|
||||
contributors:
|
||||
exclude:
|
||||
names:
|
||||
- "bclozel"
|
||||
- "jhoeller"
|
||||
- "poutsma"
|
||||
- "rstoyanchev"
|
||||
- "sbrannen"
|
||||
- "sdeleuze"
|
||||
- "simonbasle"
|
||||
- "snicoll"
|
||||
@@ -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: '17'
|
||||
description: 'The Java version to use for the build'
|
||||
java-distribution:
|
||||
required: false
|
||||
default: 'liberica'
|
||||
description: 'The Java distribution to use for the build'
|
||||
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-distribution }}
|
||||
java-version: |
|
||||
${{ inputs.java-version }}
|
||||
${{ inputs.java-toolchain == 'true' && '17' || '' }}
|
||||
- name: Set Up Gradle
|
||||
uses: gradle/actions/setup-gradle@dbbdc275be76ac10734476cc723d82dfe7ec6eda # v3.4.2
|
||||
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
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Send notification
|
||||
name: Send Notification
|
||||
description: Sends a Google Chat message as a notification of the job's outcome
|
||||
inputs:
|
||||
webhook-url:
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
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@7c95feb32008765e1b4e626b078dfd897c4340ad # v4.1.2
|
||||
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
|
||||
shell: bash
|
||||
run: |
|
||||
url=${{ format('https://repo.maven.apache.org/maven2/org/springframework/spring-context/{0}/spring-context-{0}.jar', inputs.spring-framework-version) }}
|
||||
echo "Waiting for $url"
|
||||
until curl --fail --head --silent $url > /dev/null
|
||||
do
|
||||
echo "."
|
||||
sleep 60
|
||||
done
|
||||
echo "$url is available"
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"files": [
|
||||
{
|
||||
"aql": {
|
||||
"items.find": {
|
||||
"$and": [
|
||||
{
|
||||
"@build.name": "${buildName}",
|
||||
"@build.number": "${buildNumber}",
|
||||
"path": {
|
||||
"$nmatch": "org/springframework/framework-api/*"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"target": "nexus/"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,49 +1,33 @@
|
||||
name: Build and deploy snapshot
|
||||
name: Build and Deploy Snapshot
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 6.1.x
|
||||
permissions:
|
||||
actions: write
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
jobs:
|
||||
build-and-deploy-snapshot:
|
||||
if: ${{ github.repository == 'spring-projects/spring-framework' }}
|
||||
name: Build and deploy snapshot
|
||||
name: Build and Deploy Snapshot
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.repository == 'spring-projects/spring-framework' }}
|
||||
steps:
|
||||
- name: Set up Java
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'liberica'
|
||||
java-version: 17
|
||||
- name: Check out code
|
||||
- name: Check Out Code
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up Gradle
|
||||
uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5
|
||||
- name: Build and Publish
|
||||
id: build-and-publish
|
||||
uses: ./.github/actions/build
|
||||
with:
|
||||
cache-read-only: false
|
||||
- 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: Build and publish
|
||||
id: build
|
||||
env:
|
||||
CI: 'true'
|
||||
GRADLE_ENTERPRISE_URL: 'https://ge.spring.io'
|
||||
DEVELOCITY_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }}
|
||||
run: ./gradlew -PdeploymentRepository=$(pwd)/deployment-repository build publishAllPublicationsToDeploymentRepository
|
||||
develocity-access-key: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }}
|
||||
publish: true
|
||||
- name: Deploy
|
||||
uses: spring-io/artifactory-deploy-action@v0.0.1
|
||||
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}', github.ref_name)}}
|
||||
build-name: 'spring-framework-6.1.x'
|
||||
repository: 'libs-snapshot-local'
|
||||
folder: 'deployment-repository'
|
||||
signing-key: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||
@@ -52,11 +36,24 @@ jobs:
|
||||
/**/framework-api-*.zip::zip.name=spring-framework,zip.deployed=false
|
||||
/**/framework-api-*-docs.zip::zip.type=docs
|
||||
/**/framework-api-*-schema.zip::zip.type=schema
|
||||
- name: Send notification
|
||||
- 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} | Linux | Java 17', github.ref_name) }}
|
||||
build-scan-url: ${{ steps.build-and-publish.outputs.build-scan-url }}
|
||||
run-name: ${{ format('{0} | Linux | Java 17', 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 }}
|
||||
|
||||
+16
-37
@@ -7,6 +7,8 @@ concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
jobs:
|
||||
ci:
|
||||
name: '${{ matrix.os.name}} | Java ${{ matrix.java.version}}'
|
||||
runs-on: ${{ matrix.os.id }}
|
||||
if: ${{ github.repository == 'spring-projects/spring-framework' }}
|
||||
strategy:
|
||||
matrix:
|
||||
@@ -18,61 +20,38 @@ jobs:
|
||||
toolchain: false
|
||||
- version: 21
|
||||
toolchain: true
|
||||
- version: 22
|
||||
toolchain: true
|
||||
- version: 23-ea
|
||||
distribution: temurin
|
||||
toolchain: true
|
||||
exclude:
|
||||
- os:
|
||||
name: Linux
|
||||
java:
|
||||
version: 17
|
||||
name: '${{ matrix.os.name}} | Java ${{ matrix.java.version}}'
|
||||
runs-on: ${{ matrix.os.id }}
|
||||
steps:
|
||||
- name: Set up Java
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'liberica'
|
||||
java-version: |
|
||||
${{ matrix.java.version }}
|
||||
${{ matrix.java.toolchain && '17' || '' }}
|
||||
- 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
|
||||
- name: Check Out Code
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up Gradle
|
||||
uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5
|
||||
with:
|
||||
cache-read-only: false
|
||||
- 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: ${{ matrix.java.toolchain }}
|
||||
shell: bash
|
||||
run: |
|
||||
echo toolchainVersion=${{ matrix.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', matrix.java.version) }} >> $HOME/.gradle/gradle.properties
|
||||
- name: Build
|
||||
id: build
|
||||
env:
|
||||
CI: 'true'
|
||||
GRADLE_ENTERPRISE_URL: 'https://ge.spring.io'
|
||||
DEVELOCITY_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }}
|
||||
run: ./gradlew check antora
|
||||
- name: Send notification
|
||||
uses: ./.github/actions/build
|
||||
with:
|
||||
java-version: ${{ matrix.java.version }}
|
||||
java-distribution: ${{ matrix.java.distribution || 'liberica' }}
|
||||
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) }}
|
||||
run-name: ${{ format('{0} | {1} | Java {2}', github.ref_name, matrix.os.name, matrix.java.version) }}
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
name: Release
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v6.1.[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:
|
||||
build-name: ${{ format('spring-framework-{0}', steps.build-and-publish.outputs.version)}}
|
||||
folder: 'deployment-repository'
|
||||
password: ${{ secrets.ARTIFACTORY_PASSWORD }}
|
||||
repository: 'libs-staging-local'
|
||||
signing-key: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||
signing-passphrase: ${{ secrets.GPG_PASSPHRASE }}
|
||||
uri: 'https://repo.spring.io'
|
||||
username: ${{ secrets.ARTIFACTORY_USERNAME }}
|
||||
outputs:
|
||||
version: ${{ steps.build-and-publish.outputs.version }}
|
||||
verify:
|
||||
name: Verify
|
||||
needs: build-and-stage-release
|
||||
uses: ./.github/workflows/verify.yml
|
||||
with:
|
||||
staging: true
|
||||
version: ${{ needs.build-and-stage-release.outputs.version }}
|
||||
secrets:
|
||||
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@7c95feb32008765e1b4e626b078dfd897c4340ad # v4.1.2
|
||||
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 }}
|
||||
+2
-4
@@ -1,13 +1,11 @@
|
||||
name: "Validate Gradle Wrapper"
|
||||
on: [push, pull_request]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
validation:
|
||||
name: "Validation"
|
||||
name: "Validate Gradle Wrapper"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: gradle/wrapper-validation-action@v2
|
||||
- uses: gradle/actions/wrapper-validation@dbbdc275be76ac10734476cc723d82dfe7ec6eda # v3.4.2
|
||||
@@ -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: 17
|
||||
- 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) }}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 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.
|
||||
@@ -63,8 +63,7 @@ class TestConventions {
|
||||
test.systemProperty("testGroups", project.getProperties().get("testGroups"));
|
||||
}
|
||||
test.jvmArgs("--add-opens=java.base/java.lang=ALL-UNNAMED",
|
||||
"--add-opens=java.base/java.util=ALL-UNNAMED",
|
||||
"-Djava.locale.providers=COMPAT");
|
||||
"--add-opens=java.base/java.util=ALL-UNNAMED");
|
||||
}
|
||||
|
||||
private void configureTestRetryPlugin(Project project, Test test) {
|
||||
|
||||
@@ -17,4 +17,13 @@ changelog:
|
||||
- "type: dependency-upgrade"
|
||||
contributors:
|
||||
exclude:
|
||||
names: ["bclozel", "jhoeller", "poutsma", "rstoyanchev", "sbrannen", "sdeleuze", "snicoll", "simonbasle"]
|
||||
names:
|
||||
- "bclozel"
|
||||
- "github-actions[bot]"
|
||||
- "jhoeller"
|
||||
- "poutsma"
|
||||
- "rstoyanchev"
|
||||
- "sbrannen"
|
||||
- "sdeleuze"
|
||||
- "simonbasle"
|
||||
- "snicoll"
|
||||
|
||||
+1
-1
@@ -201,7 +201,7 @@ the original class. Consider the following example:
|
||||
<!-- inject dependencies here as required -->
|
||||
</bean>
|
||||
|
||||
<!-- commandProcessor uses statefulCommandHelper -->
|
||||
<!-- commandManager uses myCommand prototype bean -->
|
||||
<bean id="commandManager" class="fiona.apple.CommandManager">
|
||||
<lookup-method name="createCommand" bean="myCommand"/>
|
||||
</bean>
|
||||
|
||||
@@ -211,19 +211,28 @@ the JDBC driver. If the count is not available, the JDBC driver returns a value
|
||||
====
|
||||
In such a scenario, with automatic setting of values on an underlying `PreparedStatement`,
|
||||
the corresponding JDBC type for each value needs to be derived from the given Java type.
|
||||
While this usually works well, there is a potential for issues (for example, with Map-contained
|
||||
`null` values). Spring, by default, calls `ParameterMetaData.getParameterType` in such a
|
||||
case, which can be expensive with your JDBC driver. You should use a recent driver
|
||||
While this usually works well, there is a potential for issues (for example, with
|
||||
Map-contained `null` values). Spring, by default, calls `ParameterMetaData.getParameterType`
|
||||
in such a case, which can be expensive with your JDBC driver. You should use a recent driver
|
||||
version and consider setting the `spring.jdbc.getParameterType.ignore` property to `true`
|
||||
(as a JVM system property or via the
|
||||
xref:appendix.adoc#appendix-spring-properties[`SpringProperties`] mechanism) if you encounter
|
||||
a performance issue (as reported on Oracle 12c, JBoss, and PostgreSQL).
|
||||
xref:appendix.adoc#appendix-spring-properties[`SpringProperties`] mechanism)
|
||||
if you encounter a specific performance issue for your application.
|
||||
|
||||
Alternatively, you might consider specifying the corresponding JDBC types explicitly,
|
||||
either through a `BatchPreparedStatementSetter` (as shown earlier), through an explicit type
|
||||
array given to a `List<Object[]>` based call, through `registerSqlType` calls on a
|
||||
custom `MapSqlParameterSource` instance, or through a `BeanPropertySqlParameterSource`
|
||||
that derives the SQL type from the Java-declared property type even for a null value.
|
||||
As of 6.1.2, Spring bypasses the default `getParameterType` resolution on PostgreSQL and
|
||||
MS SQL Server. This is a common optimization to avoid further roundtrips to the DBMS just
|
||||
for parameter type resolution which is known to make a very significant difference on
|
||||
PostgreSQL and MS SQL Server specifically, in particular for batch operations. If you
|
||||
happen to see a side effect e.g. when setting a byte array to null without specific type
|
||||
indication, you may explicitly set the `spring.jdbc.getParameterType.ignore=false` flag
|
||||
as a system property (see above) to restore full `getParameterType` resolution.
|
||||
|
||||
Alternatively, you could consider specifying the corresponding JDBC types explicitly,
|
||||
either through a `BatchPreparedStatementSetter` (as shown earlier), through an explicit
|
||||
type array given to a `List<Object[]>` based call, through `registerSqlType` calls on a
|
||||
custom `MapSqlParameterSource` instance, through a `BeanPropertySqlParameterSource`
|
||||
that derives the SQL type from the Java-declared property type even for a null value, or
|
||||
through providing individual `SqlParameterValue` instances instead of plain null values.
|
||||
====
|
||||
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ and others) and is equivalent to `required=false`.
|
||||
| For access to a part in a `multipart/form-data` request. Supports reactive types.
|
||||
See xref:web/webflux/controller/ann-methods/multipart-forms.adoc[Multipart Content] and xref:web/webflux/reactive-spring.adoc#webflux-multipart[Multipart Data].
|
||||
|
||||
| `java.util.Map`, `org.springframework.ui.Model`, and `org.springframework.ui.ModelMap`.
|
||||
| `java.util.Map` or `org.springframework.ui.Model`
|
||||
| For access to the model that is used in HTML controllers and is exposed to templates as
|
||||
part of view rendering.
|
||||
|
||||
@@ -89,9 +89,9 @@ and others) and is equivalent to `required=false`.
|
||||
Note that use of `@ModelAttribute` is optional -- for example, to set its attributes.
|
||||
See "`Any other argument`" later in this table.
|
||||
|
||||
| `Errors`, `BindingResult`
|
||||
| `Errors` or `BindingResult`
|
||||
| For access to errors from validation and data binding for a command object, i.e. a
|
||||
`@ModelAttribute` argument. An `Errors`, or `BindingResult` argument must be declared
|
||||
`@ModelAttribute` argument. An `Errors` or `BindingResult` argument must be declared
|
||||
immediately after the validated method argument.
|
||||
|
||||
| `SessionStatus` + class-level `@SessionAttributes`
|
||||
|
||||
@@ -13,7 +13,7 @@ xref:web/webflux/controller/ann-methods/multipart-forms.adoc[@RequestPart] argum
|
||||
resolvers validate a method argument individually if the method parameter is annotated
|
||||
with Jakarta `@Valid` or Spring's `@Validated`, _AND_ there is no `Errors` or
|
||||
`BindingResult` parameter immediately after, _AND_ method validation is not needed (to be
|
||||
discussed next). The exception raised in this case is `MethodArgumentNotValidException`.
|
||||
discussed next). The exception raised in this case is `WebExchangeBindException`.
|
||||
|
||||
2. When `@Constraint` annotations such as `@Min`, `@NotBlank` and others are declared
|
||||
directly on method parameters, or on the method (for the return value), then method
|
||||
@@ -21,7 +21,7 @@ validation must be applied, and that supersedes validation at the method argumen
|
||||
because method validation covers both method parameter constraints and nested constraints
|
||||
via `@Valid`. The exception raised in this case is `HandlerMethodValidationException`.
|
||||
|
||||
Applications must handle both `MethodArgumentNotValidException` and
|
||||
Applications must handle both `WebExchangeBindException` and
|
||||
`HandlerMethodValidationException` as either may be raised depending on the controller
|
||||
method signature. The two exceptions, however are designed to be very similar, and can be
|
||||
handled with almost identical code. The main difference is that the former is for a single
|
||||
@@ -39,7 +39,7 @@ method parameters with an `Errors` immediately after. If there are validation er
|
||||
any other method parameter then `HandlerMethodValidationException` is raised.
|
||||
|
||||
You can configure a `Validator` globally through the
|
||||
xref:web/webflux/config.adoc#webflux-config-validation[WebMvc config], or locally
|
||||
xref:web/webflux/config.adoc#webflux-config-validation[WebFlux config], or locally
|
||||
through an xref:web/webflux/controller/ann-initbinder.adoc[@InitBinder] method in an
|
||||
`@Controller` or `@ControllerAdvice`. You can also use multiple validators.
|
||||
|
||||
@@ -49,8 +49,8 @@ through an AOP proxy. In order to take advantage of the Spring MVC built-in supp
|
||||
method validation added in Spring Framework 6.1, you need to remove the class level
|
||||
`@Validated` annotation from the controller.
|
||||
|
||||
The xref:web/webmvc/mvc-ann-rest-exceptions.adoc[Error Responses] section provides further
|
||||
details on how `MethodArgumentNotValidException` and `HandlerMethodValidationException`
|
||||
The xref:web/webflux/ann-rest-exceptions.adoc[Error Responses] section provides further
|
||||
details on how `WebExchangeBindException` and `HandlerMethodValidationException`
|
||||
are handled, and also how their rendering can be customized through a `MessageSource` and
|
||||
locale and language specific resource bundles.
|
||||
|
||||
|
||||
+3
-1
@@ -40,7 +40,9 @@ content of the provided resource to the response `OutputStream`. Note that the
|
||||
`InputStream` should be lazily retrieved by the `Resource` handle in order to reliably
|
||||
close it after it has been copied to the response. If you are using `InputStreamResource`
|
||||
for such a purpose, make sure to construct it with an on-demand `InputStreamSource`
|
||||
(e.g. through a lambda expression that retrieves the actual `InputStream`).
|
||||
(e.g. through a lambda expression that retrieves the actual `InputStream`). Also, custom
|
||||
subclasses of `InputStreamResource` are only supported in combination with a custom
|
||||
`contentLength()` implementation which avoids consuming the stream for that purpose.
|
||||
|
||||
Spring MVC supports using a single value xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-reactive-types[reactive type]
|
||||
to produce the `ResponseEntity` asynchronously, and/or single and multi-value reactive
|
||||
|
||||
@@ -389,7 +389,7 @@ template.
|
||||
encode URI component value _after_ URI variables are expanded.
|
||||
* `NONE`: No encoding is applied.
|
||||
|
||||
The `RestTemplate` is set to `EncodingMode.URI_COMPONENT` for historic
|
||||
The `RestTemplate` is set to `EncodingMode.URI_COMPONENT` for historical
|
||||
reasons and for backwards compatibility. The `WebClient` relies on the default value
|
||||
in `DefaultUriBuilderFactory`, which was changed from `EncodingMode.URI_COMPONENT` in
|
||||
5.0.x to `EncodingMode.TEMPLATE_AND_VALUES` in 5.1.
|
||||
|
||||
@@ -8,19 +8,19 @@ javaPlatform {
|
||||
|
||||
dependencies {
|
||||
api(platform("com.fasterxml.jackson:jackson-bom:2.15.4"))
|
||||
api(platform("io.micrometer:micrometer-bom:1.12.7"))
|
||||
api(platform("io.micrometer:micrometer-bom:1.12.8"))
|
||||
api(platform("io.netty:netty-bom:4.1.111.Final"))
|
||||
api(platform("io.netty:netty5-bom:5.0.0.Alpha5"))
|
||||
api(platform("io.projectreactor:reactor-bom:2023.0.7"))
|
||||
api(platform("io.projectreactor:reactor-bom:2023.0.8"))
|
||||
api(platform("io.rsocket:rsocket-bom:1.1.3"))
|
||||
api(platform("org.apache.groovy:groovy-bom:4.0.21"))
|
||||
api(platform("org.apache.groovy:groovy-bom:4.0.22"))
|
||||
api(platform("org.apache.logging.log4j:log4j-bom:2.21.1"))
|
||||
api(platform("org.assertj:assertj-bom:3.26.0"))
|
||||
api(platform("org.eclipse.jetty:jetty-bom:12.0.10"))
|
||||
api(platform("org.eclipse.jetty.ee10:jetty-ee10-bom:12.0.10"))
|
||||
api(platform("org.eclipse.jetty:jetty-bom:12.0.11"))
|
||||
api(platform("org.eclipse.jetty.ee10:jetty-ee10-bom:12.0.11"))
|
||||
api(platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.3"))
|
||||
api(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:1.6.0"))
|
||||
api(platform("org.junit:junit-bom:5.10.2"))
|
||||
api(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:1.6.3"))
|
||||
api(platform("org.junit:junit-bom:5.10.3"))
|
||||
api(platform("org.mockito:mockito-bom:5.12.0"))
|
||||
|
||||
constraints {
|
||||
@@ -55,9 +55,9 @@ dependencies {
|
||||
api("io.r2dbc:r2dbc-spi:1.0.0.RELEASE")
|
||||
api("io.reactivex.rxjava3:rxjava:3.1.8")
|
||||
api("io.smallrye.reactive:mutiny:1.10.0")
|
||||
api("io.undertow:undertow-core:2.3.13.Final")
|
||||
api("io.undertow:undertow-servlet:2.3.13.Final")
|
||||
api("io.undertow:undertow-websockets-jsr:2.3.13.Final")
|
||||
api("io.undertow:undertow-core:2.3.14.Final")
|
||||
api("io.undertow:undertow-servlet:2.3.14.Final")
|
||||
api("io.undertow:undertow-websockets-jsr:2.3.14.Final")
|
||||
api("io.vavr:vavr:0.10.4")
|
||||
api("jakarta.activation:jakarta.activation-api:2.0.1")
|
||||
api("jakarta.annotation:jakarta.annotation-api:2.0.0")
|
||||
@@ -101,26 +101,26 @@ dependencies {
|
||||
api("org.apache.derby:derbyclient:10.16.1.1")
|
||||
api("org.apache.derby:derbytools:10.16.1.1")
|
||||
api("org.apache.httpcomponents.client5:httpclient5:5.3.1")
|
||||
api("org.apache.httpcomponents.core5:httpcore5-reactive:5.2.4")
|
||||
api("org.apache.httpcomponents.core5:httpcore5-reactive:5.2.5")
|
||||
api("org.apache.poi:poi-ooxml:5.2.5")
|
||||
api("org.apache.tomcat.embed:tomcat-embed-core:10.1.24")
|
||||
api("org.apache.tomcat.embed:tomcat-embed-websocket:10.1.24")
|
||||
api("org.apache.tomcat:tomcat-util:10.1.24")
|
||||
api("org.apache.tomcat:tomcat-websocket:10.1.24")
|
||||
api("org.apache.tomcat.embed:tomcat-embed-core:10.1.25")
|
||||
api("org.apache.tomcat.embed:tomcat-embed-websocket:10.1.25")
|
||||
api("org.apache.tomcat:tomcat-util:10.1.25")
|
||||
api("org.apache.tomcat:tomcat-websocket:10.1.25")
|
||||
api("org.aspectj:aspectjrt:1.9.22.1")
|
||||
api("org.aspectj:aspectjtools:1.9.22.1")
|
||||
api("org.aspectj:aspectjweaver:1.9.22.1")
|
||||
api("org.awaitility:awaitility:4.2.1")
|
||||
api("org.awaitility:awaitility:4.2.0")
|
||||
api("org.bouncycastle:bcpkix-jdk18on:1.72")
|
||||
api("org.codehaus.jettison:jettison:1.5.4")
|
||||
api("org.crac:crac:1.4.0")
|
||||
api("org.dom4j:dom4j:2.1.4")
|
||||
api("org.eclipse.jetty:jetty-reactive-httpclient:4.0.4")
|
||||
api("org.eclipse.jetty:jetty-reactive-httpclient:4.0.5")
|
||||
api("org.eclipse.persistence:org.eclipse.persistence.jpa:3.0.4")
|
||||
api("org.eclipse:yasson:2.0.4")
|
||||
api("org.ehcache:ehcache:3.10.8")
|
||||
api("org.ehcache:jcache:1.0.1")
|
||||
api("org.freemarker:freemarker:2.3.32")
|
||||
api("org.freemarker:freemarker:2.3.33")
|
||||
api("org.glassfish.external:opendmk_jmxremote_optional_jar:1.0-b01-ea")
|
||||
api("org.glassfish:jakarta.el:4.0.2")
|
||||
api("org.glassfish.tyrus:tyrus-container-servlet:2.1.3")
|
||||
@@ -129,16 +129,16 @@ dependencies {
|
||||
api("org.hibernate:hibernate-core-jakarta:5.6.15.Final")
|
||||
api("org.hibernate:hibernate-validator:7.0.5.Final")
|
||||
api("org.hsqldb:hsqldb:2.7.2")
|
||||
api("org.javamoney:moneta:1.4.2")
|
||||
api("org.jruby:jruby:9.4.7.0")
|
||||
api("org.javamoney:moneta:1.4.4")
|
||||
api("org.jruby:jruby:9.4.8.0")
|
||||
api("org.junit.support:testng-engine:1.0.5")
|
||||
api("org.mozilla:rhino:1.7.14")
|
||||
api("org.mozilla:rhino:1.7.15")
|
||||
api("org.ogce:xpp3:1.1.6")
|
||||
api("org.python:jython-standalone:2.7.3")
|
||||
api("org.quartz-scheduler:quartz:2.3.2")
|
||||
api("org.seleniumhq.selenium:htmlunit-driver:2.70.0")
|
||||
api("org.seleniumhq.selenium:selenium-java:3.141.59")
|
||||
api("org.skyscreamer:jsonassert:1.5.1")
|
||||
api("org.skyscreamer:jsonassert:1.5.3")
|
||||
api("org.slf4j:slf4j-api:2.0.13")
|
||||
api("org.testng:testng:7.9.0")
|
||||
api("org.webjars:underscorejs:1.8.3")
|
||||
|
||||
+2
-2
@@ -1,10 +1,10 @@
|
||||
version=6.1.10-SNAPSHOT
|
||||
version=6.1.11
|
||||
|
||||
org.gradle.caching=true
|
||||
org.gradle.jvmargs=-Xmx2048m
|
||||
org.gradle.parallel=true
|
||||
|
||||
kotlinVersion=1.9.22
|
||||
kotlinVersion=1.9.24
|
||||
|
||||
kotlin.jvm.target.validation.mode=ignore
|
||||
kotlin.stdlib.default.dependency=false
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
|
||||
+60
-1
@@ -20,6 +20,9 @@ import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.aopalliance.intercept.MethodInterceptor
|
||||
import org.aopalliance.intercept.MethodInvocation
|
||||
import org.aspectj.lang.ProceedingJoinPoint
|
||||
import org.aspectj.lang.annotation.Around
|
||||
import org.aspectj.lang.annotation.Aspect
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.springframework.aop.framework.autoproxy.AspectJAutoProxyInterceptorKotlinIntegrationTests.InterceptorConfig
|
||||
@@ -28,10 +31,18 @@ import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.context.annotation.EnableAspectJAutoProxy
|
||||
import org.springframework.stereotype.Component
|
||||
import org.springframework.test.annotation.DirtiesContext
|
||||
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig
|
||||
import org.springframework.transaction.annotation.EnableTransactionManagement
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import org.springframework.transaction.testfixture.ReactiveCallCountingTransactionManager
|
||||
import reactor.core.publisher.Mono
|
||||
import java.lang.reflect.Method
|
||||
import kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS
|
||||
import kotlin.annotation.AnnotationTarget.CLASS
|
||||
import kotlin.annotation.AnnotationTarget.FUNCTION
|
||||
import kotlin.annotation.AnnotationTarget.TYPE
|
||||
|
||||
|
||||
/**
|
||||
@@ -43,7 +54,9 @@ import java.lang.reflect.Method
|
||||
class AspectJAutoProxyInterceptorKotlinIntegrationTests(
|
||||
@Autowired val echo: Echo,
|
||||
@Autowired val firstAdvisor: TestPointcutAdvisor,
|
||||
@Autowired val secondAdvisor: TestPointcutAdvisor) {
|
||||
@Autowired val secondAdvisor: TestPointcutAdvisor,
|
||||
@Autowired val countingAspect: CountingAspect,
|
||||
@Autowired val reactiveTransactionManager: ReactiveCallCountingTransactionManager) {
|
||||
|
||||
@Test
|
||||
fun `Multiple interceptors with regular function`() {
|
||||
@@ -67,8 +80,22 @@ class AspectJAutoProxyInterceptorKotlinIntegrationTests(
|
||||
assertThat(secondAdvisor.interceptor.invocations).singleElement().matches { Mono::class.java.isAssignableFrom(it) }
|
||||
}
|
||||
|
||||
@Test // gh-33095
|
||||
fun `Aspect and reactive transactional with suspending function`() {
|
||||
assertThat(countingAspect.counter).isZero()
|
||||
assertThat(reactiveTransactionManager.commits).isZero()
|
||||
val value = "Hello!"
|
||||
runBlocking {
|
||||
assertThat(echo.suspendingTransactionalEcho(value)).isEqualTo(value)
|
||||
}
|
||||
assertThat(countingAspect.counter).`as`("aspect applied").isOne()
|
||||
assertThat(reactiveTransactionManager.begun).isOne()
|
||||
assertThat(reactiveTransactionManager.commits).`as`("transactional applied").isOne()
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableAspectJAutoProxy
|
||||
@EnableTransactionManagement
|
||||
open class InterceptorConfig {
|
||||
|
||||
@Bean
|
||||
@@ -77,6 +104,13 @@ class AspectJAutoProxyInterceptorKotlinIntegrationTests(
|
||||
@Bean
|
||||
open fun secondAdvisor() = TestPointcutAdvisor().apply { order = 1 }
|
||||
|
||||
@Bean
|
||||
open fun countingAspect() = CountingAspect()
|
||||
|
||||
@Bean
|
||||
open fun transactionManager(): ReactiveCallCountingTransactionManager {
|
||||
return ReactiveCallCountingTransactionManager()
|
||||
}
|
||||
|
||||
@Bean
|
||||
open fun echo(): Echo {
|
||||
@@ -107,6 +141,24 @@ class AspectJAutoProxyInterceptorKotlinIntegrationTests(
|
||||
}
|
||||
}
|
||||
|
||||
@Target(CLASS, FUNCTION, ANNOTATION_CLASS, TYPE)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
annotation class Counting()
|
||||
|
||||
@Aspect
|
||||
@Component
|
||||
class CountingAspect {
|
||||
|
||||
var counter: Long = 0
|
||||
|
||||
@Around("@annotation(org.springframework.aop.framework.autoproxy.AspectJAutoProxyInterceptorKotlinIntegrationTests.Counting)")
|
||||
fun logging(joinPoint: ProceedingJoinPoint): Any {
|
||||
return (joinPoint.proceed(joinPoint.args) as Mono<*>).doOnTerminate {
|
||||
counter++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open class Echo {
|
||||
|
||||
open fun echo(value: String): String {
|
||||
@@ -118,6 +170,13 @@ class AspectJAutoProxyInterceptorKotlinIntegrationTests(
|
||||
return value
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Counting
|
||||
open suspend fun suspendingTransactionalEcho(value: String): String {
|
||||
delay(1)
|
||||
return value
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+2
-1
@@ -553,7 +553,8 @@ public class AspectJExpressionPointcut extends AbstractExpressionPointcut
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
Class<?> superclass = clazz.getSuperclass();
|
||||
return (superclass != null && compiledByAjc(superclass));
|
||||
}
|
||||
|
||||
|
||||
|
||||
+15
-23
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 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.
|
||||
@@ -79,8 +79,8 @@ class ScopedProxyBeanRegistrationAotProcessor implements BeanRegistrationAotProc
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private BeanDefinition getTargetBeanDefinition(ConfigurableBeanFactory beanFactory,
|
||||
@Nullable String targetBeanName) {
|
||||
private BeanDefinition getTargetBeanDefinition(
|
||||
ConfigurableBeanFactory beanFactory, @Nullable String targetBeanName) {
|
||||
|
||||
if (targetBeanName != null && beanFactory.containsBean(targetBeanName)) {
|
||||
return beanFactory.getMergedBeanDefinition(targetBeanName);
|
||||
@@ -123,40 +123,32 @@ class ScopedProxyBeanRegistrationAotProcessor implements BeanRegistrationAotProc
|
||||
|
||||
@Override
|
||||
public CodeBlock generateSetBeanDefinitionPropertiesCode(
|
||||
GenerationContext generationContext,
|
||||
BeanRegistrationCode beanRegistrationCode,
|
||||
GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode,
|
||||
RootBeanDefinition beanDefinition, Predicate<String> attributeFilter) {
|
||||
|
||||
RootBeanDefinition processedBeanDefinition = new RootBeanDefinition(
|
||||
beanDefinition);
|
||||
processedBeanDefinition
|
||||
.setTargetType(this.targetBeanDefinition.getResolvableType());
|
||||
processedBeanDefinition.getPropertyValues()
|
||||
.removePropertyValue("targetBeanName");
|
||||
RootBeanDefinition processedBeanDefinition = new RootBeanDefinition(beanDefinition);
|
||||
processedBeanDefinition.setTargetType(this.targetBeanDefinition.getResolvableType());
|
||||
processedBeanDefinition.getPropertyValues().removePropertyValue("targetBeanName");
|
||||
return super.generateSetBeanDefinitionPropertiesCode(generationContext,
|
||||
beanRegistrationCode, processedBeanDefinition, attributeFilter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CodeBlock generateInstanceSupplierCode(GenerationContext generationContext,
|
||||
BeanRegistrationCode beanRegistrationCode, boolean allowDirectSupplierShortcut) {
|
||||
public CodeBlock generateInstanceSupplierCode(
|
||||
GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode,
|
||||
boolean allowDirectSupplierShortcut) {
|
||||
|
||||
GeneratedMethod generatedMethod = beanRegistrationCode.getMethods()
|
||||
.add("getScopedProxyInstance", method -> {
|
||||
method.addJavadoc(
|
||||
"Create the scoped proxy bean instance for '$L'.",
|
||||
method.addJavadoc("Create the scoped proxy bean instance for '$L'.",
|
||||
this.registeredBean.getBeanName());
|
||||
method.addModifiers(Modifier.PRIVATE, Modifier.STATIC);
|
||||
method.returns(ScopedProxyFactoryBean.class);
|
||||
method.addParameter(RegisteredBean.class,
|
||||
REGISTERED_BEAN_PARAMETER_NAME);
|
||||
method.addParameter(RegisteredBean.class, REGISTERED_BEAN_PARAMETER_NAME);
|
||||
method.addStatement("$T factory = new $T()",
|
||||
ScopedProxyFactoryBean.class,
|
||||
ScopedProxyFactoryBean.class);
|
||||
method.addStatement("factory.setTargetBeanName($S)",
|
||||
this.targetBeanName);
|
||||
method.addStatement(
|
||||
"factory.setBeanFactory($L.getBeanFactory())",
|
||||
ScopedProxyFactoryBean.class, ScopedProxyFactoryBean.class);
|
||||
method.addStatement("factory.setTargetBeanName($S)", this.targetBeanName);
|
||||
method.addStatement("factory.setBeanFactory($L.getBeanFactory())",
|
||||
REGISTERED_BEAN_PARAMETER_NAME);
|
||||
method.addStatement("return factory");
|
||||
});
|
||||
|
||||
-1
@@ -1098,7 +1098,6 @@ public class AutowiredAnnotationBeanPostProcessor implements SmartInstantiationA
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+3
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 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.
|
||||
@@ -213,12 +213,13 @@ public final class BeanInstanceSupplier<T> extends AutowiredElementResolver impl
|
||||
if (!(executable instanceof Method method)) {
|
||||
return beanSupplier.get();
|
||||
}
|
||||
Method priorInvokedFactoryMethod = SimpleInstantiationStrategy.getCurrentlyInvokedFactoryMethod();
|
||||
try {
|
||||
SimpleInstantiationStrategy.setCurrentlyInvokedFactoryMethod(method);
|
||||
return beanSupplier.get();
|
||||
}
|
||||
finally {
|
||||
SimpleInstantiationStrategy.setCurrentlyInvokedFactoryMethod(null);
|
||||
SimpleInstantiationStrategy.setCurrentlyInvokedFactoryMethod(priorInvokedFactoryMethod);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+13
-13
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 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,39 +58,39 @@ public class BeanRegistrationCodeFragmentsDecorator implements BeanRegistrationC
|
||||
public CodeBlock generateNewBeanDefinitionCode(GenerationContext generationContext,
|
||||
ResolvableType beanType, BeanRegistrationCode beanRegistrationCode) {
|
||||
|
||||
return this.delegate.generateNewBeanDefinitionCode(generationContext,
|
||||
beanType, beanRegistrationCode);
|
||||
return this.delegate.generateNewBeanDefinitionCode(generationContext, beanType, beanRegistrationCode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CodeBlock generateSetBeanDefinitionPropertiesCode(GenerationContext generationContext,
|
||||
BeanRegistrationCode beanRegistrationCode, RootBeanDefinition beanDefinition,
|
||||
Predicate<String> attributeFilter) {
|
||||
public CodeBlock generateSetBeanDefinitionPropertiesCode(
|
||||
GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode,
|
||||
RootBeanDefinition beanDefinition, Predicate<String> attributeFilter) {
|
||||
|
||||
return this.delegate.generateSetBeanDefinitionPropertiesCode(
|
||||
generationContext, beanRegistrationCode, beanDefinition, attributeFilter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CodeBlock generateSetBeanInstanceSupplierCode(GenerationContext generationContext,
|
||||
BeanRegistrationCode beanRegistrationCode, CodeBlock instanceSupplierCode,
|
||||
List<MethodReference> postProcessors) {
|
||||
public CodeBlock generateSetBeanInstanceSupplierCode(
|
||||
GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode,
|
||||
CodeBlock instanceSupplierCode, List<MethodReference> postProcessors) {
|
||||
|
||||
return this.delegate.generateSetBeanInstanceSupplierCode(generationContext,
|
||||
beanRegistrationCode, instanceSupplierCode, postProcessors);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CodeBlock generateInstanceSupplierCode(GenerationContext generationContext,
|
||||
BeanRegistrationCode beanRegistrationCode, boolean allowDirectSupplierShortcut) {
|
||||
public CodeBlock generateInstanceSupplierCode(
|
||||
GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode,
|
||||
boolean allowDirectSupplierShortcut) {
|
||||
|
||||
return this.delegate.generateInstanceSupplierCode(generationContext,
|
||||
beanRegistrationCode, allowDirectSupplierShortcut);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CodeBlock generateReturnCode(GenerationContext generationContext,
|
||||
BeanRegistrationCode beanRegistrationCode) {
|
||||
public CodeBlock generateReturnCode(
|
||||
GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode) {
|
||||
|
||||
return this.delegate.generateReturnCode(generationContext, beanRegistrationCode);
|
||||
}
|
||||
|
||||
+33
-37
@@ -46,8 +46,7 @@ import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.function.SingletonSupplier;
|
||||
|
||||
/**
|
||||
* Internal {@link BeanRegistrationCodeFragments} implementation used by
|
||||
* default.
|
||||
* Internal {@link BeanRegistrationCodeFragments} implementation used by default.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Stephane Nicoll
|
||||
@@ -81,7 +80,8 @@ class DefaultBeanRegistrationCodeFragments implements BeanRegistrationCodeFragme
|
||||
if (hasInstanceSupplier()) {
|
||||
String resourceDescription = registeredBean.getMergedBeanDefinition().getResourceDescription();
|
||||
throw new IllegalStateException("Error processing bean with name '" + registeredBean.getBeanName() + "'" +
|
||||
(resourceDescription != null ? " defined in " + resourceDescription : "") + ": instance supplier is not supported");
|
||||
(resourceDescription != null ? " defined in " + resourceDescription : "") +
|
||||
": instance supplier is not supported");
|
||||
}
|
||||
Class<?> target = extractDeclaringClass(registeredBean, this.instantiationDescriptor.get());
|
||||
while (target.getName().startsWith("java.") && registeredBean.isInnerBean()) {
|
||||
@@ -94,9 +94,8 @@ class DefaultBeanRegistrationCodeFragments implements BeanRegistrationCodeFragme
|
||||
|
||||
private Class<?> extractDeclaringClass(RegisteredBean registeredBean, InstantiationDescriptor instantiationDescriptor) {
|
||||
Class<?> declaringClass = ClassUtils.getUserClass(instantiationDescriptor.targetClass());
|
||||
if (instantiationDescriptor.executable() instanceof Constructor<?>
|
||||
&& AccessControl.forMember(instantiationDescriptor.executable()).isPublic()
|
||||
&& FactoryBean.class.isAssignableFrom(declaringClass)) {
|
||||
if (instantiationDescriptor.executable() instanceof Constructor<?> ctor &&
|
||||
AccessControl.forMember(ctor).isPublic() && FactoryBean.class.isAssignableFrom(declaringClass)) {
|
||||
return extractTargetClassFromFactoryBean(declaringClass, registeredBean.getBeanType());
|
||||
}
|
||||
return declaringClass;
|
||||
@@ -105,8 +104,7 @@ class DefaultBeanRegistrationCodeFragments implements BeanRegistrationCodeFragme
|
||||
/**
|
||||
* Extract the target class of a public {@link FactoryBean} based on its
|
||||
* constructor. If the implementation does not resolve the target class
|
||||
* because it itself uses a generic, attempt to extract it from the
|
||||
* bean type.
|
||||
* because it itself uses a generic, attempt to extract it from the bean type.
|
||||
* @param factoryBeanType the factory bean type
|
||||
* @param beanType the bean type
|
||||
* @return the target class to use
|
||||
@@ -127,17 +125,15 @@ class DefaultBeanRegistrationCodeFragments implements BeanRegistrationCodeFragme
|
||||
ResolvableType beanType, BeanRegistrationCode beanRegistrationCode) {
|
||||
|
||||
CodeBlock.Builder code = CodeBlock.builder();
|
||||
RootBeanDefinition mergedBeanDefinition = this.registeredBean.getMergedBeanDefinition();
|
||||
Class<?> beanClass = (mergedBeanDefinition.hasBeanClass()
|
||||
? ClassUtils.getUserClass(mergedBeanDefinition.getBeanClass()) : null);
|
||||
RootBeanDefinition mbd = this.registeredBean.getMergedBeanDefinition();
|
||||
Class<?> beanClass = (mbd.hasBeanClass() ? ClassUtils.getUserClass(mbd.getBeanClass()) : null);
|
||||
CodeBlock beanClassCode = generateBeanClassCode(
|
||||
beanRegistrationCode.getClassName().packageName(),
|
||||
(beanClass != null ? beanClass : beanType.toClass()));
|
||||
code.addStatement("$T $L = new $T($L)", RootBeanDefinition.class,
|
||||
BEAN_DEFINITION_VARIABLE, RootBeanDefinition.class, beanClassCode);
|
||||
if (targetTypeNecessary(beanType, beanClass)) {
|
||||
code.addStatement("$L.setTargetType($L)", BEAN_DEFINITION_VARIABLE,
|
||||
generateBeanTypeCode(beanType));
|
||||
code.addStatement("$L.setTargetType($L)", BEAN_DEFINITION_VARIABLE, generateBeanTypeCode(beanType));
|
||||
}
|
||||
return code.build();
|
||||
}
|
||||
@@ -162,8 +158,7 @@ class DefaultBeanRegistrationCodeFragments implements BeanRegistrationCodeFragme
|
||||
if (beanType.hasGenerics()) {
|
||||
return true;
|
||||
}
|
||||
if (beanClass != null
|
||||
&& this.registeredBean.getMergedBeanDefinition().getFactoryMethodName() != null) {
|
||||
if (beanClass != null && this.registeredBean.getMergedBeanDefinition().getFactoryMethodName() != null) {
|
||||
return true;
|
||||
}
|
||||
return (beanClass != null && !beanType.toClass().equals(beanClass));
|
||||
@@ -171,21 +166,21 @@ class DefaultBeanRegistrationCodeFragments implements BeanRegistrationCodeFragme
|
||||
|
||||
@Override
|
||||
public CodeBlock generateSetBeanDefinitionPropertiesCode(
|
||||
GenerationContext generationContext,
|
||||
BeanRegistrationCode beanRegistrationCode, RootBeanDefinition beanDefinition,
|
||||
Predicate<String> attributeFilter) {
|
||||
GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode,
|
||||
RootBeanDefinition beanDefinition, Predicate<String> attributeFilter) {
|
||||
|
||||
Loader loader = AotServices.factories(this.registeredBean.getBeanFactory().getBeanClassLoader());
|
||||
List<Delegate> additionalDelegates = loader.load(Delegate.class).asList();
|
||||
return new BeanDefinitionPropertiesCodeGenerator(generationContext.getRuntimeHints(),
|
||||
attributeFilter, beanRegistrationCode.getMethods(),
|
||||
additionalDelegates, (name, value) -> generateValueCode(generationContext, name, value)
|
||||
).generateCode(beanDefinition);
|
||||
|
||||
return new BeanDefinitionPropertiesCodeGenerator(
|
||||
generationContext.getRuntimeHints(), attributeFilter,
|
||||
beanRegistrationCode.getMethods(), additionalDelegates,
|
||||
(name, value) -> generateValueCode(generationContext, name, value))
|
||||
.generateCode(beanDefinition);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
protected CodeBlock generateValueCode(GenerationContext generationContext,
|
||||
String name, Object value) {
|
||||
|
||||
protected CodeBlock generateValueCode(GenerationContext generationContext, String name, Object value) {
|
||||
RegisteredBean innerRegisteredBean = getInnerRegisteredBean(value);
|
||||
if (innerRegisteredBean != null) {
|
||||
BeanDefinitionMethodGenerator methodGenerator = this.beanDefinitionMethodGeneratorFactory
|
||||
@@ -211,9 +206,8 @@ class DefaultBeanRegistrationCodeFragments implements BeanRegistrationCodeFragme
|
||||
|
||||
@Override
|
||||
public CodeBlock generateSetBeanInstanceSupplierCode(
|
||||
GenerationContext generationContext,
|
||||
BeanRegistrationCode beanRegistrationCode, CodeBlock instanceSupplierCode,
|
||||
List<MethodReference> postProcessors) {
|
||||
GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode,
|
||||
CodeBlock instanceSupplierCode, List<MethodReference> postProcessors) {
|
||||
|
||||
CodeBlock.Builder code = CodeBlock.builder();
|
||||
if (postProcessors.isEmpty()) {
|
||||
@@ -233,20 +227,22 @@ class DefaultBeanRegistrationCodeFragments implements BeanRegistrationCodeFragme
|
||||
}
|
||||
|
||||
@Override
|
||||
public CodeBlock generateInstanceSupplierCode(GenerationContext generationContext,
|
||||
BeanRegistrationCode beanRegistrationCode, boolean allowDirectSupplierShortcut) {
|
||||
public CodeBlock generateInstanceSupplierCode(
|
||||
GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode,
|
||||
boolean allowDirectSupplierShortcut) {
|
||||
|
||||
if (hasInstanceSupplier()) {
|
||||
throw new IllegalStateException("Default code generation is not supported for bean definitions declaring "
|
||||
+ "an instance supplier callback: " + this.registeredBean.getMergedBeanDefinition());
|
||||
throw new IllegalStateException("Default code generation is not supported for bean definitions " +
|
||||
"declaring an instance supplier callback: " + this.registeredBean.getMergedBeanDefinition());
|
||||
}
|
||||
return new InstanceSupplierCodeGenerator(generationContext, beanRegistrationCode.getClassName(),
|
||||
beanRegistrationCode.getMethods(), allowDirectSupplierShortcut).generateCode(
|
||||
this.registeredBean, this.instantiationDescriptor.get());
|
||||
return new InstanceSupplierCodeGenerator(generationContext,
|
||||
beanRegistrationCode.getClassName(), beanRegistrationCode.getMethods(), allowDirectSupplierShortcut)
|
||||
.generateCode(this.registeredBean, this.instantiationDescriptor.get());
|
||||
}
|
||||
|
||||
@Override
|
||||
public CodeBlock generateReturnCode(GenerationContext generationContext,
|
||||
BeanRegistrationCode beanRegistrationCode) {
|
||||
public CodeBlock generateReturnCode(
|
||||
GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode) {
|
||||
|
||||
CodeBlock.Builder code = CodeBlock.builder();
|
||||
code.addStatement("return $L", BEAN_DEFINITION_VARIABLE);
|
||||
|
||||
+25
-7
@@ -813,10 +813,20 @@ public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFac
|
||||
|
||||
// Common return type found: all factory methods return same type. For a non-parameterized
|
||||
// unique candidate, cache the full type declaration context of the target factory method.
|
||||
cachedReturnType = (uniqueCandidate != null ?
|
||||
ResolvableType.forMethodReturnType(uniqueCandidate) : ResolvableType.forClass(commonType));
|
||||
mbd.factoryMethodReturnType = cachedReturnType;
|
||||
return cachedReturnType.resolve();
|
||||
try {
|
||||
cachedReturnType = (uniqueCandidate != null ?
|
||||
ResolvableType.forMethodReturnType(uniqueCandidate) : ResolvableType.forClass(commonType));
|
||||
mbd.factoryMethodReturnType = cachedReturnType;
|
||||
return cachedReturnType.resolve();
|
||||
}
|
||||
catch (LinkageError err) {
|
||||
// E.g. a NoClassDefFoundError for a generic method return type
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Failed to resolve type for factory method of bean '" + beanName + "': " +
|
||||
(uniqueCandidate != null ? uniqueCandidate : commonType), err);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -833,10 +843,18 @@ public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFac
|
||||
*/
|
||||
@Override
|
||||
protected ResolvableType getTypeForFactoryBean(String beanName, RootBeanDefinition mbd, boolean allowInit) {
|
||||
ResolvableType result;
|
||||
|
||||
// Check if the bean definition itself has defined the type with an attribute
|
||||
ResolvableType result = getTypeForFactoryBeanFromAttributes(mbd);
|
||||
if (result != ResolvableType.NONE) {
|
||||
return result;
|
||||
try {
|
||||
result = getTypeForFactoryBeanFromAttributes(mbd);
|
||||
if (result != ResolvableType.NONE) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
catch (IllegalArgumentException ex) {
|
||||
throw new BeanDefinitionStoreException(mbd.getResourceDescription(), beanName,
|
||||
String.valueOf(ex.getMessage()));
|
||||
}
|
||||
|
||||
// For instance supplied beans, try the target type and bean class immediately
|
||||
|
||||
+9
-3
@@ -1716,9 +1716,15 @@ public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport imp
|
||||
* @see #getBean(String)
|
||||
*/
|
||||
protected ResolvableType getTypeForFactoryBean(String beanName, RootBeanDefinition mbd, boolean allowInit) {
|
||||
ResolvableType result = getTypeForFactoryBeanFromAttributes(mbd);
|
||||
if (result != ResolvableType.NONE) {
|
||||
return result;
|
||||
try {
|
||||
ResolvableType result = getTypeForFactoryBeanFromAttributes(mbd);
|
||||
if (result != ResolvableType.NONE) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
catch (IllegalArgumentException ex) {
|
||||
throw new BeanDefinitionStoreException(mbd.getResourceDescription(), beanName,
|
||||
String.valueOf(ex.getMessage()));
|
||||
}
|
||||
|
||||
if (allowInit && mbd.isSingleton()) {
|
||||
|
||||
+12
-11
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 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.
|
||||
@@ -55,12 +55,18 @@ public class SimpleInstantiationStrategy implements InstantiationStrategy {
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the factory method currently being invoked or {@code null} to reset.
|
||||
* Set the factory method currently being invoked or {@code null} to remove
|
||||
* the current value, if any.
|
||||
* @param method the factory method currently being invoked or {@code null}
|
||||
* @since 6.0
|
||||
*/
|
||||
public static void setCurrentlyInvokedFactoryMethod(@Nullable Method method) {
|
||||
currentlyInvokedFactoryMethod.set(method);
|
||||
if (method != null) {
|
||||
currentlyInvokedFactoryMethod.set(method);
|
||||
}
|
||||
else {
|
||||
currentlyInvokedFactoryMethod.remove();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -134,9 +140,9 @@ public class SimpleInstantiationStrategy implements InstantiationStrategy {
|
||||
try {
|
||||
ReflectionUtils.makeAccessible(factoryMethod);
|
||||
|
||||
Method priorInvokedFactoryMethod = currentlyInvokedFactoryMethod.get();
|
||||
Method priorInvokedFactoryMethod = getCurrentlyInvokedFactoryMethod();
|
||||
try {
|
||||
currentlyInvokedFactoryMethod.set(factoryMethod);
|
||||
setCurrentlyInvokedFactoryMethod(factoryMethod);
|
||||
Object result = factoryMethod.invoke(factoryBean, args);
|
||||
if (result == null) {
|
||||
result = new NullBean();
|
||||
@@ -144,12 +150,7 @@ public class SimpleInstantiationStrategy implements InstantiationStrategy {
|
||||
return result;
|
||||
}
|
||||
finally {
|
||||
if (priorInvokedFactoryMethod != null) {
|
||||
currentlyInvokedFactoryMethod.set(priorInvokedFactoryMethod);
|
||||
}
|
||||
else {
|
||||
currentlyInvokedFactoryMethod.remove();
|
||||
}
|
||||
setCurrentlyInvokedFactoryMethod(priorInvokedFactoryMethod);
|
||||
}
|
||||
}
|
||||
catch (IllegalArgumentException ex) {
|
||||
|
||||
+11
-6
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 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,9 +92,9 @@ public class PathEditor extends PropertyEditorSupport {
|
||||
// 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.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,8 +111,13 @@ public class PathEditor extends PropertyEditorSupport {
|
||||
setValue(resource.getFile().toPath());
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new IllegalArgumentException(
|
||||
"Could not retrieve file for " + resource + ": " + ex.getMessage());
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+44
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 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.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@@ -51,6 +52,7 @@ import org.springframework.beans.factory.support.DefaultListableBeanFactory;
|
||||
import org.springframework.beans.factory.support.InstanceSupplier;
|
||||
import org.springframework.beans.factory.support.RegisteredBean;
|
||||
import org.springframework.beans.factory.support.RootBeanDefinition;
|
||||
import org.springframework.beans.factory.support.SimpleInstantiationStrategy;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.core.io.DefaultResourceLoader;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
@@ -292,6 +294,33 @@ class BeanInstanceSupplierTests {
|
||||
assertThat(instance).isEqualTo("1");
|
||||
}
|
||||
|
||||
@Test // gh-33180
|
||||
void getWithNestedInvocationRetainsFactoryMethod() throws Exception {
|
||||
AtomicReference<Method> testMethodReference = new AtomicReference<>();
|
||||
AtomicReference<Method> anotherMethodReference = new AtomicReference<>();
|
||||
|
||||
BeanInstanceSupplier<Object> nestedInstanceSupplier = BeanInstanceSupplier
|
||||
.forFactoryMethod(AnotherTestStringFactory.class, "another")
|
||||
.withGenerator(registeredBean -> {
|
||||
anotherMethodReference.set(SimpleInstantiationStrategy.getCurrentlyInvokedFactoryMethod());
|
||||
return "Another";
|
||||
});
|
||||
RegisteredBean nestedRegisteredBean = new Source(String.class, nestedInstanceSupplier).registerBean(this.beanFactory);
|
||||
BeanInstanceSupplier<Object> instanceSupplier = BeanInstanceSupplier
|
||||
.forFactoryMethod(TestStringFactory.class, "test")
|
||||
.withGenerator(registeredBean -> {
|
||||
Object nested = nestedInstanceSupplier.get(nestedRegisteredBean);
|
||||
testMethodReference.set(SimpleInstantiationStrategy.getCurrentlyInvokedFactoryMethod());
|
||||
return "custom" + nested;
|
||||
});
|
||||
RegisteredBean registeredBean = new Source(String.class, instanceSupplier).registerBean(this.beanFactory);
|
||||
Object value = instanceSupplier.get(registeredBean);
|
||||
|
||||
assertThat(value).isEqualTo("customAnother");
|
||||
assertThat(testMethodReference.get()).isEqualTo(instanceSupplier.getFactoryMethod());
|
||||
assertThat(anotherMethodReference.get()).isEqualTo(nestedInstanceSupplier.getFactoryMethod());
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveArgumentsWithNoArgConstructor() {
|
||||
RootBeanDefinition beanDefinition = new RootBeanDefinition(
|
||||
@@ -1030,4 +1059,18 @@ class BeanInstanceSupplierTests {
|
||||
|
||||
}
|
||||
|
||||
static class TestStringFactory {
|
||||
|
||||
String test() {
|
||||
return "test";
|
||||
}
|
||||
}
|
||||
|
||||
static class AnotherTestStringFactory {
|
||||
|
||||
String another() {
|
||||
return "another";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+12
-2
@@ -46,9 +46,9 @@ class FileEditorTests {
|
||||
|
||||
@Test
|
||||
void testWithNonExistentResource() {
|
||||
PropertyEditor propertyEditor = new FileEditor();
|
||||
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
|
||||
@@ -71,6 +71,16 @@ class FileEditorTests {
|
||||
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;
|
||||
assertThat(file).isEqualTo(new File("."));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testUnqualifiedFileNameFound() {
|
||||
PropertyEditor fileEditor = new FileEditor();
|
||||
|
||||
+13
-2
@@ -19,6 +19,7 @@ package org.springframework.beans.propertyeditors;
|
||||
import java.beans.PropertyEditor;
|
||||
import java.io.File;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@@ -46,9 +47,9 @@ class PathEditorTests {
|
||||
|
||||
@Test
|
||||
void testWithNonExistentResource() {
|
||||
PropertyEditor propertyEditor = new PathEditor();
|
||||
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
|
||||
@@ -98,6 +99,16 @@ class PathEditorTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
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();
|
||||
|
||||
+73
-59
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 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,9 +41,11 @@ import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
/**
|
||||
* Factory that configures a FreeMarker Configuration. Can be used standalone, but
|
||||
* typically you will either use FreeMarkerConfigurationFactoryBean for preparing a
|
||||
* Configuration as bean reference, or FreeMarkerConfigurer for web views.
|
||||
* Factory that configures a FreeMarker {@link Configuration}.
|
||||
*
|
||||
* <p>Can be used standalone, but typically you will either use
|
||||
* {@link FreeMarkerConfigurationFactoryBean} for preparing a {@code Configuration}
|
||||
* as a bean reference, or {@code FreeMarkerConfigurer} for web views.
|
||||
*
|
||||
* <p>The optional "configLocation" property sets the location of a FreeMarker
|
||||
* properties file, within the current application. FreeMarker properties can be
|
||||
@@ -52,17 +54,18 @@ import org.springframework.util.CollectionUtils;
|
||||
* subject to constraints set by FreeMarker.
|
||||
*
|
||||
* <p>The "freemarkerVariables" property can be used to specify a Map of
|
||||
* shared variables that will be applied to the Configuration via the
|
||||
* shared variables that will be applied to the {@code Configuration} via the
|
||||
* {@code setAllSharedVariables()} method. Like {@code setSettings()},
|
||||
* these entries are subject to FreeMarker constraints.
|
||||
*
|
||||
* <p>The simplest way to use this class is to specify a "templateLoaderPath";
|
||||
* FreeMarker does not need any further configuration then.
|
||||
*
|
||||
* <p>Note: Spring's FreeMarker support requires FreeMarker 2.3 or higher.
|
||||
* <p>Note: Spring's FreeMarker support requires FreeMarker 2.3.21 or higher.
|
||||
*
|
||||
* @author Darren Davison
|
||||
* @author Juergen Hoeller
|
||||
* @author Sam Brannen
|
||||
* @since 03.03.2004
|
||||
* @see #setConfigLocation
|
||||
* @see #setFreemarkerSettings
|
||||
@@ -107,7 +110,7 @@ public class FreeMarkerConfigurationFactory {
|
||||
|
||||
/**
|
||||
* Set the location of the FreeMarker config file.
|
||||
* Alternatively, you can specify all setting locally.
|
||||
* <p>Alternatively, you can specify all settings locally.
|
||||
* @see #setFreemarkerSettings
|
||||
* @see #setTemplateLoaderPath
|
||||
*/
|
||||
@@ -134,25 +137,33 @@ public class FreeMarkerConfigurationFactory {
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the default encoding for the FreeMarker configuration.
|
||||
* If not specified, FreeMarker will use the platform file encoding.
|
||||
* <p>Used for template rendering unless there is an explicit encoding specified
|
||||
* for the rendering process (for example, on Spring's FreeMarkerView).
|
||||
* Set the default encoding for the FreeMarker {@link Configuration}, which
|
||||
* is used to decode byte sequences to character sequences when reading template
|
||||
* files.
|
||||
* <p>If not specified, FreeMarker will read template files using the platform
|
||||
* file encoding (defined by the JVM system property {@code file.encoding})
|
||||
* or {@code "utf-8"} if the platform file encoding is undefined.
|
||||
* <p>Note that the encoding is not used for template rendering. Instead, an
|
||||
* explicit encoding must be specified for the rendering process — for
|
||||
* example, via Spring's {@code FreeMarkerView} or {@code FreeMarkerViewResolver}.
|
||||
* @see freemarker.template.Configuration#setDefaultEncoding
|
||||
* @see org.springframework.web.servlet.view.freemarker.FreeMarkerView#setEncoding
|
||||
* @see org.springframework.web.servlet.view.freemarker.FreeMarkerView#setContentType
|
||||
* @see org.springframework.web.servlet.view.freemarker.FreeMarkerViewResolver#setContentType
|
||||
*/
|
||||
public void setDefaultEncoding(String defaultEncoding) {
|
||||
this.defaultEncoding = defaultEncoding;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a List of {@code TemplateLoader}s that will be used to search
|
||||
* for templates. For example, one or more custom loaders such as database
|
||||
* loaders could be configured and injected here.
|
||||
* <p>The {@link TemplateLoader TemplateLoaders} specified here will be
|
||||
* registered <i>before</i> the default template loaders that this factory
|
||||
* registers (such as loaders for specified "templateLoaderPaths" or any
|
||||
* loaders registered in {@link #postProcessTemplateLoaders}).
|
||||
* Set a List of {@link TemplateLoader TemplateLoaders} that will be used to
|
||||
* search for templates.
|
||||
* <p>For example, one or more custom loaders such as database loaders could
|
||||
* be configured and injected here.
|
||||
* <p>The {@code TemplateLoaders} specified here will be registered <i>before</i>
|
||||
* the default template loaders that this factory registers (such as loaders
|
||||
* for specified "templateLoaderPaths" or any loaders registered in
|
||||
* {@link #postProcessTemplateLoaders}).
|
||||
* @see #setTemplateLoaderPaths
|
||||
* @see #postProcessTemplateLoaders
|
||||
*/
|
||||
@@ -161,13 +172,14 @@ public class FreeMarkerConfigurationFactory {
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a List of {@code TemplateLoader}s that will be used to search
|
||||
* for templates. For example, one or more custom loaders such as database
|
||||
* loaders can be configured.
|
||||
* <p>The {@link TemplateLoader TemplateLoaders} specified here will be
|
||||
* registered <i>after</i> the default template loaders that this factory
|
||||
* registers (such as loaders for specified "templateLoaderPaths" or any
|
||||
* loaders registered in {@link #postProcessTemplateLoaders}).
|
||||
* Set a List of {@link TemplateLoader TemplateLoaders} that will be used to
|
||||
* search for templates.
|
||||
* <p>For example, one or more custom loaders such as database loaders could
|
||||
* be configured and injected here.
|
||||
* <p>The {@code TemplateLoaders} specified here will be registered <i>after</i>
|
||||
* the default template loaders that this factory registers (such as loaders
|
||||
* for specified "templateLoaderPaths" or any loaders registered in
|
||||
* {@link #postProcessTemplateLoaders}).
|
||||
* @see #setTemplateLoaderPaths
|
||||
* @see #postProcessTemplateLoaders
|
||||
*/
|
||||
@@ -177,7 +189,7 @@ public class FreeMarkerConfigurationFactory {
|
||||
|
||||
/**
|
||||
* Set the Freemarker template loader path via a Spring resource location.
|
||||
* See the "templateLoaderPaths" property for details on path handling.
|
||||
* <p>See the "templateLoaderPaths" property for details on path handling.
|
||||
* @see #setTemplateLoaderPaths
|
||||
*/
|
||||
public void setTemplateLoaderPath(String templateLoaderPath) {
|
||||
@@ -188,28 +200,29 @@ public class FreeMarkerConfigurationFactory {
|
||||
* Set multiple Freemarker template loader paths via Spring resource locations.
|
||||
* <p>When populated via a String, standard URLs like "file:" and "classpath:"
|
||||
* pseudo URLs are supported, as understood by ResourceEditor. Allows for
|
||||
* relative paths when running in an ApplicationContext.
|
||||
* <p>Will define a path for the default FreeMarker template loader.
|
||||
* If a specified resource cannot be resolved to a {@code java.io.File},
|
||||
* a generic SpringTemplateLoader will be used, without modification detection.
|
||||
* <p>To enforce the use of SpringTemplateLoader, i.e. to not resolve a path
|
||||
* as file system resource in any case, turn off the "preferFileSystemAccess"
|
||||
* relative paths when running in an {@code ApplicationContext}.
|
||||
* <p>Will define a path for the default FreeMarker template loader. If a
|
||||
* specified resource cannot be resolved to a {@code java.io.File}, a generic
|
||||
* {@link SpringTemplateLoader} will be used, without modification detection.
|
||||
* <p>To enforce the use of {@code SpringTemplateLoader}, i.e. to not resolve
|
||||
* a path as file system resource in any case, turn off the "preferFileSystemAccess"
|
||||
* flag. See the latter's javadoc for details.
|
||||
* <p>If you wish to specify your own list of TemplateLoaders, do not set this
|
||||
* property and instead use {@code setTemplateLoaders(List templateLoaders)}
|
||||
* property and instead use {@link #setPostTemplateLoaders(TemplateLoader...)}.
|
||||
* @see org.springframework.core.io.ResourceEditor
|
||||
* @see org.springframework.context.ApplicationContext#getResource
|
||||
* @see freemarker.template.Configuration#setDirectoryForTemplateLoading
|
||||
* @see SpringTemplateLoader
|
||||
* @see #setPreferFileSystemAccess(boolean)
|
||||
*/
|
||||
public void setTemplateLoaderPaths(String... templateLoaderPaths) {
|
||||
this.templateLoaderPaths = templateLoaderPaths;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the Spring ResourceLoader to use for loading FreeMarker template files.
|
||||
* The default is DefaultResourceLoader. Will get overridden by the
|
||||
* ApplicationContext if running in a context.
|
||||
* Set the {@link ResourceLoader} to use for loading FreeMarker template files.
|
||||
* <p>The default is {@link DefaultResourceLoader}. Will get overridden by the
|
||||
* {@code ApplicationContext} if running in a context.
|
||||
* @see org.springframework.core.io.DefaultResourceLoader
|
||||
*/
|
||||
public void setResourceLoader(ResourceLoader resourceLoader) {
|
||||
@@ -217,7 +230,7 @@ public class FreeMarkerConfigurationFactory {
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the Spring ResourceLoader to use for loading FreeMarker template files.
|
||||
* Return the {@link ResourceLoader} to use for loading FreeMarker template files.
|
||||
*/
|
||||
protected ResourceLoader getResourceLoader() {
|
||||
return this.resourceLoader;
|
||||
@@ -225,11 +238,11 @@ public class FreeMarkerConfigurationFactory {
|
||||
|
||||
/**
|
||||
* Set whether to prefer file system access for template loading.
|
||||
* File system access enables hot detection of template changes.
|
||||
* <p>File system access enables hot detection of template changes.
|
||||
* <p>If this is enabled, FreeMarkerConfigurationFactory will try to resolve
|
||||
* the specified "templateLoaderPath" as file system resource (which will work
|
||||
* for expanded class path resources and ServletContext resources too).
|
||||
* <p>Default is "true". Turn this off to always load via SpringTemplateLoader
|
||||
* <p>Default is "true". Turn this off to always load via {@link SpringTemplateLoader}
|
||||
* (i.e. as stream, without hot detection of template changes), which might
|
||||
* be necessary if some of your templates reside in an expanded classes
|
||||
* directory while others reside in jar files.
|
||||
@@ -248,8 +261,8 @@ public class FreeMarkerConfigurationFactory {
|
||||
|
||||
|
||||
/**
|
||||
* Prepare the FreeMarker Configuration and return it.
|
||||
* @return the FreeMarker Configuration object
|
||||
* Prepare the FreeMarker {@link Configuration} and return it.
|
||||
* @return the FreeMarker {@code Configuration} object
|
||||
* @throws IOException if the config file wasn't found
|
||||
* @throws TemplateException on FreeMarker initialization failure
|
||||
*/
|
||||
@@ -314,11 +327,12 @@ public class FreeMarkerConfigurationFactory {
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new Configuration object. Subclasses can override this for custom
|
||||
* initialization (e.g. specifying a FreeMarker compatibility level which is a
|
||||
* new feature in FreeMarker 2.3.21), or for using a mock object for testing.
|
||||
* <p>Called by {@code createConfiguration()}.
|
||||
* @return the Configuration object
|
||||
* Return a new {@link Configuration} object.
|
||||
* <p>Subclasses can override this for custom initialization — for example,
|
||||
* to specify a FreeMarker compatibility level (which is a new feature in
|
||||
* FreeMarker 2.3.21), or to use a mock object for testing.
|
||||
* <p>Called by {@link #createConfiguration()}.
|
||||
* @return the {@code Configuration} object
|
||||
* @throws IOException if a config file wasn't found
|
||||
* @throws TemplateException on FreeMarker initialization failure
|
||||
* @see #createConfiguration()
|
||||
@@ -328,11 +342,11 @@ public class FreeMarkerConfigurationFactory {
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine a FreeMarker TemplateLoader for the given path.
|
||||
* <p>Default implementation creates either a FileTemplateLoader or
|
||||
* a SpringTemplateLoader.
|
||||
* Determine a FreeMarker {@link TemplateLoader} for the given path.
|
||||
* <p>Default implementation creates either a {@link FileTemplateLoader} or
|
||||
* a {@link SpringTemplateLoader}.
|
||||
* @param templateLoaderPath the path to load templates from
|
||||
* @return an appropriate TemplateLoader
|
||||
* @return an appropriate {@code TemplateLoader}
|
||||
* @see freemarker.cache.FileTemplateLoader
|
||||
* @see SpringTemplateLoader
|
||||
*/
|
||||
@@ -366,9 +380,9 @@ public class FreeMarkerConfigurationFactory {
|
||||
|
||||
/**
|
||||
* To be overridden by subclasses that want to register custom
|
||||
* TemplateLoader instances after this factory created its default
|
||||
* {@link TemplateLoader} instances after this factory created its default
|
||||
* template loaders.
|
||||
* <p>Called by {@code createConfiguration()}. Note that specified
|
||||
* <p>Called by {@link #createConfiguration()}. Note that specified
|
||||
* "postTemplateLoaders" will be registered <i>after</i> any loaders
|
||||
* registered by this callback; as a consequence, they are <i>not</i>
|
||||
* included in the given List.
|
||||
@@ -381,10 +395,10 @@ public class FreeMarkerConfigurationFactory {
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a TemplateLoader based on the given TemplateLoader list.
|
||||
* If more than one TemplateLoader has been registered, a FreeMarker
|
||||
* MultiTemplateLoader needs to be created.
|
||||
* @param templateLoaders the final List of TemplateLoader instances
|
||||
* Return a {@link TemplateLoader} based on the given {@code TemplateLoader} list.
|
||||
* <p>If more than one TemplateLoader has been registered, a FreeMarker
|
||||
* {@link MultiTemplateLoader} will be created.
|
||||
* @param templateLoaders the final List of {@code TemplateLoader} instances
|
||||
* @return the aggregate TemplateLoader
|
||||
*/
|
||||
@Nullable
|
||||
@@ -404,10 +418,10 @@ public class FreeMarkerConfigurationFactory {
|
||||
|
||||
/**
|
||||
* To be overridden by subclasses that want to perform custom
|
||||
* post-processing of the Configuration object after this factory
|
||||
* post-processing of the {@link Configuration} object after this factory
|
||||
* performed its default initialization.
|
||||
* <p>Called by {@code createConfiguration()}.
|
||||
* @param config the current Configuration object
|
||||
* <p>Called by {@link #createConfiguration()}.
|
||||
* @param config the current {@code Configuration} object
|
||||
* @throws IOException if a config file wasn't found
|
||||
* @throws TemplateException on FreeMarker initialization failure
|
||||
* @see #createConfiguration()
|
||||
|
||||
+12
-9
@@ -27,22 +27,25 @@ import org.springframework.context.ResourceLoaderAware;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* Factory bean that creates a FreeMarker Configuration and provides it as
|
||||
* bean reference. This bean is intended for any kind of usage of FreeMarker
|
||||
* in application code, e.g. for generating email content. For web views,
|
||||
* FreeMarkerConfigurer is used to set up a FreeMarkerConfigurationFactory.
|
||||
* <p>
|
||||
* The simplest way to use this class is to specify just a "templateLoaderPath";
|
||||
* Factory bean that creates a FreeMarker {@link Configuration} and provides it
|
||||
* as a bean reference.
|
||||
*
|
||||
* <p>This bean is intended for any kind of usage of FreeMarker in application
|
||||
* code — for example, for generating email content. For web views,
|
||||
* {@code FreeMarkerConfigurer} is used to set up a {@link FreeMarkerConfigurationFactory}.
|
||||
*
|
||||
* <p>The simplest way to use this class is to specify just a "templateLoaderPath";
|
||||
* you do not need any further configuration then. For example, in a web
|
||||
* application context:
|
||||
*
|
||||
* <pre class="code"> <bean id="freemarkerConfiguration" class="org.springframework.ui.freemarker.FreeMarkerConfigurationFactoryBean">
|
||||
* <property name="templateLoaderPath" value="/WEB-INF/freemarker/"/>
|
||||
* </bean></pre>
|
||||
|
||||
* See the base class FreeMarkerConfigurationFactory for configuration details.
|
||||
*
|
||||
* <p>Note: Spring's FreeMarker support requires FreeMarker 2.3 or higher.
|
||||
* <p>See the {@link FreeMarkerConfigurationFactory} base class for configuration
|
||||
* details.
|
||||
*
|
||||
* <p>Note: Spring's FreeMarker support requires FreeMarker 2.3.21 or higher.
|
||||
*
|
||||
* @author Darren Davison
|
||||
* @since 03.03.2004
|
||||
|
||||
+6
-5
@@ -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.
|
||||
@@ -24,7 +24,8 @@ import freemarker.template.TemplateException;
|
||||
|
||||
/**
|
||||
* Utility class for working with FreeMarker.
|
||||
* Provides convenience methods to process a FreeMarker template with a model.
|
||||
*
|
||||
* <p>Provides convenience methods to process a FreeMarker template with a model.
|
||||
*
|
||||
* @author Juergen Hoeller
|
||||
* @since 14.03.2004
|
||||
@@ -33,12 +34,12 @@ public abstract class FreeMarkerTemplateUtils {
|
||||
|
||||
/**
|
||||
* Process the specified FreeMarker template with the given model and write
|
||||
* the result to the given Writer.
|
||||
* <p>When using this method to prepare a text for a mail to be sent with Spring's
|
||||
* the result to a String.
|
||||
* <p>When using this method to prepare text for a mail to be sent with Spring's
|
||||
* mail support, consider wrapping IO/TemplateException in MailPreparationException.
|
||||
* @param model the model object, typically a Map that contains model names
|
||||
* as keys and model objects as values
|
||||
* @return the result as String
|
||||
* @return the result as a String
|
||||
* @throws IOException if the template wasn't found or couldn't be read
|
||||
* @throws freemarker.template.TemplateException if rendering failed
|
||||
* @see org.springframework.mail.MailPreparationException
|
||||
|
||||
+7
-5
@@ -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.
|
||||
@@ -29,9 +29,11 @@ import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* FreeMarker {@link TemplateLoader} adapter that loads via a Spring {@link ResourceLoader}.
|
||||
* Used by {@link FreeMarkerConfigurationFactory} for any resource loader path that cannot
|
||||
* be resolved to a {@link java.io.File}.
|
||||
* FreeMarker {@link TemplateLoader} adapter that loads template files via a
|
||||
* Spring {@link ResourceLoader}.
|
||||
*
|
||||
* <p>Used by {@link FreeMarkerConfigurationFactory} for any resource loader path
|
||||
* that cannot be resolved to a {@link java.io.File}.
|
||||
*
|
||||
* @author Juergen Hoeller
|
||||
* @since 14.03.2004
|
||||
@@ -48,7 +50,7 @@ public class SpringTemplateLoader implements TemplateLoader {
|
||||
|
||||
|
||||
/**
|
||||
* Create a new SpringTemplateLoader.
|
||||
* Create a new {@code SpringTemplateLoader}.
|
||||
* @param resourceLoader the Spring ResourceLoader to use
|
||||
* @param templateLoaderPath the template loader path to use
|
||||
*/
|
||||
|
||||
+24
-3
@@ -503,7 +503,10 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker
|
||||
if (CompletableFuture.class.isAssignableFrom(context.getMethod().getReturnType())) {
|
||||
CompletableFuture<?> result = cache.retrieve(key);
|
||||
if (result != null) {
|
||||
return result.thenCompose(value -> (CompletableFuture<?>) evaluate(
|
||||
return result.exceptionally(ex -> {
|
||||
getErrorHandler().handleCacheGetError((RuntimeException) ex, cache, key);
|
||||
return null;
|
||||
}).thenCompose(value -> (CompletableFuture<?>) evaluate(
|
||||
(value != null ? CompletableFuture.completedFuture(unwrapCacheValue(value)) : null),
|
||||
invoker, method, contexts));
|
||||
}
|
||||
@@ -1131,12 +1134,30 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker
|
||||
if (adapter.isMultiValue()) {
|
||||
return adapter.fromPublisher(Flux.from(Mono.fromFuture(cachedFuture))
|
||||
.switchIfEmpty(Flux.defer(() -> (Flux) evaluate(null, invoker, method, contexts)))
|
||||
.flatMap(v -> evaluate(valueToFlux(v, contexts), invoker, method, contexts)));
|
||||
.flatMap(v -> evaluate(valueToFlux(v, contexts), invoker, method, contexts))
|
||||
.onErrorResume(RuntimeException.class, ex -> {
|
||||
try {
|
||||
getErrorHandler().handleCacheGetError((RuntimeException) ex, cache, key);
|
||||
return evaluate(null, invoker, method, contexts);
|
||||
}
|
||||
catch (RuntimeException exception) {
|
||||
return Flux.error(exception);
|
||||
}
|
||||
}));
|
||||
}
|
||||
else {
|
||||
return adapter.fromPublisher(Mono.fromFuture(cachedFuture)
|
||||
.switchIfEmpty(Mono.defer(() -> (Mono) evaluate(null, invoker, method, contexts)))
|
||||
.flatMap(v -> evaluate(Mono.justOrEmpty(unwrapCacheValue(v)), invoker, method, contexts)));
|
||||
.flatMap(v -> evaluate(Mono.justOrEmpty(unwrapCacheValue(v)), invoker, method, contexts))
|
||||
.onErrorResume(RuntimeException.class, ex -> {
|
||||
try {
|
||||
getErrorHandler().handleCacheGetError((RuntimeException) ex, cache, key);
|
||||
return evaluate(null, invoker, method, contexts);
|
||||
}
|
||||
catch (RuntimeException exception) {
|
||||
return Mono.error(exception);
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
return NOT_HANDLED;
|
||||
|
||||
+11
-11
@@ -756,12 +756,7 @@ public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPo
|
||||
}
|
||||
|
||||
private CodeBlock handleNull(@Nullable Object value, Supplier<CodeBlock> nonNull) {
|
||||
if (value == null) {
|
||||
return CodeBlock.of("null");
|
||||
}
|
||||
else {
|
||||
return nonNull.get();
|
||||
}
|
||||
return (value == null ? CodeBlock.of("null") : nonNull.get());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -781,8 +776,9 @@ public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPo
|
||||
}
|
||||
|
||||
@Override
|
||||
public CodeBlock generateSetBeanDefinitionPropertiesCode(GenerationContext generationContext,
|
||||
BeanRegistrationCode beanRegistrationCode, RootBeanDefinition beanDefinition, Predicate<String> attributeFilter) {
|
||||
public CodeBlock generateSetBeanDefinitionPropertiesCode(
|
||||
GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode,
|
||||
RootBeanDefinition beanDefinition, Predicate<String> attributeFilter) {
|
||||
|
||||
CodeBlock.Builder code = CodeBlock.builder();
|
||||
code.add(super.generateSetBeanDefinitionPropertiesCode(generationContext,
|
||||
@@ -793,17 +789,21 @@ public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPo
|
||||
}
|
||||
|
||||
@Override
|
||||
public CodeBlock generateInstanceSupplierCode(GenerationContext generationContext,
|
||||
BeanRegistrationCode beanRegistrationCode, boolean allowDirectSupplierShortcut) {
|
||||
public CodeBlock generateInstanceSupplierCode(
|
||||
GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode,
|
||||
boolean allowDirectSupplierShortcut) {
|
||||
|
||||
InstantiationDescriptor instantiationDescriptor = proxyInstantiationDescriptor(
|
||||
generationContext.getRuntimeHints(), this.registeredBean.resolveInstantiationDescriptor());
|
||||
|
||||
return new InstanceSupplierCodeGenerator(generationContext,
|
||||
beanRegistrationCode.getClassName(), beanRegistrationCode.getMethods(), allowDirectSupplierShortcut)
|
||||
.generateCode(this.registeredBean, instantiationDescriptor);
|
||||
}
|
||||
|
||||
private InstantiationDescriptor proxyInstantiationDescriptor(RuntimeHints runtimeHints, InstantiationDescriptor instantiationDescriptor) {
|
||||
private InstantiationDescriptor proxyInstantiationDescriptor(
|
||||
RuntimeHints runtimeHints, InstantiationDescriptor instantiationDescriptor) {
|
||||
|
||||
Executable userExecutable = instantiationDescriptor.executable();
|
||||
if (userExecutable instanceof Constructor<?> userConstructor) {
|
||||
try {
|
||||
|
||||
+10
-1
@@ -36,6 +36,7 @@ import org.apache.commons.logging.LogFactory;
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.CachedIntrospectionResults;
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.beans.factory.BeanNotOfRequiredTypeException;
|
||||
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
|
||||
@@ -949,7 +950,15 @@ public abstract class AbstractApplicationContext extends DefaultResourceLoader
|
||||
// Initialize LoadTimeWeaverAware beans early to allow for registering their transformers early.
|
||||
String[] weaverAwareNames = beanFactory.getBeanNamesForType(LoadTimeWeaverAware.class, false, false);
|
||||
for (String weaverAwareName : weaverAwareNames) {
|
||||
beanFactory.getBean(weaverAwareName, LoadTimeWeaverAware.class);
|
||||
try {
|
||||
beanFactory.getBean(weaverAwareName, LoadTimeWeaverAware.class);
|
||||
}
|
||||
catch (BeanNotOfRequiredTypeException ex) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Failed to initialize LoadTimeWeaverAware bean '" + weaverAwareName +
|
||||
"' due to unexpected type mismatch: " + ex.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Stop using the temporary ClassLoader for type matching.
|
||||
|
||||
+2
-2
@@ -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 @@ import org.springframework.util.Assert;
|
||||
/**
|
||||
* Configures basic date formatting for use with Spring, primarily for
|
||||
* {@link org.springframework.format.annotation.DateTimeFormat} declarations.
|
||||
* Applies to fields of type {@link Date}, {@link Calendar} and {@code long}.
|
||||
* Applies to fields of type {@link Date}, {@link Calendar}, and {@code long}.
|
||||
*
|
||||
* <p>Designed for direct instantiation but also exposes the static
|
||||
* {@link #addDateConverters(ConverterRegistry)} utility method for
|
||||
|
||||
+7
-7
@@ -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.
|
||||
@@ -76,8 +76,8 @@ public class DateTimeFormatterRegistrar implements FormatterRegistrar {
|
||||
|
||||
/**
|
||||
* Set whether standard ISO formatting should be applied to all date/time types.
|
||||
* Default is "false" (no).
|
||||
* <p>If set to "true", the "dateStyle", "timeStyle" and "dateTimeStyle"
|
||||
* <p>Default is "false" (no).
|
||||
* <p>If set to "true", the "dateStyle", "timeStyle", and "dateTimeStyle"
|
||||
* properties are effectively ignored.
|
||||
*/
|
||||
public void setUseIsoFormat(boolean useIsoFormat) {
|
||||
@@ -88,7 +88,7 @@ public class DateTimeFormatterRegistrar implements FormatterRegistrar {
|
||||
|
||||
/**
|
||||
* Set the default format style of {@link java.time.LocalDate} objects.
|
||||
* Default is {@link java.time.format.FormatStyle#SHORT}.
|
||||
* <p>Default is {@link java.time.format.FormatStyle#SHORT}.
|
||||
*/
|
||||
public void setDateStyle(FormatStyle dateStyle) {
|
||||
this.factories.get(Type.DATE).setDateStyle(dateStyle);
|
||||
@@ -96,7 +96,7 @@ public class DateTimeFormatterRegistrar implements FormatterRegistrar {
|
||||
|
||||
/**
|
||||
* Set the default format style of {@link java.time.LocalTime} objects.
|
||||
* Default is {@link java.time.format.FormatStyle#SHORT}.
|
||||
* <p>Default is {@link java.time.format.FormatStyle#SHORT}.
|
||||
*/
|
||||
public void setTimeStyle(FormatStyle timeStyle) {
|
||||
this.factories.get(Type.TIME).setTimeStyle(timeStyle);
|
||||
@@ -104,7 +104,7 @@ public class DateTimeFormatterRegistrar implements FormatterRegistrar {
|
||||
|
||||
/**
|
||||
* Set the default format style of {@link java.time.LocalDateTime} objects.
|
||||
* Default is {@link java.time.format.FormatStyle#SHORT}.
|
||||
* <p>Default is {@link java.time.format.FormatStyle#SHORT}.
|
||||
*/
|
||||
public void setDateTimeStyle(FormatStyle dateTimeStyle) {
|
||||
this.factories.get(Type.DATE_TIME).setDateTimeStyle(dateTimeStyle);
|
||||
@@ -138,7 +138,7 @@ public class DateTimeFormatterRegistrar implements FormatterRegistrar {
|
||||
|
||||
/**
|
||||
* Set the formatter that will be used for objects representing date and time values.
|
||||
* <p>This formatter will be used for {@link LocalDateTime}, {@link ZonedDateTime}
|
||||
* <p>This formatter will be used for {@link LocalDateTime}, {@link ZonedDateTime},
|
||||
* and {@link OffsetDateTime} types. When specified, the
|
||||
* {@link #setDateTimeStyle dateTimeStyle} and
|
||||
* {@link #setUseIsoFormat useIsoFormat} properties will be ignored.
|
||||
|
||||
+11
-3
@@ -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,18 @@ import org.springframework.util.StringUtils;
|
||||
*/
|
||||
abstract class DateTimeFormatterUtils {
|
||||
|
||||
/**
|
||||
* Create a {@link DateTimeFormatter} for the supplied pattern, configured with
|
||||
* {@linkplain ResolverStyle#STRICT strict} resolution.
|
||||
* <p>Note that the strict resolution does not affect the parsing.
|
||||
* @param pattern the pattern to use
|
||||
* @return a new {@code DateTimeFormatter}
|
||||
* @see ResolverStyle#STRICT
|
||||
*/
|
||||
static DateTimeFormatter createStrictDateTimeFormatter(String pattern) {
|
||||
// Using strict parsing to align with Joda-Time and standard DateFormat behavior:
|
||||
// Using strict resolution to align with Joda-Time and standard DateFormat behavior:
|
||||
// otherwise, an overflow like e.g. Feb 29 for a non-leap-year wouldn't get rejected.
|
||||
// However, with strict parsing, a year digit needs to be specified as 'u'...
|
||||
// However, with strict resolution, a year digit needs to be specified as 'u'...
|
||||
String patternToUse = StringUtils.replace(pattern, "yy", "uu");
|
||||
return DateTimeFormatter.ofPattern(patternToUse).withResolverStyle(ResolverStyle.STRICT);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 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,7 +46,7 @@ public class ConcurrentModel extends ConcurrentHashMap<String, Object> implement
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a new {@code ModelMap} containing the supplied attribute
|
||||
* Construct a new {@code ConcurrentModel} containing the supplied attribute
|
||||
* under the supplied name.
|
||||
* @see #addAttribute(String, Object)
|
||||
*/
|
||||
@@ -55,8 +55,8 @@ public class ConcurrentModel extends ConcurrentHashMap<String, Object> implement
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a new {@code ModelMap} containing the supplied attribute.
|
||||
* Uses attribute name generation to generate the key for the supplied model
|
||||
* Construct a new {@code ConcurrentModel} containing the supplied attribute.
|
||||
* <p>Uses attribute name generation to generate the key for the supplied model
|
||||
* object.
|
||||
* @see #addAttribute(Object)
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Generic support for UI layer concepts.
|
||||
* Provides a generic ModelMap for model holding.
|
||||
* <p>Provides generic {@code Model} and {@code ModelMap} holders for model attributes.
|
||||
*/
|
||||
@NonNullApi
|
||||
@NonNullFields
|
||||
|
||||
+6
-3
@@ -50,7 +50,6 @@ import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.ParameterNameDiscoverer;
|
||||
import org.springframework.core.annotation.AnnotationUtils;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.function.SingletonSupplier;
|
||||
import org.springframework.validation.BeanPropertyBindingResult;
|
||||
import org.springframework.validation.BindingResult;
|
||||
@@ -321,7 +320,7 @@ public class MethodValidationAdapter implements MethodValidator {
|
||||
|
||||
Object arg = argumentFunction.apply(parameter.getParameterIndex());
|
||||
|
||||
// If the arg is a container, we need to element, but the only way to extract it
|
||||
// If the arg is a container, we need the element, but the only way to extract it
|
||||
// is to check for and use a container index or key on the next node:
|
||||
// https://github.com/jakartaee/validation/issues/194
|
||||
|
||||
@@ -346,12 +345,16 @@ public class MethodValidationAdapter implements MethodValidator {
|
||||
value = map.get(key);
|
||||
container = map;
|
||||
}
|
||||
else if (arg instanceof Iterable<?>) {
|
||||
// No index or key, cannot access the specific value
|
||||
value = arg;
|
||||
container = arg;
|
||||
}
|
||||
else if (arg instanceof Optional<?> optional) {
|
||||
value = optional.orElse(null);
|
||||
container = optional;
|
||||
}
|
||||
else {
|
||||
Assert.state(!node.isInIterable(), "No way to unwrap Iterable without index");
|
||||
value = arg;
|
||||
container = null;
|
||||
}
|
||||
|
||||
+1
-1
@@ -174,7 +174,7 @@ public class MethodValidationInterceptor implements MethodInterceptor {
|
||||
Object returnValue = invocation.proceed();
|
||||
|
||||
if (this.adaptViolations) {
|
||||
this.validationAdapter.applyReturnValueValidation(target, method, null, arguments, groups);
|
||||
this.validationAdapter.applyReturnValueValidation(target, method, null, returnValue, groups);
|
||||
}
|
||||
else {
|
||||
violations = this.validationAdapter.invokeValidatorForReturnValue(target, method, returnValue, groups);
|
||||
|
||||
Vendored
+87
-1
@@ -18,8 +18,10 @@ package org.springframework.cache.annotation;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
import reactor.core.publisher.Flux;
|
||||
@@ -29,12 +31,15 @@ import org.springframework.cache.Cache;
|
||||
import org.springframework.cache.CacheManager;
|
||||
import org.springframework.cache.concurrent.ConcurrentMapCache;
|
||||
import org.springframework.cache.concurrent.ConcurrentMapCacheManager;
|
||||
import org.springframework.cache.interceptor.CacheErrorHandler;
|
||||
import org.springframework.cache.interceptor.LoggingCacheErrorHandler;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.AssertionsForClassTypes.catchThrowable;
|
||||
|
||||
/**
|
||||
* Tests for annotation-based caching methods that use reactive operators.
|
||||
@@ -113,6 +118,51 @@ class ReactiveCachingTests {
|
||||
ctx.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void cacheErrorHandlerWithLoggingCacheErrorHandler() {
|
||||
AnnotationConfigApplicationContext ctx =
|
||||
new AnnotationConfigApplicationContext(ExceptionCacheManager.class, ReactiveCacheableService.class, ErrorHandlerCachingConfiguration.class);
|
||||
ReactiveCacheableService service = ctx.getBean(ReactiveCacheableService.class);
|
||||
|
||||
Object key = new Object();
|
||||
Long r1 = service.cacheFuture(key).join();
|
||||
|
||||
assertThat(r1).isNotNull();
|
||||
assertThat(r1).as("cacheFuture").isEqualTo(0L);
|
||||
|
||||
key = new Object();
|
||||
|
||||
r1 = service.cacheMono(key).block();
|
||||
|
||||
assertThat(r1).isNotNull();
|
||||
assertThat(r1).as("cacheMono").isEqualTo(1L);
|
||||
|
||||
key = new Object();
|
||||
|
||||
r1 = service.cacheFlux(key).blockFirst();
|
||||
|
||||
assertThat(r1).isNotNull();
|
||||
assertThat(r1).as("cacheFlux blockFirst").isEqualTo(2L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void cacheErrorHandlerWithSimpleCacheErrorHandler() {
|
||||
AnnotationConfigApplicationContext ctx =
|
||||
new AnnotationConfigApplicationContext(ExceptionCacheManager.class, ReactiveCacheableService.class);
|
||||
ReactiveCacheableService service = ctx.getBean(ReactiveCacheableService.class);
|
||||
|
||||
Throwable completableFuturThrowable = catchThrowable(() -> service.cacheFuture(new Object()).join());
|
||||
assertThat(completableFuturThrowable).isInstanceOf(CompletionException.class)
|
||||
.extracting(Throwable::getCause)
|
||||
.isInstanceOf(UnsupportedOperationException.class);
|
||||
|
||||
Throwable monoThrowable = catchThrowable(() -> service.cacheMono(new Object()).block());
|
||||
assertThat(monoThrowable).isInstanceOf(UnsupportedOperationException.class);
|
||||
|
||||
Throwable fluxThrowable = catchThrowable(() -> service.cacheFlux(new Object()).blockFirst());
|
||||
assertThat(fluxThrowable).isInstanceOf(UnsupportedOperationException.class);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(classes = {EarlyCacheHitDeterminationConfig.class,
|
||||
EarlyCacheHitDeterminationWithoutNullValuesConfig.class,
|
||||
@@ -139,7 +189,6 @@ class ReactiveCachingTests {
|
||||
ctx.close();
|
||||
}
|
||||
|
||||
|
||||
@CacheConfig(cacheNames = "first")
|
||||
static class ReactiveCacheableService {
|
||||
|
||||
@@ -242,4 +291,41 @@ class ReactiveCachingTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class ErrorHandlerCachingConfiguration implements CachingConfigurer {
|
||||
|
||||
@Bean
|
||||
@Override
|
||||
public CacheErrorHandler errorHandler() {
|
||||
return new LoggingCacheErrorHandler();
|
||||
}
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@EnableCaching
|
||||
static class ExceptionCacheManager {
|
||||
|
||||
@Bean
|
||||
CacheManager cacheManager() {
|
||||
return new ConcurrentMapCacheManager("first") {
|
||||
@Override
|
||||
protected Cache createConcurrentMapCache(String name) {
|
||||
return new ConcurrentMapCache(name, isAllowNullValues()) {
|
||||
@Override
|
||||
public CompletableFuture<?> retrieve(Object key) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
throw new UnsupportedOperationException("Test exception on retrieve");
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void put(Object key, @Nullable Object value) {
|
||||
throw new UnsupportedOperationException("Test exception on put");
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+6
-7
@@ -61,12 +61,11 @@ class DateFormattingTests {
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
DateFormatterRegistrar registrar = new DateFormatterRegistrar();
|
||||
setup(registrar);
|
||||
DefaultConversionService.addDefaultConverters(conversionService);
|
||||
setup(new DateFormatterRegistrar());
|
||||
}
|
||||
|
||||
private void setup(DateFormatterRegistrar registrar) {
|
||||
DefaultConversionService.addDefaultConverters(conversionService);
|
||||
registrar.registerFormatters(conversionService);
|
||||
|
||||
SimpleDateBean bean = new SimpleDateBean();
|
||||
@@ -172,7 +171,7 @@ class DateFormattingTests {
|
||||
@Test
|
||||
@Disabled
|
||||
void testBindDateAnnotatedWithFallbackError() {
|
||||
// TODO This currently passes because of the Date(String) constructor fallback is used
|
||||
// TODO This currently passes because the Date(String) constructor fallback is used
|
||||
MutablePropertyValues propertyValues = new MutablePropertyValues();
|
||||
propertyValues.add("styleDate", "Oct 031, 2009");
|
||||
binder.bind(propertyValues);
|
||||
@@ -181,7 +180,7 @@ class DateFormattingTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBindDateAnnotatedPattern() {
|
||||
void testBindDateTimePatternAnnotated() {
|
||||
MutablePropertyValues propertyValues = new MutablePropertyValues();
|
||||
propertyValues.add("patternDate", "10/31/09 1:05");
|
||||
binder.bind(propertyValues);
|
||||
@@ -190,7 +189,7 @@ class DateFormattingTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBindDateAnnotatedPatternWithGlobalFormat() {
|
||||
void testBindDateTimePatternAnnotatedWithGlobalFormat() {
|
||||
DateFormatterRegistrar registrar = new DateFormatterRegistrar();
|
||||
DateFormatter dateFormatter = new DateFormatter();
|
||||
dateFormatter.setIso(ISO.DATE_TIME);
|
||||
@@ -205,7 +204,7 @@ class DateFormattingTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBindDateTimeOverflow() {
|
||||
void testBindDateTimePatternAnnotatedWithOverflow() {
|
||||
MutablePropertyValues propertyValues = new MutablePropertyValues();
|
||||
propertyValues.add("patternDate", "02/29/09 12:00 PM");
|
||||
binder.bind(propertyValues);
|
||||
|
||||
-4
@@ -23,10 +23,6 @@ import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @author Phillip Webb
|
||||
* @author Sam Brannen
|
||||
|
||||
+2
-2
@@ -78,8 +78,8 @@ class DateTimeFormatterFactoryTests {
|
||||
void createDateTimeFormatterInOrderOfPropertyPriority() {
|
||||
factory.setStylePattern("SS");
|
||||
String value = applyLocale(factory.createDateTimeFormatter()).format(dateTime);
|
||||
assertThat(value).startsWith("10/21/09");
|
||||
assertThat(value).endsWith("12:10 PM");
|
||||
// \p{Zs} matches any Unicode space character
|
||||
assertThat(value).startsWith("10/21/09").matches(".+?12:10\\p{Zs}PM");
|
||||
|
||||
factory.setIso(ISO.DATE);
|
||||
assertThat(applyLocale(factory.createDateTimeFormatter()).format(dateTime)).isEqualTo("2009-10-21");
|
||||
|
||||
+50
-17
@@ -42,6 +42,7 @@ 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.junit.jupiter.api.condition.EnabledForJreRange;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
|
||||
@@ -58,6 +59,8 @@ import org.springframework.validation.DataBinder;
|
||||
import org.springframework.validation.FieldError;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.jupiter.api.condition.JRE.JAVA_19;
|
||||
import static org.junit.jupiter.api.condition.JRE.JAVA_20;
|
||||
|
||||
/**
|
||||
* @author Keith Donald
|
||||
@@ -68,6 +71,12 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
*/
|
||||
class DateTimeFormattingTests {
|
||||
|
||||
// JDK <= 19 requires a standard space before "AM/PM".
|
||||
// JDK >= 20 requires a NNBSP before "AM/PM".
|
||||
// \u202F is a narrow non-breaking space (NNBSP).
|
||||
private static final String TIME_SEPARATOR = (Runtime.version().feature() < 20 ? " " : "\u202F");
|
||||
|
||||
|
||||
private final FormattingConversionService conversionService = new FormattingConversionService();
|
||||
|
||||
private DataBinder binder;
|
||||
@@ -210,10 +219,11 @@ class DateTimeFormattingTests {
|
||||
@Test
|
||||
void testBindLocalTime() {
|
||||
MutablePropertyValues propertyValues = new MutablePropertyValues();
|
||||
propertyValues.add("localTime", "12:00 PM");
|
||||
propertyValues.add("localTime", "12:00%sPM".formatted(TIME_SEPARATOR));
|
||||
binder.bind(propertyValues);
|
||||
assertThat(binder.getBindingResult().getErrorCount()).isZero();
|
||||
assertThat(binder.getBindingResult().getFieldValue("localTime")).isEqualTo("12:00 PM");
|
||||
// \p{Zs} matches any Unicode space character
|
||||
assertThat(binder.getBindingResult().getFieldValue("localTime")).asString().matches("12:00\\p{Zs}PM");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -222,7 +232,8 @@ class DateTimeFormattingTests {
|
||||
propertyValues.add("localTime", "12:00:00");
|
||||
binder.bind(propertyValues);
|
||||
assertThat(binder.getBindingResult().getErrorCount()).isZero();
|
||||
assertThat(binder.getBindingResult().getFieldValue("localTime")).isEqualTo("12:00 PM");
|
||||
// \p{Zs} matches any Unicode space character
|
||||
assertThat(binder.getBindingResult().getFieldValue("localTime")).asString().matches("12:00\\p{Zs}PM");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -231,10 +242,11 @@ class DateTimeFormattingTests {
|
||||
registrar.setTimeStyle(FormatStyle.MEDIUM);
|
||||
setup(registrar);
|
||||
MutablePropertyValues propertyValues = new MutablePropertyValues();
|
||||
propertyValues.add("localTime", "12:00:00 PM");
|
||||
propertyValues.add("localTime", "12:00:00%sPM".formatted(TIME_SEPARATOR));
|
||||
binder.bind(propertyValues);
|
||||
assertThat(binder.getBindingResult().getErrorCount()).isZero();
|
||||
assertThat(binder.getBindingResult().getFieldValue("localTime")).isEqualTo("12:00:00 PM");
|
||||
// \p{Zs} matches any Unicode space character
|
||||
assertThat(binder.getBindingResult().getFieldValue("localTime")).asString().matches("12:00:00\\p{Zs}PM");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -252,10 +264,11 @@ class DateTimeFormattingTests {
|
||||
@Test
|
||||
void testBindLocalTimeAnnotated() {
|
||||
MutablePropertyValues propertyValues = new MutablePropertyValues();
|
||||
propertyValues.add("styleLocalTime", "12:00:00 PM");
|
||||
propertyValues.add("styleLocalTime", "12:00:00%sPM".formatted(TIME_SEPARATOR));
|
||||
binder.bind(propertyValues);
|
||||
assertThat(binder.getBindingResult().getErrorCount()).isZero();
|
||||
assertThat(binder.getBindingResult().getFieldValue("styleLocalTime")).isEqualTo("12:00:00 PM");
|
||||
// \p{Zs} matches any Unicode space character
|
||||
assertThat(binder.getBindingResult().getFieldValue("styleLocalTime")).asString().matches("12:00:00\\p{Zs}PM");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -264,7 +277,8 @@ class DateTimeFormattingTests {
|
||||
propertyValues.add("localTime", new GregorianCalendar(1970, 0, 0, 12, 0));
|
||||
binder.bind(propertyValues);
|
||||
assertThat(binder.getBindingResult().getErrorCount()).isZero();
|
||||
assertThat(binder.getBindingResult().getFieldValue("localTime")).isEqualTo("12:00 PM");
|
||||
// \p{Zs} matches any Unicode space character
|
||||
assertThat(binder.getBindingResult().getFieldValue("localTime")).asString().matches("12:00\\p{Zs}PM");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -274,7 +288,8 @@ class DateTimeFormattingTests {
|
||||
binder.bind(propertyValues);
|
||||
assertThat(binder.getBindingResult().getErrorCount()).isZero();
|
||||
String value = binder.getBindingResult().getFieldValue("localDateTime").toString();
|
||||
assertThat(value).startsWith("10/31/09").endsWith("12:00 PM");
|
||||
// \p{Zs} matches any Unicode space character
|
||||
assertThat(value).startsWith("10/31/09").matches(".+?12:00\\p{Zs}PM");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -284,7 +299,8 @@ class DateTimeFormattingTests {
|
||||
binder.bind(propertyValues);
|
||||
assertThat(binder.getBindingResult().getErrorCount()).isZero();
|
||||
String value = binder.getBindingResult().getFieldValue("localDateTime").toString();
|
||||
assertThat(value).startsWith("10/31/09").endsWith("12:00 PM");
|
||||
// \p{Zs} matches any Unicode space character
|
||||
assertThat(value).startsWith("10/31/09").matches(".+?12:00\\p{Zs}PM");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -294,7 +310,8 @@ class DateTimeFormattingTests {
|
||||
binder.bind(propertyValues);
|
||||
assertThat(binder.getBindingResult().getErrorCount()).isZero();
|
||||
String value = binder.getBindingResult().getFieldValue("styleLocalDateTime").toString();
|
||||
assertThat(value).startsWith("Oct 31, 2009").endsWith("12:00:00 PM");
|
||||
// \p{Zs} matches any Unicode space character
|
||||
assertThat(value).startsWith("Oct 31, 2009").matches(".+?12:00:00\\p{Zs}PM");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -304,7 +321,8 @@ class DateTimeFormattingTests {
|
||||
binder.bind(propertyValues);
|
||||
assertThat(binder.getBindingResult().getErrorCount()).isZero();
|
||||
String value = binder.getBindingResult().getFieldValue("localDateTime").toString();
|
||||
assertThat(value).startsWith("10/31/09").endsWith("12:00 PM");
|
||||
// \p{Zs} matches any Unicode space character
|
||||
assertThat(value).startsWith("10/31/09").matches(".+?12:00\\p{Zs}PM");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -317,7 +335,8 @@ class DateTimeFormattingTests {
|
||||
binder.bind(propertyValues);
|
||||
assertThat(binder.getBindingResult().getErrorCount()).isZero();
|
||||
String value = binder.getBindingResult().getFieldValue("localDateTime").toString();
|
||||
assertThat(value).startsWith("Oct 31, 2009").endsWith("12:00:00 PM");
|
||||
// \p{Zs} matches any Unicode space character
|
||||
assertThat(value).startsWith("Oct 31, 2009").matches(".+?12:00:00\\p{Zs}PM");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -558,18 +577,32 @@ class DateTimeFormattingTests {
|
||||
assertThat(bindingResult.getFieldValue(propertyName)).isEqualTo("2021-03-02");
|
||||
}
|
||||
|
||||
@EnabledForJreRange(max = JAVA_19)
|
||||
@ParameterizedTest(name = "input date: {0}")
|
||||
// @ValueSource(strings = {"12:00:00\u202FPM", "12:00:00", "12:00"})
|
||||
// JDK <= 19 requires a standard space before the "PM".
|
||||
@ValueSource(strings = {"12:00:00 PM", "12:00:00", "12:00"})
|
||||
void styleLocalTime(String propertyValue) {
|
||||
void styleLocalTime_PreJDK20(String propertyValue) {
|
||||
styleLocalTime(propertyValue);
|
||||
}
|
||||
|
||||
@EnabledForJreRange(min = JAVA_20)
|
||||
@ParameterizedTest(name = "input date: {0}")
|
||||
// JDK >= 20 requires a NNBSP before the "PM".
|
||||
// \u202F is a narrow non-breaking space (NNBSP).
|
||||
@ValueSource(strings = {"12:00:00\u202FPM", "12:00:00", "12:00"})
|
||||
void styleLocalTime_PostJDK20(String propertyValue) {
|
||||
styleLocalTime(propertyValue);
|
||||
}
|
||||
|
||||
private void styleLocalTime(String propertyValue) {
|
||||
String propertyName = "styleLocalTimeWithFallbackPatterns";
|
||||
MutablePropertyValues propertyValues = new MutablePropertyValues();
|
||||
propertyValues.add(propertyName, propertyValue);
|
||||
binder.bind(propertyValues);
|
||||
BindingResult bindingResult = binder.getBindingResult();
|
||||
assertThat(bindingResult.getErrorCount()).isZero();
|
||||
// assertThat(bindingResult.getFieldValue(propertyName)).asString().matches("12:00:00\\SPM");
|
||||
assertThat(bindingResult.getFieldValue(propertyName)).isEqualTo("12:00:00 PM");
|
||||
// \p{Zs} matches any Unicode space character
|
||||
assertThat(bindingResult.getFieldValue(propertyName)).asString().matches("12:00:00\\p{Zs}PM");
|
||||
}
|
||||
|
||||
@ParameterizedTest(name = "input date: {0}")
|
||||
|
||||
+19
-2
@@ -19,6 +19,7 @@ package org.springframework.validation.beanvalidation;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
@@ -201,9 +202,7 @@ class MethodValidationAdapterTests {
|
||||
Method method = getMethod(target, "addHobbies");
|
||||
|
||||
testArgs(target, method, new Object[] {List.of(" ")}, ex -> {
|
||||
|
||||
assertThat(ex.getAllValidationResults()).hasSize(1);
|
||||
|
||||
assertValueResult(ex.getValueResults().get(0), 0, " ", List.of("""
|
||||
org.springframework.context.support.DefaultMessageSourceResolvable: \
|
||||
codes [NotBlank.myService#addHobbies.hobbies,NotBlank.hobbies,NotBlank.java.util.List,NotBlank]; \
|
||||
@@ -213,6 +212,22 @@ class MethodValidationAdapterTests {
|
||||
});
|
||||
}
|
||||
|
||||
@Test // gh-33150
|
||||
void validateValueSetArgument() {
|
||||
MyService target = new MyService();
|
||||
Method method = getMethod(target, "addUniqueHobbies");
|
||||
|
||||
testArgs(target, method, new Object[] {Set.of("test", " ")}, ex -> {
|
||||
assertThat(ex.getAllValidationResults()).hasSize(1);
|
||||
assertValueResult(ex.getValueResults().get(0), 0, Set.of("test", " "), List.of("""
|
||||
org.springframework.context.support.DefaultMessageSourceResolvable: \
|
||||
codes [NotBlank.myService#addUniqueHobbies.hobbies,NotBlank.hobbies,NotBlank.java.util.Set,NotBlank]; \
|
||||
arguments [org.springframework.context.support.DefaultMessageSourceResolvable: \
|
||||
codes [myService#addUniqueHobbies.hobbies,hobbies]; \
|
||||
arguments []; default message [hobbies]]; default message [must not be blank]"""));
|
||||
});
|
||||
}
|
||||
|
||||
private void testArgs(Object target, Method method, Object[] args, Consumer<MethodValidationResult> consumer) {
|
||||
consumer.accept(this.validationAdapter.validateArguments(target, method, null, args, new Class<?>[0]));
|
||||
}
|
||||
@@ -271,6 +286,8 @@ class MethodValidationAdapterTests {
|
||||
public void addHobbies(List<@NotBlank String> hobbies) {
|
||||
}
|
||||
|
||||
public void addUniqueHobbies(Set<@NotBlank String> hobbies) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
+34
-18
@@ -20,7 +20,8 @@ import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import jakarta.validation.ValidationException;
|
||||
import jakarta.validation.ConstraintViolationException;
|
||||
import jakarta.validation.Validation;
|
||||
import jakarta.validation.Validator;
|
||||
import jakarta.validation.constraints.Max;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
@@ -28,6 +29,8 @@ import jakarta.validation.groups.Default;
|
||||
import org.aopalliance.intercept.MethodInterceptor;
|
||||
import org.aopalliance.intercept.MethodInvocation;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
|
||||
import org.springframework.aop.framework.ProxyFactory;
|
||||
import org.springframework.beans.MutablePropertyValues;
|
||||
@@ -46,6 +49,7 @@ import org.springframework.scheduling.annotation.AsyncAnnotationBeanPostProcesso
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.validation.method.MethodValidationException;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
@@ -59,52 +63,64 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
*/
|
||||
class MethodValidationProxyTests {
|
||||
|
||||
@Test
|
||||
@ParameterizedTest
|
||||
@ValueSource(booleans = {true, false})
|
||||
@SuppressWarnings("unchecked")
|
||||
public void testMethodValidationInterceptor() {
|
||||
void testMethodValidationInterceptor(boolean adaptViolations) {
|
||||
MyValidBean bean = new MyValidBean();
|
||||
ProxyFactory factory = new ProxyFactory(bean);
|
||||
factory.addAdvice(new MethodValidationInterceptor());
|
||||
factory.addAdvice(adaptViolations ?
|
||||
new MethodValidationInterceptor(() -> Validation.buildDefaultValidatorFactory().getValidator(), true) :
|
||||
new MethodValidationInterceptor());
|
||||
factory.addAdvisor(new AsyncAnnotationAdvisor());
|
||||
doTestProxyValidation((MyValidInterface<String>) factory.getProxy());
|
||||
doTestProxyValidation((MyValidInterface<String>) factory.getProxy(),
|
||||
(adaptViolations ? MethodValidationException.class : ConstraintViolationException.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@ParameterizedTest
|
||||
@ValueSource(booleans = {true, false})
|
||||
@SuppressWarnings("unchecked")
|
||||
public void testMethodValidationPostProcessor() {
|
||||
void testMethodValidationPostProcessor(boolean adaptViolations) {
|
||||
StaticApplicationContext context = new StaticApplicationContext();
|
||||
context.registerSingleton("mvpp", MethodValidationPostProcessor.class);
|
||||
context.registerBean(MethodValidationPostProcessor.class, adaptViolations ?
|
||||
() -> {
|
||||
MethodValidationPostProcessor postProcessor = new MethodValidationPostProcessor();
|
||||
postProcessor.setAdaptConstraintViolations(true);
|
||||
return postProcessor;
|
||||
} :
|
||||
MethodValidationPostProcessor::new);
|
||||
MutablePropertyValues pvs = new MutablePropertyValues();
|
||||
pvs.add("beforeExistingAdvisors", false);
|
||||
context.registerSingleton("aapp", AsyncAnnotationBeanPostProcessor.class, pvs);
|
||||
context.registerSingleton("bean", MyValidBean.class);
|
||||
context.refresh();
|
||||
doTestProxyValidation(context.getBean("bean", MyValidInterface.class));
|
||||
doTestProxyValidation(context.getBean("bean", MyValidInterface.class),
|
||||
adaptViolations ? MethodValidationException.class : ConstraintViolationException.class);
|
||||
context.close();
|
||||
}
|
||||
|
||||
@Test // gh-29782
|
||||
public void testMethodValidationPostProcessorForInterfaceOnlyProxy() {
|
||||
void testMethodValidationPostProcessorForInterfaceOnlyProxy() {
|
||||
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
|
||||
context.register(MethodValidationPostProcessor.class);
|
||||
context.registerBean(MyValidInterface.class, () ->
|
||||
ProxyFactory.getProxy(MyValidInterface.class, new MyValidClientInterfaceMethodInterceptor()));
|
||||
context.refresh();
|
||||
doTestProxyValidation(context.getBean(MyValidInterface.class));
|
||||
doTestProxyValidation(context.getBean(MyValidInterface.class), ConstraintViolationException.class);
|
||||
context.close();
|
||||
}
|
||||
|
||||
@SuppressWarnings("DataFlowIssue")
|
||||
private void doTestProxyValidation(MyValidInterface<String> proxy) {
|
||||
private void doTestProxyValidation(MyValidInterface<String> proxy, Class<? extends Exception> expectedExceptionClass) {
|
||||
assertThat(proxy.myValidMethod("value", 5)).isNotNull();
|
||||
assertThatExceptionOfType(ValidationException.class).isThrownBy(() -> proxy.myValidMethod("value", 15));
|
||||
assertThatExceptionOfType(ValidationException.class).isThrownBy(() -> proxy.myValidMethod(null, 5));
|
||||
assertThatExceptionOfType(ValidationException.class).isThrownBy(() -> proxy.myValidMethod("value", 0));
|
||||
assertThatExceptionOfType(expectedExceptionClass).isThrownBy(() -> proxy.myValidMethod("value", 15));
|
||||
assertThatExceptionOfType(expectedExceptionClass).isThrownBy(() -> proxy.myValidMethod(null, 5));
|
||||
assertThatExceptionOfType(expectedExceptionClass).isThrownBy(() -> proxy.myValidMethod("value", 0));
|
||||
proxy.myValidAsyncMethod("value", 5);
|
||||
assertThatExceptionOfType(ValidationException.class).isThrownBy(() -> proxy.myValidAsyncMethod("value", 15));
|
||||
assertThatExceptionOfType(ValidationException.class).isThrownBy(() -> proxy.myValidAsyncMethod(null, 5));
|
||||
assertThatExceptionOfType(expectedExceptionClass).isThrownBy(() -> proxy.myValidAsyncMethod("value", 15));
|
||||
assertThatExceptionOfType(expectedExceptionClass).isThrownBy(() -> proxy.myValidAsyncMethod(null, 5));
|
||||
assertThat(proxy.myGenericMethod("myValue")).isEqualTo("myValue");
|
||||
assertThatExceptionOfType(ValidationException.class).isThrownBy(() -> proxy.myGenericMethod(null));
|
||||
assertThatExceptionOfType(expectedExceptionClass).isThrownBy(() -> proxy.myGenericMethod(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -40,6 +40,13 @@ import org.springframework.util.Assert;
|
||||
* times. This also applies when constructed with an {@code InputStreamSource}
|
||||
* which lazily obtains the stream but only allows for single access as well.
|
||||
*
|
||||
* <p><b>NOTE: This class does not provide an independent {@link #contentLength()}
|
||||
* implementation: Any such call will consume the given {@code InputStream}!</b>
|
||||
* Consider overriding {@code #contentLength()} with a custom implementation if
|
||||
* possible. For any other purpose, it is not recommended to extend from this
|
||||
* class; this is particularly true when used with Spring's web resource rendering
|
||||
* which specifically skips {@code #contentLength()} for this exact class only.
|
||||
*
|
||||
* @author Juergen Hoeller
|
||||
* @author Sam Brannen
|
||||
* @since 28.12.2003
|
||||
@@ -132,8 +139,8 @@ public class InputStreamResource extends AbstractResource {
|
||||
@Override
|
||||
public InputStream getInputStream() throws IOException, IllegalStateException {
|
||||
if (this.read) {
|
||||
throw new IllegalStateException("InputStream has already been read - " +
|
||||
"do not use InputStreamResource if a stream needs to be read multiple times");
|
||||
throw new IllegalStateException("InputStream has already been read (possibly for early content length " +
|
||||
"determination) - do not use InputStreamResource if a stream needs to be read multiple times");
|
||||
}
|
||||
this.read = true;
|
||||
return this.inputStreamSource.getInputStream();
|
||||
|
||||
+43
-9
@@ -177,13 +177,28 @@ public class FunctionReference extends SpelNodeImpl {
|
||||
int spelParamCount = functionArgs.length;
|
||||
int declaredParamCount = declaredParams.parameterCount();
|
||||
|
||||
// We don't use methodHandle.isVarargsCollector(), because a MethodHandle created via
|
||||
// MethodHandle#bindTo() is "never a variable-arity method handle, even if the original
|
||||
// target method handle was." Thus, we merely assume/suspect that varargs are supported
|
||||
// if the last parameter type is an array.
|
||||
boolean isSuspectedVarargs = declaredParams.lastParameterType().isArray();
|
||||
|
||||
if (spelParamCount < declaredParamCount || (spelParamCount > declaredParamCount && !isSuspectedVarargs)) {
|
||||
// incorrect number, including more arguments and not a vararg
|
||||
// perhaps a subset of arguments was provided but the MethodHandle wasn't bound?
|
||||
if (isSuspectedVarargs) {
|
||||
if (spelParamCount < declaredParamCount - 1) {
|
||||
// Varargs, but the number of provided arguments (potentially 0) is insufficient
|
||||
// for a varargs invocation for the number of declared parameters.
|
||||
//
|
||||
// As stated in the Javadoc for MethodHandle#asVarargsCollector(), "the caller
|
||||
// must supply, at a minimum, N-1 arguments, where N is the arity of the target."
|
||||
throw new SpelEvaluationException(SpelMessage.INCORRECT_NUMBER_OF_ARGUMENTS_TO_FUNCTION,
|
||||
this.name, spelParamCount, (declaredParamCount - 1) + " or more");
|
||||
}
|
||||
}
|
||||
else if (spelParamCount != declaredParamCount) {
|
||||
// Incorrect number and not varargs. Perhaps a subset of arguments was provided,
|
||||
// but the MethodHandle wasn't bound?
|
||||
throw new SpelEvaluationException(SpelMessage.INCORRECT_NUMBER_OF_ARGUMENTS_TO_FUNCTION,
|
||||
this.name, functionArgs.length, declaredParamCount);
|
||||
this.name, spelParamCount, declaredParamCount);
|
||||
}
|
||||
|
||||
// simplest case: the MethodHandle is fully bound or represents a static method with no params:
|
||||
@@ -202,7 +217,7 @@ public class FunctionReference extends SpelNodeImpl {
|
||||
}
|
||||
}
|
||||
|
||||
// more complex case, we need to look at conversion and vararg repacking
|
||||
// more complex case, we need to look at conversion and varargs repackaging
|
||||
Integer varArgPosition = null;
|
||||
if (isSuspectedVarargs) {
|
||||
varArgPosition = declaredParamCount - 1;
|
||||
@@ -210,10 +225,29 @@ public class FunctionReference extends SpelNodeImpl {
|
||||
TypeConverter converter = state.getEvaluationContext().getTypeConverter();
|
||||
ReflectionHelper.convertAllMethodHandleArguments(converter, functionArgs, methodHandle, varArgPosition);
|
||||
|
||||
if (isSuspectedVarargs && declaredParamCount == 1) {
|
||||
// we only repack the varargs if it is the ONLY argument
|
||||
functionArgs = ReflectionHelper.setupArgumentsForVarargsInvocation(
|
||||
methodHandle.type().parameterArray(), functionArgs);
|
||||
if (isSuspectedVarargs) {
|
||||
if (declaredParamCount == 1) {
|
||||
// We only repackage the varargs if it is the ONLY argument -- for example,
|
||||
// when we are dealing with a bound MethodHandle.
|
||||
functionArgs = ReflectionHelper.setupArgumentsForVarargsInvocation(
|
||||
methodHandle.type().parameterArray(), functionArgs);
|
||||
}
|
||||
else if (spelParamCount == declaredParamCount) {
|
||||
// If the varargs were supplied already packaged in an array, we have to create
|
||||
// a new array, add the non-varargs arguments to the beginning of that array,
|
||||
// and add the unpackaged varargs arguments to the end of that array. The reason
|
||||
// is that MethodHandle.invokeWithArguments(Object...) does not expect varargs
|
||||
// to be packaged in an array, in contrast to how method invocation works with
|
||||
// reflection.
|
||||
int actualVarargsIndex = functionArgs.length - 1;
|
||||
if (actualVarargsIndex >= 0 && functionArgs[actualVarargsIndex].getClass().isArray()) {
|
||||
Object[] argsToUnpack = (Object[]) functionArgs[actualVarargsIndex];
|
||||
Object[] newArgs = new Object[actualVarargsIndex + argsToUnpack.length];
|
||||
System.arraycopy(functionArgs, 0, newArgs, 0, actualVarargsIndex);
|
||||
System.arraycopy(argsToUnpack, 0, newArgs, actualVarargsIndex, argsToUnpack.length);
|
||||
functionArgs = newArgs;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
+70
-65
@@ -49,12 +49,12 @@ public abstract class ReflectionHelper {
|
||||
|
||||
/**
|
||||
* Compare argument arrays and return information about whether they match.
|
||||
* A supplied type converter and conversionAllowed flag allow for matches to take
|
||||
* into account that a type may be transformed into a different type by the converter.
|
||||
* <p>The supplied type converter allows for matches to take into account that a type
|
||||
* may be transformed into a different type by the converter.
|
||||
* @param expectedArgTypes the types the method/constructor is expecting
|
||||
* @param suppliedArgTypes the types that are being supplied at the point of invocation
|
||||
* @param typeConverter a registered type converter
|
||||
* @return a MatchInfo object indicating what kind of match it was,
|
||||
* @return an {@code ArgumentsMatchInfo} object indicating what kind of match it was,
|
||||
* or {@code null} if it was not a match
|
||||
*/
|
||||
@Nullable
|
||||
@@ -62,7 +62,7 @@ public abstract class ReflectionHelper {
|
||||
List<TypeDescriptor> expectedArgTypes, List<TypeDescriptor> suppliedArgTypes, TypeConverter typeConverter) {
|
||||
|
||||
Assert.isTrue(expectedArgTypes.size() == suppliedArgTypes.size(),
|
||||
"Expected argument types and supplied argument types should be arrays of same length");
|
||||
"Expected argument types and supplied argument types should be lists of the same size");
|
||||
|
||||
ArgumentsMatchKind match = ArgumentsMatchKind.EXACT;
|
||||
for (int i = 0; i < expectedArgTypes.size() && match != null; i++) {
|
||||
@@ -136,13 +136,14 @@ public abstract class ReflectionHelper {
|
||||
|
||||
/**
|
||||
* Compare argument arrays and return information about whether they match.
|
||||
* A supplied type converter and conversionAllowed flag allow for matches to
|
||||
* take into account that a type may be transformed into a different type by the
|
||||
* converter. This variant of compareArguments also allows for a varargs match.
|
||||
* <p>The supplied type converter allows for matches to take into account that a type
|
||||
* may be transformed into a different type by the converter.
|
||||
* <p>This variant of {@link #compareArguments(List, List, TypeConverter)} also allows
|
||||
* for a varargs match.
|
||||
* @param expectedArgTypes the types the method/constructor is expecting
|
||||
* @param suppliedArgTypes the types that are being supplied at the point of invocation
|
||||
* @param typeConverter a registered type converter
|
||||
* @return a MatchInfo object indicating what kind of match it was,
|
||||
* @return an {@code ArgumentsMatchInfo} object indicating what kind of match it was,
|
||||
* or {@code null} if it was not a match
|
||||
*/
|
||||
@Nullable
|
||||
@@ -200,26 +201,26 @@ public abstract class ReflectionHelper {
|
||||
// Now... we have the final argument in the method we are checking as a match and we have 0
|
||||
// or more other arguments left to pass to it.
|
||||
TypeDescriptor varargsDesc = expectedArgTypes.get(expectedArgTypes.size() - 1);
|
||||
TypeDescriptor elementDesc = varargsDesc.getElementTypeDescriptor();
|
||||
Assert.state(elementDesc != null, "No element type");
|
||||
Class<?> varargsParamType = elementDesc.getType();
|
||||
TypeDescriptor componentTypeDesc = varargsDesc.getElementTypeDescriptor();
|
||||
Assert.state(componentTypeDesc != null, "Component type must not be null for a varargs array");
|
||||
Class<?> varargsComponentType = componentTypeDesc.getType();
|
||||
|
||||
// All remaining parameters must be of this type or convertible to this type
|
||||
for (int i = expectedArgTypes.size() - 1; i < suppliedArgTypes.size(); i++) {
|
||||
TypeDescriptor suppliedArg = suppliedArgTypes.get(i);
|
||||
if (suppliedArg == null) {
|
||||
if (varargsParamType.isPrimitive()) {
|
||||
if (varargsComponentType.isPrimitive()) {
|
||||
match = null;
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (varargsParamType != suppliedArg.getType()) {
|
||||
if (ClassUtils.isAssignable(varargsParamType, suppliedArg.getType())) {
|
||||
if (varargsComponentType != suppliedArg.getType()) {
|
||||
if (ClassUtils.isAssignable(varargsComponentType, suppliedArg.getType())) {
|
||||
if (match != ArgumentsMatchKind.REQUIRES_CONVERSION) {
|
||||
match = ArgumentsMatchKind.CLOSE;
|
||||
}
|
||||
}
|
||||
else if (typeConverter.canConvert(suppliedArg, TypeDescriptor.valueOf(varargsParamType))) {
|
||||
else if (typeConverter.canConvert(suppliedArg, TypeDescriptor.valueOf(varargsComponentType))) {
|
||||
match = ArgumentsMatchKind.REQUIRES_CONVERSION;
|
||||
}
|
||||
else {
|
||||
@@ -234,19 +235,22 @@ public abstract class ReflectionHelper {
|
||||
}
|
||||
|
||||
|
||||
// TODO could do with more refactoring around argument handling and varargs
|
||||
/**
|
||||
* Convert a supplied set of arguments into the requested types. If the parameterTypes are related to
|
||||
* a varargs method then the final entry in the parameterTypes array is going to be an array itself whose
|
||||
* component type should be used as the conversion target for extraneous arguments. (For example, if the
|
||||
* parameterTypes are {Integer, String[]} and the input arguments are {Integer, boolean, float} then both
|
||||
* the boolean and float must be converted to strings). This method does *not* repackage the arguments
|
||||
* into a form suitable for the varargs invocation - a subsequent call to setupArgumentsForVarargsInvocation handles that.
|
||||
* Convert the supplied set of arguments into the parameter types of the supplied
|
||||
* {@link Method}.
|
||||
* <p>If the supplied method is a varargs method, the final parameter type must be an
|
||||
* array whose component type should be used as the conversion target for extraneous
|
||||
* arguments. For example, if the parameter types are <code>{Integer, String[]}</code>
|
||||
* and the input arguments are <code>{Integer, boolean, float}</code>, then both the
|
||||
* {@code boolean} and the {@code float} must be converted to strings.
|
||||
* <p>This method does <strong>not</strong> repackage the arguments into a form suitable
|
||||
* for the varargs invocation: a subsequent call to
|
||||
* {@link #setupArgumentsForVarargsInvocation(Class[], Object...)} is required for that.
|
||||
* @param converter the converter to use for type conversions
|
||||
* @param arguments the arguments to convert to the requested parameter types
|
||||
* @param method the target Method
|
||||
* @return true if some kind of conversion occurred on the argument
|
||||
* @throws SpelEvaluationException if there is a problem with conversion
|
||||
* @param arguments the arguments to convert to the required parameter types
|
||||
* @param method the target {@code Method}
|
||||
* @return {@code true} if some kind of conversion occurred on an argument
|
||||
* @throws SpelEvaluationException if a problem occurs during conversion
|
||||
*/
|
||||
public static boolean convertAllArguments(TypeConverter converter, Object[] arguments, Method method)
|
||||
throws SpelEvaluationException {
|
||||
@@ -256,11 +260,12 @@ public abstract class ReflectionHelper {
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes an input set of argument values and converts them to the types specified as the
|
||||
* required parameter types. The arguments are converted 'in-place' in the input array.
|
||||
* @param converter the type converter to use for attempting conversions
|
||||
* @param arguments the actual arguments that need conversion
|
||||
* @param executable the target Method or Constructor
|
||||
* Convert the supplied set of arguments into the parameter types of the supplied
|
||||
* {@link Executable}, taking the varargs position into account.
|
||||
* <p>The arguments are converted 'in-place' in the input array.
|
||||
* @param converter the converter to use for type conversions
|
||||
* @param arguments the arguments to convert to the required parameter types
|
||||
* @param executable the target {@code Method} or {@code Constructor}
|
||||
* @param varargsPosition the known position of the varargs argument, if any
|
||||
* ({@code null} if not varargs)
|
||||
* @return {@code true} if some kind of conversion occurred on an argument
|
||||
@@ -288,30 +293,31 @@ public abstract class ReflectionHelper {
|
||||
}
|
||||
|
||||
MethodParameter methodParam = MethodParameter.forExecutable(executable, varargsPosition);
|
||||
TypeDescriptor targetType = new TypeDescriptor(methodParam);
|
||||
TypeDescriptor componentTypeDesc = targetType.getElementTypeDescriptor();
|
||||
Assert.state(componentTypeDesc != null, "Component type must not be null for a varargs array");
|
||||
|
||||
// If the target is varargs and there is just one more argument, then convert it here.
|
||||
if (varargsPosition == arguments.length - 1) {
|
||||
Object argument = arguments[varargsPosition];
|
||||
TypeDescriptor targetType = new TypeDescriptor(methodParam);
|
||||
TypeDescriptor sourceType = TypeDescriptor.forObject(argument);
|
||||
if (argument == null) {
|
||||
// Perform the equivalent of GenericConversionService.convertNullSource() for a single argument.
|
||||
TypeDescriptor elementDesc = targetType.getElementTypeDescriptor();
|
||||
if (elementDesc != null && elementDesc.getObjectType() == Optional.class) {
|
||||
if (componentTypeDesc.getObjectType() == Optional.class) {
|
||||
arguments[varargsPosition] = Optional.empty();
|
||||
conversionOccurred = true;
|
||||
}
|
||||
}
|
||||
// If the argument type is equal to the varargs element type, there is no need to
|
||||
// If the argument type is assignable to the varargs component type, there is no need to
|
||||
// convert it or wrap it in an array. For example, using StringToArrayConverter to
|
||||
// convert a String containing a comma would result in the String being split and
|
||||
// repackaged in an array when it should be used as-is.
|
||||
else if (!sourceType.equals(targetType.getElementTypeDescriptor())) {
|
||||
else if (!sourceType.isAssignableTo(componentTypeDesc)) {
|
||||
arguments[varargsPosition] = converter.convertValue(argument, sourceType, targetType);
|
||||
}
|
||||
// Possible outcomes of the above if-else block:
|
||||
// 1) the input argument was null, and nothing was done.
|
||||
// 2) the input argument was null; the varargs element type is Optional; and the argument was converted to Optional.empty().
|
||||
// 2) the input argument was null; the varargs component type is Optional; and the argument was converted to Optional.empty().
|
||||
// 3) the input argument was correct type but not wrapped in an array, and nothing was done.
|
||||
// 4) the input argument was already compatible (i.e., array of valid type), and nothing was done.
|
||||
// 5) the input argument was the wrong type and got converted and wrapped in an array.
|
||||
@@ -320,13 +326,12 @@ public abstract class ReflectionHelper {
|
||||
conversionOccurred = true; // case 5
|
||||
}
|
||||
}
|
||||
// Otherwise, convert remaining arguments to the varargs element type.
|
||||
// Otherwise, convert remaining arguments to the varargs component type.
|
||||
else {
|
||||
TypeDescriptor targetType = new TypeDescriptor(methodParam).getElementTypeDescriptor();
|
||||
Assert.state(targetType != null, "No element type");
|
||||
for (int i = varargsPosition; i < arguments.length; i++) {
|
||||
Object argument = arguments[i];
|
||||
arguments[i] = converter.convertValue(argument, TypeDescriptor.forObject(argument), targetType);
|
||||
TypeDescriptor sourceType = TypeDescriptor.forObject(argument);
|
||||
arguments[i] = converter.convertValue(argument, sourceType, componentTypeDesc);
|
||||
conversionOccurred |= (argument != arguments[i]);
|
||||
}
|
||||
}
|
||||
@@ -335,11 +340,12 @@ public abstract class ReflectionHelper {
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes an input set of argument values and converts them to the types specified as the
|
||||
* required parameter types. The arguments are converted 'in-place' in the input array.
|
||||
* @param converter the type converter to use for attempting conversions
|
||||
* @param arguments the actual arguments that need conversion
|
||||
* @param methodHandle the target MethodHandle
|
||||
* Convert the supplied set of arguments into the parameter types of the supplied
|
||||
* {@link MethodHandle}, taking the varargs position into account.
|
||||
* <p>The arguments are converted 'in-place' in the input array.
|
||||
* @param converter the converter to use for type conversions
|
||||
* @param arguments the arguments to convert to the required parameter types
|
||||
* @param methodHandle the target {@code MethodHandle}
|
||||
* @param varargsPosition the known position of the varargs argument, if any
|
||||
* ({@code null} if not varargs)
|
||||
* @return {@code true} if some kind of conversion occurred on an argument
|
||||
@@ -350,10 +356,10 @@ public abstract class ReflectionHelper {
|
||||
MethodHandle methodHandle, @Nullable Integer varargsPosition) throws EvaluationException {
|
||||
|
||||
boolean conversionOccurred = false;
|
||||
final MethodType methodHandleArgumentTypes = methodHandle.type();
|
||||
MethodType methodHandleType = methodHandle.type();
|
||||
if (varargsPosition == null) {
|
||||
for (int i = 0; i < arguments.length; i++) {
|
||||
Class<?> argumentClass = methodHandleArgumentTypes.parameterType(i);
|
||||
Class<?> argumentClass = methodHandleType.parameterType(i);
|
||||
ResolvableType resolvableType = ResolvableType.forClass(argumentClass);
|
||||
TypeDescriptor targetType = new TypeDescriptor(resolvableType, argumentClass, null);
|
||||
|
||||
@@ -365,7 +371,7 @@ public abstract class ReflectionHelper {
|
||||
else {
|
||||
// Convert everything up to the varargs position
|
||||
for (int i = 0; i < varargsPosition; i++) {
|
||||
Class<?> argumentClass = methodHandleArgumentTypes.parameterType(i);
|
||||
Class<?> argumentClass = methodHandleType.parameterType(i);
|
||||
ResolvableType resolvableType = ResolvableType.forClass(argumentClass);
|
||||
TypeDescriptor targetType = new TypeDescriptor(resolvableType, argumentClass, null);
|
||||
|
||||
@@ -374,9 +380,11 @@ public abstract class ReflectionHelper {
|
||||
conversionOccurred |= (argument != arguments[i]);
|
||||
}
|
||||
|
||||
final Class<?> varArgClass = methodHandleArgumentTypes.lastParameterType().componentType();
|
||||
Class<?> varArgClass = methodHandleType.lastParameterType();
|
||||
ResolvableType varArgResolvableType = ResolvableType.forClass(varArgClass);
|
||||
TypeDescriptor varArgContentType = new TypeDescriptor(varArgResolvableType, varArgClass, null);
|
||||
TypeDescriptor targetType = new TypeDescriptor(varArgResolvableType, varArgClass.componentType(), null);
|
||||
TypeDescriptor componentTypeDesc = targetType.getElementTypeDescriptor();
|
||||
Assert.state(componentTypeDesc != null, "Component type must not be null for a varargs array");
|
||||
|
||||
// If the target is varargs and there is just one more argument, then convert it here.
|
||||
if (varargsPosition == arguments.length - 1) {
|
||||
@@ -384,22 +392,21 @@ public abstract class ReflectionHelper {
|
||||
TypeDescriptor sourceType = TypeDescriptor.forObject(argument);
|
||||
if (argument == null) {
|
||||
// Perform the equivalent of GenericConversionService.convertNullSource() for a single argument.
|
||||
TypeDescriptor elementDesc = varArgContentType.getElementTypeDescriptor();
|
||||
if (elementDesc != null && elementDesc.getObjectType() == Optional.class) {
|
||||
if (componentTypeDesc.getObjectType() == Optional.class) {
|
||||
arguments[varargsPosition] = Optional.empty();
|
||||
conversionOccurred = true;
|
||||
}
|
||||
}
|
||||
// If the argument type is equal to the varargs element type, there is no need to
|
||||
// If the argument type is assignable to the varargs component type, there is no need to
|
||||
// convert it or wrap it in an array. For example, using StringToArrayConverter to
|
||||
// convert a String containing a comma would result in the String being split and
|
||||
// repackaged in an array when it should be used as-is.
|
||||
else if (!sourceType.equals(varArgContentType.getElementTypeDescriptor())) {
|
||||
arguments[varargsPosition] = converter.convertValue(argument, sourceType, varArgContentType);
|
||||
else if (!sourceType.isAssignableTo(componentTypeDesc)) {
|
||||
arguments[varargsPosition] = converter.convertValue(argument, sourceType, targetType);
|
||||
}
|
||||
// Possible outcomes of the above if-else block:
|
||||
// 1) the input argument was null, and nothing was done.
|
||||
// 2) the input argument was null; the varargs element type is Optional; and the argument was converted to Optional.empty().
|
||||
// 2) the input argument was null; the varargs component type is Optional; and the argument was converted to Optional.empty().
|
||||
// 3) the input argument was correct type but not wrapped in an array, and nothing was done.
|
||||
// 4) the input argument was already compatible (i.e., array of valid type), and nothing was done.
|
||||
// 5) the input argument was the wrong type and got converted and wrapped in an array.
|
||||
@@ -408,11 +415,11 @@ public abstract class ReflectionHelper {
|
||||
conversionOccurred = true; // case 5
|
||||
}
|
||||
}
|
||||
// Otherwise, convert remaining arguments to the varargs element type.
|
||||
// Otherwise, convert remaining arguments to the varargs component type.
|
||||
else {
|
||||
for (int i = varargsPosition; i < arguments.length; i++) {
|
||||
Object argument = arguments[i];
|
||||
arguments[i] = converter.convertValue(argument, TypeDescriptor.forObject(argument), varArgContentType);
|
||||
arguments[i] = converter.convertValue(argument, TypeDescriptor.forObject(argument), componentTypeDesc);
|
||||
conversionOccurred |= (argument != arguments[i]);
|
||||
}
|
||||
}
|
||||
@@ -448,7 +455,7 @@ public abstract class ReflectionHelper {
|
||||
* {@code [1, new String[] {"a", "b"}]} in order to match the expected types.
|
||||
* @param requiredParameterTypes the types of the parameters for the invocation
|
||||
* @param args the arguments to be set up for the invocation
|
||||
* @return a repackaged array of arguments where any varargs setup has performed
|
||||
* @return a repackaged array of arguments where any varargs setup has been performed
|
||||
*/
|
||||
public static Object[] setupArgumentsForVarargsInvocation(Class<?>[] requiredParameterTypes, Object... args) {
|
||||
Assert.notEmpty(requiredParameterTypes, "Required parameter types array must not be empty");
|
||||
@@ -505,11 +512,9 @@ public abstract class ReflectionHelper {
|
||||
|
||||
|
||||
/**
|
||||
* An instance of ArgumentsMatchInfo describes what kind of match was achieved
|
||||
* An instance of {@code ArgumentsMatchInfo} describes what kind of match was achieved
|
||||
* between two sets of arguments - the set that a method/constructor is expecting
|
||||
* and the set that are being supplied at the point of invocation. If the kind
|
||||
* indicates that conversion is required for some of the arguments then the arguments
|
||||
* that require conversion are listed in the argsRequiringConversion array.
|
||||
* and the set that is being supplied at the point of invocation.
|
||||
*
|
||||
* @param kind the kind of match that was achieved
|
||||
*/
|
||||
@@ -529,7 +534,7 @@ public abstract class ReflectionHelper {
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ArgumentMatchInfo: " + this.kind;
|
||||
return "ArgumentsMatchInfo: " + this.kind;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+40
@@ -293,6 +293,46 @@ class MethodInvocationTests extends AbstractExpressionTests {
|
||||
evaluate("aVarargsMethod3('foo', 'bar,baz')", "foo-bar,baz", String.class);
|
||||
}
|
||||
|
||||
@Test // gh-33013
|
||||
void testVarargsWithObjectArrayType() {
|
||||
// Calling 'public String formatObjectVarargs(String format, Object... args)' -> String.format(format, args)
|
||||
|
||||
// No var-args and no conversion necessary
|
||||
evaluate("formatObjectVarargs('x')", "x", String.class);
|
||||
|
||||
// No var-args but conversion necessary
|
||||
evaluate("formatObjectVarargs(9)", "9", String.class);
|
||||
|
||||
// No conversion necessary
|
||||
evaluate("formatObjectVarargs('x -> %s', '')", "x -> ", String.class);
|
||||
evaluate("formatObjectVarargs('x -> %s', ' ')", "x -> ", String.class);
|
||||
evaluate("formatObjectVarargs('x -> %s', 'a')", "x -> a", String.class);
|
||||
evaluate("formatObjectVarargs('x -> %s %s %s', 'a', 'b', 'c')", "x -> a b c", String.class);
|
||||
evaluate("formatObjectVarargs('x -> %s', new Object[]{''})", "x -> ", String.class);
|
||||
evaluate("formatObjectVarargs('x -> %s', new String[]{''})", "x -> ", String.class);
|
||||
evaluate("formatObjectVarargs('x -> %s', new Object[]{' '})", "x -> ", String.class);
|
||||
evaluate("formatObjectVarargs('x -> %s', new String[]{' '})", "x -> ", String.class);
|
||||
evaluate("formatObjectVarargs('x -> %s', new Object[]{'a'})", "x -> a", String.class);
|
||||
evaluate("formatObjectVarargs('x -> %s', new String[]{'a'})", "x -> a", String.class);
|
||||
evaluate("formatObjectVarargs('x -> %s %s %s', new Object[]{'a', 'b', 'c'})", "x -> a b c", String.class);
|
||||
evaluate("formatObjectVarargs('x -> %s %s %s', new String[]{'a', 'b', 'c'})", "x -> a b c", String.class);
|
||||
|
||||
// Conversion necessary
|
||||
evaluate("formatObjectVarargs('x -> %s %s', 2, 3)", "x -> 2 3", String.class);
|
||||
evaluate("formatObjectVarargs('x -> %s %s', 'a', 3.0d)", "x -> a 3.0", String.class);
|
||||
|
||||
// Individual string contains a comma with multiple varargs arguments
|
||||
evaluate("formatObjectVarargs('foo -> %s %s', ',', 'baz')", "foo -> , baz", String.class);
|
||||
evaluate("formatObjectVarargs('foo -> %s %s', 'bar', ',baz')", "foo -> bar ,baz", String.class);
|
||||
evaluate("formatObjectVarargs('foo -> %s %s', 'bar,', 'baz')", "foo -> bar, baz", String.class);
|
||||
|
||||
// Individual string contains a comma with single varargs argument.
|
||||
evaluate("formatObjectVarargs('foo -> %s', ',')", "foo -> ,", String.class);
|
||||
evaluate("formatObjectVarargs('foo -> %s', ',bar')", "foo -> ,bar", String.class);
|
||||
evaluate("formatObjectVarargs('foo -> %s', 'bar,')", "foo -> bar,", String.class);
|
||||
evaluate("formatObjectVarargs('foo -> %s', 'bar,baz')", "foo -> bar,baz", String.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testVarargsOptionalInvocation() {
|
||||
// Calling 'public String optionalVarargsMethod(Optional<String>... values)'
|
||||
|
||||
+13
-5
@@ -69,6 +69,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatException;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
import static org.assertj.core.api.InstanceOfAssertFactories.list;
|
||||
|
||||
/**
|
||||
* Reproduction tests cornering various reported SpEL issues.
|
||||
@@ -1436,13 +1437,20 @@ class SpelReproTests extends AbstractExpressionTests {
|
||||
assertThat(expression.getValue(new NamedUser())).isEqualTo(NamedUser.class.getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void SPR12522() {
|
||||
@Test // gh-17127, SPR-12522
|
||||
void arraysAsListWithNoArguments() {
|
||||
SpelExpressionParser parser = new SpelExpressionParser();
|
||||
Expression expression = parser.parseExpression("T(java.util.Arrays).asList()");
|
||||
List<?> value = expression.getValue(List.class);
|
||||
assertThat(value).isEmpty();
|
||||
}
|
||||
|
||||
@Test // gh-33013
|
||||
void arraysAsListWithSingleEmptyStringArgument() {
|
||||
SpelExpressionParser parser = new SpelExpressionParser();
|
||||
Expression expression = parser.parseExpression("T(java.util.Arrays).asList('')");
|
||||
Object value = expression.getValue();
|
||||
assertThat(value).isInstanceOf(List.class);
|
||||
assertThat(((List<?>) value)).isEmpty();
|
||||
List<?> value = expression.getValue(List.class);
|
||||
assertThat(value).asInstanceOf(list(String.class)).containsExactly("");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+18
@@ -100,6 +100,16 @@ class TestScenarioCreator {
|
||||
MethodHandle messageStaticFullyBound = messageStaticPartiallyBound
|
||||
.bindTo(new String[] { "prerecorded", "3", "Oh Hello World", "ignored"});
|
||||
testContext.registerFunction("messageStaticBound", messageStaticFullyBound);
|
||||
|
||||
// #formatObjectVarargs(format, args...)
|
||||
MethodHandle formatObjectVarargs = MethodHandles.lookup().findStatic(TestScenarioCreator.class,
|
||||
"formatObjectVarargs", MethodType.methodType(String.class, String.class, Object[].class));
|
||||
testContext.registerFunction("formatObjectVarargs", formatObjectVarargs);
|
||||
|
||||
// #add(int, int)
|
||||
MethodHandle add = MethodHandles.lookup().findStatic(TestScenarioCreator.class,
|
||||
"add", MethodType.methodType(int.class, int.class, int.class));
|
||||
testContext.registerFunction("add", add);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -154,4 +164,12 @@ class TestScenarioCreator {
|
||||
return template.formatted((Object[]) args);
|
||||
}
|
||||
|
||||
public static String formatObjectVarargs(String format, Object... args) {
|
||||
return String.format(format, args);
|
||||
}
|
||||
|
||||
public static int add(int x, int y) {
|
||||
return x + y;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+49
-3
@@ -53,9 +53,13 @@ class VariableAndFunctionTests extends AbstractExpressionTests {
|
||||
evaluateAndCheckError("#reverseInt(1,2)", INCORRECT_NUMBER_OF_ARGUMENTS_TO_FUNCTION, 0, "reverseInt", 2, 3);
|
||||
evaluateAndCheckError("#reverseInt(1,2,3,4)", INCORRECT_NUMBER_OF_ARGUMENTS_TO_FUNCTION, 0, "reverseInt", 4, 3);
|
||||
|
||||
// MethodHandle: #message(template, args...)
|
||||
evaluateAndCheckError("#message()", INCORRECT_NUMBER_OF_ARGUMENTS_TO_FUNCTION, 0, "message", 0, 2);
|
||||
evaluateAndCheckError("#message('%s')", INCORRECT_NUMBER_OF_ARGUMENTS_TO_FUNCTION, 0, "message", 1, 2);
|
||||
// MethodHandle: #message(String, Object...)
|
||||
evaluateAndCheckError("#message()", INCORRECT_NUMBER_OF_ARGUMENTS_TO_FUNCTION, 0, "message", 0, "1 or more");
|
||||
|
||||
// MethodHandle: #add(int, int)
|
||||
evaluateAndCheckError("#add()", INCORRECT_NUMBER_OF_ARGUMENTS_TO_FUNCTION, 0, "add", 0, 2);
|
||||
evaluateAndCheckError("#add(1)", INCORRECT_NUMBER_OF_ARGUMENTS_TO_FUNCTION, 0, "add", 1, 2);
|
||||
evaluateAndCheckError("#add(1, 2, 3)", INCORRECT_NUMBER_OF_ARGUMENTS_TO_FUNCTION, 0, "add", 3, 2);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -101,6 +105,48 @@ class VariableAndFunctionTests extends AbstractExpressionTests {
|
||||
evaluate("#varargsFunction2(9,'a',null,'b')", "9-[a, null, b]", String.class);
|
||||
}
|
||||
|
||||
@Test // gh-33013
|
||||
void functionWithVarargsViaMethodHandle() {
|
||||
// Calling 'public static String formatObjectVarargs(String format, Object... args)' -> String.format(format, args)
|
||||
|
||||
// No var-args and no conversion necessary
|
||||
evaluate("#formatObjectVarargs('x')", "x", String.class);
|
||||
|
||||
// No var-args but conversion necessary
|
||||
evaluate("#formatObjectVarargs(9)", "9", String.class);
|
||||
|
||||
// No conversion necessary
|
||||
evaluate("#add(3, 4)", 7, Integer.class);
|
||||
evaluate("#formatObjectVarargs('x -> %s', '')", "x -> ", String.class);
|
||||
evaluate("#formatObjectVarargs('x -> %s', ' ')", "x -> ", String.class);
|
||||
evaluate("#formatObjectVarargs('x -> %s', 'a')", "x -> a", String.class);
|
||||
evaluate("#formatObjectVarargs('x -> %s %s %s', 'a', 'b', 'c')", "x -> a b c", String.class);
|
||||
evaluate("#formatObjectVarargs('x -> %s', new Object[]{''})", "x -> ", String.class);
|
||||
evaluate("#formatObjectVarargs('x -> %s', new String[]{''})", "x -> ", String.class);
|
||||
evaluate("#formatObjectVarargs('x -> %s', new Object[]{' '})", "x -> ", String.class);
|
||||
evaluate("#formatObjectVarargs('x -> %s', new String[]{' '})", "x -> ", String.class);
|
||||
evaluate("#formatObjectVarargs('x -> %s', new Object[]{'a'})", "x -> a", String.class);
|
||||
evaluate("#formatObjectVarargs('x -> %s', new String[]{'a'})", "x -> a", String.class);
|
||||
evaluate("#formatObjectVarargs('x -> %s %s %s', new Object[]{'a', 'b', 'c'})", "x -> a b c", String.class);
|
||||
evaluate("#formatObjectVarargs('x -> %s %s %s', new String[]{'a', 'b', 'c'})", "x -> a b c", String.class);
|
||||
|
||||
// Conversion necessary
|
||||
evaluate("#add('2', 5.0)", 7, Integer.class);
|
||||
evaluate("#formatObjectVarargs('x -> %s %s', 2, 3)", "x -> 2 3", String.class);
|
||||
evaluate("#formatObjectVarargs('x -> %s %s', 'a', 3.0d)", "x -> a 3.0", String.class);
|
||||
|
||||
// Individual string contains a comma with multiple varargs arguments
|
||||
evaluate("#formatObjectVarargs('foo -> %s %s', ',', 'baz')", "foo -> , baz", String.class);
|
||||
evaluate("#formatObjectVarargs('foo -> %s %s', 'bar', ',baz')", "foo -> bar ,baz", String.class);
|
||||
evaluate("#formatObjectVarargs('foo -> %s %s', 'bar,', 'baz')", "foo -> bar, baz", String.class);
|
||||
|
||||
// Individual string contains a comma with single varargs argument.
|
||||
evaluate("#formatObjectVarargs('foo -> %s', ',')", "foo -> ,", String.class);
|
||||
evaluate("#formatObjectVarargs('foo -> %s', ',bar')", "foo -> ,bar", String.class);
|
||||
evaluate("#formatObjectVarargs('foo -> %s', 'bar,')", "foo -> bar,", String.class);
|
||||
evaluate("#formatObjectVarargs('foo -> %s', 'bar,baz')", "foo -> bar,baz", String.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void functionMethodMustBeStatic() throws Exception {
|
||||
SpelExpressionParser parser = new SpelExpressionParser();
|
||||
|
||||
+5
@@ -217,6 +217,11 @@ public class Inventor {
|
||||
return str1 + "-" + String.join("-", strings);
|
||||
}
|
||||
|
||||
public String formatObjectVarargs(String format, Object... args) {
|
||||
return String.format(format, args);
|
||||
}
|
||||
|
||||
|
||||
public Inventor(String... strings) {
|
||||
if (strings.length > 0) {
|
||||
this.name = strings[0];
|
||||
|
||||
+7
-2
@@ -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.
|
||||
@@ -265,7 +265,12 @@ public class ProtobufMessageConverter extends AbstractMessageConverter {
|
||||
throws IOException, MessageConversionException {
|
||||
|
||||
if (contentType.isCompatibleWith(APPLICATION_JSON)) {
|
||||
this.parser.merge(message.getPayload().toString(), builder);
|
||||
if (message.getPayload() instanceof byte[] bytes) {
|
||||
this.parser.merge(new String(bytes, charset), builder);
|
||||
}
|
||||
else {
|
||||
this.parser.merge(message.getPayload().toString(), builder);
|
||||
}
|
||||
}
|
||||
else {
|
||||
throw new MessageConversionException(
|
||||
|
||||
+1
-1
@@ -76,7 +76,7 @@ public abstract class AbstractNamedValueMethodArgumentResolver implements Handle
|
||||
protected AbstractNamedValueMethodArgumentResolver(ConversionService conversionService,
|
||||
@Nullable ConfigurableBeanFactory beanFactory) {
|
||||
|
||||
// Fallback on shared ConversionService for now for historic reasons.
|
||||
// Fallback on shared ConversionService for now for historical reasons.
|
||||
// Possibly remove after discussion in gh-23882.
|
||||
|
||||
//noinspection ConstantConditions
|
||||
|
||||
+6
-5
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 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,6 +16,7 @@
|
||||
|
||||
package org.springframework.messaging.converter;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -49,14 +50,14 @@ class ProtobufMessageConverterTests {
|
||||
|
||||
private Message<byte[]> messageWithoutContentType = MessageBuilder.withPayload(this.testMsg.toByteArray()).build();
|
||||
|
||||
private final Message<String> messageJson = MessageBuilder.withPayload("""
|
||||
private final Message<byte[]> messageJson = MessageBuilder.withPayload("""
|
||||
{
|
||||
"foo": "Foo",
|
||||
"blah": {
|
||||
"blah": 123
|
||||
}
|
||||
}
|
||||
""")
|
||||
""".getBytes(StandardCharsets.UTF_8))
|
||||
.setHeader(CONTENT_TYPE, APPLICATION_JSON)
|
||||
.build();
|
||||
|
||||
@@ -113,10 +114,10 @@ class ProtobufMessageConverterTests {
|
||||
Message<?> message = converter.toMessage(testMsg, new MessageHeaders(Map.of(CONTENT_TYPE, APPLICATION_JSON)));
|
||||
assertThat(message).isNotNull();
|
||||
assertThat(message.getHeaders().get(CONTENT_TYPE)).isEqualTo(APPLICATION_JSON);
|
||||
JSONAssert.assertEquals(messageJson.getPayload(), message.getPayload().toString(), true);
|
||||
JSONAssert.assertEquals(new String(messageJson.getPayload()), message.getPayload().toString(), true);
|
||||
|
||||
//convertFrom
|
||||
assertThat(converter.fromMessage(message, Msg.class)).isEqualTo(testMsg);
|
||||
assertThat(converter.fromMessage(messageJson, Msg.class)).isEqualTo(testMsg);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+1
-42
@@ -23,12 +23,8 @@ import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
import io.vavr.control.Try;
|
||||
import kotlin.coroutines.Continuation;
|
||||
import kotlin.coroutines.CoroutineContext;
|
||||
import kotlinx.coroutines.Job;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.reactivestreams.Publisher;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@@ -36,7 +32,6 @@ import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.beans.factory.BeanFactoryAware;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.beans.factory.annotation.BeanFactoryAnnotationUtils;
|
||||
import org.springframework.core.CoroutinesUtils;
|
||||
import org.springframework.core.KotlinDetector;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.NamedThreadLocal;
|
||||
@@ -355,10 +350,6 @@ public abstract class TransactionAspectSupport implements BeanFactoryAware, Init
|
||||
boolean isSuspendingFunction = KotlinDetector.isSuspendingFunction(method);
|
||||
boolean hasSuspendingFlowReturnType = isSuspendingFunction &&
|
||||
COROUTINES_FLOW_CLASS_NAME.equals(new MethodParameter(method, -1).getParameterType().getName());
|
||||
if (isSuspendingFunction && !(invocation instanceof CoroutinesInvocationCallback)) {
|
||||
throw new IllegalStateException("Coroutines invocation not supported: " + method);
|
||||
}
|
||||
CoroutinesInvocationCallback corInv = (isSuspendingFunction ? (CoroutinesInvocationCallback) invocation : null);
|
||||
|
||||
ReactiveTransactionSupport txSupport = this.transactionSupportCache.computeIfAbsent(method, key -> {
|
||||
Class<?> reactiveType =
|
||||
@@ -371,11 +362,7 @@ public abstract class TransactionAspectSupport implements BeanFactoryAware, Init
|
||||
return new ReactiveTransactionSupport(adapter);
|
||||
});
|
||||
|
||||
InvocationCallback callback = invocation;
|
||||
if (corInv != null) {
|
||||
callback = () -> KotlinDelegate.invokeSuspendingFunction(method, corInv);
|
||||
}
|
||||
return txSupport.invokeWithinTransaction(method, targetClass, callback, txAttr, rtm);
|
||||
return txSupport.invokeWithinTransaction(method, targetClass, invocation, txAttr, rtm);
|
||||
}
|
||||
|
||||
PlatformTransactionManager ptm = asPlatformTransactionManager(tm);
|
||||
@@ -829,22 +816,6 @@ public abstract class TransactionAspectSupport implements BeanFactoryAware, Init
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Coroutines-supporting extension of the callback interface.
|
||||
*/
|
||||
protected interface CoroutinesInvocationCallback extends InvocationCallback {
|
||||
|
||||
Object getTarget();
|
||||
|
||||
Object[] getArguments();
|
||||
|
||||
default Object getContinuation() {
|
||||
Object[] args = getArguments();
|
||||
return args[args.length - 1];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Internal holder class for a Throwable in a callback transaction model.
|
||||
*/
|
||||
@@ -891,18 +862,6 @@ public abstract class TransactionAspectSupport implements BeanFactoryAware, Init
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inner class to avoid a hard dependency on Kotlin at runtime.
|
||||
*/
|
||||
private static class KotlinDelegate {
|
||||
|
||||
public static Publisher<?> invokeSuspendingFunction(Method method, CoroutinesInvocationCallback callback) {
|
||||
CoroutineContext coroutineContext = ((Continuation<?>) callback.getContinuation()).getContext().minusKey(Job.Key);
|
||||
return CoroutinesUtils.invokeSuspendingFunction(coroutineContext, method, callback.getTarget(), callback.getArguments());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Delegate for Reactor-based management of transactional methods with a
|
||||
|
||||
+1
-15
@@ -116,21 +116,7 @@ public class TransactionInterceptor extends TransactionAspectSupport implements
|
||||
Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null);
|
||||
|
||||
// Adapt to TransactionAspectSupport's invokeWithinTransaction...
|
||||
return invokeWithinTransaction(invocation.getMethod(), targetClass, new CoroutinesInvocationCallback() {
|
||||
@Override
|
||||
@Nullable
|
||||
public Object proceedWithInvocation() throws Throwable {
|
||||
return invocation.proceed();
|
||||
}
|
||||
@Override
|
||||
public Object getTarget() {
|
||||
return invocation.getThis();
|
||||
}
|
||||
@Override
|
||||
public Object[] getArguments() {
|
||||
return invocation.getArguments();
|
||||
}
|
||||
});
|
||||
return invokeWithinTransaction(invocation.getMethod(), targetClass, invocation::proceed);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -175,7 +175,7 @@ class JdkClientHttpRequest extends AbstractStreamingClientHttpRequest {
|
||||
/**
|
||||
* By default, {@link HttpRequest} does not allow {@code Connection},
|
||||
* {@code Content-Length}, {@code Expect}, {@code Host}, or {@code Upgrade}
|
||||
* headers to be set, but this can be overriden with the
|
||||
* headers to be set, but this can be overridden with the
|
||||
* {@code jdk.httpclient.allowRestrictedHeaders} system property.
|
||||
* @see jdk.internal.net.http.common.Utils#getDisallowedHeaders()
|
||||
*/
|
||||
|
||||
+1
-1
@@ -140,7 +140,7 @@ final class ReactorNettyClientRequest extends AbstractStreamingClientHttpRequest
|
||||
return ioEx;
|
||||
}
|
||||
}
|
||||
return new IOException(ex.getMessage(), cause);
|
||||
return new IOException(ex.getMessage(), (cause != null ? cause : ex));
|
||||
}
|
||||
|
||||
|
||||
|
||||
+23
-1
@@ -44,6 +44,7 @@ import org.springframework.util.Assert;
|
||||
* @author Brian Clozel
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Sebastien Deleuze
|
||||
* @author Juergen Hoeller
|
||||
* @since 5.0
|
||||
* @see reactor.netty.http.client.HttpClient
|
||||
*/
|
||||
@@ -63,6 +64,8 @@ public class ReactorClientHttpConnector implements ClientHttpConnector, SmartLif
|
||||
@Nullable
|
||||
private volatile HttpClient httpClient;
|
||||
|
||||
private boolean lazyStart = false;
|
||||
|
||||
private final Object lifecycleMonitor = new Object();
|
||||
|
||||
|
||||
@@ -112,6 +115,9 @@ public class ReactorClientHttpConnector implements ClientHttpConnector, SmartLif
|
||||
if (resourceFactory.isRunning()) {
|
||||
this.httpClient = createHttpClient(resourceFactory, mapper);
|
||||
}
|
||||
else {
|
||||
this.lazyStart = true;
|
||||
}
|
||||
}
|
||||
|
||||
private static HttpClient createHttpClient(ReactorResourceFactory factory, Function<HttpClient, HttpClient> mapper) {
|
||||
@@ -127,7 +133,21 @@ public class ReactorClientHttpConnector implements ClientHttpConnector, SmartLif
|
||||
HttpClient httpClient = this.httpClient;
|
||||
if (httpClient == null) {
|
||||
Assert.state(this.resourceFactory != null && this.mapper != null, "Illegal configuration");
|
||||
httpClient = createHttpClient(this.resourceFactory, this.mapper);
|
||||
if (this.resourceFactory.isRunning()) {
|
||||
// Retain HttpClient instance if resource factory has been started in the meantime,
|
||||
// considering this connector instance as lazily started as well.
|
||||
synchronized (this.lifecycleMonitor) {
|
||||
httpClient = this.httpClient;
|
||||
if (httpClient == null && this.lazyStart) {
|
||||
httpClient = createHttpClient(this.resourceFactory, this.mapper);
|
||||
this.httpClient = httpClient;
|
||||
this.lazyStart = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (httpClient == null) {
|
||||
httpClient = createHttpClient(this.resourceFactory, this.mapper);
|
||||
}
|
||||
}
|
||||
|
||||
HttpClient.RequestSender requestSender = httpClient
|
||||
@@ -176,6 +196,7 @@ public class ReactorClientHttpConnector implements ClientHttpConnector, SmartLif
|
||||
synchronized (this.lifecycleMonitor) {
|
||||
if (this.httpClient == null) {
|
||||
this.httpClient = createHttpClient(this.resourceFactory, this.mapper);
|
||||
this.lazyStart = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -190,6 +211,7 @@ public class ReactorClientHttpConnector implements ClientHttpConnector, SmartLif
|
||||
if (this.resourceFactory != null && this.mapper != null) {
|
||||
synchronized (this.lifecycleMonitor) {
|
||||
this.httpClient = null;
|
||||
this.lazyStart = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+10
-2
@@ -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.
|
||||
@@ -109,7 +109,15 @@ public abstract class KotlinSerializationBinaryDecoder<T extends BinaryFormat> e
|
||||
}
|
||||
return this.byteArrayDecoder
|
||||
.decodeToMono(inputStream, elementType, mimeType, hints)
|
||||
.map(byteArray -> format().decodeFromByteArray(serializer, byteArray));
|
||||
.handle((byteArray, sink) -> {
|
||||
try {
|
||||
sink.next(format().decodeFromByteArray(serializer, byteArray));
|
||||
sink.complete();
|
||||
}
|
||||
catch (IllegalArgumentException ex) {
|
||||
sink.error(new DecodingException("Decoding error: " + ex.getMessage(), ex));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+22
-2
@@ -26,6 +26,7 @@ import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.codec.CodecException;
|
||||
import org.springframework.core.codec.Decoder;
|
||||
import org.springframework.core.codec.DecodingException;
|
||||
import org.springframework.core.codec.StringDecoder;
|
||||
@@ -101,7 +102,14 @@ public abstract class KotlinSerializationStringDecoder<T extends StringFormat> e
|
||||
}
|
||||
return this.stringDecoder
|
||||
.decode(inputStream, elementType, mimeType, hints)
|
||||
.map(string -> format().decodeFromString(serializer, string));
|
||||
.handle((string, sink) -> {
|
||||
try {
|
||||
sink.next(format().decodeFromString(serializer, string));
|
||||
}
|
||||
catch (IllegalArgumentException ex) {
|
||||
sink.error(processException(ex));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -115,8 +123,20 @@ public abstract class KotlinSerializationStringDecoder<T extends StringFormat> e
|
||||
}
|
||||
return this.stringDecoder
|
||||
.decodeToMono(inputStream, elementType, mimeType, hints)
|
||||
.map(string -> format().decodeFromString(serializer, string));
|
||||
.handle((string, sink) -> {
|
||||
try {
|
||||
sink.next(format().decodeFromString(serializer, string));
|
||||
sink.complete();
|
||||
}
|
||||
catch (IllegalArgumentException ex) {
|
||||
sink.error(processException(ex));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private CodecException processException(IllegalArgumentException ex) {
|
||||
return new DecodingException("Decoding error: " + ex.getMessage(), ex);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 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,18 +66,25 @@ final class DefaultErrorResponseBuilder implements ErrorResponse.Builder {
|
||||
|
||||
@Override
|
||||
public ErrorResponse.Builder header(String headerName, String... headerValues) {
|
||||
this.headers = (this.headers != null ? this.headers : new HttpHeaders());
|
||||
for (String headerValue : headerValues) {
|
||||
this.headers.add(headerName, headerValue);
|
||||
getHeaders().add(headerName, headerValue);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ErrorResponse.Builder headers(Consumer<HttpHeaders> headersConsumer) {
|
||||
headersConsumer.accept(getHeaders());
|
||||
return this;
|
||||
}
|
||||
|
||||
private HttpHeaders getHeaders() {
|
||||
if (this.headers == null) {
|
||||
this.headers = new HttpHeaders();
|
||||
}
|
||||
return this.headers;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ErrorResponse.Builder type(URI type) {
|
||||
this.problemDetail.setType(type);
|
||||
|
||||
+9
-3
@@ -23,6 +23,7 @@ import io.micrometer.observation.Observation;
|
||||
import io.micrometer.observation.ObservationRegistry;
|
||||
import jakarta.servlet.AsyncEvent;
|
||||
import jakarta.servlet.AsyncListener;
|
||||
import jakarta.servlet.DispatcherType;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.RequestDispatcher;
|
||||
import jakarta.servlet.ServletException;
|
||||
@@ -97,6 +98,11 @@ public class ServerHttpObservationFilter extends OncePerRequestFilter {
|
||||
return Optional.ofNullable((ServerRequestObservationContext) request.getAttribute(CURRENT_OBSERVATION_CONTEXT_ATTRIBUTE));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean shouldNotFilterAsyncDispatch() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("try")
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||
@@ -116,8 +122,9 @@ public class ServerHttpObservationFilter extends OncePerRequestFilter {
|
||||
if (request.isAsyncStarted()) {
|
||||
request.getAsyncContext().addListener(new ObservationAsyncListener(observation));
|
||||
}
|
||||
// Stop Observation right now if async processing has not been started.
|
||||
else {
|
||||
// scope is opened for ASYNC dispatches, but the observation will be closed
|
||||
// by the async listener.
|
||||
else if (request.getDispatcherType() != DispatcherType.ASYNC){
|
||||
Throwable error = fetchException(request);
|
||||
if (error != null) {
|
||||
observation.error(error);
|
||||
@@ -176,7 +183,6 @@ public class ServerHttpObservationFilter extends OncePerRequestFilter {
|
||||
@Override
|
||||
public void onError(AsyncEvent event) {
|
||||
this.currentObservation.error(unwrapServletException(event.getThrowable()));
|
||||
this.currentObservation.stop();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+32
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 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,17 +16,21 @@
|
||||
|
||||
package org.springframework.http.client.reactive;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.netty.http.client.HttpClient;
|
||||
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.client.ReactorResourceFactory;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* @author Sebastien Deleuze
|
||||
* @author Juergen Hoeller
|
||||
* @since 6.1
|
||||
*/
|
||||
class ReactorClientHttpConnectorTests {
|
||||
@@ -41,6 +45,8 @@ class ReactorClientHttpConnectorTests {
|
||||
assertThat(connector.isRunning()).isTrue();
|
||||
connector.start();
|
||||
assertThat(connector.isRunning()).isTrue();
|
||||
connector.stop();
|
||||
assertThat(connector.isRunning()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -54,6 +60,8 @@ class ReactorClientHttpConnectorTests {
|
||||
assertThat(connector.isRunning()).isTrue();
|
||||
connector.start();
|
||||
assertThat(connector.isRunning()).isTrue();
|
||||
connector.stop();
|
||||
assertThat(connector.isRunning()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -69,6 +77,8 @@ class ReactorClientHttpConnectorTests {
|
||||
assertThat(connector.isRunning()).isFalse();
|
||||
connector.start();
|
||||
assertThat(connector.isRunning()).isTrue();
|
||||
connector.stop();
|
||||
assertThat(connector.isRunning()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -84,6 +94,27 @@ class ReactorClientHttpConnectorTests {
|
||||
assertThat(connector.isRunning()).isFalse();
|
||||
connector.start();
|
||||
assertThat(connector.isRunning()).isTrue();
|
||||
connector.stop();
|
||||
assertThat(connector.isRunning()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void lazyStartWithExternalResourceFactory() throws Exception {
|
||||
ReactorResourceFactory resourceFactory = new ReactorResourceFactory();
|
||||
Function<HttpClient, HttpClient> mapper = Function.identity();
|
||||
ReactorClientHttpConnector connector = new ReactorClientHttpConnector(resourceFactory, mapper);
|
||||
assertThat(connector.isRunning()).isFalse();
|
||||
resourceFactory.start();
|
||||
connector.connect(HttpMethod.GET, new URI(""), request -> Mono.empty());
|
||||
assertThat(connector.isRunning()).isTrue();
|
||||
connector.stop();
|
||||
assertThat(connector.isRunning()).isFalse();
|
||||
connector.connect(HttpMethod.GET, new URI(""), request -> Mono.empty());
|
||||
assertThat(connector.isRunning()).isFalse();
|
||||
connector.start();
|
||||
assertThat(connector.isRunning()).isTrue();
|
||||
connector.stop();
|
||||
assertThat(connector.isRunning()).isFalse();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+2
-1
@@ -128,9 +128,10 @@ class GsonFactoryBeanTests {
|
||||
cal.set(Calendar.DATE, 1);
|
||||
Date date = cal.getTime();
|
||||
bean.setDate(date);
|
||||
// \p{Zs} matches any Unicode space character
|
||||
assertThat(gson.toJson(bean))
|
||||
.startsWith("{\"date\":\"Jan 1, 2014")
|
||||
.endsWith("12:00:00 AM\"}");
|
||||
.matches(".+?12:00:00\\p{Zs}AM\"}");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
import static java.util.Map.entry;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link ErrorResponse}.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
*/
|
||||
class ErrorResponseTests {
|
||||
|
||||
@Test
|
||||
void createWithHttpHeader() {
|
||||
ErrorResponse response = ErrorResponse.builder(new IllegalStateException(), HttpStatus.BAD_REQUEST, "test")
|
||||
.header("header", "value").build();
|
||||
assertThat(response.getHeaders()).containsOnly(entry("header", List.of("value")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void createWithHttpHeadersConsumer() {
|
||||
ErrorResponse response = ErrorResponse.builder(new IllegalStateException(), HttpStatus.BAD_REQUEST, "test")
|
||||
.header("header", "value")
|
||||
.headers(headers -> {
|
||||
headers.add("header", "value2");
|
||||
headers.add("another", "value3");
|
||||
}).build();
|
||||
assertThat(response.getHeaders()).containsOnly(entry("header", List.of("value", "value2")),
|
||||
entry("another", List.of("value3")));
|
||||
}
|
||||
|
||||
}
|
||||
+65
-7
@@ -16,15 +16,24 @@
|
||||
|
||||
package org.springframework.web.filter;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import io.micrometer.observation.ObservationRegistry;
|
||||
import io.micrometer.observation.tck.TestObservationRegistry;
|
||||
import io.micrometer.observation.tck.TestObservationRegistryAssert;
|
||||
import jakarta.servlet.AsyncEvent;
|
||||
import jakarta.servlet.AsyncListener;
|
||||
import jakarta.servlet.DispatcherType;
|
||||
import jakarta.servlet.RequestDispatcher;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServlet;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.server.observation.ServerRequestObservationContext;
|
||||
import org.springframework.web.testfixture.servlet.MockAsyncContext;
|
||||
import org.springframework.web.testfixture.servlet.MockFilterChain;
|
||||
import org.springframework.web.testfixture.servlet.MockHttpServletRequest;
|
||||
import org.springframework.web.testfixture.servlet.MockHttpServletResponse;
|
||||
@@ -41,18 +50,18 @@ class ServerHttpObservationFilterTests {
|
||||
|
||||
private final TestObservationRegistry observationRegistry = TestObservationRegistry.create();
|
||||
|
||||
private final ServerHttpObservationFilter filter = new ServerHttpObservationFilter(this.observationRegistry);
|
||||
|
||||
private final MockFilterChain mockFilterChain = new MockFilterChain();
|
||||
|
||||
private final MockHttpServletRequest request = new MockHttpServletRequest(HttpMethod.GET.name(), "/resource/test");
|
||||
|
||||
private final MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
|
||||
private MockFilterChain mockFilterChain = new MockFilterChain();
|
||||
|
||||
private ServerHttpObservationFilter filter = new ServerHttpObservationFilter(this.observationRegistry);
|
||||
|
||||
|
||||
@Test
|
||||
void filterShouldNotProcessAsyncDispatch() {
|
||||
assertThat(this.filter.shouldNotFilterAsyncDispatch()).isTrue();
|
||||
void filterShouldProcessAsyncDispatch() {
|
||||
assertThat(this.filter.shouldNotFilterAsyncDispatch()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -68,6 +77,12 @@ class ServerHttpObservationFilterTests {
|
||||
assertThatHttpObservation().hasLowCardinalityKeyValue("outcome", "SUCCESS").hasBeenStopped();
|
||||
}
|
||||
|
||||
@Test
|
||||
void filterShouldOpenScope() throws Exception {
|
||||
this.mockFilterChain = new MockFilterChain(new ScopeCheckingServlet(this.observationRegistry));
|
||||
filter.doFilter(this.request, this.response, this.mockFilterChain);
|
||||
}
|
||||
|
||||
@Test
|
||||
void filterShouldAcceptNoOpObservationContext() throws Exception {
|
||||
ServerHttpObservationFilter filter = new ServerHttpObservationFilter(ObservationRegistry.NOOP);
|
||||
@@ -124,9 +139,52 @@ class ServerHttpObservationFilterTests {
|
||||
assertThatHttpObservation().hasLowCardinalityKeyValue("outcome", "SUCCESS").hasBeenStopped();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCloseObservationAfterAsyncError() throws Exception {
|
||||
this.request.setAsyncSupported(true);
|
||||
this.request.startAsync();
|
||||
this.filter.doFilter(this.request, this.response, this.mockFilterChain);
|
||||
MockAsyncContext asyncContext = (MockAsyncContext) this.request.getAsyncContext();
|
||||
for (AsyncListener listener : asyncContext.getListeners()) {
|
||||
listener.onError(new AsyncEvent(this.request.getAsyncContext(), new IllegalStateException("test error")));
|
||||
}
|
||||
asyncContext.complete();
|
||||
assertThatHttpObservation().hasLowCardinalityKeyValue("exception", "IllegalStateException").hasBeenStopped();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotCloseObservationDuringAsyncDispatch() throws Exception {
|
||||
this.mockFilterChain = new MockFilterChain(new ScopeCheckingServlet(this.observationRegistry));
|
||||
this.request.setDispatcherType(DispatcherType.ASYNC);
|
||||
this.filter.doFilter(this.request, this.response, this.mockFilterChain);
|
||||
TestObservationRegistryAssert.assertThat(this.observationRegistry)
|
||||
.hasObservationWithNameEqualTo("http.server.requests")
|
||||
.that().isNotStopped();
|
||||
}
|
||||
|
||||
private TestObservationRegistryAssert.TestObservationRegistryAssertReturningObservationContextAssert assertThatHttpObservation() {
|
||||
TestObservationRegistryAssert.assertThat(this.observationRegistry)
|
||||
.hasNumberOfObservationsWithNameEqualTo("http.server.requests", 1);
|
||||
|
||||
return TestObservationRegistryAssert.assertThat(this.observationRegistry)
|
||||
.hasObservationWithNameEqualTo("http.server.requests").that();
|
||||
.hasObservationWithNameEqualTo("http.server.requests")
|
||||
.that()
|
||||
.hasBeenStopped();
|
||||
}
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
static class ScopeCheckingServlet extends HttpServlet {
|
||||
|
||||
private final ObservationRegistry observationRegistry;
|
||||
|
||||
public ScopeCheckingServlet(ObservationRegistry observationRegistry) {
|
||||
this.observationRegistry = observationRegistry;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
|
||||
assertThat(this.observationRegistry.getCurrentObservation()).isNotNull();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+24
-2
@@ -21,14 +21,13 @@ import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.springframework.core.Ordered
|
||||
import org.springframework.core.ResolvableType
|
||||
import org.springframework.core.codec.DecodingException
|
||||
import org.springframework.core.io.buffer.DataBuffer
|
||||
import org.springframework.core.testfixture.codec.AbstractDecoderTests
|
||||
import org.springframework.http.MediaType
|
||||
import reactor.core.publisher.Flux
|
||||
import reactor.core.publisher.Mono
|
||||
import reactor.test.StepVerifier
|
||||
import reactor.test.StepVerifier.FirstStep
|
||||
import java.lang.UnsupportedOperationException
|
||||
import java.math.BigDecimal
|
||||
import java.nio.charset.Charset
|
||||
import java.nio.charset.StandardCharsets
|
||||
@@ -82,6 +81,29 @@ class KotlinSerializationJsonDecoderTests : AbstractDecoderTests<KotlinSerializa
|
||||
}, null, null)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun decodeWithUnexpectedFormat() {
|
||||
val input = Flux.concat(
|
||||
stringBuffer("{\"ba\":\"b1\",\"fo\":\"f1\"}\n"),
|
||||
)
|
||||
|
||||
testDecode(input, ResolvableType.forClass(Pojo::class.java), { step: FirstStep<Pojo> ->
|
||||
step
|
||||
.expectError(DecodingException::class.java)
|
||||
.verify() }, null, null)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun decodeToMonoWithUnexpectedFormat() {
|
||||
val input = Flux.concat(
|
||||
stringBuffer("{\"ba\":\"b1\",\"fo\":\"f1\"}\n"),
|
||||
)
|
||||
|
||||
testDecodeToMono(input, ResolvableType.forClass(Pojo::class.java), { step: FirstStep<Pojo> ->
|
||||
step.expectError(DecodingException::class.java)
|
||||
.verify() }, null, null)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun decodeStreamWithSingleBuffer() {
|
||||
val input = Flux.concat(
|
||||
|
||||
+16
@@ -24,6 +24,7 @@ import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.springframework.core.Ordered
|
||||
import org.springframework.core.ResolvableType
|
||||
import org.springframework.core.codec.DecodingException
|
||||
import org.springframework.core.io.buffer.DataBuffer
|
||||
import org.springframework.core.testfixture.codec.AbstractDecoderTests
|
||||
import org.springframework.http.MediaType
|
||||
@@ -86,6 +87,21 @@ class KotlinSerializationProtobufDecoderTests : AbstractDecoderTests<KotlinSeria
|
||||
}, null, null)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun decodeToMonoWithUnexpectedFormat() {
|
||||
val input = Mono.just(
|
||||
bufferFactory.allocateBuffer(0),
|
||||
)
|
||||
|
||||
val elementType = ResolvableType.forClass(Pojo::class.java)
|
||||
|
||||
testDecodeToMono(input, elementType, { step: FirstStep<Any> ->
|
||||
step
|
||||
.expectError(DecodingException::class.java)
|
||||
.verify()
|
||||
}, null, null)
|
||||
}
|
||||
|
||||
private fun byteBuffer(value: Any): Mono<DataBuffer> {
|
||||
return Mono.defer {
|
||||
val bytes = ProtoBuf.Default.encodeToByteArray(serializer(Pojo::class.java), value)
|
||||
|
||||
+14
@@ -272,6 +272,20 @@ public class WebClientResponseException extends WebClientException {
|
||||
this.bodyDecodeFunction = decoderFunction;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMessage() {
|
||||
String message = String.valueOf(super.getMessage());
|
||||
if (shouldHintAtResponseFailure()) {
|
||||
return message + ", but response failed with cause: " + getCause();
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
private boolean shouldHintAtResponseFailure() {
|
||||
return this.statusCode.is1xxInformational() ||
|
||||
this.statusCode.is2xxSuccessful() ||
|
||||
this.statusCode.is3xxRedirection();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create {@code WebClientResponseException} or an HTTP status specific subclass.
|
||||
|
||||
+7
-6
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2016 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,8 +20,9 @@ import freemarker.template.Configuration;
|
||||
|
||||
/**
|
||||
* Interface to be implemented by objects that configure and manage a
|
||||
* FreeMarker Configuration object in a web environment. Detected and
|
||||
* used by {@link FreeMarkerView}.
|
||||
* FreeMarker {@link Configuration} object in a web environment.
|
||||
*
|
||||
* <p>Detected and used by {@link FreeMarkerView}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.0
|
||||
@@ -29,11 +30,11 @@ import freemarker.template.Configuration;
|
||||
public interface FreeMarkerConfig {
|
||||
|
||||
/**
|
||||
* Return the FreeMarker Configuration object for the current
|
||||
* Return the FreeMarker {@link Configuration} object for the current
|
||||
* web application context.
|
||||
* <p>A FreeMarker Configuration object may be used to set FreeMarker
|
||||
* <p>A FreeMarker {@code Configuration} object may be used to set FreeMarker
|
||||
* properties and shared objects, and allows to retrieve templates.
|
||||
* @return the FreeMarker Configuration
|
||||
* @return the FreeMarker {@code Configuration}
|
||||
*/
|
||||
Configuration getConfiguration();
|
||||
|
||||
|
||||
+18
-16
@@ -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.
|
||||
@@ -31,9 +31,10 @@ import org.springframework.ui.freemarker.FreeMarkerConfigurationFactory;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Configures FreeMarker for web usage via the "configLocation" and/or
|
||||
* "freemarkerSettings" and/or "templateLoaderPath" properties.
|
||||
* The simplest way to use this class is to specify just a "templateLoaderPath"
|
||||
* Configures FreeMarker for web usage via the "configLocation",
|
||||
* "freemarkerSettings", or "templateLoaderPath" properties.
|
||||
*
|
||||
* <p>The simplest way to use this class is to specify just a "templateLoaderPath"
|
||||
* (e.g. "classpath:templates"); you do not need any further configuration then.
|
||||
*
|
||||
* <p>This bean must be included in the application context of any application
|
||||
@@ -42,9 +43,9 @@ import org.springframework.util.Assert;
|
||||
* by {@code FreeMarkerView}. Implements {@link FreeMarkerConfig} to be found by
|
||||
* {@code FreeMarkerView} without depending on the bean name of the configurer.
|
||||
*
|
||||
* <p>Note that you can also refer to a pre-configured FreeMarker Configuration
|
||||
* <p>Note that you can also refer to a pre-configured FreeMarker {@code Configuration}
|
||||
* instance via the "configuration" property. This allows to share a FreeMarker
|
||||
* Configuration for web and email usage for example.
|
||||
* {@code Configuration} for web and email usage for example.
|
||||
*
|
||||
* <p>This configurer registers a template loader for this package, allowing to
|
||||
* reference the "spring.ftl" macro library contained in this package:
|
||||
@@ -54,7 +55,7 @@ import org.springframework.util.Assert;
|
||||
* <@spring.bind "person.age"/>
|
||||
* age is ${spring.status.value}</pre>
|
||||
*
|
||||
* Note: Spring's FreeMarker support requires FreeMarker 2.3 or higher.
|
||||
* <p>Note: Spring's FreeMarker support requires FreeMarker 2.3.21 or higher.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.0
|
||||
@@ -72,10 +73,10 @@ public class FreeMarkerConfigurer extends FreeMarkerConfigurationFactory
|
||||
|
||||
|
||||
/**
|
||||
* Set a pre-configured Configuration to use for the FreeMarker web config,
|
||||
* e.g. a shared one for web and email usage. If this is not set,
|
||||
* FreeMarkerConfigurationFactory's properties (inherited by this class)
|
||||
* have to be specified.
|
||||
* Set a preconfigured {@link Configuration} to use for the FreeMarker web
|
||||
* config — for example, a shared one for web and email usage.
|
||||
* <p>If this is not set, FreeMarkerConfigurationFactory's properties (inherited
|
||||
* by this class) have to be specified.
|
||||
*/
|
||||
public void setConfiguration(Configuration configuration) {
|
||||
this.configuration = configuration;
|
||||
@@ -83,9 +84,10 @@ public class FreeMarkerConfigurer extends FreeMarkerConfigurationFactory
|
||||
|
||||
|
||||
/**
|
||||
* Initialize FreeMarkerConfigurationFactory's Configuration
|
||||
* if not overridden by a pre-configured FreeMarker Configuration.
|
||||
* <p>Sets up a ClassTemplateLoader to use for loading Spring macros.
|
||||
* Initialize FreeMarkerConfigurationFactory's {@link Configuration}
|
||||
* if not overridden by a pre-configured FreeMarker {@link Configuration}.
|
||||
* <p>Indirectly sets up a {@link ClassTemplateLoader} to use for loading
|
||||
* Spring macros.
|
||||
* @see #createConfiguration
|
||||
* @see #setConfiguration
|
||||
*/
|
||||
@@ -97,7 +99,7 @@ public class FreeMarkerConfigurer extends FreeMarkerConfigurationFactory
|
||||
}
|
||||
|
||||
/**
|
||||
* This implementation registers an additional ClassTemplateLoader
|
||||
* This implementation registers an additional {@link ClassTemplateLoader}
|
||||
* for the Spring-provided macros, added to the end of the list.
|
||||
*/
|
||||
@Override
|
||||
@@ -107,7 +109,7 @@ public class FreeMarkerConfigurer extends FreeMarkerConfigurationFactory
|
||||
|
||||
|
||||
/**
|
||||
* Return the Configuration object wrapped by this bean.
|
||||
* Return the {@link Configuration} object wrapped by this bean.
|
||||
*/
|
||||
@Override
|
||||
public Configuration getConfiguration() {
|
||||
|
||||
+56
-13
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 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.
|
||||
@@ -56,16 +56,40 @@ import org.springframework.web.server.ServerWebExchange;
|
||||
/**
|
||||
* A {@code View} implementation that uses the FreeMarker template engine.
|
||||
*
|
||||
* <p>Exposes the following configuration properties:
|
||||
* <ul>
|
||||
* <li><b>{@link #setUrl(String) url}</b>: the location of the FreeMarker template
|
||||
* relative to the FreeMarkerConfigurer's
|
||||
* {@link FreeMarkerConfigurer#setTemplateLoaderPath templateLoaderPath}.</li>
|
||||
* <li><b>{@link #setEncoding(String) encoding}</b>: the encoding used to decode
|
||||
* byte sequences to character sequences when reading the FreeMarker template file.
|
||||
* Default is determined by the FreeMarker {@link Configuration}.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>Depends on a single {@link FreeMarkerConfig} object such as
|
||||
* {@link FreeMarkerConfigurer} being accessible in the application context.
|
||||
* Alternatively the FreeMarker {@link Configuration} can be set directly on this
|
||||
* class via {@link #setConfiguration}.
|
||||
* Alternatively the FreeMarker {@link Configuration} can be set directly via
|
||||
* {@link #setConfiguration}.
|
||||
*
|
||||
* <p>The {@link #setUrl(String) url} property is the location of the FreeMarker
|
||||
* template relative to the FreeMarkerConfigurer's
|
||||
* {@link FreeMarkerConfigurer#setTemplateLoaderPath templateLoaderPath}.
|
||||
* <p><b>Note:</b> To ensure that the correct encoding is used when rendering the
|
||||
* response as well as when the client reads the response, the following steps
|
||||
* must be taken.
|
||||
* <ul>
|
||||
* <li>Either set the {@linkplain Configuration#setDefaultEncoding(String)
|
||||
* default encoding} in the FreeMarker {@code Configuration} or set the
|
||||
* {@linkplain #setEncoding(String) encoding} for this view.</li>
|
||||
* <li>Configure the supported media type with a {@code charset} equal to the
|
||||
* configured {@code encoding} via {@link #setSupportedMediaTypes(java.util.List)}
|
||||
* or {@link FreeMarkerViewResolver#setSupportedMediaTypes(java.util.List)}.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>Note: Spring's FreeMarker support requires FreeMarker 2.3 or higher.
|
||||
* Note, however, that {@link FreeMarkerConfigurer} sets the default encoding in
|
||||
* the FreeMarker {@code Configuration} to "UTF-8" and that
|
||||
* {@link org.springframework.web.reactive.result.view.AbstractView AbstractView}
|
||||
* sets the supported media type to {@code "text/html;charset=UTF-8"} by default.
|
||||
* Thus, those default values are likely suitable for most applications.
|
||||
*
|
||||
* <p>Note: Spring's FreeMarker support requires FreeMarker 2.3.21 or higher.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Sam Brannen
|
||||
@@ -124,18 +148,37 @@ public class FreeMarkerView extends AbstractUrlBasedView {
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the encoding of the FreeMarker template file.
|
||||
* <p>By default {@link FreeMarkerConfigurer} sets the default encoding in
|
||||
* the FreeMarker configuration to "UTF-8". It's recommended to specify the
|
||||
* encoding in the FreeMarker {@link Configuration} rather than per template
|
||||
* if all your templates share a common encoding.
|
||||
* Set the encoding used to decode byte sequences to character sequences when
|
||||
* reading the FreeMarker template file for this view.
|
||||
* <p>Defaults to {@code null} to signal that the FreeMarker
|
||||
* {@link Configuration} should be used to determine the encoding.
|
||||
* <p>A non-null encoding will override the default encoding determined by
|
||||
* the FreeMarker {@code Configuration}.
|
||||
* <p>If the encoding is not explicitly set here or in the FreeMarker
|
||||
* {@code Configuration}, FreeMarker will read template files using the platform
|
||||
* file encoding (defined by the JVM system property {@code file.encoding})
|
||||
* or {@code "utf-8"} if the platform file encoding is undefined. Note,
|
||||
* however, that {@link FreeMarkerConfigurer} sets the default encoding in the
|
||||
* FreeMarker {@code Configuration} to "UTF-8".
|
||||
* <p>It's recommended to specify the encoding in the FreeMarker {@code Configuration}
|
||||
* rather than per template if all your templates share a common encoding.
|
||||
* <p>Note that the specified or default encoding is not used for template
|
||||
* rendering. Instead, an explicit encoding must be specified for the rendering
|
||||
* process. See the note in the {@linkplain FreeMarkerView class-level
|
||||
* documentation} for details.
|
||||
* @see freemarker.template.Configuration#setDefaultEncoding
|
||||
* @see #getEncoding()
|
||||
*/
|
||||
public void setEncoding(@Nullable String encoding) {
|
||||
this.encoding = encoding;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the encoding for the FreeMarker template.
|
||||
* Get the encoding used to decode byte sequences to character sequences
|
||||
* when reading the FreeMarker template file for this view, or {@code null}
|
||||
* to signal that the FreeMarker {@link Configuration} should be used to
|
||||
* determine the encoding.
|
||||
* @see #setEncoding(String)
|
||||
*/
|
||||
@Nullable
|
||||
protected String getEncoding() {
|
||||
|
||||
+207
@@ -0,0 +1,207 @@
|
||||
/*
|
||||
* 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.reactive.config;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import freemarker.cache.ClassTemplateLoader;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.reactive.DispatcherHandler;
|
||||
import org.springframework.web.reactive.result.view.freemarker.FreeMarkerConfigurer;
|
||||
import org.springframework.web.reactive.result.view.freemarker.FreeMarkerViewResolver;
|
||||
import org.springframework.web.testfixture.http.server.reactive.MockServerHttpRequest;
|
||||
import org.springframework.web.testfixture.http.server.reactive.MockServerHttpResponse;
|
||||
import org.springframework.web.testfixture.server.MockServerWebExchange;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.ISO_8859_1;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatRuntimeException;
|
||||
|
||||
/**
|
||||
* Integration tests for view resolution with {@code @EnableWebFlux}.
|
||||
*
|
||||
* @author Sam Brannen
|
||||
* @since 6.1.11
|
||||
* @see org.springframework.web.servlet.config.annotation.ViewResolutionIntegrationTests
|
||||
*/
|
||||
class WebFluxViewResolutionIntegrationTests {
|
||||
|
||||
private static final MediaType TEXT_HTML_UTF8 = MediaType.parseMediaType("text/html;charset=UTF-8");
|
||||
|
||||
private static final MediaType TEXT_HTML_ISO_8859_1 = MediaType.parseMediaType("text/html;charset=ISO-8859-1");
|
||||
|
||||
private static final String EXPECTED_BODY = "<html><body>Hello, Java Café</body></html>";
|
||||
|
||||
|
||||
@Nested
|
||||
class FreeMarkerTests {
|
||||
|
||||
private static final ClassTemplateLoader classTemplateLoader =
|
||||
new ClassTemplateLoader(WebFluxViewResolutionIntegrationTests.class, "");
|
||||
|
||||
@Test
|
||||
void freemarkerWithInvalidConfig() {
|
||||
assertThatRuntimeException()
|
||||
.isThrownBy(() -> runTest(InvalidFreeMarkerWebFluxConfig.class))
|
||||
.withMessageContaining("In addition to a FreeMarker view resolver ");
|
||||
}
|
||||
|
||||
@Test
|
||||
void freemarkerWithDefaults() throws Exception {
|
||||
MockServerHttpResponse response = runTest(FreeMarkerWebFluxConfig.class);
|
||||
StepVerifier.create(response.getBodyAsString()).expectNext(EXPECTED_BODY).expectComplete().verify();
|
||||
assertThat(response.getHeaders().getContentType()).isEqualTo(TEXT_HTML_UTF8);
|
||||
}
|
||||
|
||||
@Test
|
||||
void freemarkerWithExplicitDefaultEncoding() throws Exception {
|
||||
MockServerHttpResponse response = runTest(ExplicitDefaultEncodingConfig.class);
|
||||
StepVerifier.create(response.getBodyAsString()).expectNext(EXPECTED_BODY).expectComplete().verify();
|
||||
assertThat(response.getHeaders().getContentType()).isEqualTo(TEXT_HTML_UTF8);
|
||||
}
|
||||
|
||||
@Test
|
||||
void freemarkerWithExplicitDefaultEncodingAndContentType() throws Exception {
|
||||
MockServerHttpResponse response = runTest(ExplicitDefaultEncodingAndContentTypeConfig.class);
|
||||
StepVerifier.create(response.getBodyAsString()).expectNext(EXPECTED_BODY).expectComplete().verify();
|
||||
// When the Content-Type (supported media type) is explicitly set on the view resolver, it should be used.
|
||||
assertThat(response.getHeaders().getContentType()).isEqualTo(TEXT_HTML_ISO_8859_1);
|
||||
}
|
||||
|
||||
|
||||
@EnableWebFlux
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class InvalidFreeMarkerWebFluxConfig implements WebFluxConfigurer {
|
||||
|
||||
@Override
|
||||
public void configureViewResolvers(ViewResolverRegistry registry) {
|
||||
registry.freeMarker();
|
||||
}
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class FreeMarkerWebFluxConfig extends AbstractWebFluxConfig {
|
||||
|
||||
@Override
|
||||
public void configureViewResolvers(ViewResolverRegistry registry) {
|
||||
registry.freeMarker();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public FreeMarkerConfigurer freeMarkerConfigurer() {
|
||||
FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
|
||||
configurer.setPreTemplateLoaders(classTemplateLoader);
|
||||
return configurer;
|
||||
}
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class ExplicitDefaultEncodingConfig extends AbstractWebFluxConfig {
|
||||
|
||||
@Override
|
||||
public void configureViewResolvers(ViewResolverRegistry registry) {
|
||||
registry.freeMarker();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public FreeMarkerConfigurer freeMarkerConfigurer() {
|
||||
FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
|
||||
configurer.setPreTemplateLoaders(classTemplateLoader);
|
||||
configurer.setDefaultEncoding(UTF_8.name());
|
||||
return configurer;
|
||||
}
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class ExplicitDefaultEncodingAndContentTypeConfig extends AbstractWebFluxConfig {
|
||||
|
||||
@Autowired
|
||||
ApplicationContext applicationContext;
|
||||
|
||||
@Override
|
||||
public void configureViewResolvers(ViewResolverRegistry registry) {
|
||||
FreeMarkerViewResolver resolver = new FreeMarkerViewResolver("", ".ftl");
|
||||
resolver.setSupportedMediaTypes(List.of(TEXT_HTML_ISO_8859_1));
|
||||
resolver.setApplicationContext(this.applicationContext);
|
||||
registry.viewResolver(resolver);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public FreeMarkerConfigurer freeMarkerConfigurer() {
|
||||
FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
|
||||
configurer.setPreTemplateLoaders(classTemplateLoader);
|
||||
configurer.setDefaultEncoding(ISO_8859_1.name());
|
||||
return configurer;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Bean
|
||||
public SampleController sampleController() {
|
||||
return new SampleController("index_ISO-8859-1");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static MockServerHttpResponse runTest(Class<?> configClass) throws Exception {
|
||||
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(configClass);
|
||||
MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/"));
|
||||
new DispatcherHandler(context).handle(exchange).block(Duration.ofSeconds(1));
|
||||
return exchange.getResponse();
|
||||
}
|
||||
|
||||
|
||||
@EnableWebFlux
|
||||
abstract static class AbstractWebFluxConfig implements WebFluxConfigurer {
|
||||
|
||||
@Bean
|
||||
public SampleController sampleController() {
|
||||
return new SampleController("index_UTF-8");
|
||||
}
|
||||
}
|
||||
|
||||
@Controller
|
||||
static class SampleController {
|
||||
|
||||
private final String viewName;
|
||||
|
||||
SampleController(String viewName) {
|
||||
this.viewName = viewName;
|
||||
}
|
||||
|
||||
@GetMapping("/")
|
||||
String index(Map<String, Object> model) {
|
||||
model.put("hello", "Hello");
|
||||
return this.viewName;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* 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.reactive.function.client;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link WebClientResponseException}.
|
||||
*
|
||||
* @author Simon Baslé
|
||||
*/
|
||||
class WebClientResponseExceptionTests {
|
||||
|
||||
@Test
|
||||
void constructWithSuccessStatusCodeAndNoCauseAdditionalMessage() {
|
||||
assertThat(new WebClientResponseException(200, "OK", null, null, null))
|
||||
.hasNoCause()
|
||||
.hasMessage("200 OK, but response failed with cause: null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void constructWith1xxStatusCodeAndCauseAdditionalMessage() {
|
||||
WebClientResponseException ex = new WebClientResponseException(100, "reasonPhrase", null, null, null);
|
||||
ex.initCause(new RuntimeException("example cause"));
|
||||
assertThat(ex).hasMessage("100 reasonPhrase, but response failed with cause: java.lang.RuntimeException: example cause");
|
||||
}
|
||||
|
||||
@Test
|
||||
void constructWith2xxStatusCodeAndCauseAdditionalMessage() {
|
||||
WebClientResponseException ex = new WebClientResponseException(200, "reasonPhrase", null, null, null);
|
||||
ex.initCause(new RuntimeException("example cause"));
|
||||
assertThat(ex).hasMessage("200 reasonPhrase, but response failed with cause: java.lang.RuntimeException: example cause");
|
||||
}
|
||||
|
||||
@Test
|
||||
void constructWith3xxStatusCodeAndCauseAdditionalMessage() {
|
||||
WebClientResponseException ex = new WebClientResponseException(300, "reasonPhrase", null, null, null);
|
||||
ex.initCause(new RuntimeException("example cause"));
|
||||
assertThat(ex).hasMessage("300 reasonPhrase, but response failed with cause: java.lang.RuntimeException: example cause");
|
||||
}
|
||||
|
||||
@Test
|
||||
void constructWithExplicitMessageAndNotErrorCodeAdditionalMessage() {
|
||||
WebClientResponseException ex = new WebClientResponseException("explicit message", 100, "reasonPhrase", null, null, null);
|
||||
assertThat(ex).hasMessage("explicit message, but response failed with cause: null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void constructWithExplicitMessageAndNotErrorCodeAndCauseAdditionalMessage() {
|
||||
WebClientResponseException ex = new WebClientResponseException("explicit message", 100, "reasonPhrase", null, null, null);
|
||||
ex.initCause(new RuntimeException("example cause"));
|
||||
assertThat(ex).hasMessage("explicit message, but response failed with cause: java.lang.RuntimeException: example cause")
|
||||
.hasRootCauseMessage("example cause");
|
||||
}
|
||||
|
||||
@Test
|
||||
void constructWithExplicitMessageAndErrorCodeAndCauseNoAdditionalMessage() {
|
||||
WebClientResponseException ex = new WebClientResponseException("explicit message", 404, "reasonPhrase", null, null, null);
|
||||
ex.initCause(new RuntimeException("example cause"));
|
||||
assertThat(ex).hasMessage("explicit message").hasRootCauseMessage("example cause");
|
||||
}
|
||||
|
||||
@Test
|
||||
void constructWith4xxStatusCodeAndCauseNoAdditionalMessage() {
|
||||
WebClientResponseException ex = new WebClientResponseException(400, "reasonPhrase", null, null, null);
|
||||
ex.initCause(new RuntimeException("example cause"));
|
||||
assertThat(ex).hasMessage("400 reasonPhrase").hasRootCauseMessage("example cause");
|
||||
}
|
||||
|
||||
@Test
|
||||
void constructWith5xxStatusCodeAndCauseNoAdditionalMessage() {
|
||||
WebClientResponseException ex = new WebClientResponseException(500, "reasonPhrase", null, null, null);
|
||||
ex.initCause(new RuntimeException("example cause"));
|
||||
assertThat(ex).hasMessage("500 reasonPhrase").hasRootCauseMessage("example cause");
|
||||
}
|
||||
|
||||
}
|
||||
+5
-4
@@ -111,10 +111,11 @@ class RequestMappingViewResolutionIntegrationTests extends AbstractRequestMappin
|
||||
|
||||
@Bean
|
||||
public FreeMarkerConfigurer freeMarkerConfig() {
|
||||
FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
|
||||
configurer.setPreferFileSystemAccess(false);
|
||||
configurer.setTemplateLoaderPath("classpath*:org/springframework/web/reactive/view/freemarker/");
|
||||
return configurer;
|
||||
// No need to configure a custom template loader path via setTemplateLoaderPath(),
|
||||
// since FreeMarkerConfigurer already registers a
|
||||
// new ClassTemplateLoader(FreeMarkerConfigurer.class, ""), which automatically
|
||||
// finds template files in the same package as this test class.
|
||||
return new FreeMarkerConfigurer();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+8
-7
@@ -18,11 +18,11 @@ package org.springframework.web.reactive.result.method.annotation;
|
||||
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.testfixture.beans.TestBean;
|
||||
import org.springframework.ui.ModelMap;
|
||||
import org.springframework.web.bind.annotation.SessionAttributes;
|
||||
import org.springframework.web.server.WebSession;
|
||||
import org.springframework.web.testfixture.server.MockWebSession;
|
||||
@@ -31,7 +31,8 @@ import static java.util.Arrays.asList;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Test fixture with {@link SessionAttributesHandler}.
|
||||
* Tests for {@link SessionAttributesHandler}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
*/
|
||||
class SessionAttributesHandlerTests {
|
||||
@@ -86,11 +87,11 @@ class SessionAttributesHandlerTests {
|
||||
|
||||
@Test
|
||||
void storeAttributes() {
|
||||
|
||||
ModelMap model = new ModelMap();
|
||||
model.put("attr1", "value1");
|
||||
model.put("attr2", "value2");
|
||||
model.put("attr3", new TestBean());
|
||||
Map<String, Object> model = Map.of(
|
||||
"attr1", "value1",
|
||||
"attr2", "value2",
|
||||
"attr3", new TestBean()
|
||||
);
|
||||
|
||||
WebSession session = new MockWebSession();
|
||||
sessionAttributesHandler.storeAttributes(session, model);
|
||||
|
||||
+3
-4
@@ -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.
|
||||
@@ -20,7 +20,6 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.context.support.GenericApplicationContext;
|
||||
import org.springframework.ui.ModelMap;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.util.UriComponents;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
@@ -38,7 +37,7 @@ public class DummyMacroRequestContext {
|
||||
|
||||
private final ServerWebExchange exchange;
|
||||
|
||||
private final ModelMap model;
|
||||
private final Map<String, Object> model;
|
||||
|
||||
private final GenericApplicationContext context;
|
||||
|
||||
@@ -46,7 +45,7 @@ public class DummyMacroRequestContext {
|
||||
|
||||
private String contextPath;
|
||||
|
||||
public DummyMacroRequestContext(ServerWebExchange exchange, ModelMap model, GenericApplicationContext context) {
|
||||
public DummyMacroRequestContext(ServerWebExchange exchange, Map<String, Object> model, GenericApplicationContext context) {
|
||||
this.exchange = exchange;
|
||||
this.model = model;
|
||||
this.context = context;
|
||||
|
||||
+23
-26
@@ -17,11 +17,10 @@
|
||||
package org.springframework.web.reactive.result.view;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import reactor.test.StepVerifier;
|
||||
@@ -30,8 +29,6 @@ import org.springframework.core.codec.CharSequenceEncoder;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.codec.json.Jackson2JsonEncoder;
|
||||
import org.springframework.http.codec.xml.Jaxb2XmlEncoder;
|
||||
import org.springframework.ui.ExtendedModelMap;
|
||||
import org.springframework.ui.ModelMap;
|
||||
import org.springframework.web.testfixture.http.server.reactive.MockServerHttpRequest;
|
||||
import org.springframework.web.testfixture.server.MockServerWebExchange;
|
||||
|
||||
@@ -48,7 +45,7 @@ class HttpMessageWriterViewTests {
|
||||
|
||||
private HttpMessageWriterView view = new HttpMessageWriterView(new Jackson2JsonEncoder());
|
||||
|
||||
private final ModelMap model = new ExtendedModelMap();
|
||||
private final Map<String, Object> model = new HashMap<>();
|
||||
|
||||
private final MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/"));
|
||||
|
||||
@@ -63,18 +60,18 @@ class HttpMessageWriterViewTests {
|
||||
|
||||
@Test
|
||||
void singleMatch() throws Exception {
|
||||
this.view.setModelKeys(Collections.singleton("foo2"));
|
||||
this.model.addAttribute("foo1", Collections.singleton("bar1"));
|
||||
this.model.addAttribute("foo2", Collections.singleton("bar2"));
|
||||
this.model.addAttribute("foo3", Collections.singleton("bar3"));
|
||||
this.view.setModelKeys(Set.of("foo2"));
|
||||
this.model.put("foo1", Set.of("bar1"));
|
||||
this.model.put("foo2", Set.of("bar2"));
|
||||
this.model.put("foo3", Set.of("bar3"));
|
||||
|
||||
assertThat(doRender()).isEqualTo("[\"bar2\"]");
|
||||
}
|
||||
|
||||
@Test
|
||||
void noMatch() throws Exception {
|
||||
this.view.setModelKeys(Collections.singleton("foo2"));
|
||||
this.model.addAttribute("foo1", "bar1");
|
||||
this.view.setModelKeys(Set.of("foo2"));
|
||||
this.model.put("foo1", "bar1");
|
||||
|
||||
assertThat(doRender()).isEmpty();
|
||||
}
|
||||
@@ -82,18 +79,18 @@ class HttpMessageWriterViewTests {
|
||||
@Test
|
||||
void noMatchBecauseNotSupported() throws Exception {
|
||||
this.view = new HttpMessageWriterView(new Jaxb2XmlEncoder());
|
||||
this.view.setModelKeys(new HashSet<>(Collections.singletonList("foo1")));
|
||||
this.model.addAttribute("foo1", "bar1");
|
||||
this.view.setModelKeys(Set.of("foo1"));
|
||||
this.model.put("foo1", "bar1");
|
||||
|
||||
assertThat(doRender()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void multipleMatches() throws Exception {
|
||||
this.view.setModelKeys(new HashSet<>(Arrays.asList("foo1", "foo2")));
|
||||
this.model.addAttribute("foo1", Collections.singleton("bar1"));
|
||||
this.model.addAttribute("foo2", Collections.singleton("bar2"));
|
||||
this.model.addAttribute("foo3", Collections.singleton("bar3"));
|
||||
this.view.setModelKeys(Set.of("foo1", "foo2"));
|
||||
this.model.put("foo1", Set.of("bar1"));
|
||||
this.model.put("foo2", Set.of("bar2"));
|
||||
this.model.put("foo3", Set.of("bar3"));
|
||||
|
||||
assertThat(doRender()).isEqualTo("{\"foo1\":[\"bar1\"],\"foo2\":[\"bar2\"]}");
|
||||
}
|
||||
@@ -101,13 +98,13 @@ class HttpMessageWriterViewTests {
|
||||
@Test
|
||||
void multipleMatchesNotSupported() throws Exception {
|
||||
this.view = new HttpMessageWriterView(CharSequenceEncoder.allMimeTypes());
|
||||
this.view.setModelKeys(new HashSet<>(Arrays.asList("foo1", "foo2")));
|
||||
this.model.addAttribute("foo1", "bar1");
|
||||
this.model.addAttribute("foo2", "bar2");
|
||||
this.view.setModelKeys(Set.of("foo1", "foo2"));
|
||||
this.model.put("foo1", "bar1");
|
||||
this.model.put("foo2", "bar2");
|
||||
|
||||
assertThatIllegalStateException().isThrownBy(
|
||||
this::doRender)
|
||||
.withMessageContaining("Map rendering is not supported");
|
||||
assertThatIllegalStateException()
|
||||
.isThrownBy(this::doRender)
|
||||
.withMessageContaining("Map rendering is not supported");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -115,8 +112,8 @@ class HttpMessageWriterViewTests {
|
||||
Map<String, String> pojoData = new LinkedHashMap<>();
|
||||
pojoData.put("foo", "f");
|
||||
pojoData.put("bar", "b");
|
||||
this.model.addAttribute("pojoData", pojoData);
|
||||
this.view.setModelKeys(Collections.singleton("pojoData"));
|
||||
this.model.put("pojoData", pojoData);
|
||||
this.view.setModelKeys(Set.of("pojoData"));
|
||||
|
||||
this.view.render(this.model, MediaType.APPLICATION_JSON, exchange).block(Duration.ZERO);
|
||||
|
||||
|
||||
+1
-3
@@ -37,8 +37,6 @@ import org.springframework.beans.testfixture.beans.TestBean;
|
||||
import org.springframework.context.support.GenericApplicationContext;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.ui.ExtendedModelMap;
|
||||
import org.springframework.ui.ModelMap;
|
||||
import org.springframework.util.FileCopyUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.reactive.result.view.BindStatus;
|
||||
@@ -322,7 +320,7 @@ class FreeMarkerMacroTests {
|
||||
names.put("Fred", "Fred Bloggs");
|
||||
names.put("Rob&Harrop", "Rob Harrop");
|
||||
|
||||
ModelMap model = new ExtendedModelMap();
|
||||
Map<String, Object> model = new HashMap<>();
|
||||
DummyMacroRequestContext rc = new DummyMacroRequestContext(this.exchange, model,
|
||||
this.applicationContext);
|
||||
rc.setMessageMap(msgMap);
|
||||
|
||||
+4
-7
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 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 @@ package org.springframework.web.reactive.result.view.freemarker;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
import freemarker.template.Configuration;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
@@ -28,8 +29,6 @@ import reactor.test.StepVerifier;
|
||||
import org.springframework.context.ApplicationContextException;
|
||||
import org.springframework.context.support.GenericApplicationContext;
|
||||
import org.springframework.http.codec.ServerCodecConfigurer;
|
||||
import org.springframework.ui.ExtendedModelMap;
|
||||
import org.springframework.ui.ModelMap;
|
||||
import org.springframework.web.reactive.result.view.ZeroDemandResponse;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.adapter.DefaultServerWebExchange;
|
||||
@@ -125,8 +124,7 @@ class FreeMarkerViewTests {
|
||||
freeMarkerView.setConfiguration(this.freeMarkerConfig);
|
||||
freeMarkerView.setUrl("test.ftl");
|
||||
|
||||
ModelMap model = new ExtendedModelMap();
|
||||
model.addAttribute("hello", "hi FreeMarker");
|
||||
Map<String, Object> model = Map.of("hello", "hi FreeMarker");
|
||||
freeMarkerView.render(model, null, this.exchange).block(Duration.ofMillis(5000));
|
||||
|
||||
StepVerifier.create(this.exchange.getResponse().getBody())
|
||||
@@ -148,8 +146,7 @@ class FreeMarkerViewTests {
|
||||
freeMarkerView.setConfiguration(this.freeMarkerConfig);
|
||||
freeMarkerView.setUrl("test.ftl");
|
||||
|
||||
ModelMap model = new ExtendedModelMap();
|
||||
model.addAttribute("hello", "hi FreeMarker");
|
||||
Map<String, Object> model = Map.of("hello", "hi FreeMarker");
|
||||
freeMarkerView.render(model, null, exchange).subscribe();
|
||||
|
||||
response.cancelWrite();
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
<html><body>${hello}, Java Café</body></html>
|
||||
+1
@@ -0,0 +1 @@
|
||||
<html><body>${hello}, Java Café</body></html>
|
||||
@@ -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.
|
||||
@@ -495,7 +495,7 @@ public abstract class AbstractView extends WebApplicationObjectSupport implement
|
||||
}
|
||||
|
||||
protected String formatViewName() {
|
||||
return (getBeanName() != null ? "name '" + getBeanName() + "'" : "[" + getClass().getSimpleName() + "]");
|
||||
return (getBeanName() != null ? "name [" + getBeanName() + "]" : "[" + getClass().getSimpleName() + "]");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user