Compare commits

...

67 Commits

Author SHA1 Message Date
Stéphane Nicoll 9b59345547 Release v6.1.11 2024-07-11 09:40:59 +02:00
rstoyanchev 611d3e5551 Correct type names in docs on Validation in WebFlux
Closes gh-33061
2024-07-10 17:55:19 +01:00
Sam Brannen fa2a58b9db Ensure varargs component type for MethodHandle is not null in SpEL
This commit ensures that the varargs component type for a MethodHandle
cannot be null in ReflectionHelper's
convertAllMethodHandleArguments(...) method in SpEL.

Closes gh-33193
2024-07-10 16:46:47 +02:00
Sam Brannen 83ca2c0cff Support MethodHandle function invocation with varargs array in SpEL
Prior to this commit, the Spring Expression Language (SpEL) could not
invoke a varargs MethodHandle function with an array containing the
variable arguments, although that is supported for a varargs Method
function. Attempting to do so resulted in the array being supplied as a
single argument to the MethodHandle.

This commit addresses this by updating the
executeFunctionViaMethodHandle(...) method in FunctionReference as
follows when the user supplies the varargs already packaged in an array.

- Creates a new array large enough to hold the non-varargs arguments
  and the unpackaged varargs arguments.

- Adds the non-varargs arguments to the beginning of that array and
  adds the unpackaged varargs arguments to the end of that array.

- Invokes the MethodHandle with the new arguments array.

Closes gh-33191
2024-07-10 16:16:41 +02:00
Sam Brannen dc16f3cffb Polish SpEL function invocation support 2024-07-10 16:16:41 +02:00
Juergen Hoeller f2b3263fff Polishing 2024-07-10 15:56:56 +02:00
Simon Baslé 1d890a8952 Make coroutines with custom AOP aspects work with @Transactional
Previous to this change, the transactional aspect would supersed the
user-defined AspectJ aspect, shortcircuiting to calling the original
Kotlin suspending function.

This change simplifies the TransactionAspectSupport way of dealing with
transactional coroutines, thanks to the fact that lower level support
for AOP has been introduced in c8169e5c.

Closes gh-33095
2024-07-10 15:35:43 +02:00
Juergen Hoeller 3ccaefe38f Polishing 2024-07-10 15:15:32 +02:00
Juergen Hoeller 300f4585be Update spring.jdbc.getParameterType.ignore note for 6.1.2
See gh-25679
2024-07-10 15:15:26 +02:00
Sam Brannen a0f5c16627 Support MethodHandle function invocation with zero varargs in SpEL
Prior to this commit, the Spring Expression Language (SpEL) could not
invoke a varargs MethodHandle function with zero variable arguments,
even though the variable arguments are not required. Attempting to do
so resulted in a SpelEvaluationException with an
INCORRECT_NUMBER_OF_ARGUMENTS_TO_FUNCTION message.

This commit addresses this by updating the
executeFunctionViaMethodHandle(...) method in FunctionReference so that
it properly checks the required number of arguments for both varargs
and non-varargs MethodHandle invocations.

This commit also improves the error message for varargs invocations
with too few arguments. For example, if the MethodHandle requires at
least 1 argument plus a variable number of additional arguments and 0
arguments were supplied, the error message now states:

"Incorrect number of arguments for function 'myFunc': 0 supplied but function takes 1 or more"

Instead of:

"Incorrect number of arguments for function 'myFunc': 0 supplied but function takes 2"

Closes gh-33190
2024-07-10 13:39:48 +02:00
Stéphane Nicoll b5a86dec92 Retain previous factory method in case of nested invocation with AOT
This commit harmonizes the invocation of a bean supplier with what
SimpleInstantiationStrategy does. Previously, the current factory method
was set to `null` once the invocation completes. This did not take
into account recursive scenarios where an instance supplier triggers
another instance supplier.

For consistency, the thread local is removed now if we attempt to set
the current method to null. SimpleInstantiationStrategy itself uses
the shortcut to align the code as much as possible.

