mirror of
https://github.com/spring-projects/spring-framework
synced 2026-06-08 17:33:33 +00:00
Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f1b128b88d | |||
| 8a44eaa6c5 | |||
| f44d13cb78 | |||
| f00bc7bb24 | |||
| 582bfccbb7 | |||
| 406b33d380 | |||
| f9c3d00af0 | |||
| d2715d2fa1 | |||
| 57b02da8b9 | |||
| df33bf27bd | |||
| f75cebd8f2 | |||
| 26f2dad388 | |||
| 7e390784ba | |||
| d5c5c91f41 | |||
| 1847d2f686 | |||
| 351a17a28b | |||
| dffb6c5bb6 | |||
| 286f5f295e | |||
| d8e5613ef7 | |||
| e319bd7016 | |||
| c8e9ad6289 | |||
| 562351f42f | |||
| 2302b30c2b | |||
| 96e3aa68a8 | |||
| 2303ea92fe | |||
| fa05de1d96 | |||
| 6b62b93d43 | |||
| 2eed02ea44 | |||
| bafb1c7e2b | |||
| c8df1861bf | |||
| ef81f06528 | |||
| a65ea7983d |
@@ -0,0 +1,20 @@
|
||||
name: Await HTTP Resource
|
||||
description: Waits for an HTTP resource to be available (a HEAD request succeeds)
|
||||
inputs:
|
||||
url:
|
||||
description: 'The URL of the resource to await'
|
||||
required: true
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Await HTTP resource
|
||||
shell: bash
|
||||
run: |
|
||||
url=${{ inputs.url }}
|
||||
echo "Waiting for $url"
|
||||
until curl --fail --head --silent ${{ inputs.url }} > /dev/null
|
||||
do
|
||||
echo "."
|
||||
sleep 60
|
||||
done
|
||||
echo "$url is available"
|
||||
@@ -0,0 +1,56 @@
|
||||
name: 'Build'
|
||||
description: 'Builds the project, optionally publishing it to a local deployment repository'
|
||||
inputs:
|
||||
java-version:
|
||||
required: false
|
||||
default: '8'
|
||||
description: 'The Java version to compile and test with'
|
||||
java-early-access:
|
||||
required: false
|
||||
default: 'false'
|
||||
description: 'Whether the Java version is in early access'
|
||||
java-toolchain:
|
||||
required: false
|
||||
default: 'false'
|
||||
description: 'Whether a Java toolchain should be used'
|
||||
publish:
|
||||
required: false
|
||||
default: 'false'
|
||||
description: 'Whether to publish artifacts ready for deployment to Artifactory'
|
||||
develocity-access-key:
|
||||
required: false
|
||||
description: 'The access key for authentication with ge.spring.io'
|
||||
outputs:
|
||||
build-scan-url:
|
||||
description: 'The URL, if any, of the build scan produced by the build'
|
||||
value: ${{ (inputs.publish == 'true' && steps.publish.outputs.build-scan-url) || steps.build.outputs.build-scan-url }}
|
||||
version:
|
||||
description: 'The version that was built'
|
||||
value: ${{ steps.read-version.outputs.version }}
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Prepare Gradle Build
|
||||
uses: ./.github/actions/prepare-gradle-build
|
||||
with:
|
||||
develocity-access-key: ${{ inputs.develocity-access-key }}
|
||||
java-version: ${{ inputs.java-version }}
|
||||
java-early-access: ${{ inputs.java-early-access }}
|
||||
java-toolchain: ${{ inputs.java-toolchain }}
|
||||
- name: Build
|
||||
id: build
|
||||
if: ${{ inputs.publish == 'false' }}
|
||||
shell: bash
|
||||
run: ./gradlew check
|
||||
- name: Publish
|
||||
id: publish
|
||||
if: ${{ inputs.publish == 'true' }}
|
||||
shell: bash
|
||||
run: ./gradlew -PdeploymentRepository=$(pwd)/deployment-repository build publishAllPublicationsToDeploymentRepository
|
||||
- name: Read Version From gradle.properties
|
||||
id: read-version
|
||||
shell: bash
|
||||
run: |
|
||||
version=$(sed -n 's/version=\(.*\)/\1/p' gradle.properties)
|
||||
echo "Version is $version"
|
||||
echo "version=$version" >> $GITHUB_OUTPUT
|
||||
@@ -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
|
||||
+9
-1
@@ -17,4 +17,12 @@ changelog:
|
||||
- "type: dependency-upgrade"
|
||||
contributors:
|
||||
exclude:
|
||||
names: ["bclozel", "jhoeller", "poutsma", "rstoyanchev", "sbrannen", "sdeleuze", "snicoll", "simonbasle"]
|
||||
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: '8'
|
||||
description: 'The Java version to use for the build'
|
||||
java-early-access:
|
||||
required: false
|
||||
default: 'false'
|
||||
description: 'Whether the Java version is in early access'
|
||||
java-toolchain:
|
||||
required: false
|
||||
default: 'false'
|
||||
description: 'Whether a Java toolchain should be used'
|
||||
develocity-access-key:
|
||||
required: false
|
||||
description: 'The access key for authentication with ge.spring.io'
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Set Up Java
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: ${{ inputs.java-early-access == 'true' && 'temurin' || 'liberica' }}
|
||||
java-version: |
|
||||
${{ inputs.java-early-access == 'true' && format('{0}-ea', inputs.java-version) || inputs.java-version }}
|
||||
${{ inputs.java-toolchain == 'true' && '8' || '' }}
|
||||
- name: Set Up Gradle
|
||||
uses: gradle/actions/setup-gradle@d9c87d481d55275bb5441eef3fe0e46805f9ef70 # v3.5.0
|
||||
with:
|
||||
cache-read-only: false
|
||||
develocity-access-key: ${{ inputs.develocity-access-key }}
|
||||
- name: Configure Gradle Properties
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir -p $HOME/.gradle
|
||||
echo 'systemProp.user.name=spring-builds+github' >> $HOME/.gradle/gradle.properties
|
||||
echo 'systemProp.org.gradle.internal.launcher.welcomeMessageEnabled=false' >> $HOME/.gradle/gradle.properties
|
||||
echo 'org.gradle.daemon=false' >> $HOME/.gradle/gradle.properties
|
||||
echo 'org.gradle.daemon=4' >> $HOME/.gradle/gradle.properties
|
||||
- name: Configure Toolchain Properties
|
||||
if: ${{ inputs.java-toolchain == 'true' }}
|
||||
shell: bash
|
||||
run: |
|
||||
echo toolchainVersion=${{ inputs.java-version }} >> $HOME/.gradle/gradle.properties
|
||||
echo systemProp.org.gradle.java.installations.auto-detect=false >> $HOME/.gradle/gradle.properties
|
||||
echo systemProp.org.gradle.java.installations.auto-download=false >> $HOME/.gradle/gradle.properties
|
||||
echo systemProp.org.gradle.java.installations.paths=${{ format('$JAVA_HOME_{0}_X64', inputs.java-version) }} >> $HOME/.gradle/gradle.properties
|
||||
@@ -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,43 @@
|
||||
name: Sync to Maven Central
|
||||
description: Syncs a release to Maven Central and waits for it to be available for use
|
||||
inputs:
|
||||
jfrog-cli-config-token:
|
||||
description: 'Config token for the JFrog CLI'
|
||||
required: true
|
||||
spring-framework-version:
|
||||
description: 'The version of Spring Framework that is being synced to Central'
|
||||
required: true
|
||||
ossrh-s01-token-username:
|
||||
description: 'Username for authentication with s01.oss.sonatype.org'
|
||||
required: true
|
||||
ossrh-s01-token-password:
|
||||
description: 'Password for authentication with s01.oss.sonatype.org'
|
||||
required: true
|
||||
ossrh-s01-staging-profile:
|
||||
description: 'Staging profile to use when syncing to Central'
|
||||
required: true
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Set Up JFrog CLI
|
||||
uses: jfrog/setup-jfrog-cli@105617d23456a69a92485207c4f28ae12297581d # v4.2.1
|
||||
env:
|
||||
JF_ENV_SPRING: ${{ inputs.jfrog-cli-config-token }}
|
||||
- name: Download Release Artifacts
|
||||
shell: bash
|
||||
run: jf rt download --spec ${{ format('{0}/artifacts.spec', github.action_path) }} --spec-vars 'buildName=${{ format('spring-framework-{0}', inputs.spring-framework-version) }};buildNumber=${{ github.run_number }}'
|
||||
- name: Sync
|
||||
uses: spring-io/nexus-sync-action@42477a2230a2f694f9eaa4643fa9e76b99b7ab84 # v0.0.1
|
||||
with:
|
||||
username: ${{ inputs.ossrh-s01-token-username }}
|
||||
password: ${{ inputs.ossrh-s01-token-password }}
|
||||
staging-profile-name: ${{ inputs.ossrh-s01-staging-profile }}
|
||||
create: true
|
||||
upload: true
|
||||
close: true
|
||||
release: true
|
||||
generate-checksums: true
|
||||
- name: Await
|
||||
uses: ./.github/actions/await-http-resource
|
||||
with:
|
||||
url: ${{ format('https://repo.maven.apache.org/maven2/org/springframework/spring-context/{0}/spring-context-{0}.jar', inputs.spring-framework-version) }}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"files": [
|
||||
{
|
||||
"aql": {
|
||||
"items.find": {
|
||||
"$and": [
|
||||
{
|
||||
"@build.name": "${buildName}",
|
||||
"@build.number": "${buildNumber}",
|
||||
"path": {
|
||||
"$nmatch": "org/springframework/spring-*.zip"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"target": "nexus/"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Build and deploy snapshot
|
||||
name: Build and Deploy Snapshot
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
@@ -7,43 +7,26 @@ 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
|
||||
timeout-minutes: 60
|
||||
if: ${{ github.repository == 'spring-projects/spring-framework' }}
|
||||
steps:
|
||||
- name: Set up Java
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'liberica'
|
||||
java-version: 8
|
||||
- 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-5.3.x'
|
||||
repository: 'libs-snapshot-local'
|
||||
folder: 'deployment-repository'
|
||||
signing-key: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||
@@ -53,11 +36,24 @@ jobs:
|
||||
/**/spring-*-docs.zip::zip.type=docs
|
||||
/**/spring-*-dist.zip::zip.type=dist
|
||||
/**/spring-*-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 8', github.ref_name) }}
|
||||
build-scan-url: ${{ steps.build-and-publish.outputs.build-scan-url }}
|
||||
run-name: ${{ format('{0} | Linux | Java 8', github.ref_name) }}
|
||||
outputs:
|
||||
version: ${{ steps.build-and-publish.outputs.version }}
|
||||
verify:
|
||||
name: Verify
|
||||
needs: build-and-deploy-snapshot
|
||||
uses: ./.github/workflows/verify.yml
|
||||
secrets:
|
||||
google-chat-webhook-url: ${{ secrets.GOOGLE_CHAT_WEBHOOK_URL }}
|
||||
repository-password: ${{ secrets.ARTIFACTORY_PASSWORD }}
|
||||
repository-username: ${{ secrets.ARTIFACTORY_USERNAME }}
|
||||
token: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
|
||||
with:
|
||||
version: ${{ needs.build-and-deploy-snapshot.outputs.version }}
|
||||
|
||||
+11
-36
@@ -7,6 +7,9 @@ concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
jobs:
|
||||
ci:
|
||||
name: '${{ matrix.os.name}} | Java ${{ matrix.java.version}}'
|
||||
runs-on: ${{ matrix.os.id }}
|
||||
timeout-minutes: 60
|
||||
if: ${{ github.repository == 'spring-projects/spring-framework' }}
|
||||
strategy:
|
||||
matrix:
|
||||
@@ -25,52 +28,24 @@ jobs:
|
||||
name: Linux
|
||||
java:
|
||||
version: 8
|
||||
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 && '8' || '' }}
|
||||
- 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
|
||||
- name: Send notification
|
||||
uses: ./.github/actions/build
|
||||
with:
|
||||
java-version: ${{ matrix.java.version }}
|
||||
java-early-access: ${{ matrix.java.early-access || 'false' }}
|
||||
java-toolchain: ${{ matrix.java.toolchain }}
|
||||
develocity-access-key: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }}
|
||||
- name: Send Notification
|
||||
uses: ./.github/actions/send-notification
|
||||
if: always()
|
||||
with:
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
name: Release
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v5.3.[0-9]+
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
jobs:
|
||||
build-and-stage-release:
|
||||
if: ${{ github.repository == 'spring-projects/spring-framework' }}
|
||||
name: Build and Stage Release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check Out Code
|
||||
uses: actions/checkout@v4
|
||||
- name: Build and Publish
|
||||
id: build-and-publish
|
||||
uses: ./.github/actions/build
|
||||
with:
|
||||
develocity-access-key: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }}
|
||||
publish: true
|
||||
- name: Stage Release
|
||||
uses: spring-io/artifactory-deploy-action@26bbe925a75f4f863e1e529e85be2d0093cac116 # v0.0.1
|
||||
with:
|
||||
uri: 'https://repo.spring.io'
|
||||
username: ${{ secrets.ARTIFACTORY_USERNAME }}
|
||||
password: ${{ secrets.ARTIFACTORY_PASSWORD }}
|
||||
build-name: ${{ format('spring-framework-{0}', steps.build-and-publish.outputs.version)}}
|
||||
repository: 'libs-staging-local'
|
||||
folder: 'deployment-repository'
|
||||
signing-key: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||
signing-passphrase: ${{ secrets.GPG_PASSPHRASE }}
|
||||
artifact-properties: |
|
||||
/**/spring-*.zip::zip.name=spring-framework,zip.deployed=false
|
||||
/**/spring-*-docs.zip::zip.type=docs
|
||||
/**/spring-*-dist.zip::zip.type=dist
|
||||
/**/spring-*-schema.zip::zip.type=schema
|
||||
outputs:
|
||||
version: ${{ steps.build-and-publish.outputs.version }}
|
||||
verify:
|
||||
name: Verify
|
||||
needs: build-and-stage-release
|
||||
uses: ./.github/workflows/verify.yml
|
||||
with:
|
||||
staging: true
|
||||
version: ${{ needs.build-and-stage-release.outputs.version }}
|
||||
secrets:
|
||||
google-chat-webhook-url: ${{ secrets.GOOGLE_CHAT_WEBHOOK_URL }}
|
||||
repository-password: ${{ secrets.ARTIFACTORY_PASSWORD }}
|
||||
repository-username: ${{ secrets.ARTIFACTORY_USERNAME }}
|
||||
token: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
|
||||
sync-to-maven-central:
|
||||
name: Sync to Maven Central
|
||||
needs:
|
||||
- build-and-stage-release
|
||||
- verify
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check Out Code
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- name: Sync to Maven Central
|
||||
uses: ./.github/actions/sync-to-maven-central
|
||||
with:
|
||||
jfrog-cli-config-token: ${{ secrets.JF_ARTIFACTORY_SPRING }}
|
||||
ossrh-s01-staging-profile: ${{ secrets.OSSRH_S01_STAGING_PROFILE }}
|
||||
ossrh-s01-token-password: ${{ secrets.OSSRH_S01_TOKEN_PASSWORD }}
|
||||
ossrh-s01-token-username: ${{ secrets.OSSRH_S01_TOKEN_USERNAME }}
|
||||
spring-framework-version: ${{ needs.build-and-stage-release.outputs.version }}
|
||||
promote-release:
|
||||
name: Promote Release
|
||||
needs:
|
||||
- build-and-stage-release
|
||||
- sync-to-maven-central
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Set up JFrog CLI
|
||||
uses: jfrog/setup-jfrog-cli@105617d23456a69a92485207c4f28ae12297581d # v4.2.1
|
||||
env:
|
||||
JF_ENV_SPRING: ${{ secrets.JF_ARTIFACTORY_SPRING }}
|
||||
- name: Promote build
|
||||
run: jfrog rt build-promote ${{ format('spring-framework-{0}', needs.build-and-stage-release.outputs.version)}} ${{ github.run_number }} libs-release-local
|
||||
create-github-release:
|
||||
name: Create GitHub Release
|
||||
needs:
|
||||
- build-and-stage-release
|
||||
- promote-release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check Out Code
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- name: Create GitHub Release
|
||||
uses: ./.github/actions/create-github-release
|
||||
with:
|
||||
milestone: ${{ needs.build-and-stage-release.outputs.version }}
|
||||
token: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
|
||||
+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@d9c87d481d55275bb5441eef3fe0e46805f9ef70 # v3.5.0
|
||||
@@ -0,0 +1,71 @@
|
||||
name: Verify
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
version:
|
||||
required: true
|
||||
type: string
|
||||
staging:
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
secrets:
|
||||
repository-username:
|
||||
required: false
|
||||
repository-password:
|
||||
required: false
|
||||
google-chat-webhook-url:
|
||||
required: true
|
||||
token:
|
||||
required: true
|
||||
jobs:
|
||||
verify:
|
||||
name: Verify
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check Out Release Verification Tests
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: spring-projects/spring-framework-release-verification
|
||||
ref: 'v0.0.2'
|
||||
token: ${{ secrets.token }}
|
||||
- name: Check Out Send Notification Action
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
path: spring-framework
|
||||
sparse-checkout: .github/actions/send-notification
|
||||
- name: Set Up Java
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'liberica'
|
||||
java-version: 8
|
||||
- name: Set Up Gradle
|
||||
uses: gradle/actions/setup-gradle@dbbdc275be76ac10734476cc723d82dfe7ec6eda # v3.4.2
|
||||
with:
|
||||
cache-read-only: false
|
||||
- name: Configure Gradle Properties
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir -p $HOME/.gradle
|
||||
echo 'org.gradle.daemon=false' >> $HOME/.gradle/gradle.properties
|
||||
- name: Run Release Verification Tests
|
||||
env:
|
||||
RVT_VERSION: ${{ inputs.version }}
|
||||
RVT_RELEASE_TYPE: oss
|
||||
RVT_STAGING: ${{ inputs.staging }}
|
||||
RVT_OSS_REPOSITORY_USERNAME: ${{ secrets.repository-username }}
|
||||
RVT_OSS_REPOSITORY_PASSWORD: ${{ secrets.repository-password }}
|
||||
run: ./gradlew spring-framework-release-verification-tests:test
|
||||
- name: Upload Build Reports on Failure
|
||||
uses: actions/upload-artifact@v4
|
||||
if: failure()
|
||||
with:
|
||||
name: build-reports
|
||||
path: '**/build/reports/'
|
||||
- name: Send Notification
|
||||
uses: ./spring-framework/.github/actions/send-notification
|
||||
if: failure()
|
||||
with:
|
||||
webhook-url: ${{ secrets.google-chat-webhook-url }}
|
||||
status: ${{ job.status }}
|
||||
run-name: ${{ format('{0} | Verification | {1}', github.ref_name, inputs.version) }}
|
||||
+2
-2
@@ -28,8 +28,8 @@ configure(allprojects) { project ->
|
||||
dependencyManagement {
|
||||
imports {
|
||||
mavenBom "com.fasterxml.jackson:jackson-bom:2.12.7"
|
||||
mavenBom "io.netty:netty-bom:4.1.111.Final"
|
||||
mavenBom "io.projectreactor:reactor-bom:2020.0.45"
|
||||
mavenBom "io.netty:netty-bom:4.1.112.Final"
|
||||
mavenBom "io.projectreactor:reactor-bom:2020.0.47"
|
||||
mavenBom "io.r2dbc:r2dbc-bom:Arabba-SR13"
|
||||
mavenBom "io.rsocket:rsocket-bom:1.1.3"
|
||||
mavenBom "org.eclipse.jetty:jetty-bom:9.4.54.v20240208"
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
== Spring Framework Concourse pipeline
|
||||
|
||||
NOTE: CI is being migrated to GitHub Actions.
|
||||
|
||||
The Spring Framework uses https://concourse-ci.org/[Concourse] for its CI build and other automated tasks.
|
||||
The Spring team has a dedicated Concourse instance available at https://ci.spring.io with a build pipeline
|
||||
for https://ci.spring.io/teams/spring-framework/pipelines/spring-framework-5.3.x[Spring Framework 5.3.x].
|
||||
|
||||
=== Setting up your development environment
|
||||
|
||||
If you're part of the Spring Framework project on GitHub, you can get access to CI management features.
|
||||
First, you need to go to https://ci.spring.io and install the client CLI for your platform (see bottom right of the screen).
|
||||
|
||||
You can then login with the instance using:
|
||||
|
||||
[source]
|
||||
----
|
||||
$ fly -t spring login -n spring-framework -c https://ci.spring.io
|
||||
----
|
||||
|
||||
Once logged in, you should get something like:
|
||||
|
||||
[source]
|
||||
----
|
||||
$ fly ts
|
||||
name url team expiry
|
||||
spring https://ci.spring.io spring-framework Wed, 25 Mar 2020 17:45:26 UTC
|
||||
----
|
||||
|
||||
=== Pipeline configuration and structure
|
||||
|
||||
The build pipelines are described in `pipeline.yml` file.
|
||||
|
||||
This file is listing Concourse resources, i.e. build inputs and outputs such as container images, artifact repositories, source repositories, notification services, etc.
|
||||
|
||||
It also describes jobs (a job is a sequence of inputs, tasks and outputs); jobs are organized by groups.
|
||||
|
||||
The `pipeline.yml` definition contains `((parameters))` which are loaded from the `parameters.yml` file or from our https://docs.cloudfoundry.org/credhub/[credhub instance].
|
||||
|
||||
You'll find in this folder the following resources:
|
||||
|
||||
* `pipeline.yml` the build pipeline
|
||||
* `parameters.yml` the build parameters used for the pipeline
|
||||
* `images/` holds the container images definitions used in this pipeline
|
||||
* `scripts/` holds the build scripts that ship within the CI container images
|
||||
* `tasks` contains the task definitions used in the main `pipeline.yml`
|
||||
|
||||
=== Updating the build pipeline
|
||||
|
||||
Updating files on the repository is not enough to update the build pipeline, as changes need to be applied.
|
||||
|
||||
The pipeline can be deployed using the following command:
|
||||
|
||||
[source]
|
||||
----
|
||||
$ fly -t spring set-pipeline -p spring-framework-5.3.x -c ci/pipeline.yml -l ci/parameters.yml
|
||||
----
|
||||
|
||||
NOTE: This assumes that you have credhub integration configured with the appropriate secrets.
|
||||
@@ -1,10 +0,0 @@
|
||||
logging:
|
||||
level:
|
||||
io.spring.concourse: DEBUG
|
||||
spring:
|
||||
main:
|
||||
banner-mode: off
|
||||
sonatype:
|
||||
exclude:
|
||||
- 'build-info\.json'
|
||||
- '.*\.zip'
|
||||
@@ -1,21 +0,0 @@
|
||||
== CI Images
|
||||
|
||||
These images are used by CI to run the actual builds.
|
||||
|
||||
To build the image locally run the following from this directory:
|
||||
|
||||
----
|
||||
$ docker build --no-cache -f <image-folder>/Dockerfile .
|
||||
----
|
||||
|
||||
For example
|
||||
|
||||
----
|
||||
$ docker build --no-cache -f spring-framework-ci-image/Dockerfile .
|
||||
----
|
||||
|
||||
To test run:
|
||||
|
||||
----
|
||||
$ docker run -it --entrypoint /bin/bash <SHA>
|
||||
----
|
||||
@@ -1,10 +0,0 @@
|
||||
FROM ubuntu:jammy-20240125
|
||||
|
||||
ADD setup.sh /setup.sh
|
||||
ADD get-jdk-url.sh /get-jdk-url.sh
|
||||
RUN ./setup.sh java8
|
||||
|
||||
ENV JAVA_HOME /opt/openjdk/java8
|
||||
ENV JDK17 /opt/openjdk/java17
|
||||
|
||||
ENV PATH $JAVA_HOME/bin:$PATH
|
||||
@@ -1,14 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
case "$1" in
|
||||
java8)
|
||||
echo "https://github.com/bell-sw/Liberica/releases/download/8u402%2B7/bellsoft-jdk8u402+7-linux-amd64.tar.gz"
|
||||
;;
|
||||
java17)
|
||||
echo "https://github.com/bell-sw/Liberica/releases/download/17.0.10%2B13/bellsoft-jdk17.0.10+13-linux-amd64.tar.gz"
|
||||
;;
|
||||
*)
|
||||
echo $"Unknown java version"
|
||||
exit 1
|
||||
esac
|
||||
@@ -1,40 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -ex
|
||||
|
||||
###########################################################
|
||||
# UTILS
|
||||
###########################################################
|
||||
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get update
|
||||
apt-get install --no-install-recommends -y tzdata ca-certificates net-tools libxml2-utils git curl libudev1 libxml2-utils iptables iproute2 jq fontconfig
|
||||
ln -fs /usr/share/zoneinfo/UTC /etc/localtime
|
||||
dpkg-reconfigure --frontend noninteractive tzdata
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
curl https://raw.githubusercontent.com/spring-io/concourse-java-scripts/v0.0.4/concourse-java.sh > /opt/concourse-java.sh
|
||||
|
||||
###########################################################
|
||||
# JAVA
|
||||
###########################################################
|
||||
|
||||
mkdir -p /opt/openjdk
|
||||
pushd /opt/openjdk > /dev/null
|
||||
for jdk in java8 java17
|
||||
do
|
||||
JDK_URL=$( /get-jdk-url.sh $jdk )
|
||||
mkdir $jdk
|
||||
pushd $jdk > /dev/null
|
||||
curl -L ${JDK_URL} | tar zx --strip-components=1
|
||||
test -f bin/java
|
||||
test -f bin/javac
|
||||
popd > /dev/null
|
||||
done
|
||||
popd
|
||||
|
||||
###########################################################
|
||||
# GRADLE ENTERPRISE
|
||||
###########################################################
|
||||
cd /
|
||||
mkdir ~/.gradle
|
||||
echo 'systemProp.user.name=concourse' > ~/.gradle/gradle.properties
|
||||
@@ -1,10 +0,0 @@
|
||||
github-repo: "https://github.com/spring-projects/spring-framework.git"
|
||||
github-repo-name: "spring-projects/spring-framework"
|
||||
sonatype-staging-profile: "org.springframework"
|
||||
docker-hub-organization: "springci"
|
||||
artifactory-server: "https://repo.spring.io"
|
||||
branch: "5.3.x"
|
||||
milestone: "5.3.x"
|
||||
build-name: "spring-framework"
|
||||
pipeline-name: "spring-framework"
|
||||
concourse-url: "https://ci.spring.io"
|
||||
-291
@@ -1,291 +0,0 @@
|
||||
anchors:
|
||||
git-repo-resource-source: &git-repo-resource-source
|
||||
uri: ((github-repo))
|
||||
username: ((github-username))
|
||||
password: ((github-ci-release-token))
|
||||
branch: ((branch))
|
||||
gradle-enterprise-task-params: &gradle-enterprise-task-params
|
||||
DEVELOCITY_ACCESS_KEY: ((gradle_enterprise_secret_access_key))
|
||||
sonatype-task-params: &sonatype-task-params
|
||||
SONATYPE_USERNAME: ((sonatype-username))
|
||||
SONATYPE_PASSWORD: ((sonatype-password))
|
||||
SONATYPE_URL: ((sonatype-url))
|
||||
SONATYPE_STAGING_PROFILE: ((sonatype-staging-profile))
|
||||
artifactory-task-params: &artifactory-task-params
|
||||
ARTIFACTORY_SERVER: ((artifactory-server))
|
||||
ARTIFACTORY_USERNAME: ((artifactory-username))
|
||||
ARTIFACTORY_PASSWORD: ((artifactory-password))
|
||||
build-project-task-params: &build-project-task-params
|
||||
BRANCH: ((branch))
|
||||
<<: *gradle-enterprise-task-params
|
||||
docker-resource-source: &docker-resource-source
|
||||
username: ((docker-hub-username))
|
||||
password: ((docker-hub-password))
|
||||
changelog-task-params: &changelog-task-params
|
||||
name: generated-changelog/tag
|
||||
tag: generated-changelog/tag
|
||||
body: generated-changelog/changelog.md
|
||||
github-task-params: &github-task-params
|
||||
GITHUB_USERNAME: ((github-username))
|
||||
GITHUB_TOKEN: ((github-ci-release-token))
|
||||
|
||||
resource_types:
|
||||
- name: registry-image
|
||||
type: registry-image
|
||||
source:
|
||||
<<: *docker-resource-source
|
||||
repository: concourse/registry-image-resource
|
||||
tag: 1.8.0
|
||||
- name: artifactory-resource
|
||||
type: registry-image
|
||||
source:
|
||||
<<: *docker-resource-source
|
||||
repository: springio/artifactory-resource
|
||||
tag: 0.0.18
|
||||
- name: github-release
|
||||
type: registry-image
|
||||
source:
|
||||
<<: *docker-resource-source
|
||||
repository: concourse/github-release-resource
|
||||
tag: 1.8.0
|
||||
- name: github-status-resource
|
||||
type: registry-image
|
||||
source:
|
||||
<<: *docker-resource-source
|
||||
repository: dpb587/github-status-resource
|
||||
tag: master
|
||||
resources:
|
||||
- name: git-repo
|
||||
type: git
|
||||
icon: github
|
||||
source:
|
||||
<<: *git-repo-resource-source
|
||||
- name: ci-images-git-repo
|
||||
type: git
|
||||
icon: github
|
||||
source:
|
||||
uri: ((github-repo))
|
||||
branch: ((branch))
|
||||
paths: ["ci/images/*"]
|
||||
- name: ci-image
|
||||
type: registry-image
|
||||
icon: docker
|
||||
source:
|
||||
<<: *docker-resource-source
|
||||
repository: ((docker-hub-organization))/spring-framework-ci
|
||||
tag: ((milestone))
|
||||
- name: artifactory-repo
|
||||
type: artifactory-resource
|
||||
icon: package-variant
|
||||
source:
|
||||
uri: ((artifactory-server))
|
||||
username: ((artifactory-username))
|
||||
password: ((artifactory-password))
|
||||
build_name: ((build-name))
|
||||
- name: github-pre-release
|
||||
type: github-release
|
||||
icon: briefcase-download-outline
|
||||
source:
|
||||
owner: spring-projects
|
||||
repository: spring-framework
|
||||
access_token: ((github-ci-release-token))
|
||||
pre_release: true
|
||||
release: false
|
||||
- name: github-release
|
||||
type: github-release
|
||||
icon: briefcase-download
|
||||
source:
|
||||
owner: spring-projects
|
||||
repository: spring-framework
|
||||
access_token: ((github-ci-release-token))
|
||||
pre_release: false
|
||||
jobs:
|
||||
- name: build-ci-images
|
||||
plan:
|
||||
- get: git-repo
|
||||
- get: ci-images-git-repo
|
||||
trigger: true
|
||||
- task: build-ci-image
|
||||
privileged: true
|
||||
file: git-repo/ci/tasks/build-ci-image.yml
|
||||
output_mapping:
|
||||
image: ci-image
|
||||
vars:
|
||||
ci-image-name: ci-image
|
||||
<<: *docker-resource-source
|
||||
- put: ci-image
|
||||
params:
|
||||
image: ci-image/image.tar
|
||||
- name: stage-milestone
|
||||
serial: true
|
||||
plan:
|
||||
- get: ci-image
|
||||
- get: git-repo
|
||||
trigger: false
|
||||
- task: stage
|
||||
image: ci-image
|
||||
file: git-repo/ci/tasks/stage-version.yml
|
||||
params:
|
||||
RELEASE_TYPE: M
|
||||
<<: *gradle-enterprise-task-params
|
||||
- put: artifactory-repo
|
||||
params: &artifactory-params
|
||||
signing_key: ((signing-key))
|
||||
signing_passphrase: ((signing-passphrase))
|
||||
repo: libs-staging-local
|
||||
folder: distribution-repository
|
||||
build_uri: "https://ci.spring.io/teams/${BUILD_TEAM_NAME}/pipelines/${BUILD_PIPELINE_NAME}/jobs/${BUILD_JOB_NAME}/builds/${BUILD_NAME}"
|
||||
build_number: "${BUILD_PIPELINE_NAME}-${BUILD_JOB_NAME}-${BUILD_NAME}"
|
||||
disable_checksum_uploads: true
|
||||
threads: 8
|
||||
artifact_set:
|
||||
- include:
|
||||
- "/**/spring-*.zip"
|
||||
properties:
|
||||
"zip.name": "spring-framework"
|
||||
"zip.displayname": "Spring Framework"
|
||||
"zip.deployed": "false"
|
||||
- include:
|
||||
- "/**/spring-*-docs.zip"
|
||||
properties:
|
||||
"zip.type": "docs"
|
||||
- include:
|
||||
- "/**/spring-*-dist.zip"
|
||||
properties:
|
||||
"zip.type": "dist"
|
||||
- include:
|
||||
- "/**/spring-*-schema.zip"
|
||||
properties:
|
||||
"zip.type": "schema"
|
||||
- put: git-repo
|
||||
params:
|
||||
repository: stage-git-repo
|
||||
- name: promote-milestone
|
||||
serial: true
|
||||
plan:
|
||||
- get: ci-image
|
||||
- get: git-repo
|
||||
trigger: false
|
||||
- get: artifactory-repo
|
||||
trigger: false
|
||||
passed: [stage-milestone]
|
||||
params:
|
||||
download_artifacts: false
|
||||
save_build_info: true
|
||||
- task: promote
|
||||
file: git-repo/ci/tasks/promote-version.yml
|
||||
params:
|
||||
RELEASE_TYPE: M
|
||||
<<: *artifactory-task-params
|
||||
- task: generate-changelog
|
||||
file: git-repo/ci/tasks/generate-changelog.yml
|
||||
params:
|
||||
RELEASE_TYPE: M
|
||||
<<: *github-task-params
|
||||
- put: github-pre-release
|
||||
params:
|
||||
<<: *changelog-task-params
|
||||
- name: stage-rc
|
||||
serial: true
|
||||
plan:
|
||||
- get: ci-image
|
||||
- get: git-repo
|
||||
trigger: false
|
||||
- task: stage
|
||||
image: ci-image
|
||||
file: git-repo/ci/tasks/stage-version.yml
|
||||
params:
|
||||
RELEASE_TYPE: RC
|
||||
<<: *gradle-enterprise-task-params
|
||||
- put: artifactory-repo
|
||||
params:
|
||||
<<: *artifactory-params
|
||||
- put: git-repo
|
||||
params:
|
||||
repository: stage-git-repo
|
||||
- name: promote-rc
|
||||
serial: true
|
||||
plan:
|
||||
- get: ci-image
|
||||
- get: git-repo
|
||||
trigger: false
|
||||
- get: artifactory-repo
|
||||
trigger: false
|
||||
passed: [stage-rc]
|
||||
params:
|
||||
download_artifacts: false
|
||||
save_build_info: true
|
||||
- task: promote
|
||||
file: git-repo/ci/tasks/promote-version.yml
|
||||
params:
|
||||
RELEASE_TYPE: RC
|
||||
<<: *artifactory-task-params
|
||||
- task: generate-changelog
|
||||
file: git-repo/ci/tasks/generate-changelog.yml
|
||||
params:
|
||||
RELEASE_TYPE: RC
|
||||
<<: *github-task-params
|
||||
- put: github-pre-release
|
||||
params:
|
||||
<<: *changelog-task-params
|
||||
- name: stage-release
|
||||
serial: true
|
||||
plan:
|
||||
- get: ci-image
|
||||
- get: git-repo
|
||||
trigger: false
|
||||
- task: stage
|
||||
image: ci-image
|
||||
file: git-repo/ci/tasks/stage-version.yml
|
||||
params:
|
||||
RELEASE_TYPE: RELEASE
|
||||
<<: *gradle-enterprise-task-params
|
||||
- put: artifactory-repo
|
||||
params:
|
||||
<<: *artifactory-params
|
||||
- put: git-repo
|
||||
params:
|
||||
repository: stage-git-repo
|
||||
- name: promote-release
|
||||
serial: true
|
||||
plan:
|
||||
- get: ci-image
|
||||
- get: git-repo
|
||||
trigger: false
|
||||
- get: artifactory-repo
|
||||
trigger: false
|
||||
passed: [stage-release]
|
||||
params:
|
||||
download_artifacts: true
|
||||
save_build_info: true
|
||||
- task: promote
|
||||
file: git-repo/ci/tasks/promote-version.yml
|
||||
params:
|
||||
RELEASE_TYPE: RELEASE
|
||||
<<: *artifactory-task-params
|
||||
<<: *sonatype-task-params
|
||||
- name: create-github-release
|
||||
serial: true
|
||||
plan:
|
||||
- get: ci-image
|
||||
- get: git-repo
|
||||
- get: artifactory-repo
|
||||
trigger: true
|
||||
passed: [promote-release]
|
||||
params:
|
||||
download_artifacts: false
|
||||
save_build_info: true
|
||||
- task: generate-changelog
|
||||
file: git-repo/ci/tasks/generate-changelog.yml
|
||||
params:
|
||||
RELEASE_TYPE: RELEASE
|
||||
<<: *github-task-params
|
||||
- put: github-release
|
||||
params:
|
||||
<<: *changelog-task-params
|
||||
|
||||
groups:
|
||||
- name: "releases"
|
||||
jobs: ["stage-milestone", "stage-rc", "stage-release", "promote-milestone", "promote-rc", "promote-release", "create-github-release"]
|
||||
- name: "ci-images"
|
||||
jobs: ["build-ci-images"]
|
||||
@@ -1,2 +0,0 @@
|
||||
source /opt/concourse-java.sh
|
||||
setup_symlinks
|
||||
@@ -1,12 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
CONFIG_DIR=git-repo/ci/config
|
||||
version=$( cat artifactory-repo/build-info.json | jq -r '.buildInfo.modules[0].id' | sed 's/.*:.*:\(.*\)/\1/' )
|
||||
|
||||
java -jar /github-changelog-generator.jar \
|
||||
--spring.config.location=${CONFIG_DIR}/changelog-generator.yml \
|
||||
${version} generated-changelog/changelog.md
|
||||
|
||||
echo ${version} > generated-changelog/version
|
||||
echo v${version} > generated-changelog/tag
|
||||
@@ -1,17 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
CONFIG_DIR=git-repo/ci/config
|
||||
|
||||
version=$( cat artifactory-repo/build-info.json | jq -r '.buildInfo.modules[0].id' | sed 's/.*:.*:\(.*\)/\1/' )
|
||||
export BUILD_INFO_LOCATION=$(pwd)/artifactory-repo/build-info.json
|
||||
|
||||
java -jar /concourse-release-scripts.jar \
|
||||
--spring.config.location=${CONFIG_DIR}/release-scripts.yml \
|
||||
publishToCentral $RELEASE_TYPE $BUILD_INFO_LOCATION artifactory-repo || { exit 1; }
|
||||
|
||||
java -jar /concourse-release-scripts.jar \
|
||||
--spring.config.location=${CONFIG_DIR}/release-scripts.yml \
|
||||
promote $RELEASE_TYPE $BUILD_INFO_LOCATION || { exit 1; }
|
||||
|
||||
echo "Promotion complete"
|
||||
echo $version > version/version
|
||||
@@ -1,50 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
source $(dirname $0)/common.sh
|
||||
repository=$(pwd)/distribution-repository
|
||||
|
||||
pushd git-repo > /dev/null
|
||||
git fetch --tags --all > /dev/null
|
||||
popd > /dev/null
|
||||
|
||||
git clone git-repo stage-git-repo > /dev/null
|
||||
|
||||
pushd stage-git-repo > /dev/null
|
||||
|
||||
snapshotVersion=$( awk -F '=' '$1 == "version" { print $2 }' gradle.properties )
|
||||
if [[ $RELEASE_TYPE = "M" ]]; then
|
||||
stageVersion=$( get_next_milestone_release $snapshotVersion)
|
||||
nextVersion=$snapshotVersion
|
||||
elif [[ $RELEASE_TYPE = "RC" ]]; then
|
||||
stageVersion=$( get_next_rc_release $snapshotVersion)
|
||||
nextVersion=$snapshotVersion
|
||||
elif [[ $RELEASE_TYPE = "RELEASE" ]]; then
|
||||
stageVersion=$( get_next_release $snapshotVersion)
|
||||
nextVersion=$( bump_version_number $snapshotVersion)
|
||||
else
|
||||
echo "Unknown release type $RELEASE_TYPE" >&2; exit 1;
|
||||
fi
|
||||
|
||||
echo "Staging $stageVersion (next version will be $nextVersion)"
|
||||
sed -i "s/version=$snapshotVersion/version=$stageVersion/" gradle.properties
|
||||
|
||||
git config user.name "Spring Builds" > /dev/null
|
||||
git config user.email "spring-builds@users.noreply.github.com" > /dev/null
|
||||
git add gradle.properties > /dev/null
|
||||
git commit -m"Release v$stageVersion" > /dev/null
|
||||
git tag -a "v$stageVersion" -m"Release v$stageVersion" > /dev/null
|
||||
|
||||
./gradlew --no-daemon --max-workers=4 -PdeploymentRepository=${repository} build publishAllPublicationsToDeploymentRepository
|
||||
|
||||
git reset --hard HEAD^ > /dev/null
|
||||
if [[ $nextVersion != $snapshotVersion ]]; then
|
||||
echo "Setting next development version (v$nextVersion)"
|
||||
sed -i "s/version=$snapshotVersion/version=$nextVersion/" gradle.properties
|
||||
git add gradle.properties > /dev/null
|
||||
git commit -m"Next development version (v$nextVersion)" > /dev/null
|
||||
fi;
|
||||
|
||||
echo "Staging Complete"
|
||||
|
||||
popd > /dev/null
|
||||
@@ -1,30 +0,0 @@
|
||||
---
|
||||
platform: linux
|
||||
image_resource:
|
||||
type: registry-image
|
||||
source:
|
||||
repository: concourse/oci-build-task
|
||||
tag: 0.10.0
|
||||
username: ((docker-hub-username))
|
||||
password: ((docker-hub-password))
|
||||
inputs:
|
||||
- name: ci-images-git-repo
|
||||
outputs:
|
||||
- name: image
|
||||
caches:
|
||||
- path: ci-image-cache
|
||||
params:
|
||||
CONTEXT: ci-images-git-repo/ci/images
|
||||
DOCKERFILE: ci-images-git-repo/ci/images/ci-image/Dockerfile
|
||||
DOCKER_HUB_AUTH: ((docker-hub-auth))
|
||||
run:
|
||||
path: /bin/sh
|
||||
args:
|
||||
- "-c"
|
||||
- |
|
||||
mkdir -p /root/.docker
|
||||
cat > /root/.docker/config.json <<EOF
|
||||
{ "auths": { "https://index.docker.io/v1/": { "auth": "$DOCKER_HUB_AUTH" }}}
|
||||
EOF
|
||||
build
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
---
|
||||
platform: linux
|
||||
image_resource:
|
||||
type: registry-image
|
||||
source:
|
||||
repository: springio/github-changelog-generator
|
||||
tag: '0.0.8'
|
||||
username: ((docker-hub-username))
|
||||
password: ((docker-hub-password))
|
||||
inputs:
|
||||
- name: git-repo
|
||||
- name: artifactory-repo
|
||||
outputs:
|
||||
- name: generated-changelog
|
||||
params:
|
||||
GITHUB_ORGANIZATION:
|
||||
GITHUB_REPO:
|
||||
GITHUB_USERNAME:
|
||||
GITHUB_TOKEN:
|
||||
RELEASE_TYPE:
|
||||
run:
|
||||
path: git-repo/ci/scripts/generate-changelog.sh
|
||||
@@ -1,25 +0,0 @@
|
||||
---
|
||||
platform: linux
|
||||
image_resource:
|
||||
type: registry-image
|
||||
source:
|
||||
repository: springio/concourse-release-scripts
|
||||
tag: '0.4.0'
|
||||
username: ((docker-hub-username))
|
||||
password: ((docker-hub-password))
|
||||
inputs:
|
||||
- name: git-repo
|
||||
- name: artifactory-repo
|
||||
outputs:
|
||||
- name: version
|
||||
params:
|
||||
RELEASE_TYPE:
|
||||
ARTIFACTORY_SERVER:
|
||||
ARTIFACTORY_USERNAME:
|
||||
ARTIFACTORY_PASSWORD:
|
||||
SONATYPE_USER:
|
||||
SONATYPE_PASSWORD:
|
||||
SONATYPE_URL:
|
||||
SONATYPE_STAGING_PROFILE:
|
||||
run:
|
||||
path: git-repo/ci/scripts/promote-version.sh
|
||||
@@ -1,17 +0,0 @@
|
||||
---
|
||||
platform: linux
|
||||
inputs:
|
||||
- name: git-repo
|
||||
outputs:
|
||||
- name: stage-git-repo
|
||||
- name: distribution-repository
|
||||
params:
|
||||
RELEASE_TYPE:
|
||||
CI: true
|
||||
GRADLE_ENTERPRISE_CACHE_USERNAME:
|
||||
GRADLE_ENTERPRISE_CACHE_PASSWORD:
|
||||
GRADLE_ENTERPRISE_URL: https://ge.spring.io
|
||||
caches:
|
||||
- path: gradle
|
||||
run:
|
||||
path: git-repo/ci/scripts/stage-version.sh
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
version=5.3.37-SNAPSHOT
|
||||
version=5.3.39
|
||||
org.gradle.jvmargs=-Xmx2048m
|
||||
org.gradle.caching=true
|
||||
org.gradle.parallel=true
|
||||
|
||||
+2
-1
@@ -549,7 +549,8 @@ public class AspectJExpressionPointcut extends AbstractExpressionPointcut
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
Class<?> superclass = clazz.getSuperclass();
|
||||
return (superclass != null && compiledByAjc(superclass));
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -239,7 +239,7 @@ public interface BeanFactory {
|
||||
* specific type, specify the actual bean type as an argument here and subsequently
|
||||
* use {@link ObjectProvider#orderedStream()} or its lazy streaming/iteration options.
|
||||
* <p>Also, generics matching is strict here, as per the Java assignment rules.
|
||||
* For lenient fallback matching with unchecked semantics (similar to the ´unchecked´
|
||||
* For lenient fallback matching with unchecked semantics (similar to the 'unchecked'
|
||||
* Java compiler warning), consider calling {@link #getBeanProvider(Class)} with the
|
||||
* raw type as a second step if no full generic match is
|
||||
* {@link ObjectProvider#getIfAvailable() available} with this variant.
|
||||
|
||||
+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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+32
-29
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -31,79 +31,82 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException
|
||||
* @author Chris Beams
|
||||
* @author Juergen Hoeller
|
||||
*/
|
||||
public class FileEditorTests {
|
||||
class FileEditorTests {
|
||||
|
||||
@Test
|
||||
public void testClasspathFileName() throws Exception {
|
||||
void testClasspathFileName() {
|
||||
PropertyEditor fileEditor = new FileEditor();
|
||||
fileEditor.setAsText("classpath:" + ClassUtils.classPackageAsResourcePath(getClass()) + "/" +
|
||||
ClassUtils.getShortName(getClass()) + ".class");
|
||||
Object value = fileEditor.getValue();
|
||||
boolean condition = value instanceof File;
|
||||
assertThat(condition).isTrue();
|
||||
assertThat(value).isInstanceOf(File.class);
|
||||
File file = (File) value;
|
||||
assertThat(file.exists()).isTrue();
|
||||
assertThat(file).exists();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWithNonExistentResource() throws Exception {
|
||||
PropertyEditor propertyEditor = new FileEditor();
|
||||
void testWithNonExistentResource() {
|
||||
PropertyEditor fileEditor = new FileEditor();
|
||||
assertThatIllegalArgumentException().isThrownBy(() ->
|
||||
propertyEditor.setAsText("classpath:no_way_this_file_is_found.doc"));
|
||||
fileEditor.setAsText("classpath:no_way_this_file_is_found.doc"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWithNonExistentFile() throws Exception {
|
||||
void testWithNonExistentFile() {
|
||||
PropertyEditor fileEditor = new FileEditor();
|
||||
fileEditor.setAsText("file:no_way_this_file_is_found.doc");
|
||||
Object value = fileEditor.getValue();
|
||||
boolean condition1 = value instanceof File;
|
||||
assertThat(condition1).isTrue();
|
||||
assertThat(value).isInstanceOf(File.class);
|
||||
File file = (File) value;
|
||||
boolean condition = !file.exists();
|
||||
assertThat(condition).isTrue();
|
||||
assertThat(file).doesNotExist();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAbsoluteFileName() throws Exception {
|
||||
void testAbsoluteFileName() {
|
||||
PropertyEditor fileEditor = new FileEditor();
|
||||
fileEditor.setAsText("/no_way_this_file_is_found.doc");
|
||||
Object value = fileEditor.getValue();
|
||||
boolean condition1 = value instanceof File;
|
||||
assertThat(condition1).isTrue();
|
||||
assertThat(value).isInstanceOf(File.class);
|
||||
File file = (File) value;
|
||||
boolean condition = !file.exists();
|
||||
assertThat(condition).isTrue();
|
||||
assertThat(file).doesNotExist();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUnqualifiedFileNameFound() throws Exception {
|
||||
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();
|
||||
String fileName = ClassUtils.classPackageAsResourcePath(getClass()) + "/" +
|
||||
ClassUtils.getShortName(getClass()) + ".class";
|
||||
fileEditor.setAsText(fileName);
|
||||
Object value = fileEditor.getValue();
|
||||
boolean condition = value instanceof File;
|
||||
assertThat(condition).isTrue();
|
||||
assertThat(value).isInstanceOf(File.class);
|
||||
File file = (File) value;
|
||||
assertThat(file.exists()).isTrue();
|
||||
assertThat(file).exists();
|
||||
String absolutePath = file.getAbsolutePath().replace('\\', '/');
|
||||
assertThat(absolutePath.endsWith(fileName)).isTrue();
|
||||
assertThat(absolutePath).endsWith(fileName);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUnqualifiedFileNameNotFound() throws Exception {
|
||||
void testUnqualifiedFileNameNotFound() {
|
||||
PropertyEditor fileEditor = new FileEditor();
|
||||
String fileName = ClassUtils.classPackageAsResourcePath(getClass()) + "/" +
|
||||
ClassUtils.getShortName(getClass()) + ".clazz";
|
||||
fileEditor.setAsText(fileName);
|
||||
Object value = fileEditor.getValue();
|
||||
boolean condition = value instanceof File;
|
||||
assertThat(condition).isTrue();
|
||||
assertThat(value).isInstanceOf(File.class);
|
||||
File file = (File) value;
|
||||
assertThat(file.exists()).isFalse();
|
||||
assertThat(file).doesNotExist();
|
||||
String absolutePath = file.getAbsolutePath().replace('\\', '/');
|
||||
assertThat(absolutePath.endsWith(fileName)).isTrue();
|
||||
assertThat(absolutePath).endsWith(fileName);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+39
-28
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -19,6 +19,7 @@ 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;
|
||||
|
||||
@@ -31,65 +32,65 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException
|
||||
* @author Juergen Hoeller
|
||||
* @since 4.3.2
|
||||
*/
|
||||
public class PathEditorTests {
|
||||
class PathEditorTests {
|
||||
|
||||
@Test
|
||||
public void testClasspathPathName() {
|
||||
void testClasspathPathName() {
|
||||
PropertyEditor pathEditor = new PathEditor();
|
||||
pathEditor.setAsText("classpath:" + ClassUtils.classPackageAsResourcePath(getClass()) + "/" +
|
||||
ClassUtils.getShortName(getClass()) + ".class");
|
||||
Object value = pathEditor.getValue();
|
||||
assertThat(value instanceof Path).isTrue();
|
||||
assertThat(value).isInstanceOf(Path.class);
|
||||
Path path = (Path) value;
|
||||
assertThat(path.toFile().exists()).isTrue();
|
||||
assertThat(path.toFile()).exists();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWithNonExistentResource() {
|
||||
PropertyEditor propertyEditor = new PathEditor();
|
||||
void testWithNonExistentResource() {
|
||||
PropertyEditor pathEditor = new PathEditor();
|
||||
assertThatIllegalArgumentException().isThrownBy(() ->
|
||||
propertyEditor.setAsText("classpath:/no_way_this_file_is_found.doc"));
|
||||
pathEditor.setAsText("classpath:/no_way_this_file_is_found.doc"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWithNonExistentPath() {
|
||||
void testWithNonExistentPath() {
|
||||
PropertyEditor pathEditor = new PathEditor();
|
||||
pathEditor.setAsText("file:/no_way_this_file_is_found.doc");
|
||||
Object value = pathEditor.getValue();
|
||||
assertThat(value instanceof Path).isTrue();
|
||||
assertThat(value).isInstanceOf(Path.class);
|
||||
Path path = (Path) value;
|
||||
assertThat(!path.toFile().exists()).isTrue();
|
||||
assertThat(path.toFile()).doesNotExist();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAbsolutePath() {
|
||||
void testAbsolutePath() {
|
||||
PropertyEditor pathEditor = new PathEditor();
|
||||
pathEditor.setAsText("/no_way_this_file_is_found.doc");
|
||||
Object value = pathEditor.getValue();
|
||||
assertThat(value instanceof Path).isTrue();
|
||||
assertThat(value).isInstanceOf(Path.class);
|
||||
Path path = (Path) value;
|
||||
assertThat(!path.toFile().exists()).isTrue();
|
||||
assertThat(path.toFile()).doesNotExist();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWindowsAbsolutePath() {
|
||||
void testWindowsAbsolutePath() {
|
||||
PropertyEditor pathEditor = new PathEditor();
|
||||
pathEditor.setAsText("C:\\no_way_this_file_is_found.doc");
|
||||
Object value = pathEditor.getValue();
|
||||
assertThat(value instanceof Path).isTrue();
|
||||
assertThat(value).isInstanceOf(Path.class);
|
||||
Path path = (Path) value;
|
||||
assertThat(!path.toFile().exists()).isTrue();
|
||||
assertThat(path.toFile()).doesNotExist();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWindowsAbsoluteFilePath() {
|
||||
void testWindowsAbsoluteFilePath() {
|
||||
PropertyEditor pathEditor = new PathEditor();
|
||||
try {
|
||||
pathEditor.setAsText("file://C:\\no_way_this_file_is_found.doc");
|
||||
Object value = pathEditor.getValue();
|
||||
assertThat(value instanceof Path).isTrue();
|
||||
assertThat(value).isInstanceOf(Path.class);
|
||||
Path path = (Path) value;
|
||||
assertThat(!path.toFile().exists()).isTrue();
|
||||
assertThat(path.toFile()).doesNotExist();
|
||||
}
|
||||
catch (IllegalArgumentException ex) {
|
||||
if (File.separatorChar == '\\') { // on Windows, otherwise silently ignore
|
||||
@@ -99,39 +100,49 @@ public class PathEditorTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUnqualifiedPathNameFound() {
|
||||
void testCurrentDirectory() {
|
||||
PropertyEditor pathEditor = new PathEditor();
|
||||
pathEditor.setAsText("file:.");
|
||||
Object value = pathEditor.getValue();
|
||||
assertThat(value).isInstanceOf(Path.class);
|
||||
Path path = (Path) value;
|
||||
assertThat(path).isEqualTo(Paths.get("."));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testUnqualifiedPathNameFound() {
|
||||
PropertyEditor pathEditor = new PathEditor();
|
||||
String fileName = ClassUtils.classPackageAsResourcePath(getClass()) + "/" +
|
||||
ClassUtils.getShortName(getClass()) + ".class";
|
||||
pathEditor.setAsText(fileName);
|
||||
Object value = pathEditor.getValue();
|
||||
assertThat(value instanceof Path).isTrue();
|
||||
assertThat(value).isInstanceOf(Path.class);
|
||||
Path path = (Path) value;
|
||||
File file = path.toFile();
|
||||
assertThat(file.exists()).isTrue();
|
||||
assertThat(file).exists();
|
||||
String absolutePath = file.getAbsolutePath();
|
||||
if (File.separatorChar == '\\') {
|
||||
absolutePath = absolutePath.replace('\\', '/');
|
||||
}
|
||||
assertThat(absolutePath.endsWith(fileName)).isTrue();
|
||||
assertThat(absolutePath).endsWith(fileName);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUnqualifiedPathNameNotFound() {
|
||||
void testUnqualifiedPathNameNotFound() {
|
||||
PropertyEditor pathEditor = new PathEditor();
|
||||
String fileName = ClassUtils.classPackageAsResourcePath(getClass()) + "/" +
|
||||
ClassUtils.getShortName(getClass()) + ".clazz";
|
||||
pathEditor.setAsText(fileName);
|
||||
Object value = pathEditor.getValue();
|
||||
assertThat(value instanceof Path).isTrue();
|
||||
assertThat(value).isInstanceOf(Path.class);
|
||||
Path path = (Path) value;
|
||||
File file = path.toFile();
|
||||
assertThat(file.exists()).isFalse();
|
||||
assertThat(file).doesNotExist();
|
||||
String absolutePath = file.getAbsolutePath();
|
||||
if (File.separatorChar == '\\') {
|
||||
absolutePath = absolutePath.replace('\\', '/');
|
||||
}
|
||||
assertThat(absolutePath.endsWith(fileName)).isTrue();
|
||||
assertThat(absolutePath).endsWith(fileName);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2012 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -29,7 +29,7 @@ import org.springframework.context.ApplicationEvent;
|
||||
public abstract class ApplicationContextEvent extends ApplicationEvent {
|
||||
|
||||
/**
|
||||
* Create a new ContextStartedEvent.
|
||||
* Create a new {@code ApplicationContextEvent}.
|
||||
* @param source the {@code ApplicationContext} that the event is raised for
|
||||
* (must not be {@code null})
|
||||
*/
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2012 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -29,7 +29,7 @@ import org.springframework.context.ApplicationContext;
|
||||
public class ContextClosedEvent extends ApplicationContextEvent {
|
||||
|
||||
/**
|
||||
* Creates a new ContextClosedEvent.
|
||||
* Create a new {@code ContextClosedEvent}.
|
||||
* @param source the {@code ApplicationContext} that has been closed
|
||||
* (must not be {@code null})
|
||||
*/
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2012 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -29,7 +29,7 @@ import org.springframework.context.ApplicationContext;
|
||||
public class ContextRefreshedEvent extends ApplicationContextEvent {
|
||||
|
||||
/**
|
||||
* Create a new ContextRefreshedEvent.
|
||||
* Create a new {@code ContextRefreshedEvent}.
|
||||
* @param source the {@code ApplicationContext} that has been initialized
|
||||
* or refreshed (must not be {@code null})
|
||||
*/
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2012 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -30,7 +30,7 @@ import org.springframework.context.ApplicationContext;
|
||||
public class ContextStartedEvent extends ApplicationContextEvent {
|
||||
|
||||
/**
|
||||
* Create a new ContextStartedEvent.
|
||||
* Create a new {@code ContextStartedEvent}.
|
||||
* @param source the {@code ApplicationContext} that has been started
|
||||
* (must not be {@code null})
|
||||
*/
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2012 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -30,7 +30,7 @@ import org.springframework.context.ApplicationContext;
|
||||
public class ContextStoppedEvent extends ApplicationContextEvent {
|
||||
|
||||
/**
|
||||
* Create a new ContextStoppedEvent.
|
||||
* Create a new {@code ContextStoppedEvent}.
|
||||
* @param source the {@code ApplicationContext} that has been stopped
|
||||
* (must not be {@code null})
|
||||
*/
|
||||
|
||||
+44
-8
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -27,6 +27,7 @@ import javax.inject.Provider;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.beans.factory.ObjectFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
@@ -44,6 +45,7 @@ import org.springframework.context.support.GenericApplicationContext;
|
||||
import org.springframework.core.annotation.AliasFor;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@@ -92,8 +94,8 @@ class AutowiredConfigurationTests {
|
||||
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
|
||||
OptionalAutowiredMethodConfig.class);
|
||||
|
||||
assertThat(context.getBeansOfType(Colour.class).isEmpty()).isTrue();
|
||||
assertThat(context.getBean(TestBean.class).getName()).isEqualTo("");
|
||||
assertThat(context.getBeansOfType(Colour.class)).isEmpty();
|
||||
assertThat(context.getBean(TestBean.class).getName()).isEmpty();
|
||||
context.close();
|
||||
}
|
||||
|
||||
@@ -184,14 +186,22 @@ class AutowiredConfigurationTests {
|
||||
context.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testValueInjectionWithAccidentalAutowiredAnnotations() {
|
||||
AnnotationConfigApplicationContext context =
|
||||
new AnnotationConfigApplicationContext(ValueConfigWithAccidentalAutowiredAnnotations.class);
|
||||
doTestValueInjection(context);
|
||||
context.close();
|
||||
}
|
||||
|
||||
private void doTestValueInjection(BeanFactory context) {
|
||||
System.clearProperty("myProp");
|
||||
|
||||
TestBean testBean = context.getBean("testBean", TestBean.class);
|
||||
assertThat((Object) testBean.getName()).isNull();
|
||||
assertThat(testBean.getName()).isNull();
|
||||
|
||||
testBean = context.getBean("testBean2", TestBean.class);
|
||||
assertThat((Object) testBean.getName()).isNull();
|
||||
assertThat(testBean.getName()).isNull();
|
||||
|
||||
System.setProperty("myProp", "foo");
|
||||
|
||||
@@ -204,10 +214,10 @@ class AutowiredConfigurationTests {
|
||||
System.clearProperty("myProp");
|
||||
|
||||
testBean = context.getBean("testBean", TestBean.class);
|
||||
assertThat((Object) testBean.getName()).isNull();
|
||||
assertThat(testBean.getName()).isNull();
|
||||
|
||||
testBean = context.getBean("testBean2", TestBean.class);
|
||||
assertThat((Object) testBean.getName()).isNull();
|
||||
assertThat(testBean.getName()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -271,7 +281,7 @@ class AutowiredConfigurationTests {
|
||||
return new TestBean("");
|
||||
}
|
||||
else {
|
||||
return new TestBean(colour.get().toString() + "-" + colours.get().get(0).toString());
|
||||
return new TestBean(colour.get() + "-" + colours.get().get(0).toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -484,6 +494,32 @@ class AutowiredConfigurationTests {
|
||||
}
|
||||
|
||||
|
||||
@Configuration
|
||||
static class ValueConfigWithAccidentalAutowiredAnnotations implements InitializingBean {
|
||||
|
||||
boolean invoked;
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() {
|
||||
Assert.state(!invoked, "Factory method must not get invoked on startup");
|
||||
}
|
||||
|
||||
@Bean @Scope("prototype")
|
||||
@Autowired
|
||||
public TestBean testBean(@Value("#{systemProperties[myProp]}") Provider<String> name) {
|
||||
invoked = true;
|
||||
return new TestBean(name.get());
|
||||
}
|
||||
|
||||
@Bean @Scope("prototype")
|
||||
@Autowired
|
||||
public TestBean testBean2(@Value("#{systemProperties[myProp]}") Provider<String> name2) {
|
||||
invoked = true;
|
||||
return new TestBean(name2.get());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Configuration
|
||||
static class PropertiesConfig {
|
||||
|
||||
|
||||
+3
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2017 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -34,6 +34,7 @@ import org.springframework.util.ObjectUtils;
|
||||
*
|
||||
* @author Keith Donald
|
||||
* @author Phillip Webb
|
||||
* @author Sam Brannen
|
||||
* @since 3.0
|
||||
*/
|
||||
final class ArrayToArrayConverter implements ConditionalGenericConverter {
|
||||
@@ -64,7 +65,7 @@ final class ArrayToArrayConverter implements ConditionalGenericConverter {
|
||||
public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
|
||||
if (this.conversionService instanceof GenericConversionService) {
|
||||
TypeDescriptor targetElement = targetType.getElementTypeDescriptor();
|
||||
if (targetElement != null &&
|
||||
if (targetElement != null && targetType.getType().isInstance(source) &&
|
||||
((GenericConversionService) this.conversionService).canBypassConvert(
|
||||
sourceType.getElementTypeDescriptor(), targetElement)) {
|
||||
return source;
|
||||
|
||||
+21
-3
@@ -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.
|
||||
@@ -626,9 +626,27 @@ class DefaultConversionServiceTests {
|
||||
assertThat(result[2]).isEqualTo(3);
|
||||
}
|
||||
|
||||
@Test // gh-33212
|
||||
void convertIntArrayToObjectArray() {
|
||||
Object[] result = conversionService.convert(new int[] {1, 2}, Object[].class);
|
||||
assertThat(result).containsExactly(1, 2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void convertByteArrayToWrapperArray() {
|
||||
byte[] byteArray = new byte[] {1, 2, 3};
|
||||
void convertIntArrayToFloatArray() {
|
||||
Float[] result = conversionService.convert(new int[] {1, 2}, Float[].class);
|
||||
assertThat(result).containsExactly(1.0F, 2.0F);
|
||||
}
|
||||
|
||||
@Test
|
||||
void convertIntArrayToPrimitiveFloatArray() {
|
||||
float[] result = conversionService.convert(new int[] {1, 2}, float[].class);
|
||||
assertThat(result).containsExactly(1.0F, 2.0F);
|
||||
}
|
||||
|
||||
@Test
|
||||
void convertPrimitiveByteArrayToByteWrapperArray() {
|
||||
byte[] byteArray = {1, 2, 3};
|
||||
Byte[] converted = conversionService.convert(byteArray, Byte[].class);
|
||||
assertThat(converted).isEqualTo(new Byte[]{1, 2, 3});
|
||||
}
|
||||
|
||||
+15
-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.
|
||||
@@ -132,4 +132,18 @@ public interface EvaluationContext {
|
||||
@Nullable
|
||||
Object lookupVariable(String name);
|
||||
|
||||
/**
|
||||
* Determine if assignment is enabled within expressions evaluated by this evaluation
|
||||
* context.
|
||||
* <p>If this method returns {@code false}, the assignment ({@code =}), increment
|
||||
* ({@code ++}), and decrement ({@code --}) operators are disabled.
|
||||
* <p>By default, this method returns {@code true}. Concrete implementations may override
|
||||
* this <em>default</em> method to disable assignment.
|
||||
* @return {@code true} if assignment is enabled; {@code false} otherwise
|
||||
* @since 5.3.38
|
||||
*/
|
||||
default boolean isAssignmentEnabled() {
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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,8 @@ package org.springframework.expression.spel.ast;
|
||||
import org.springframework.expression.EvaluationException;
|
||||
import org.springframework.expression.TypedValue;
|
||||
import org.springframework.expression.spel.ExpressionState;
|
||||
import org.springframework.expression.spel.SpelEvaluationException;
|
||||
import org.springframework.expression.spel.SpelMessage;
|
||||
|
||||
/**
|
||||
* Represents assignment. An alternative to calling {@code setValue}
|
||||
@@ -39,6 +41,9 @@ public class Assign extends SpelNodeImpl {
|
||||
|
||||
@Override
|
||||
public TypedValue getValueInternal(ExpressionState state) throws EvaluationException {
|
||||
if (!state.getEvaluationContext().isAssignmentEnabled()) {
|
||||
throw new SpelEvaluationException(getStartPosition(), SpelMessage.NOT_ASSIGNABLE, toStringAST());
|
||||
}
|
||||
return this.children[0].setValueInternal(state, () -> this.children[1].getValueInternal(state));
|
||||
}
|
||||
|
||||
|
||||
+8
-1
@@ -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.
|
||||
@@ -268,6 +268,13 @@ public class ConstructorReference extends SpelNodeImpl {
|
||||
}
|
||||
|
||||
String type = (String) intendedArrayType;
|
||||
|
||||
if (state.getEvaluationContext().getConstructorResolvers().isEmpty()) {
|
||||
// No constructor resolver -> no array construction either (as of 5.3.38)
|
||||
throw new SpelEvaluationException(getStartPosition(), SpelMessage.CONSTRUCTOR_NOT_FOUND,
|
||||
type + "[]", "[]");
|
||||
}
|
||||
|
||||
Class<?> componentType;
|
||||
TypeCode arrayTypeCode = TypeCode.forName(type);
|
||||
if (arrayTypeCode == TypeCode.OBJECT) {
|
||||
|
||||
@@ -634,6 +634,8 @@ public class Indexer extends SpelNodeImpl {
|
||||
throw new SpelEvaluationException(getStartPosition(), ex,
|
||||
SpelMessage.EXCEPTION_DURING_PROPERTY_WRITE, this.name, ex.getMessage());
|
||||
}
|
||||
throw new SpelEvaluationException(getStartPosition(),
|
||||
SpelMessage.INDEXING_NOT_SUPPORTED_FOR_TYPE, this.targetObjectTypeDescriptor.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -34,6 +34,7 @@ import org.springframework.util.Assert;
|
||||
* @author Andy Clement
|
||||
* @author Juergen Hoeller
|
||||
* @author Giovanni Dall'Oglio Risso
|
||||
* @author Sam Brannen
|
||||
* @since 3.2
|
||||
*/
|
||||
public class OpDec extends Operator {
|
||||
@@ -50,6 +51,10 @@ public class OpDec extends Operator {
|
||||
|
||||
@Override
|
||||
public TypedValue getValueInternal(ExpressionState state) throws EvaluationException {
|
||||
if (!state.getEvaluationContext().isAssignmentEnabled()) {
|
||||
throw new SpelEvaluationException(getStartPosition(), SpelMessage.OPERAND_NOT_DECREMENTABLE, toStringAST());
|
||||
}
|
||||
|
||||
SpelNodeImpl operand = getLeftOperand();
|
||||
|
||||
// The operand is going to be read and then assigned to, we don't want to evaluate it twice.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -34,6 +34,7 @@ import org.springframework.util.Assert;
|
||||
* @author Andy Clement
|
||||
* @author Juergen Hoeller
|
||||
* @author Giovanni Dall'Oglio Risso
|
||||
* @author Sam Brannen
|
||||
* @since 3.2
|
||||
*/
|
||||
public class OpInc extends Operator {
|
||||
@@ -50,6 +51,10 @@ public class OpInc extends Operator {
|
||||
|
||||
@Override
|
||||
public TypedValue getValueInternal(ExpressionState state) throws EvaluationException {
|
||||
if (!state.getEvaluationContext().isAssignmentEnabled()) {
|
||||
throw new SpelEvaluationException(getStartPosition(), SpelMessage.OPERAND_NOT_INCREMENTABLE, toStringAST());
|
||||
}
|
||||
|
||||
SpelNodeImpl operand = getLeftOperand();
|
||||
ValueRef valueRef = operand.getValueRef(state);
|
||||
|
||||
@@ -104,7 +109,7 @@ public class OpInc extends Operator {
|
||||
}
|
||||
}
|
||||
|
||||
// set the name value
|
||||
// set the new value
|
||||
try {
|
||||
valueRef.setValue(newValue.getValue());
|
||||
}
|
||||
|
||||
+72
-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.
|
||||
@@ -51,25 +51,26 @@ import org.springframework.lang.Nullable;
|
||||
* SpEL language syntax, e.g. excluding references to Java types, constructors,
|
||||
* and bean references.
|
||||
*
|
||||
* <p>When creating a {@code SimpleEvaluationContext} you need to choose the
|
||||
* level of support that you need for property access in SpEL expressions:
|
||||
* <p>When creating a {@code SimpleEvaluationContext} you need to choose the level of
|
||||
* support that you need for data binding in SpEL expressions:
|
||||
* <ul>
|
||||
* <li>A custom {@code PropertyAccessor} (typically not reflection-based),
|
||||
* potentially combined with a {@link DataBindingPropertyAccessor}</li>
|
||||
* <li>Data binding properties for read-only access</li>
|
||||
* <li>Data binding properties for read and write</li>
|
||||
* <li>Data binding for read-only access</li>
|
||||
* <li>Data binding for read and write access</li>
|
||||
* <li>A custom {@code PropertyAccessor} (typically not reflection-based), potentially
|
||||
* combined with a {@link DataBindingPropertyAccessor}</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>Conveniently, {@link SimpleEvaluationContext#forReadOnlyDataBinding()}
|
||||
* enables read access to properties via {@link DataBindingPropertyAccessor};
|
||||
* same for {@link SimpleEvaluationContext#forReadWriteDataBinding()} when
|
||||
* write access is needed as well. Alternatively, configure custom accessors
|
||||
* via {@link SimpleEvaluationContext#forPropertyAccessors}, and potentially
|
||||
* <p>Conveniently, {@link SimpleEvaluationContext#forReadOnlyDataBinding()} enables
|
||||
* read-only access to properties via {@link DataBindingPropertyAccessor}. Similarly,
|
||||
* {@link SimpleEvaluationContext#forReadWriteDataBinding()} enables read and write access
|
||||
* to properties. Alternatively, configure custom accessors via
|
||||
* {@link SimpleEvaluationContext#forPropertyAccessors}, potentially
|
||||
* {@linkplain Builder#withAssignmentDisabled() disable assignment}, and optionally
|
||||
* activate method resolution and/or a type converter through the builder.
|
||||
*
|
||||
* <p>Note that {@code SimpleEvaluationContext} is typically not configured
|
||||
* with a default root object. Instead it is meant to be created once and
|
||||
* used repeatedly through {@code getValue} calls on a pre-compiled
|
||||
* used repeatedly through {@code getValue} calls on a predefined
|
||||
* {@link org.springframework.expression.Expression} with both an
|
||||
* {@code EvaluationContext} and a root object as arguments:
|
||||
* {@link org.springframework.expression.Expression#getValue(EvaluationContext, Object)}.
|
||||
@@ -81,9 +82,9 @@ import org.springframework.lang.Nullable;
|
||||
* @author Juergen Hoeller
|
||||
* @author Sam Brannen
|
||||
* @since 4.3.15
|
||||
* @see #forPropertyAccessors
|
||||
* @see #forReadOnlyDataBinding()
|
||||
* @see #forReadWriteDataBinding()
|
||||
* @see #forPropertyAccessors
|
||||
* @see StandardEvaluationContext
|
||||
* @see StandardTypeConverter
|
||||
* @see DataBindingPropertyAccessor
|
||||
@@ -109,14 +110,17 @@ public final class SimpleEvaluationContext implements EvaluationContext {
|
||||
|
||||
private final Map<String, Object> variables = new HashMap<>();
|
||||
|
||||
private final boolean assignmentEnabled;
|
||||
|
||||
|
||||
private SimpleEvaluationContext(List<PropertyAccessor> accessors, List<MethodResolver> resolvers,
|
||||
@Nullable TypeConverter converter, @Nullable TypedValue rootObject) {
|
||||
@Nullable TypeConverter converter, @Nullable TypedValue rootObject, boolean assignmentEnabled) {
|
||||
|
||||
this.propertyAccessors = accessors;
|
||||
this.methodResolvers = resolvers;
|
||||
this.typeConverter = (converter != null ? converter : new StandardTypeConverter());
|
||||
this.rootObject = (rootObject != null ? rootObject : TypedValue.NULL);
|
||||
this.assignmentEnabled = assignmentEnabled;
|
||||
}
|
||||
|
||||
|
||||
@@ -224,15 +228,35 @@ public final class SimpleEvaluationContext implements EvaluationContext {
|
||||
return this.variables.get(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if assignment is enabled within expressions evaluated by this evaluation
|
||||
* context.
|
||||
* <p>If this method returns {@code false}, the assignment ({@code =}), increment
|
||||
* ({@code ++}), and decrement ({@code --}) operators are disabled.
|
||||
* @return {@code true} if assignment is enabled; {@code false} otherwise
|
||||
* @since 5.3.38
|
||||
* @see #forReadOnlyDataBinding()
|
||||
* @see Builder#withAssignmentDisabled()
|
||||
*/
|
||||
@Override
|
||||
public boolean isAssignmentEnabled() {
|
||||
return this.assignmentEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@code SimpleEvaluationContext} for the specified {@link PropertyAccessor}
|
||||
* delegates: typically a custom {@code PropertyAccessor} specific to a use case
|
||||
* (e.g. attribute resolution in a custom data structure), potentially combined with
|
||||
* a {@link DataBindingPropertyAccessor} if property dereferences are needed as well.
|
||||
* delegates: typically a custom {@code PropertyAccessor} specific to a use case —
|
||||
* for example, for attribute resolution in a custom data structure — potentially
|
||||
* combined with a {@link DataBindingPropertyAccessor} if property dereferences are
|
||||
* needed as well.
|
||||
* <p>By default, assignment is enabled within expressions evaluated by the context
|
||||
* created via this factory method; however, assignment can be disabled via
|
||||
* {@link Builder#withAssignmentDisabled()}.
|
||||
* @param accessors the accessor delegates to use
|
||||
* @see DataBindingPropertyAccessor#forReadOnlyAccess()
|
||||
* @see DataBindingPropertyAccessor#forReadWriteAccess()
|
||||
* @see #isAssignmentEnabled()
|
||||
* @see Builder#withAssignmentDisabled()
|
||||
*/
|
||||
public static Builder forPropertyAccessors(PropertyAccessor... accessors) {
|
||||
for (PropertyAccessor accessor : accessors) {
|
||||
@@ -247,18 +271,28 @@ public final class SimpleEvaluationContext implements EvaluationContext {
|
||||
/**
|
||||
* Create a {@code SimpleEvaluationContext} for read-only access to
|
||||
* public properties via {@link DataBindingPropertyAccessor}.
|
||||
* <p>Assignment is disabled within expressions evaluated by the context created via
|
||||
* this factory method.
|
||||
* @see DataBindingPropertyAccessor#forReadOnlyAccess()
|
||||
* @see #forPropertyAccessors
|
||||
* @see #isAssignmentEnabled()
|
||||
* @see Builder#withAssignmentDisabled()
|
||||
*/
|
||||
public static Builder forReadOnlyDataBinding() {
|
||||
return new Builder(DataBindingPropertyAccessor.forReadOnlyAccess());
|
||||
return new Builder(DataBindingPropertyAccessor.forReadOnlyAccess()).withAssignmentDisabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@code SimpleEvaluationContext} for read-write access to
|
||||
* public properties via {@link DataBindingPropertyAccessor}.
|
||||
* <p>By default, assignment is enabled within expressions evaluated by the context
|
||||
* created via this factory method. Assignment can be disabled via
|
||||
* {@link Builder#withAssignmentDisabled()}; however, it is preferable to use
|
||||
* {@link #forReadOnlyDataBinding()} if you desire read-only access.
|
||||
* @see DataBindingPropertyAccessor#forReadWriteAccess()
|
||||
* @see #forPropertyAccessors
|
||||
* @see #isAssignmentEnabled()
|
||||
* @see Builder#withAssignmentDisabled()
|
||||
*/
|
||||
public static Builder forReadWriteDataBinding() {
|
||||
return new Builder(DataBindingPropertyAccessor.forReadWriteAccess());
|
||||
@@ -268,7 +302,7 @@ public final class SimpleEvaluationContext implements EvaluationContext {
|
||||
/**
|
||||
* Builder for {@code SimpleEvaluationContext}.
|
||||
*/
|
||||
public static class Builder {
|
||||
public static final class Builder {
|
||||
|
||||
private final List<PropertyAccessor> accessors;
|
||||
|
||||
@@ -280,10 +314,24 @@ public final class SimpleEvaluationContext implements EvaluationContext {
|
||||
@Nullable
|
||||
private TypedValue rootObject;
|
||||
|
||||
public Builder(PropertyAccessor... accessors) {
|
||||
private boolean assignmentEnabled = true;
|
||||
|
||||
|
||||
private Builder(PropertyAccessor... accessors) {
|
||||
this.accessors = Arrays.asList(accessors);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Disable assignment within expressions evaluated by this evaluation context.
|
||||
* @since 5.3.38
|
||||
* @see SimpleEvaluationContext#isAssignmentEnabled()
|
||||
*/
|
||||
public Builder withAssignmentDisabled() {
|
||||
this.assignmentEnabled = false;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the specified {@link MethodResolver} delegates for
|
||||
* a combination of property access and method resolution.
|
||||
@@ -315,7 +363,6 @@ public final class SimpleEvaluationContext implements EvaluationContext {
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Register a custom {@link ConversionService}.
|
||||
* <p>By default a {@link StandardTypeConverter} backed by a
|
||||
@@ -327,6 +374,7 @@ public final class SimpleEvaluationContext implements EvaluationContext {
|
||||
this.typeConverter = new StandardTypeConverter(conversionService);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a custom {@link TypeConverter}.
|
||||
* <p>By default a {@link StandardTypeConverter} backed by a
|
||||
@@ -362,7 +410,8 @@ public final class SimpleEvaluationContext implements EvaluationContext {
|
||||
}
|
||||
|
||||
public SimpleEvaluationContext build() {
|
||||
return new SimpleEvaluationContext(this.accessors, this.resolvers, this.typeConverter, this.rootObject);
|
||||
return new SimpleEvaluationContext(this.accessors, this.resolvers, this.typeConverter, this.rootObject,
|
||||
this.assignmentEnabled);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+16
-4
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -18,17 +18,21 @@ package org.springframework.expression.spel;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.expression.EvaluationContext;
|
||||
import org.springframework.expression.Expression;
|
||||
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
||||
import org.springframework.expression.spel.support.SimpleEvaluationContext;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
|
||||
/**
|
||||
* Test construction of arrays.
|
||||
*
|
||||
* @author Andy Clement
|
||||
* @author Sam Brannen
|
||||
* @author Juergen Hoeller
|
||||
*/
|
||||
class ArrayConstructorTests extends AbstractExpressionTests {
|
||||
|
||||
@@ -97,7 +101,7 @@ class ArrayConstructorTests extends AbstractExpressionTests {
|
||||
void typeArrayConstructors() {
|
||||
evaluate("new String[]{'a','b','c','d'}[1]", "b", String.class);
|
||||
evaluateAndCheckError("new String[]{'a','b','c','d'}.size()", SpelMessage.METHOD_NOT_FOUND, 30, "size()",
|
||||
"java.lang.String[]");
|
||||
"java.lang.String[]");
|
||||
evaluate("new String[]{'a','b','c','d'}.length", 4, Integer.class);
|
||||
}
|
||||
|
||||
@@ -110,10 +114,18 @@ class ArrayConstructorTests extends AbstractExpressionTests {
|
||||
void multiDimensionalArrays() {
|
||||
evaluate("new String[2][2]", "[Ljava.lang.String;[2]{[2]{null,null},[2]{null,null}}", String[][].class);
|
||||
evaluate("new String[3][2][1]",
|
||||
"[[Ljava.lang.String;[3]{[2]{[1]{null},[1]{null}},[2]{[1]{null},[1]{null}},[2]{[1]{null},[1]{null}}}",
|
||||
String[][][].class);
|
||||
"[[Ljava.lang.String;[3]{[2]{[1]{null},[1]{null}},[2]{[1]{null},[1]{null}},[2]{[1]{null},[1]{null}}}",
|
||||
String[][][].class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void noArrayConstruction() {
|
||||
EvaluationContext context = SimpleEvaluationContext.forReadWriteDataBinding().build();
|
||||
assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() ->
|
||||
parser.parseExpression("new int[2]").getValue(context));
|
||||
}
|
||||
|
||||
|
||||
private void evaluateArrayBuildingExpression(String expression, String expectedToString) {
|
||||
SpelExpressionParser parser = new SpelExpressionParser();
|
||||
Expression e = parser.parseExpression(expression);
|
||||
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.expression.spel;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.asm.MethodVisitor;
|
||||
import org.springframework.expression.AccessException;
|
||||
import org.springframework.expression.EvaluationContext;
|
||||
import org.springframework.expression.TypedValue;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* This is a local COPY of {@link org.springframework.context.expression.MapAccessor}.
|
||||
*
|
||||
* @author Juergen Hoeller
|
||||
* @author Andy Clement
|
||||
* @since 4.1
|
||||
*/
|
||||
public class CompilableMapAccessor implements CompilablePropertyAccessor {
|
||||
|
||||
@Override
|
||||
public Class<?>[] getSpecificTargetClasses() {
|
||||
return new Class<?>[] {Map.class};
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canRead(EvaluationContext context, @Nullable Object target, String name) throws AccessException {
|
||||
return (target instanceof Map && ((Map<?, ?>) target).containsKey(name));
|
||||
}
|
||||
|
||||
@Override
|
||||
public TypedValue read(EvaluationContext context, @Nullable Object target, String name) throws AccessException {
|
||||
Assert.state(target instanceof Map, "Target must be of type Map");
|
||||
Map<?, ?> map = (Map<?, ?>) target;
|
||||
Object value = map.get(name);
|
||||
if (value == null && !map.containsKey(name)) {
|
||||
throw new MapAccessException(name);
|
||||
}
|
||||
return new TypedValue(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canWrite(EvaluationContext context, @Nullable Object target, String name) throws AccessException {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public void write(EvaluationContext context, @Nullable Object target, String name, @Nullable Object newValue)
|
||||
throws AccessException {
|
||||
|
||||
Assert.state(target instanceof Map, "Target must be a Map");
|
||||
Map<Object, Object> map = (Map<Object, Object>) target;
|
||||
map.put(name, newValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCompilable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> getPropertyType() {
|
||||
return Object.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void generateCode(String propertyName, MethodVisitor mv, CodeFlow cf) {
|
||||
String descriptor = cf.lastDescriptor();
|
||||
if (descriptor == null || !descriptor.equals("Ljava/util/Map")) {
|
||||
if (descriptor == null) {
|
||||
cf.loadTarget(mv);
|
||||
}
|
||||
CodeFlow.insertCheckCast(mv, "Ljava/util/Map");
|
||||
}
|
||||
mv.visitLdcInsn(propertyName);
|
||||
mv.visitMethodInsn(INVOKEINTERFACE, "java/util/Map", "get","(Ljava/lang/Object;)Ljava/lang/Object;",true);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Exception thrown from {@code read} in order to reset a cached
|
||||
* PropertyAccessor, allowing other accessors to have a try.
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
private static class MapAccessException extends AccessException {
|
||||
|
||||
private final String key;
|
||||
|
||||
public MapAccessException(String key) {
|
||||
super("");
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMessage() {
|
||||
return "Map does not contain a value for key '" + this.key + "'";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+23
-13
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -21,6 +21,7 @@ import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.assertj.core.api.ThrowableTypeAssert;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.core.convert.TypeDescriptor;
|
||||
@@ -83,11 +84,11 @@ public class PropertyAccessTests extends AbstractExpressionTests {
|
||||
void accessingOnNullObject() {
|
||||
SpelExpression expr = (SpelExpression) parser.parseExpression("madeup");
|
||||
EvaluationContext context = new StandardEvaluationContext(null);
|
||||
assertThatExceptionOfType(SpelEvaluationException.class)
|
||||
assertThatSpelEvaluationException()
|
||||
.isThrownBy(() -> expr.getValue(context))
|
||||
.extracting(SpelEvaluationException::getMessageCode).isEqualTo(SpelMessage.PROPERTY_OR_FIELD_NOT_READABLE_ON_NULL);
|
||||
assertThat(expr.isWritable(context)).isFalse();
|
||||
assertThatExceptionOfType(SpelEvaluationException.class)
|
||||
assertThatSpelEvaluationException()
|
||||
.isThrownBy(() -> expr.setValue(context, "abc"))
|
||||
.extracting(SpelEvaluationException::getMessageCode).isEqualTo(SpelMessage.PROPERTY_OR_FIELD_NOT_WRITABLE_ON_NULL);
|
||||
}
|
||||
@@ -117,8 +118,7 @@ public class PropertyAccessTests extends AbstractExpressionTests {
|
||||
assertThat((int) i).isEqualTo(99);
|
||||
|
||||
// Cannot set it to a string value
|
||||
assertThatExceptionOfType(EvaluationException.class).isThrownBy(() ->
|
||||
flibbleexpr.setValue(ctx, "not allowed"));
|
||||
assertThatSpelEvaluationException().isThrownBy(() -> flibbleexpr.setValue(ctx, "not allowed"));
|
||||
// message will be: EL1063E:(pos 20): A problem occurred whilst attempting to set the property
|
||||
// 'flibbles': 'Cannot set flibbles to an object of type 'class java.lang.String''
|
||||
// System.out.println(e.getMessage());
|
||||
@@ -173,8 +173,7 @@ public class PropertyAccessTests extends AbstractExpressionTests {
|
||||
@Test
|
||||
void noGetClassAccess() {
|
||||
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
|
||||
assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() ->
|
||||
parser.parseExpression("'a'.class.name").getValue(context));
|
||||
assertThatSpelEvaluationException().isThrownBy(() -> parser.parseExpression("'a'.class.name").getValue(context));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -187,8 +186,13 @@ public class PropertyAccessTests extends AbstractExpressionTests {
|
||||
target.setName("p2");
|
||||
assertThat(expr.getValue(context, target)).isEqualTo("p2");
|
||||
|
||||
assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() ->
|
||||
parser.parseExpression("name='p3'").getValue(context, target));
|
||||
assertThatSpelEvaluationException()
|
||||
.isThrownBy(() -> parser.parseExpression("name='p3'").getValue(context, target))
|
||||
.extracting(SpelEvaluationException::getMessageCode).isEqualTo(SpelMessage.NOT_ASSIGNABLE);
|
||||
|
||||
assertThatSpelEvaluationException()
|
||||
.isThrownBy(() -> parser.parseExpression("['name']='p4'").getValue(context, target))
|
||||
.extracting(SpelEvaluationException::getMessageCode).isEqualTo(SpelMessage.NOT_ASSIGNABLE);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -201,8 +205,9 @@ public class PropertyAccessTests extends AbstractExpressionTests {
|
||||
RecordPerson target2 = new RecordPerson("p2");
|
||||
assertThat(expr.getValue(context, target2)).isEqualTo("p2");
|
||||
|
||||
assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() ->
|
||||
parser.parseExpression("name='p3'").getValue(context, target2));
|
||||
assertThatSpelEvaluationException()
|
||||
.isThrownBy(() -> parser.parseExpression("name='p3'").getValue(context, target2))
|
||||
.extracting(SpelEvaluationException::getMessageCode).isEqualTo(SpelMessage.NOT_ASSIGNABLE);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -248,7 +253,7 @@ public class PropertyAccessTests extends AbstractExpressionTests {
|
||||
void propertyAccessWithoutMethodResolver() {
|
||||
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
|
||||
Person target = new Person("p1");
|
||||
assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() ->
|
||||
assertThatSpelEvaluationException().isThrownBy(() ->
|
||||
parser.parseExpression("name.substring(1)").getValue(context, target));
|
||||
}
|
||||
|
||||
@@ -274,12 +279,17 @@ public class PropertyAccessTests extends AbstractExpressionTests {
|
||||
void propertyAccessWithArrayIndexOutOfBounds() {
|
||||
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
|
||||
Expression expression = parser.parseExpression("stringArrayOfThreeItems[3]");
|
||||
assertThatExceptionOfType(SpelEvaluationException.class)
|
||||
assertThatSpelEvaluationException()
|
||||
.isThrownBy(() -> expression.getValue(context, new Inventor()))
|
||||
.extracting(SpelEvaluationException::getMessageCode).isEqualTo(SpelMessage.ARRAY_INDEX_OUT_OF_BOUNDS);
|
||||
}
|
||||
|
||||
|
||||
private ThrowableTypeAssert<SpelEvaluationException> assertThatSpelEvaluationException() {
|
||||
return assertThatExceptionOfType(SpelEvaluationException.class);
|
||||
}
|
||||
|
||||
|
||||
// This can resolve the property 'flibbles' on any String (very useful...)
|
||||
private static class StringyPropertyAccessor implements PropertyAccessor {
|
||||
|
||||
|
||||
-78
@@ -5796,84 +5796,6 @@ public class SpelCompilationCoverageTests extends AbstractExpressionTests {
|
||||
}
|
||||
|
||||
|
||||
static class CompilableMapAccessor implements CompilablePropertyAccessor {
|
||||
|
||||
@Override
|
||||
public boolean canRead(EvaluationContext context, Object target, String name) throws AccessException {
|
||||
Map<?,?> map = (Map<?,?>) target;
|
||||
return map.containsKey(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TypedValue read(EvaluationContext context, Object target, String name) throws AccessException {
|
||||
Map<?,?> map = (Map<?,?>) target;
|
||||
Object value = map.get(name);
|
||||
if (value == null && !map.containsKey(name)) {
|
||||
throw new MapAccessException(name);
|
||||
}
|
||||
return new TypedValue(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canWrite(EvaluationContext context, Object target, String name) throws AccessException {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public void write(EvaluationContext context, Object target, String name, Object newValue) throws AccessException {
|
||||
Map<String, Object> map = (Map<String, Object>) target;
|
||||
map.put(name, newValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?>[] getSpecificTargetClasses() {
|
||||
return new Class<?>[] {Map.class};
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCompilable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> getPropertyType() {
|
||||
return Object.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void generateCode(String propertyName, MethodVisitor mv, CodeFlow cf) {
|
||||
String descriptor = cf.lastDescriptor();
|
||||
if (descriptor == null) {
|
||||
cf.loadTarget(mv);
|
||||
}
|
||||
mv.visitLdcInsn(propertyName);
|
||||
mv.visitMethodInsn(INVOKEINTERFACE, "java/util/Map", "get","(Ljava/lang/Object;)Ljava/lang/Object;",true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Exception thrown from {@code read} in order to reset a cached
|
||||
* PropertyAccessor, allowing other accessors to have a try.
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
private static class MapAccessException extends AccessException {
|
||||
|
||||
private final String key;
|
||||
|
||||
public MapAccessException(String key) {
|
||||
super(null);
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMessage() {
|
||||
return "Map does not contain a value for key '" + this.key + "'";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static class Greeter {
|
||||
|
||||
public String getWorld() {
|
||||
|
||||
+530
@@ -0,0 +1,530 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.expression.spel.support;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.assertj.core.api.ThrowableTypeAssert;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.expression.Expression;
|
||||
import org.springframework.expression.spel.CompilableMapAccessor;
|
||||
import org.springframework.expression.spel.SpelEvaluationException;
|
||||
import org.springframework.expression.spel.SpelMessage;
|
||||
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.assertj.core.api.Assertions.entry;
|
||||
|
||||
/**
|
||||
* Tests for {@link SimpleEvaluationContext}.
|
||||
*
|
||||
* <p>Some of the use cases in this test class are duplicated elsewhere within the test
|
||||
* suite; however, we include them here to consistently focus on related features in this
|
||||
* test class.
|
||||
*
|
||||
* @author Sam Brannen
|
||||
*/
|
||||
class SimpleEvaluationContextTests {
|
||||
|
||||
private final SpelExpressionParser parser = new SpelExpressionParser();
|
||||
|
||||
private final Model model = new Model();
|
||||
|
||||
|
||||
@Test
|
||||
void forReadWriteDataBinding() {
|
||||
SimpleEvaluationContext context = SimpleEvaluationContext.forReadWriteDataBinding().build();
|
||||
|
||||
assertReadWriteMode(context);
|
||||
}
|
||||
|
||||
@Test
|
||||
void forReadOnlyDataBinding() {
|
||||
SimpleEvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
|
||||
|
||||
assertCommonReadOnlyModeBehavior(context);
|
||||
|
||||
// WRITE -- via assignment operator
|
||||
|
||||
// Variable
|
||||
assertAssignmentDisabled(context, "#myVar = 'rejected'");
|
||||
|
||||
// Property
|
||||
assertAssignmentDisabled(context, "name = 'rejected'");
|
||||
assertIncrementDisabled(context, "count++");
|
||||
assertIncrementDisabled(context, "++count");
|
||||
assertDecrementDisabled(context, "count--");
|
||||
assertDecrementDisabled(context, "--count");
|
||||
|
||||
// Array Index
|
||||
assertAssignmentDisabled(context, "array[0] = 'rejected'");
|
||||
assertIncrementDisabled(context, "numbers[0]++");
|
||||
assertIncrementDisabled(context, "++numbers[0]");
|
||||
assertDecrementDisabled(context, "numbers[0]--");
|
||||
assertDecrementDisabled(context, "--numbers[0]");
|
||||
|
||||
// List Index
|
||||
assertAssignmentDisabled(context, "list[0] = 'rejected'");
|
||||
|
||||
// Map Index -- key as String
|
||||
assertAssignmentDisabled(context, "map['red'] = 'rejected'");
|
||||
|
||||
// Map Index -- key as pseudo property name
|
||||
assertAssignmentDisabled(context, "map[yellow] = 'rejected'");
|
||||
|
||||
// String Index
|
||||
assertAssignmentDisabled(context, "name[0] = 'rejected'");
|
||||
|
||||
// Object Index
|
||||
assertAssignmentDisabled(context, "['name'] = 'rejected'");
|
||||
}
|
||||
|
||||
@Test
|
||||
void forPropertyAccessorsInReadWriteMode() {
|
||||
SimpleEvaluationContext context = SimpleEvaluationContext
|
||||
.forPropertyAccessors(new CompilableMapAccessor(), DataBindingPropertyAccessor.forReadWriteAccess())
|
||||
.build();
|
||||
|
||||
assertReadWriteMode(context);
|
||||
|
||||
// Map -- with key as property name supported by CompilableMapAccessor
|
||||
|
||||
Expression expression;
|
||||
expression = parser.parseExpression("map.yellow");
|
||||
expression.setValue(context, model, "pineapple");
|
||||
assertThat(expression.getValue(context, model, String.class)).isEqualTo("pineapple");
|
||||
|
||||
expression = parser.parseExpression("map.yellow = 'banana'");
|
||||
assertThat(expression.getValue(context, model, String.class)).isEqualTo("banana");
|
||||
expression = parser.parseExpression("map.yellow");
|
||||
assertThat(expression.getValue(context, model, String.class)).isEqualTo("banana");
|
||||
}
|
||||
|
||||
/**
|
||||
* We call this "mixed" read-only mode, because write access via PropertyAccessors is
|
||||
* disabled, but write access via the Indexer is not disabled.
|
||||
*/
|
||||
@Test
|
||||
void forPropertyAccessorsInMixedReadOnlyMode() {
|
||||
SimpleEvaluationContext context = SimpleEvaluationContext
|
||||
.forPropertyAccessors(new CompilableMapAccessor(), DataBindingPropertyAccessor.forReadOnlyAccess())
|
||||
.build();
|
||||
|
||||
assertCommonReadOnlyModeBehavior(context);
|
||||
|
||||
// Map -- with key as property name supported by CompilableMapAccessor
|
||||
|
||||
Expression expression;
|
||||
expression = parser.parseExpression("map.yellow");
|
||||
expression.setValue(context, model, "pineapple");
|
||||
assertThat(expression.getValue(context, model, String.class)).isEqualTo("pineapple");
|
||||
|
||||
expression = parser.parseExpression("map.yellow = 'banana'");
|
||||
assertThat(expression.getValue(context, model, String.class)).isEqualTo("banana");
|
||||
expression = parser.parseExpression("map.yellow");
|
||||
assertThat(expression.getValue(context, model, String.class)).isEqualTo("banana");
|
||||
|
||||
// WRITE -- via assignment operator
|
||||
|
||||
// Variable
|
||||
assertThatSpelEvaluationException()
|
||||
.isThrownBy(() -> parser.parseExpression("#myVar = 'rejected'").getValue(context, model))
|
||||
.satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.VARIABLE_ASSIGNMENT_NOT_SUPPORTED));
|
||||
|
||||
// Property
|
||||
assertThatSpelEvaluationException()
|
||||
.isThrownBy(() -> parser.parseExpression("name = 'rejected'").getValue(context, model))
|
||||
.satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.PROPERTY_OR_FIELD_NOT_WRITABLE));
|
||||
|
||||
// Array Index
|
||||
parser.parseExpression("array[0]").setValue(context, model, "foo");
|
||||
assertThat(model.array).containsExactly("foo");
|
||||
|
||||
// List Index
|
||||
parser.parseExpression("list[0]").setValue(context, model, "cat");
|
||||
assertThat(model.list).containsExactly("cat");
|
||||
|
||||
// Map Index -- key as String
|
||||
parser.parseExpression("map['red']").setValue(context, model, "cherry");
|
||||
assertThat(model.map).containsOnly(entry("red", "cherry"), entry("yellow", "banana"));
|
||||
|
||||
// Map Index -- key as pseudo property name
|
||||
parser.parseExpression("map[yellow]").setValue(context, model, "lemon");
|
||||
assertThat(model.map).containsOnly(entry("red", "cherry"), entry("yellow", "lemon"));
|
||||
|
||||
// String Index
|
||||
// The Indexer does not support writes when indexing into a String.
|
||||
assertThatSpelEvaluationException()
|
||||
.isThrownBy(() -> parser.parseExpression("name[0] = 'rejected'").getValue(context, model))
|
||||
.satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.INDEXING_NOT_SUPPORTED_FOR_TYPE));
|
||||
|
||||
// Object Index
|
||||
assertThatSpelEvaluationException()
|
||||
.isThrownBy(() -> parser.parseExpression("['name'] = 'rejected'").getValue(context, model))
|
||||
.satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.INDEXING_NOT_SUPPORTED_FOR_TYPE));
|
||||
|
||||
// WRITE -- via increment and decrement operators
|
||||
|
||||
assertIncrementAndDecrementWritesForIndexedStructures(context);
|
||||
}
|
||||
|
||||
@Test
|
||||
void forPropertyAccessorsWithAssignmentDisabled() {
|
||||
SimpleEvaluationContext context = SimpleEvaluationContext
|
||||
.forPropertyAccessors(new CompilableMapAccessor(), DataBindingPropertyAccessor.forReadOnlyAccess())
|
||||
.withAssignmentDisabled()
|
||||
.build();
|
||||
|
||||
assertCommonReadOnlyModeBehavior(context);
|
||||
|
||||
// Map -- with key as property name supported by CompilableMapAccessor
|
||||
|
||||
Expression expression;
|
||||
expression = parser.parseExpression("map.yellow");
|
||||
// setValue() is supported even though assignment is not.
|
||||
expression.setValue(context, model, "pineapple");
|
||||
assertThat(expression.getValue(context, model, String.class)).isEqualTo("pineapple");
|
||||
|
||||
// WRITE -- via assignment operator
|
||||
|
||||
// Variable
|
||||
assertAssignmentDisabled(context, "#myVar = 'rejected'");
|
||||
|
||||
// Property
|
||||
assertAssignmentDisabled(context, "name = 'rejected'");
|
||||
assertAssignmentDisabled(context, "map.yellow = 'rejected'");
|
||||
assertIncrementDisabled(context, "count++");
|
||||
assertIncrementDisabled(context, "++count");
|
||||
assertDecrementDisabled(context, "count--");
|
||||
assertDecrementDisabled(context, "--count");
|
||||
|
||||
// Array Index
|
||||
assertAssignmentDisabled(context, "array[0] = 'rejected'");
|
||||
assertIncrementDisabled(context, "numbers[0]++");
|
||||
assertIncrementDisabled(context, "++numbers[0]");
|
||||
assertDecrementDisabled(context, "numbers[0]--");
|
||||
assertDecrementDisabled(context, "--numbers[0]");
|
||||
|
||||
// List Index
|
||||
assertAssignmentDisabled(context, "list[0] = 'rejected'");
|
||||
|
||||
// Map Index -- key as String
|
||||
assertAssignmentDisabled(context, "map['red'] = 'rejected'");
|
||||
|
||||
// Map Index -- key as pseudo property name
|
||||
assertAssignmentDisabled(context, "map[yellow] = 'rejected'");
|
||||
|
||||
// String Index
|
||||
assertAssignmentDisabled(context, "name[0] = 'rejected'");
|
||||
|
||||
// Object Index
|
||||
assertAssignmentDisabled(context, "['name'] = 'rejected'");
|
||||
}
|
||||
|
||||
|
||||
private void assertReadWriteMode(SimpleEvaluationContext context) {
|
||||
// Variables can always be set programmatically within an EvaluationContext.
|
||||
context.setVariable("myVar", "enigma");
|
||||
|
||||
// WRITE -- via setValue()
|
||||
|
||||
// Property
|
||||
parser.parseExpression("name").setValue(context, model, "test");
|
||||
assertThat(model.name).isEqualTo("test");
|
||||
parser.parseExpression("count").setValue(context, model, 42);
|
||||
assertThat(model.count).isEqualTo(42);
|
||||
|
||||
// Array Index
|
||||
parser.parseExpression("array[0]").setValue(context, model, "foo");
|
||||
assertThat(model.array).containsExactly("foo");
|
||||
|
||||
// List Index
|
||||
parser.parseExpression("list[0]").setValue(context, model, "cat");
|
||||
assertThat(model.list).containsExactly("cat");
|
||||
|
||||
// Map Index -- key as String
|
||||
parser.parseExpression("map['red']").setValue(context, model, "cherry");
|
||||
assertThat(model.map).containsOnly(entry("red", "cherry"), entry("yellow", "replace me"));
|
||||
|
||||
// Map Index -- key as pseudo property name
|
||||
parser.parseExpression("map[yellow]").setValue(context, model, "lemon");
|
||||
assertThat(model.map).containsOnly(entry("red", "cherry"), entry("yellow", "lemon"));
|
||||
|
||||
// READ
|
||||
assertReadAccess(context);
|
||||
|
||||
// WRITE -- via assignment operator
|
||||
|
||||
// Variable assignment is always disabled in a SimpleEvaluationContext.
|
||||
assertThatSpelEvaluationException()
|
||||
.isThrownBy(() -> parser.parseExpression("#myVar = 'rejected'").getValue(context, model))
|
||||
.satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.VARIABLE_ASSIGNMENT_NOT_SUPPORTED));
|
||||
|
||||
Expression expression;
|
||||
|
||||
// Property
|
||||
expression = parser.parseExpression("name = 'changed'");
|
||||
assertThat(expression.getValue(context, model, String.class)).isEqualTo("changed");
|
||||
expression = parser.parseExpression("name");
|
||||
assertThat(expression.getValue(context, model, String.class)).isEqualTo("changed");
|
||||
|
||||
// Array Index
|
||||
expression = parser.parseExpression("array[0] = 'bar'");
|
||||
assertThat(expression.getValue(context, model, String.class)).isEqualTo("bar");
|
||||
expression = parser.parseExpression("array[0]");
|
||||
assertThat(expression.getValue(context, model, String.class)).isEqualTo("bar");
|
||||
|
||||
// List Index
|
||||
expression = parser.parseExpression("list[0] = 'dog'");
|
||||
assertThat(expression.getValue(context, model, String.class)).isEqualTo("dog");
|
||||
expression = parser.parseExpression("list[0]");
|
||||
assertThat(expression.getValue(context, model, String.class)).isEqualTo("dog");
|
||||
|
||||
// Map Index -- key as String
|
||||
expression = parser.parseExpression("map['red'] = 'strawberry'");
|
||||
assertThat(expression.getValue(context, model, String.class)).isEqualTo("strawberry");
|
||||
expression = parser.parseExpression("map['red']");
|
||||
assertThat(expression.getValue(context, model, String.class)).isEqualTo("strawberry");
|
||||
|
||||
// Map Index -- key as pseudo property name
|
||||
expression = parser.parseExpression("map[yellow] = 'banana'");
|
||||
assertThat(expression.getValue(context, model, String.class)).isEqualTo("banana");
|
||||
expression = parser.parseExpression("map[yellow]");
|
||||
assertThat(expression.getValue(context, model, String.class)).isEqualTo("banana");
|
||||
|
||||
// String Index
|
||||
// The Indexer does not support writes when indexing into a String.
|
||||
assertThatSpelEvaluationException()
|
||||
.isThrownBy(() -> parser.parseExpression("name[0] = 'rejected'").getValue(context, model))
|
||||
.satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.INDEXING_NOT_SUPPORTED_FOR_TYPE));
|
||||
|
||||
// Object Index
|
||||
expression = parser.parseExpression("['name'] = 'new name'");
|
||||
assertThat(expression.getValue(context, model, String.class)).isEqualTo("new name");
|
||||
expression = parser.parseExpression("['name']");
|
||||
assertThat(expression.getValue(context, model, String.class)).isEqualTo("new name");
|
||||
|
||||
// WRITE -- via increment and decrement operators
|
||||
|
||||
assertIncrementAndDecrementWritesForProperties(context);
|
||||
assertIncrementAndDecrementWritesForIndexedStructures(context);
|
||||
}
|
||||
|
||||
private void assertCommonReadOnlyModeBehavior(SimpleEvaluationContext context) {
|
||||
// Variables can always be set programmatically within an EvaluationContext.
|
||||
context.setVariable("myVar", "enigma");
|
||||
|
||||
// WRITE -- via setValue()
|
||||
|
||||
// Note: forReadOnlyDataBinding() disables programmatic writes via setValue() for
|
||||
// properties but allows programmatic writes via setValue() for indexed structures.
|
||||
|
||||
// Property
|
||||
assertThatSpelEvaluationException()
|
||||
.isThrownBy(() -> parser.parseExpression("name").setValue(context, model, "test"))
|
||||
.satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.PROPERTY_OR_FIELD_NOT_WRITABLE));
|
||||
assertThatSpelEvaluationException()
|
||||
.isThrownBy(() -> parser.parseExpression("count").setValue(context, model, 42))
|
||||
.satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.PROPERTY_OR_FIELD_NOT_WRITABLE));
|
||||
|
||||
// Array Index
|
||||
parser.parseExpression("array[0]").setValue(context, model, "foo");
|
||||
assertThat(model.array).containsExactly("foo");
|
||||
|
||||
// List Index
|
||||
parser.parseExpression("list[0]").setValue(context, model, "cat");
|
||||
assertThat(model.list).containsExactly("cat");
|
||||
|
||||
// Map Index -- key as String
|
||||
parser.parseExpression("map['red']").setValue(context, model, "cherry");
|
||||
assertThat(model.map).containsOnly(entry("red", "cherry"), entry("yellow", "replace me"));
|
||||
|
||||
// Map Index -- key as pseudo property name
|
||||
parser.parseExpression("map[yellow]").setValue(context, model, "lemon");
|
||||
assertThat(model.map).containsOnly(entry("red", "cherry"), entry("yellow", "lemon"));
|
||||
|
||||
// Since the setValue() attempts for "name" and "count" failed above, we have to set
|
||||
// them directly for assertReadAccess().
|
||||
model.name = "test";
|
||||
model.count = 42;
|
||||
|
||||
// READ
|
||||
assertReadAccess(context);
|
||||
}
|
||||
|
||||
private void assertReadAccess(SimpleEvaluationContext context) {
|
||||
Expression expression;
|
||||
|
||||
// Variable
|
||||
expression = parser.parseExpression("#myVar");
|
||||
assertThat(expression.getValue(context, model, String.class)).isEqualTo("enigma");
|
||||
|
||||
// Property
|
||||
expression = parser.parseExpression("name");
|
||||
assertThat(expression.getValue(context, model, String.class)).isEqualTo("test");
|
||||
expression = parser.parseExpression("count");
|
||||
assertThat(expression.getValue(context, model, Integer.class)).isEqualTo(42);
|
||||
|
||||
// Array Index
|
||||
expression = parser.parseExpression("array[0]");
|
||||
assertThat(expression.getValue(context, model, String.class)).isEqualTo("foo");
|
||||
|
||||
// List Index
|
||||
expression = parser.parseExpression("list[0]");
|
||||
assertThat(expression.getValue(context, model, String.class)).isEqualTo("cat");
|
||||
|
||||
// Map Index -- key as String
|
||||
expression = parser.parseExpression("map['red']");
|
||||
assertThat(expression.getValue(context, model, String.class)).isEqualTo("cherry");
|
||||
|
||||
// Map Index -- key as pseudo property name
|
||||
expression = parser.parseExpression("map[yellow]");
|
||||
assertThat(expression.getValue(context, model, String.class)).isEqualTo("lemon");
|
||||
|
||||
// String Index
|
||||
expression = parser.parseExpression("name[0]");
|
||||
assertThat(expression.getValue(context, model, String.class)).isEqualTo("t");
|
||||
|
||||
// Object Index
|
||||
expression = parser.parseExpression("['name']");
|
||||
assertThat(expression.getValue(context, model, String.class)).isEqualTo("test");
|
||||
}
|
||||
|
||||
private void assertIncrementAndDecrementWritesForProperties(SimpleEvaluationContext context) {
|
||||
Expression expression;
|
||||
expression = parser.parseExpression("count++");
|
||||
assertThat(expression.getValue(context, model, Integer.class)).isEqualTo(42);
|
||||
expression = parser.parseExpression("count");
|
||||
assertThat(expression.getValue(context, model, Integer.class)).isEqualTo(43);
|
||||
|
||||
expression = parser.parseExpression("++count");
|
||||
assertThat(expression.getValue(context, model, Integer.class)).isEqualTo(44);
|
||||
expression = parser.parseExpression("count");
|
||||
assertThat(expression.getValue(context, model, Integer.class)).isEqualTo(44);
|
||||
|
||||
expression = parser.parseExpression("count--");
|
||||
assertThat(expression.getValue(context, model, Integer.class)).isEqualTo(44);
|
||||
expression = parser.parseExpression("count");
|
||||
assertThat(expression.getValue(context, model, Integer.class)).isEqualTo(43);
|
||||
|
||||
expression = parser.parseExpression("--count");
|
||||
assertThat(expression.getValue(context, model, Integer.class)).isEqualTo(42);
|
||||
expression = parser.parseExpression("count");
|
||||
assertThat(expression.getValue(context, model, Integer.class)).isEqualTo(42);
|
||||
}
|
||||
|
||||
private void assertIncrementAndDecrementWritesForIndexedStructures(SimpleEvaluationContext context) {
|
||||
Expression expression;
|
||||
expression = parser.parseExpression("numbers[0]++");
|
||||
assertThat(expression.getValue(context, model, Integer.class)).isEqualTo(99);
|
||||
expression = parser.parseExpression("numbers[0]");
|
||||
assertThat(expression.getValue(context, model, Integer.class)).isEqualTo(100);
|
||||
|
||||
expression = parser.parseExpression("++numbers[0]");
|
||||
assertThat(expression.getValue(context, model, Integer.class)).isEqualTo(101);
|
||||
expression = parser.parseExpression("numbers[0]");
|
||||
assertThat(expression.getValue(context, model, Integer.class)).isEqualTo(101);
|
||||
|
||||
expression = parser.parseExpression("numbers[0]--");
|
||||
assertThat(expression.getValue(context, model, Integer.class)).isEqualTo(101);
|
||||
expression = parser.parseExpression("numbers[0]");
|
||||
assertThat(expression.getValue(context, model, Integer.class)).isEqualTo(100);
|
||||
|
||||
expression = parser.parseExpression("--numbers[0]");
|
||||
assertThat(expression.getValue(context, model, Integer.class)).isEqualTo(99);
|
||||
expression = parser.parseExpression("numbers[0]");
|
||||
assertThat(expression.getValue(context, model, Integer.class)).isEqualTo(99);
|
||||
}
|
||||
|
||||
private ThrowableTypeAssert<SpelEvaluationException> assertThatSpelEvaluationException() {
|
||||
return assertThatExceptionOfType(SpelEvaluationException.class);
|
||||
}
|
||||
|
||||
private void assertAssignmentDisabled(SimpleEvaluationContext context, String expression) {
|
||||
assertEvaluationException(context, expression, SpelMessage.NOT_ASSIGNABLE);
|
||||
}
|
||||
|
||||
private void assertIncrementDisabled(SimpleEvaluationContext context, String expression) {
|
||||
assertEvaluationException(context, expression, SpelMessage.OPERAND_NOT_INCREMENTABLE);
|
||||
}
|
||||
|
||||
private void assertDecrementDisabled(SimpleEvaluationContext context, String expression) {
|
||||
assertEvaluationException(context, expression, SpelMessage.OPERAND_NOT_DECREMENTABLE);
|
||||
}
|
||||
|
||||
private void assertEvaluationException(SimpleEvaluationContext context, String expression, SpelMessage spelMessage) {
|
||||
assertThatSpelEvaluationException()
|
||||
.isThrownBy(() -> parser.parseExpression(expression).getValue(context, model))
|
||||
.satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(spelMessage));
|
||||
}
|
||||
|
||||
|
||||
static class Model {
|
||||
|
||||
private String name = "replace me";
|
||||
private int count = 0;
|
||||
private final String[] array = {"replace me"};
|
||||
private final int[] numbers = {99};
|
||||
private final List<String> list = new ArrayList<>();
|
||||
private final Map<String, String> map = new HashMap<>();
|
||||
|
||||
Model() {
|
||||
this.list.add("replace me");
|
||||
this.map.put("red", "replace me");
|
||||
this.map.put("yellow", "replace me");
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public int getCount() {
|
||||
return this.count;
|
||||
}
|
||||
|
||||
public void setCount(int count) {
|
||||
this.count = count;
|
||||
}
|
||||
|
||||
public String[] getArray() {
|
||||
return this.array;
|
||||
}
|
||||
|
||||
public int[] getNumbers() {
|
||||
return this.numbers;
|
||||
}
|
||||
|
||||
public List<String> getList() {
|
||||
return this.list;
|
||||
}
|
||||
|
||||
public Map<String, String> getMap() {
|
||||
return this.map;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.http;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Represents an ETag for HTTP conditional requests.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.3.38
|
||||
* @see <a href="https://datatracker.ietf.org/doc/html/rfc7232">RFC 7232</a>
|
||||
*/
|
||||
public class ETag {
|
||||
|
||||
private static final Log logger = LogFactory.getLog(ETag.class);
|
||||
|
||||
private static final ETag WILDCARD = new ETag("*", false);
|
||||
|
||||
|
||||
private final String tag;
|
||||
|
||||
private final boolean weak;
|
||||
|
||||
|
||||
public ETag(String tag, boolean weak) {
|
||||
this.tag = tag;
|
||||
this.weak = weak;
|
||||
}
|
||||
|
||||
|
||||
public String tag() {
|
||||
return this.tag;
|
||||
}
|
||||
|
||||
public boolean weak() {
|
||||
return this.weak;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this a wildcard tag matching to any entity tag value.
|
||||
*/
|
||||
public boolean isWildcard() {
|
||||
return (this == WILDCARD);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the fully formatted tag including "W/" prefix and quotes.
|
||||
*/
|
||||
public String formattedTag() {
|
||||
if (this == WILDCARD) {
|
||||
return "*";
|
||||
}
|
||||
return (this.weak ? "W/" : "") + "\"" + this.tag + "\"";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return formattedTag();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Parse entity tags from an "If-Match" or "If-None-Match" header.
|
||||
* @param source the source string to parse
|
||||
* @return the parsed ETags
|
||||
*/
|
||||
public static List<ETag> parse(String source) {
|
||||
|
||||
List<ETag> result = new ArrayList<>();
|
||||
State state = State.BEFORE_QUOTES;
|
||||
int startIndex = -1;
|
||||
boolean weak = false;
|
||||
|
||||
for (int i = 0; i < source.length(); i++) {
|
||||
char c = source.charAt(i);
|
||||
|
||||
if (state == State.IN_QUOTES) {
|
||||
if (c == '"') {
|
||||
String tag = source.substring(startIndex, i);
|
||||
if (StringUtils.hasText(tag)) {
|
||||
result.add(new ETag(tag, weak));
|
||||
}
|
||||
state = State.AFTER_QUOTES;
|
||||
startIndex = -1;
|
||||
weak = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Character.isWhitespace(c)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c == ',') {
|
||||
state = State.BEFORE_QUOTES;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (state == State.BEFORE_QUOTES) {
|
||||
if (c == '*') {
|
||||
result.add(WILDCARD);
|
||||
state = State.AFTER_QUOTES;
|
||||
continue;
|
||||
}
|
||||
if (c == '"') {
|
||||
state = State.IN_QUOTES;
|
||||
startIndex = i + 1;
|
||||
continue;
|
||||
}
|
||||
if (c == 'W' && source.length() > i + 2) {
|
||||
if (source.charAt(i + 1) == '/' && source.charAt(i + 2) == '"') {
|
||||
state = State.IN_QUOTES;
|
||||
i = i + 2;
|
||||
startIndex = i + 1;
|
||||
weak = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Unexpected char at index " + i);
|
||||
}
|
||||
}
|
||||
|
||||
if (state != State.IN_QUOTES && logger.isDebugEnabled()) {
|
||||
logger.debug("Expected closing '\"'");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
private enum State {
|
||||
|
||||
BEFORE_QUOTES, IN_QUOTES, AFTER_QUOTES
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -40,8 +40,6 @@ import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.StringJoiner;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
@@ -393,12 +391,6 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable
|
||||
*/
|
||||
public static final HttpHeaders EMPTY = new ReadOnlyHttpHeaders(new LinkedMultiValueMap<>());
|
||||
|
||||
/**
|
||||
* Pattern matching ETag multiple field values in headers such as "If-Match", "If-None-Match".
|
||||
* @see <a href="https://tools.ietf.org/html/rfc7232#section-2.3">Section 2.3 of RFC 7232</a>
|
||||
*/
|
||||
private static final Pattern ETAG_HEADER_VALUE_PATTERN = Pattern.compile("\\*|\\s*((W\\/)?(\"[^\"]*\"))\\s*,?");
|
||||
|
||||
private static final DecimalFormatSymbols DECIMAL_FORMAT_SYMBOLS = new DecimalFormatSymbols(Locale.ENGLISH);
|
||||
|
||||
private static final ZoneId GMT = ZoneId.of("GMT");
|
||||
@@ -1037,9 +1029,8 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable
|
||||
*/
|
||||
public void setETag(@Nullable String etag) {
|
||||
if (etag != null) {
|
||||
Assert.isTrue(etag.startsWith("\"") || etag.startsWith("W/"),
|
||||
"Invalid ETag: does not start with W/ or \"");
|
||||
Assert.isTrue(etag.endsWith("\""), "Invalid ETag: does not end with \"");
|
||||
Assert.isTrue(etag.startsWith("\"") || etag.startsWith("W/\""), "ETag does not start with W/\" or \"");
|
||||
Assert.isTrue(etag.endsWith("\""), "ETag does not end with \"");
|
||||
set(ETAG, etag);
|
||||
}
|
||||
else {
|
||||
@@ -1569,35 +1560,27 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable
|
||||
|
||||
/**
|
||||
* Retrieve a combined result from the field values of the ETag header.
|
||||
* @param headerName the header name
|
||||
* @param name the header name
|
||||
* @return the combined result
|
||||
* @throws IllegalArgumentException if parsing fails
|
||||
* @since 4.3
|
||||
*/
|
||||
protected List<String> getETagValuesAsList(String headerName) {
|
||||
List<String> values = get(headerName);
|
||||
if (values != null) {
|
||||
List<String> result = new ArrayList<>();
|
||||
for (String value : values) {
|
||||
if (value != null) {
|
||||
Matcher matcher = ETAG_HEADER_VALUE_PATTERN.matcher(value);
|
||||
while (matcher.find()) {
|
||||
if ("*".equals(matcher.group())) {
|
||||
result.add(matcher.group());
|
||||
}
|
||||
else {
|
||||
result.add(matcher.group(1));
|
||||
}
|
||||
}
|
||||
if (result.isEmpty()) {
|
||||
throw new IllegalArgumentException(
|
||||
"Could not parse header '" + headerName + "' with value '" + value + "'");
|
||||
}
|
||||
protected List<String> getETagValuesAsList(String name) {
|
||||
List<String> values = get(name);
|
||||
if (values == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<String> result = new ArrayList<>();
|
||||
for (String value : values) {
|
||||
if (value != null) {
|
||||
List<ETag> tags = ETag.parse(value);
|
||||
Assert.notEmpty(tags, "Could not parse header '" + name + "' with value '" + value + "'");
|
||||
for (ETag tag : tags) {
|
||||
result.add(tag.formattedTag());
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return Collections.emptyList();
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+5
-13
@@ -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.
|
||||
@@ -26,13 +26,12 @@ import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.TimeZone;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.servlet.http.HttpSession;
|
||||
|
||||
import org.springframework.http.ETag;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
@@ -54,12 +53,6 @@ public class ServletWebRequest extends ServletRequestAttributes implements Nativ
|
||||
|
||||
private static final List<String> SAFE_METHODS = Arrays.asList("GET", "HEAD");
|
||||
|
||||
/**
|
||||
* Pattern matching ETag multiple field values in headers such as "If-Match", "If-None-Match".
|
||||
* @see <a href="https://tools.ietf.org/html/rfc7232#section-2.3">Section 2.3 of RFC 7232</a>
|
||||
*/
|
||||
private static final Pattern ETAG_HEADER_VALUE_PATTERN = Pattern.compile("\\*|\\s*((W\\/)?(\"[^\"]*\"))\\s*,?");
|
||||
|
||||
/**
|
||||
* Date formats as specified in the HTTP RFC.
|
||||
* @see <a href="https://tools.ietf.org/html/rfc7231#section-7.1.1.1">Section 7.1.1.1 of RFC 7231</a>
|
||||
@@ -289,11 +282,10 @@ public class ServletWebRequest extends ServletRequestAttributes implements Nativ
|
||||
etag = etag.substring(2);
|
||||
}
|
||||
while (ifNoneMatch.hasMoreElements()) {
|
||||
String clientETags = ifNoneMatch.nextElement();
|
||||
Matcher etagMatcher = ETAG_HEADER_VALUE_PATTERN.matcher(clientETags);
|
||||
// Compare weak/strong ETags as per https://tools.ietf.org/html/rfc7232#section-2.3
|
||||
while (etagMatcher.find()) {
|
||||
if (StringUtils.hasLength(etagMatcher.group()) && etag.equals(etagMatcher.group(3))) {
|
||||
for (ETag requestedETag : ETag.parse(ifNoneMatch.nextElement())) {
|
||||
String tag = requestedETag.tag();
|
||||
if (StringUtils.hasLength(tag) && etag.equals(padEtagIfNecessary(tag))) {
|
||||
this.notModified = true;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -192,11 +192,17 @@ public class HttpHeadersTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void illegalETag() {
|
||||
void illegalETagWithoutQuotes() {
|
||||
String eTag = "v2.6";
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> headers.setETag(eTag));
|
||||
}
|
||||
|
||||
@Test
|
||||
void illegalWeakETagWithoutLeadingQuote() {
|
||||
String etag = "W/v2.6\"";
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> headers.setETag(etag));
|
||||
}
|
||||
|
||||
@Test
|
||||
void ifMatch() {
|
||||
String ifMatch = "\"v2.6\"";
|
||||
|
||||
@@ -4547,7 +4547,7 @@ dataSource.url=jdbc:mysql:mydb
|
||||
----
|
||||
|
||||
This example file can be used with a container definition that contains a bean called
|
||||
`dataSource` that has `driver` and `url` properties.
|
||||
`dataSource` that has `driverClassName` and `url` properties.
|
||||
|
||||
Compound property names are also supported, as long as every component of the path
|
||||
except the final property being overridden is already non-null (presumably initialized
|
||||
|
||||
@@ -2396,7 +2396,7 @@ your `@Configuration` classes, as the following example shows:
|
||||
By default, the infrastructure looks for a bean named `jmsListenerContainerFactory`
|
||||
as the source for the factory to use to create message listener containers. In this
|
||||
case (and ignoring the JMS infrastructure setup), you can invoke the `processOrder`
|
||||
method with a core poll size of three threads and a maximum pool size of ten threads.
|
||||
method with a core pool size of three threads and a maximum pool size of ten threads.
|
||||
|
||||
You can customize the listener container factory to use for each annotation or you can
|
||||
configure an explicit default by implementing the `JmsListenerConfigurer` interface.
|
||||
|
||||
Reference in New Issue
Block a user