Closes gh-33180
2024-07-10 12:38:56 +02:00
Sam Brannen c55c64427c Support single String argument for varargs invocations in SpEL
Prior to this commit, the Spring Expression Language (SpEL) incorrectly
split single String arguments by comma for Object... varargs method and
constructor invocations.

This commit addresses this by checking if the single argument type is
already "assignable" to the varargs component type instead of "equal"
to the varargs component type.

Closes gh-33013
2024-07-10 11:51:55 +02:00
Sam Brannen a4fcd301f2 Polish SpEL's ReflectionHelper 2024-07-10 11:19:36 +02:00
Stéphane Nicoll ec4558dc14 Polish 2024-07-10 10:19:50 +02:00
Stéphane Nicoll d6c623be40 Release from GHA
This commit adds a workflow to release the Spring Framework from GitHub
Actions.

Closes gh-33179
2024-07-10 10:11:32 +02:00
Stéphane Nicoll 86da45ba15 Upgrade to Reactor 2023.0.8
Closes gh-33169
2024-07-09 15:52:09 +02:00
Stéphane Nicoll 6a60298024 Upgrade to Micrometer 1.12.8
Closes gh-33168
2024-07-09 14:39:10 +02:00
Sébastien Deleuze f8b448e949 Upgrade to Kotlin Serialization 1.6.3
Closes gh-33175
2024-07-09 14:31:56 +02:00
Sébastien Deleuze 52a0e6f900 Upgrade to Kotlin 1.9.24
Closes gh-33177
2024-07-09 14:31:47 +02:00
Stéphane Nicoll 3d2fb3af3d Add verification tests
In preparation for releasing Spring Framework from GitHub actions, this
commit adds a `verify` workflow that validates that the released bits
are correct by running sample projects against them.

Closes gh-33178
2024-07-09 14:23:44 +02:00
Sébastien Deleuze 4f38079042 Throw proper DecodingException in Kotlin Serialization decoders
Closes gh-33138
2024-07-09 12:05:24 +02:00
rstoyanchev 3008d97f93 Polishing contribution
Closes gh-33150
2024-07-09 06:53:14 +01:00
Sébastien NUSSBAUMER f0c758498b Allow validation of Set method parameters
See gh-33150
2024-07-09 06:53:14 +01:00
Juergen Hoeller bc2deef951 Upgrade to JRuby 9.4.8, Rhino 1.7.15, Moneta 1.4.4, JSONassert 1.5.3 2024-07-08 22:55:54 +02:00
Juergen Hoeller 7322a0d3c5 Upgrade to Groovy 4.0.22, Tomcat 10.1.25, Jetty 12.0.11, Undertow 2.3.14 2024-07-08 19:43:19 +02:00
Juergen Hoeller 7227c30917 Allow tests to pass on Windows JVM (non-UTF-8 default encoding)
See gh-33071
2024-07-08 19:42:28 +02:00
Stéphane Nicoll 7e7f55cb23 Merge pull request #33166 from hunhee98
* pr/33166:
  Polish contribution
  Harmonize phrasing in URI Encoding section

Closes gh-33166
2024-07-08 16:51:51 +02:00
Stéphane Nicoll 84f20cd0a7 Polish contribution
See gh-33166
2024-07-08 16:51:23 +02:00
Hunhee Lee 3d8f1fb00f Harmonize phrasing in URI Encoding section
See gh-33166
2024-07-08 16:51:09 +02:00
Stéphane Nicoll e06115c06f Merge pull request #33156 from
* pr/33156:
  Polish "Implement DefaultErrorResponseBuilder#headers(Consumer)"
  Implement DefaultErrorResponseBuilder#headers(Consumer)

Closes gh-33156
2024-07-08 13:15:29 +02:00
Stéphane Nicoll 69850cad27 Polish "Implement DefaultErrorResponseBuilder#headers(Consumer)"
See gh-33156
2024-07-08 13:11:31 +02:00
Harry Yang bbbc95f773 Implement DefaultErrorResponseBuilder#headers(Consumer)
See gh-33156
2024-07-08 13:09:26 +02:00
Sam Brannen 83f7996c79 Polishing 2024-07-05 19:00:39 +02:00
Stéphane Nicoll 0544dfe090 Enable building against Java 23-ea
This commit updates the CI workflow to build against Java 23-ea as
well, using the temurin distribution. For consistency, we're also
building against Java 22.

Closes gh-32090
2024-07-05 10:00:17 +02:00
Stéphane Nicoll eacfd77d6a Harmonize CI configuration
This commit harmonizes our CI configuration with Spring Boot, in
particular the clever use of reusable custom actions that simplify
the workflow definition quite a bit.

One main difference compared to Spring Boot is that we can now
specify a different distribution for a Java version to test, in
preparation for the support of building against 23-ea

See gh-32090
2024-07-05 09:46:45 +02:00
Sam Brannen 89338c91a9 Stop using using legacy locale data for Date/Time formatting tests
Commit 84714fbae9 introduced usage of the
-Djava.locale.providers=COMPAT command-line argument for javac in order
to allow our JDK 20 builds to pass by using legacy locale data.

That was done to ensure that Date/Time formats using AM/PM produced a
standard space (" ") before the "AM" or "PM" instead of a narrow
non-breaking space (NNBSP "\u202F"), which was introduced in Java 20
due to adoption of Unicode Common Locale Data Repository (CLDR-14032).

This commit removes usage of the -Djava.locale.providers=COMPAT
command-line argument and updates all affected tests to:

- Use an NNBSP before "AM" or "PM" in input text when running on Java 20
  or higher.

- Leniently match against any Unicode space character in formatted
  values containing "AM" or "PM".

See https://jdk.java.net/20/release-notes#JDK-8284840
See https://unicode-org.atlassian.net/browse/CLDR-14032
See gh-30185
Closes gh-33144
2024-07-04 17:03:15 +02:00
Stéphane Nicoll 51641ece72 Polish
See gh-33127
2024-07-04 16:51:10 +02:00
Simon Baslé 06d267f04e Improve WebClientResponseException message in case of 1xx/2xx/3xx status
When a response fails to be completely emitted by the remote (connection
termination during the transmission of the response for example), a
WebClientResponseException can be propagated with a confusing message
which mainly reflects the status code and reason phrase, leading to
messages like "200 OK" in such an exception.

This change improves the situation by appending a hint at the underlying
cause whenever getMessage() is called on a WebClientResponseException
which was created with a non-error status code.

Closes gh-33127
2024-07-04 15:20:46 +02:00
Stéphane Nicoll dfa6b4bd42 Upgrade to Gradle 8.8
Closes gh-33146
2024-07-04 10:23:15 +02:00
Stéphane Nicoll 099a97740e Downgrade Awaitility to 4.2.0
This is required to build against ea builds

Closes gh-33143
2024-07-04 04:20:43 +02:00
Juergen Hoeller daea3f0eae Apply fallback resolution for non-hierarchical URIs such as "file:."
Includes meaningful exception message for file system resolution.

Closes gh-33124
2024-07-03 16:36:18 +02:00
zizare 8974da2a5a Use error handler for reactive cache aspect
This change ensures that the cache error handler is used in case of
future-based or publisher-based asynchronous caching completing with an
exception.

Closes gh-33073
2024-07-02 11:29:53 +02:00
Brian Clozel ab236c7741 Re-enable async dispatches in Observation Filter
Prior to this commit, the fix for gh-32730 disabled the involvment of
the osbervation filter for async dispatches. Instead of relying on ASYNC
dispatches to close the observation for async requests, this is now
using an async listener instead: async dispatches are not guaranteed to
happen once the async request is handled.

This change caused another side-effect: because async dispatches are not
considered anymore by this filter, the observation scope is not
reinstated for async dispatches. For example, `ResponseBodyAdvice`
implementations do not have the observation scope opened during their
execution.

This commit re-enables async dispatches for this filter, but ensures
that observations are not closed during such dispatches as this will be
done by the async listener.

Fixes gh-33091
2024-07-01 11:42:48 +02:00
Juergen Hoeller 61adf2dd25 Formal null safety for exception message through String.valueOf
See gh-33117
2024-06-28 18:09:39 +02:00
Juergen Hoeller 61894af0bd Expose FactoryBean attribute exception as BeanDefinitionStoreException
Closes gh-33117
2024-06-28 17:55:45 +02:00
rstoyanchev c74666a883 Polishing contribution
Closes gh-33105
2024-06-28 15:32:43 +01:00
vatsal 976b4f3533 Fix return value validation
Fix argument in call to applyReturnValueValidation()
method in MethodValidationInterceptor.java. Method
argument was passed instead of the return value of the
method that was being validated.

See gh-33105
2024-06-28 15:00:11 +01:00
Juergen Hoeller 100da83913 Detect ajc markers in superclasses as well (for weaving check)
Closes gh-33113
2024-06-28 11:36:17 +02:00
Sam Brannen e881c70a93 Upgrade to JUnit 5.10.3 2024-06-27 17:00:43 +02:00
Juergen Hoeller fea237c065 Lazily start and retain HttpClient once resource factory is running
Closes gh-33093
2024-06-27 12:03:10 +02:00
Sam Brannen 8b11ee9ee2 Document that ModelMap is not a supported argument type in WebFlux
Prior to this commit, the "Method Arguments" documentation for WebFlux
in the reference manual stated that WebFlux controller methods can
accept arguments of type Map, Model, or ModelMap to access the model.
However, ModelMap is actually not supported and results in exception
due to a type mismatch.

This commit updates the documentation to reflect this.

In addition, this commit updates related Javadoc and tests to avoid
mentioning or using ModelMap in WebFlux.

Closes gh-33107
2024-06-27 11:33:50 +02:00
Sam Brannen 1cf5264163 Polishing 2024-06-27 10:46:00 +02:00
Sam Brannen c68c6faa03 Fix comment in Method Injection example in reference manual
Prior to this commit, the comment in the XML configuration example in
the Method Injection section of the reference manual referred to the
wrong bean names.

Closes gh-33096
2024-06-26 16:45:06 +02:00
Brian Clozel 6dd5c85ed0 Support byte array payloads in ProtobufMessageConverter
Prior to this commit, the `ProtobufMessageConverter` used in messaging
would try and serialize the message payload by calling "toString()" on
it in order to pass it to the Protobuf JSON encoder.
While this works for `String` payloads, this fails for `byte[]` types.

This commit ensures that such `byte[]` are first converted to `String`
instances using the given charset first.

Fixes gh-27408
2024-06-25 21:16:50 +02:00
Sam Brannen d133ab60ee Improve documentation regarding encoding in FreeMarker support
This commit also introduces integration tests to test the status quo
regarding encoding.

Closes gh-33071
2024-06-25 16:56:48 +02:00
Sam Brannen 5d6e143ff4 Remove invalid configuration in RequestMappingViewResolutionIntegrationTests
Prior to this commit, RequestMappingViewResolutionIntegrationTests
invoked the following:

configurer.setTemplateLoaderPath(
	"classpath*:org/springframework/web/reactive/view/freemarker/");

However, that configuration is invalid since `classpath*:` is not
supported for a `templateLoaderPath`.

Despite that, the tests still passed since FreeMarkerConfigurer already
registers a new ClassTemplateLoader(FreeMarkerConfigurer.class, ""),
which automatically finds template files in the same package as
FreeMarkerConfigurer (for the "spring.ftl" macro library support) and
coincidentally RequestMappingViewResolutionIntegrationTests as well
(which resides in the same package).

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

Some files were not shown because too many files have changed in this diff Show More