Compare commits

...

58 Commits

Author SHA1 Message Date
Spring Buildmaster 83c6d10095 Release v5.3.6 2021-04-13 11:08:55 +00:00
Juergen Hoeller f31933e67e Nullability refinements 2021-04-13 12:26:52 +02:00
Juergen Hoeller c7989c78c8 Polishing 2021-04-13 12:26:31 +02:00
Sam Brannen f03ccd5cc9 Fix Javadoc in AcceptHeaderLocaleResolver
Closes gh-26793
2021-04-13 11:36:28 +02:00
Sam Brannen bcc6171a72 Polish contribution
See gh-26785
2021-04-13 11:21:56 +02:00
Sviatoslav Hryb a80c5fcfaf Improve @EventListener documentation in reference manual
Closes gh-26785
2021-04-13 11:17:09 +02:00
Juergen Hoeller 04ce8e0ac4 Make URL path tests compatible with Windows
See gh-26775
2021-04-13 09:52:23 +02:00
Stephane Nicoll 74f7eb11be Polish "Upgrade to Reactor 2020.0.6"
See gh-26767
2021-04-13 08:12:02 +02:00
Juergen Hoeller e05b584623 Ignore testWindowsAbsoluteFilePath exception if not on Windows
See gh-26702
2021-04-12 23:46:27 +02:00
Juergen Hoeller 29955a2898 Polishing 2021-04-12 22:41:00 +02:00
Juergen Hoeller 4b6b12bf2f Bypass root path resolution for "file:" prefix only
Closes gh-26702
2021-04-12 22:40:53 +02:00
Rossen Stoyanchev ab0e8f0617 Upgrade to Reactor 2020.0.6
Closes gh-26767
2021-04-12 20:22:38 +01:00
Rossen Stoyanchev 0a6a6d48e2 Avoid CI failures with UTF-8 chars in test filenames
See gh-26775
2021-04-12 20:14:48 +01:00
Rossen Stoyanchev a08593b44b Correct matching of static resources with parsed patterns
Closes gh-26775
2021-04-12 19:54:20 +01:00
Rebwon 7954dc7a00 Polish tests
Closes gh-26708
2021-04-12 20:11:14 +02:00
Sam Brannen 22d9012081 Retain root cause for parsing patterns in @DateTimeFormat
The support for fallback parsing patterns in @DateTimeFormat introduced
in gh-20292 introduced a regression in that the original cause of the
parsing exception was no longer retained.

This commit ensures that the original DateTimeParseException is set as
the cause for any newly created DateTimeParseException, thereby
retaining the original exception as the root cause.

Closes gh-26777
2021-04-12 19:55:43 +02:00
Arjen Poutsma 327e761536 Recreate file storage directory when removed in DefaultPartHttpMessageReader
Some operating systems delete temp files not just when booting up, but
also during operation. This commit makes sure that the
DefaultPartHttpMessageReader recreates the directory used to store
files in, if it's not there.

Closes gh-26790
2021-04-12 16:27:10 +02:00
Juergen Hoeller 4e42b5dfbc Upgrade to Tomcat 9.0.45, Jetty 9.4.39, Undertow 2.2.7, Netty 4.1.63, RxJava 3.0.12 2021-04-12 14:15:45 +02:00
Brandon Fergerson 432c720dd0 Allow spring-expression to be more easily repackaged
This commit allows the spring-expression module to be more easily repackaged
for embedding in third-party JARs -- for example, via the Shadow Gradle plugin.

Closes gh-26779
2021-04-09 17:29:29 +02:00
Sviatoslav Hryb f7606ba51b Update ref docs regarding RequiredAnnotationBPP registration
The RequiredAnnotationBeanPostProcessor is no longer automatically
registered via XML namespace elements.

Closes gh-26783
2021-04-09 16:59:48 +02:00
Sam Brannen 0f54585f05 Polish contribution
See gh-26782
2021-04-09 16:54:49 +02:00
Stephane Nicoll d3d6e100da Merge pull request #26781 from runningcode
* pr/26781:
  Update to Gradle Enterprise Gradle Plugin 3.6.1

Closes gh-26781
2021-04-09 16:47:04 +02:00
Nelson Osacky a48c5f6a0d Update to Gradle Enterprise Gradle Plugin 3.6.1
See gh-26781
2021-04-09 16:46:53 +02:00
Sviatoslav Hryb 35ea33e13a Update documentation for <context:annotation-config/>
Closes gh-26782
2021-04-09 16:32:17 +02:00
Arjen Poutsma a2d91a562d Support "Accept-Patch" for unsupported media type
This commit introduces support in both servlet and webflux for the
"Accept-Patch" header, which is sent when the client sends unsupported
data in PATCH requests.
See  section 2.2 of RFC 5789.

Closes gh-26759
2021-04-08 14:46:59 +02:00
Arjen Poutsma 97f3846971 Support "Accept-Patch" for OPTIONS requests
This commit introduces support in both servlet and webflux for the
"Accept-Patch" header in OPTIONS requests, as defined in section 3.1 of
RFC 5789.

See gh-26759
2021-04-08 14:43:49 +02:00
Stephane Nicoll 44e1d6d1bf Merge pull request #26776 from KENNYSOFT
* pr/26776:
  Update copyright year of changed file
  Fix javadoc link syntax

Closes gh-26776
2021-04-08 14:24:39 +02:00
Stephane Nicoll 7adac25e7e Update copyright year of changed file
See gh-26776
2021-04-08 14:24:18 +02:00
Hyeonmin Park 7dca6a36a6 Fix javadoc link syntax
See gh-26776
2021-04-08 14:21:11 +02:00
Rossen Stoyanchev c5ac8e8ab6 Use createUnresolved for forwarded header address
Avoid DNS resolution.

See gh-26748
2021-04-07 12:50:07 +01:00
Stephane Nicoll fa4c62cab2 Start building against Reactor 2020.0.6 snapshots
See gh-26767
2021-04-07 09:34:43 +02:00
Rossen Stoyanchev 8a1182a678 Additional validation for forwarded header address value
Closes gh-26748
2021-04-06 19:43:13 +01:00
Stephane Nicoll 01bea34569 Merge pull request #26755 from izeye
* pr/26755:
  Add since to WebClient.ResponseSpec.awaitBodyOrNull()

Closes gh-26755
2021-04-02 17:46:13 +02:00
Johnny Lim aec151e014 Add since to WebClient.ResponseSpec.awaitBodyOrNull()
See gh-26755
2021-04-02 17:45:57 +02:00
Valentin 58e40d1aa2 Add awaitBodyOrNull to WebClient.ResponseSpec
Such variant is already provided for ClientResponse but not
for WebClient.ResponseSpec.

Closes gh-26731
2021-04-01 17:06:48 +02:00
Sébastien Deleuze 85dbd2a4c3 Upgrade Kotlin to 1.4.32 2021-04-01 16:06:14 +02:00
Sébastien Deleuze eca87b87de Improve Kotlin support in BridgeMethodResolver#findBridgedMethod
This commit adds support for Kotlin non-nullable type which resolves
to primitive Java types in BridgeMethodResolver#findBridgedMethod.

Closes gh-26585
2021-04-01 15:39:53 +02:00
Rossen Stoyanchev 5e1e29a2dd Fix failing tests
See gh-26746
2021-03-31 12:25:39 +01:00
Rossen Stoyanchev 7dc3a55648 Deferred buffer allocation in SSE message writer
See gh-26746
2021-03-31 11:47:22 +01:00
Arjen Poutsma f982fd99d7 Fix timezone offset in CronExpressionTests
Closes gh-26744
2021-03-30 16:37:26 +02:00
Arjen Poutsma ab18ab6025 Fix CronExpression issue with ZonedDateTime & DST
This commit fixes an issue with CronExpression when used in combination
with ZonedDateTime and daylight saving time.

Closes gh-26744
2021-03-30 15:20:59 +02:00
Arjen Poutsma d83fb09914 Change header encoding to UTF8 in DefaultPartHttpMessageReader
This commit changes the encoding used to parse multipart headers from
ISO-8859-1 to UTF-8, in accordance with RFC 7578.

Closes gh-26736
2021-03-30 10:51:19 +02:00
Rossen Stoyanchev b651c10e83 Fix error messages to match commit #1a9556
See gh-26679
2021-03-29 18:30:49 +01:00
Rossen Stoyanchev 5651695fd1 Fix typo in Javadoc
Closes gh-26740
2021-03-29 17:59:19 +01:00
Rossen Stoyanchev 1a9556925d missingAfterConversion flag for missing request values
Closes gh-26679
2021-03-29 15:23:29 +01:00
nara 2dc203ffdc Polish web docs example 2021-03-29 11:40:56 +01:00
Brian Clozel 610de3ae78 Upgrade to Concourse Release Scripts 0.3.2 2021-03-23 13:59:53 +01:00
Juergen Hoeller 5b2ace5742 Upgrade to Hibernate ORM 5.4.30, Tomcat 9.0.44, FreeMarker 2.3.31, Log4J 2.14.1, TestNG 7.4, HtmlUnit 2.48 2021-03-22 23:34:13 +01:00
Juergen Hoeller 5b227e5677 Upgrade to Objenesis 3.2
Closes gh-26714
2021-03-22 23:11:30 +01:00
Rossen Stoyanchev 41d5048280 Add BlockHoundIntegration for spring-web
Closes gh-26712
2021-03-22 21:18:48 +00:00
Rossen Stoyanchev a931ff12f6 MappedInterceptor falls back on PathMatcher
Closes gh-26690
2021-03-22 07:12:10 +00:00
Sam Brannen e4f753e3e3 Honor class-level @DirtiesContext if test class is disabled via SpEL
Prior to this commit, if a test class annotated with @DirtiesContext
and @EnabledIf/@DisabledIf with `loadContext = true` was disabled due
to the evaluated SpEL expression, the ApplicationContext would not be
marked as dirty and closed.

The reason is that @EnabledIf/@DisabledIf are implemented via JUnit
Jupiter's ExecutionCondition extension API which results in the entire
test class (as well as any associated extension callbacks) being
skipped if the condition evaluates to `disabled`. This effectively
prevents any of Spring's TestExecutionListener APIs from being invoked.
Consequently, the DirtiesContextTestExecutionListener does not get a
chance to honor the class-level @DirtiesContext declaration.

This commit fixes this by implementing part of the logic of
DirtiesContextTestExecutionListener in
AbstractExpressionEvaluatingCondition (i.e., the base class for
@EnabledIf/@DisabledIf support). Specifically, if the test class for an
eagerly loaded ApplicationContext is disabled,
AbstractExpressionEvaluatingCondition will now mark the test
ApplicationContext as dirty if the test class is annotated with
@DirtiesContext.

Closes gh-26694
2021-03-18 13:51:32 +01:00
Rossen Stoyanchev 4982b5fcb9 Improve docs on SSE tests for Spring MVC
Closes gh-26687
2021-03-16 17:55:43 +00:00
Sam Brannen f7678cdcdd Polish core-beans.adoc 2021-03-16 13:09:36 +01:00
Brian Clozel 3ab39eda08 Fix Gradle Toolchain configuration
See gh-25787
2021-03-16 13:04:11 +01:00
Brian Clozel 14a6909c4a Fix release pipeline for Maven Central publication
This commit fixes the missing pieces in our Maven Central publication
pipeline. Our first attempt at releasing with it showed a few problems:

* the promote task did not have the artifacts downladed with the
  artifactory repository
* we applied the wrong Sonatype credentials to the task
* the github changelog task would fail because of docker rate limiting
  since we were not using the right type of resource, which is
  configured with the proper caching mechanism

See gh-26654
2021-03-16 13:04:11 +01:00
Rebwon fd17738f76 Polish TestContextAnnotationsUtilsTests
Closes gh-26684
2021-03-16 12:15:00 +01:00
Spring Buildmaster f674652dd0 Next development version (v5.3.6-SNAPSHOT) 2021-03-16 08:12:35 +00:00
89 changed files with 1634 additions and 355 deletions
+16 -16
View File
@@ -1,7 +1,7 @@
plugins {
id 'io.spring.dependency-management' version '1.0.9.RELEASE' apply false
id 'io.spring.nohttp' version '0.0.5.RELEASE'
id 'org.jetbrains.kotlin.jvm' version '1.4.31' apply false
id 'org.jetbrains.kotlin.jvm' version '1.4.32' apply false
id 'org.jetbrains.dokka' version '0.10.1' apply false
id 'org.asciidoctor.jvm.convert' version '3.1.0'
id 'org.asciidoctor.jvm.pdf' version '3.1.0'
@@ -10,7 +10,7 @@ plugins {
id "com.github.ben-manes.versions" version '0.28.0'
id "com.github.johnrengelman.shadow" version "6.1.0" apply false
id "me.champeau.gradle.jmh" version "0.5.2" apply false
id "org.jetbrains.kotlin.plugin.serialization" version "1.4.31" apply false
id "org.jetbrains.kotlin.plugin.serialization" version "1.4.32" apply false
}
ext {
@@ -27,17 +27,17 @@ configure(allprojects) { project ->
dependencyManagement {
imports {
mavenBom "com.fasterxml.jackson:jackson-bom:2.12.2"
mavenBom "io.netty:netty-bom:4.1.60.Final"
mavenBom "io.projectreactor:reactor-bom:2020.0.5"
mavenBom "io.netty:netty-bom:4.1.63.Final"
mavenBom "io.projectreactor:reactor-bom:2020.0.6"
mavenBom "io.r2dbc:r2dbc-bom:Arabba-SR9"
mavenBom "io.rsocket:rsocket-bom:1.1.0"
mavenBom "org.eclipse.jetty:jetty-bom:9.4.38.v20210224"
mavenBom "org.jetbrains.kotlin:kotlin-bom:1.4.30"
mavenBom "org.eclipse.jetty:jetty-bom:9.4.39.v20210325"
mavenBom "org.jetbrains.kotlin:kotlin-bom:1.4.32"
mavenBom "org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.4.3"
mavenBom "org.junit:junit-bom:5.7.1"
}
dependencies {
dependencySet(group: 'org.apache.logging.log4j', version: '2.14.0') {
dependencySet(group: 'org.apache.logging.log4j', version: '2.14.1') {
entry 'log4j-api'
entry 'log4j-core'
entry 'log4j-jul'
@@ -65,7 +65,7 @@ configure(allprojects) { project ->
dependency "io.reactivex:rxjava:1.3.8"
dependency "io.reactivex:rxjava-reactive-streams:1.2.1"
dependency "io.reactivex.rxjava2:rxjava:2.2.21"
dependency "io.reactivex.rxjava3:rxjava:3.0.11"
dependency "io.reactivex.rxjava3:rxjava:3.0.12"
dependency "io.projectreactor.tools:blockhound:1.0.4.RELEASE"
dependency "com.caucho:hessian:4.0.63"
@@ -117,30 +117,30 @@ configure(allprojects) { project ->
}
dependency "org.apache.poi:poi-ooxml:4.1.2"
dependency "org.apache-extras.beanshell:bsh:2.0b6"
dependency "org.freemarker:freemarker:2.3.30"
dependency "org.freemarker:freemarker:2.3.31"
dependency "org.hsqldb:hsqldb:2.5.1"
dependency "org.quartz-scheduler:quartz:2.3.2"
dependency "org.codehaus.fabric3.api:commonj:1.1.0"
dependency "net.sf.ehcache:ehcache:2.10.6"
dependency "org.ehcache:jcache:1.0.1"
dependency "org.ehcache:ehcache:3.4.0"
dependency "org.hibernate:hibernate-core:5.4.29.Final"
dependency "org.hibernate:hibernate-core:5.4.30.Final"
dependency "org.hibernate:hibernate-validator:6.2.0.Final"
dependency "org.webjars:webjars-locator-core:0.46"
dependency "org.webjars:underscorejs:1.8.3"
dependencySet(group: 'org.apache.tomcat', version: '9.0.43') {
dependencySet(group: 'org.apache.tomcat', version: '9.0.45') {
entry 'tomcat-util'
entry('tomcat-websocket') {
exclude group: "org.apache.tomcat", name: "tomcat-websocket-api"
exclude group: "org.apache.tomcat", name: "tomcat-servlet-api"
}
}
dependencySet(group: 'org.apache.tomcat.embed', version: '9.0.43') {
dependencySet(group: 'org.apache.tomcat.embed', version: '9.0.45') {
entry 'tomcat-embed-core'
entry 'tomcat-embed-websocket'
}
dependencySet(group: 'io.undertow', version: '2.2.5.Final') {
dependencySet(group: 'io.undertow', version: '2.2.7.Final') {
entry 'undertow-core'
entry('undertow-websockets-jsr') {
exclude group: "org.jboss.spec.javax.websocket", name: "jboss-websocket-api_1.1_spec"
@@ -188,7 +188,7 @@ configure(allprojects) { project ->
dependency("de.bechte.junit:junit-hierarchicalcontextrunner:4.12.1") {
exclude group: "junit", name: "junit"
}
dependency "org.testng:testng:7.3.0"
dependency "org.testng:testng:7.4.0"
dependency "org.hamcrest:hamcrest:2.1"
dependency "org.awaitility:awaitility:3.1.6"
dependency "org.assertj:assertj-core:3.19.0"
@@ -206,10 +206,10 @@ configure(allprojects) { project ->
}
dependency "io.mockk:mockk:1.10.2"
dependency("net.sourceforge.htmlunit:htmlunit:2.47.1") {
dependency("net.sourceforge.htmlunit:htmlunit:2.48.0") {
exclude group: "commons-logging", name: "commons-logging"
}
dependency("org.seleniumhq.selenium:htmlunit-driver:2.47.1") {
dependency("org.seleniumhq.selenium:htmlunit-driver:2.48.0") {
exclude group: "commons-logging", name: "commons-logging"
}
dependency("org.seleniumhq.selenium:selenium-java:3.141.59") {
+1 -1
View File
@@ -14,7 +14,7 @@ rm -rf /var/lib/apt/lists/*
curl https://raw.githubusercontent.com/spring-io/concourse-java-scripts/v0.0.4/concourse-java.sh > /opt/concourse-java.sh
curl --output /opt/concourse-release-scripts.jar https://repo.spring.io/release/io/spring/concourse/releasescripts/concourse-release-scripts/0.3.0/concourse-release-scripts-0.3.0.jar
curl --output /opt/concourse-release-scripts.jar https://repo.spring.io/release/io/spring/concourse/releasescripts/concourse-release-scripts/0.3.2/concourse-release-scripts-0.3.2.jar
###########################################################
# JAVA
+3 -3
View File
@@ -9,8 +9,8 @@ anchors:
GRADLE_ENTERPRISE_CACHE_USERNAME: ((gradle_enterprise_cache_user.username))
GRADLE_ENTERPRISE_CACHE_PASSWORD: ((gradle_enterprise_cache_user.password))
sonatype-task-params: &sonatype-task-params
SONATYPE_USER_TOKEN: ((sonatype-user-token))
SONATYPE_PASSWORD_TOKEN: ((sonatype-user-token-password))
SONATYPE_USERNAME: ((sonatype-username))
SONATYPE_PASSWORD: ((sonatype-password))
SONATYPE_URL: ((sonatype-url))
SONATYPE_STAGING_PROFILE_ID: ((sonatype-staging-profile-id))
artifactory-task-params: &artifactory-task-params
@@ -376,7 +376,7 @@ jobs:
trigger: false
passed: [stage-release]
params:
download_artifacts: false
download_artifacts: true
save_build_info: true
- task: promote
image: ci-image
+1 -1
View File
@@ -1,7 +1,7 @@
---
platform: linux
image_resource:
type: docker-image
type: registry-image
source:
repository: springio/github-changelog-generator
tag: '0.0.6'
+2 -2
View File
@@ -10,8 +10,8 @@ params:
ARTIFACTORY_SERVER:
ARTIFACTORY_USERNAME:
ARTIFACTORY_PASSWORD:
SONATYPE_USER_TOKEN:
SONATYPE_PASSWORD_TOKEN:
SONATYPE_USER:
SONATYPE_PASSWORD:
SONATYPE_URL:
SONATYPE_STAGING_PROFILE_ID:
run:
+1 -1
View File
@@ -1,4 +1,4 @@
version=5.3.5-SNAPSHOT
version=5.3.6
org.gradle.jvmargs=-Xmx1536M
org.gradle.caching=true
org.gradle.parallel=true
+15 -18
View File
@@ -28,13 +28,11 @@
*
* @author Brian Clozel
*/
def mainToolchain = 'mainToolchain'
def testToolchain = 'testToolchain'
plugins.withType(JavaPlugin) {
// Configure the Java Toolchain if the 'mainToolchain' property is defined
if (project.hasProperty(mainToolchain)) {
def mainLanguageVersion = JavaLanguageVersion.of(project.property(mainToolchain).toString())
if (project.hasProperty('mainToolchain') && project.mainToolchain) {
def mainLanguageVersion = JavaLanguageVersion.of(project.mainToolchain.toString())
java {
toolchain {
languageVersion = mainLanguageVersion
@@ -48,8 +46,8 @@ plugins.withType(JavaPlugin) {
}
}
// Configure a specific Java Toolchain for compiling and running tests if the 'testToolchain' property is defined
if (project.hasProperty(testToolchain)) {
def testLanguageVersion = JavaLanguageVersion.of(project.property(testToolchain).toString());
if (project.hasProperty('testToolchain') && project.testToolchain) {
def testLanguageVersion = JavaLanguageVersion.of(project.testToolchain.toString());
tasks.withType(JavaCompile).matching { it.name.contains("Test") }.configureEach {
javaCompiler = javaToolchains.compilerFor {
languageVersion = testLanguageVersion
@@ -65,7 +63,7 @@ plugins.withType(JavaPlugin) {
plugins.withType(GroovyPlugin) {
// Fallback to JDK8
if (!project.hasProperty(mainToolchain)) {
if (!project.hasProperty('mainToolchain')) {
compileGroovy {
sourceCompatibility = JavaVersion.VERSION_1_8
}
@@ -74,8 +72,8 @@ plugins.withType(GroovyPlugin) {
// Configure the Kotlin compiler if the 'mainToolchain' property is defined
pluginManager.withPlugin("kotlin") {
if (project.hasProperty(mainToolchain)) {
def mainLanguageVersion = JavaLanguageVersion.of(project.property(mainToolchain).toString());
if (project.hasProperty('mainToolchain') && project.mainToolchain) {
def mainLanguageVersion = JavaLanguageVersion.of(project.mainToolchain.toString());
def compiler = javaToolchains.compilerFor {
languageVersion = mainLanguageVersion
}
@@ -96,15 +94,21 @@ pluginManager.withPlugin("kotlin") {
}
}
else {
// Fallback to JDK8
compileKotlin {
kotlinOptions {
jvmTarget = '1.8'
}
}
compileTestKotlin {
kotlinOptions {
jvmTarget = '1.8'
}
}
}
if (project.hasProperty(testToolchain)) {
def testLanguageVersion = JavaLanguageVersion.of(project.property(testToolchain).toString());
if (project.hasProperty('testToolchain') && project.testToolchain) {
def testLanguageVersion = JavaLanguageVersion.of(project.testToolchain.toString());
def compiler = javaToolchains.compilerFor {
languageVersion = testLanguageVersion
}
@@ -117,11 +121,4 @@ pluginManager.withPlugin("kotlin") {
}
}
}
else {
compileTestKotlin {
kotlinOptions {
jvmTarget = '1.8'
}
}
}
}
+1 -1
View File
@@ -6,7 +6,7 @@ pluginManagement {
}
plugins {
id "com.gradle.enterprise" version "3.5.1"
id "com.gradle.enterprise" version "3.6.1"
id "io.spring.ge.conventions" version "0.0.7"
}
@@ -26,8 +26,8 @@ import java.nio.file.Paths;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceEditor;
import org.springframework.core.io.ResourceLoader;
import org.springframework.util.Assert;
import org.springframework.util.ResourceUtils;
/**
* Editor for {@code java.nio.file.Path}, to directly populate a Path
@@ -74,7 +74,7 @@ public class PathEditor extends PropertyEditorSupport {
@Override
public void setAsText(String text) throws IllegalArgumentException {
boolean nioPathCandidate = !text.startsWith(ResourceLoader.CLASSPATH_URL_PREFIX);
boolean nioPathCandidate = !text.startsWith(ResourceUtils.CLASSPATH_URL_PREFIX);
if (nioPathCandidate && !text.startsWith("/")) {
try {
URI uri = new URI(text);
@@ -85,9 +85,13 @@ public class PathEditor extends PropertyEditorSupport {
return;
}
}
catch (URISyntaxException | FileSystemNotFoundException ex) {
// Not a valid URI (let's try as Spring resource location),
// or a URI scheme not registered for NIO (let's try URL
catch (URISyntaxException ex) {
// Not a valid URI; potentially a Windows-style path after
// 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).
}
}
@@ -97,8 +101,7 @@ public class PathEditor extends PropertyEditorSupport {
if (resource == null) {
setValue(null);
}
else if (!resource.isFile() && !resource.exists() && nioPathCandidate) {
// Prefer getFile().toPath() below for non-existent file handles
else if (nioPathCandidate && !resource.exists()) {
setValue(Paths.get(text).normalize());
}
else {
@@ -39,8 +39,7 @@ public class PathEditorTests {
pathEditor.setAsText("classpath:" + ClassUtils.classPackageAsResourcePath(getClass()) + "/" +
ClassUtils.getShortName(getClass()) + ".class");
Object value = pathEditor.getValue();
boolean condition = value instanceof Path;
assertThat(condition).isTrue();
assertThat(value instanceof Path).isTrue();
Path path = (Path) value;
assertThat(path.toFile().exists()).isTrue();
}
@@ -57,11 +56,9 @@ public class PathEditorTests {
PropertyEditor pathEditor = new PathEditor();
pathEditor.setAsText("file:/no_way_this_file_is_found.doc");
Object value = pathEditor.getValue();
boolean condition1 = value instanceof Path;
assertThat(condition1).isTrue();
assertThat(value instanceof Path).isTrue();
Path path = (Path) value;
boolean condition = !path.toFile().exists();
assertThat(condition).isTrue();
assertThat(!path.toFile().exists()).isTrue();
}
@Test
@@ -69,11 +66,9 @@ public class PathEditorTests {
PropertyEditor pathEditor = new PathEditor();
pathEditor.setAsText("/no_way_this_file_is_found.doc");
Object value = pathEditor.getValue();
boolean condition1 = value instanceof Path;
assertThat(condition1).isTrue();
assertThat(value instanceof Path).isTrue();
Path path = (Path) value;
boolean condition = !path.toFile().exists();
assertThat(condition).isTrue();
assertThat(!path.toFile().exists()).isTrue();
}
@Test
@@ -81,23 +76,26 @@ public class PathEditorTests {
PropertyEditor pathEditor = new PathEditor();
pathEditor.setAsText("C:\\no_way_this_file_is_found.doc");
Object value = pathEditor.getValue();
boolean condition1 = value instanceof Path;
assertThat(condition1).isTrue();
assertThat(value instanceof Path).isTrue();
Path path = (Path) value;
boolean condition = !path.toFile().exists();
assertThat(condition).isTrue();
assertThat(!path.toFile().exists()).isTrue();
}
@Test
public void testWindowsAbsoluteFilePath() {
PropertyEditor pathEditor = new PathEditor();
pathEditor.setAsText("file://C:\\no_way_this_file_is_found.doc");
Object value = pathEditor.getValue();
boolean condition1 = value instanceof Path;
assertThat(condition1).isTrue();
Path path = (Path) value;
boolean condition = !path.toFile().exists();
assertThat(condition).isTrue();
try {
pathEditor.setAsText("file://C:\\no_way_this_file_is_found.doc");
Object value = pathEditor.getValue();
assertThat(value instanceof Path).isTrue();
Path path = (Path) value;
assertThat(!path.toFile().exists()).isTrue();
}
catch (IllegalArgumentException ex) {
if (File.separatorChar == '\\') { // on Windows, otherwise silently ignore
throw ex;
}
}
}
@Test
@@ -107,8 +105,7 @@ public class PathEditorTests {
ClassUtils.getShortName(getClass()) + ".class";
pathEditor.setAsText(fileName);
Object value = pathEditor.getValue();
boolean condition = value instanceof Path;
assertThat(condition).isTrue();
assertThat(value instanceof Path).isTrue();
Path path = (Path) value;
File file = path.toFile();
assertThat(file.exists()).isTrue();
@@ -126,8 +123,7 @@ public class PathEditorTests {
ClassUtils.getShortName(getClass()) + ".clazz";
pathEditor.setAsText(fileName);
Object value = pathEditor.getValue();
boolean condition = value instanceof Path;
assertThat(condition).isTrue();
assertThat(value instanceof Path).isTrue();
Path path = (Path) value;
File file = path.toFile();
assertThat(file.exists()).isFalse();
@@ -101,7 +101,7 @@ public final class TemporalAccessorParser implements Parser<TemporalAccessor> {
if (this.source != null) {
throw new DateTimeParseException(
String.format("Unable to parse date time value \"%s\" using configuration from %s", text, this.source),
text, ex.getErrorIndex());
text, ex.getErrorIndex(), ex);
}
// else rethrow original exception
throw ex;
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -237,7 +237,12 @@ abstract class CronField {
public <T extends Temporal & Comparable<? super T>> T elapseUntil(T temporal, int goal) {
int current = get(temporal);
if (current < goal) {
return this.field.getBaseUnit().addTo(temporal, goal - current);
T result = this.field.getBaseUnit().addTo(temporal, goal - current);
current = get(result);
if (current > goal) { // can occur due to daylight saving, see gh-26744
result = this.field.getBaseUnit().addTo(result, goal - current);
}
return result;
}
else {
ValueRange range = temporal.range(this.field);
@@ -16,6 +16,7 @@
package org.springframework.format.datetime.standard;
import java.time.DateTimeException;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDate;
@@ -320,6 +321,35 @@ class DateTimeFormattingTests {
assertThat(binder.getBindingResult().getFieldValue("isoLocalDate")).isEqualTo("2009-10-31");
}
@Test
void isoLocalDateWithInvalidFormat() {
MutablePropertyValues propertyValues = new MutablePropertyValues();
String propertyName = "isoLocalDate";
propertyValues.add(propertyName, "2009-31-10");
binder.bind(propertyValues);
BindingResult bindingResult = binder.getBindingResult();
assertThat(bindingResult.getErrorCount()).isEqualTo(1);
FieldError fieldError = bindingResult.getFieldError(propertyName);
assertThat(fieldError.unwrap(TypeMismatchException.class))
.hasMessageContaining("for property 'isoLocalDate'")
.hasCauseInstanceOf(ConversionFailedException.class).getCause()
.hasMessageContaining("for value '2009-31-10'")
.hasCauseInstanceOf(IllegalArgumentException.class).getCause()
.hasMessageContaining("Parse attempt failed for value [2009-31-10]")
.hasCauseInstanceOf(DateTimeParseException.class).getCause()
// Unable to parse date time value "2009-31-10" using configuration from
// @org.springframework.format.annotation.DateTimeFormat(pattern=, style=SS, iso=DATE, fallbackPatterns=[])
.hasMessageContainingAll(
"Unable to parse date time value \"2009-31-10\" using configuration from",
"@org.springframework.format.annotation.DateTimeFormat",
"iso=DATE", "fallbackPatterns=[]")
.hasCauseInstanceOf(DateTimeParseException.class).getCause()
.hasMessageStartingWith("Text '2009-31-10'")
.hasCauseInstanceOf(DateTimeException.class).getCause()
.hasMessageContaining("Invalid value for MonthOfYear (valid values 1 - 12): 31")
.hasNoCause();
}
@Test
void testBindISOTime() {
MutablePropertyValues propertyValues = new MutablePropertyValues();
@@ -519,9 +549,12 @@ class DateTimeFormattingTests {
.hasMessageContainingAll(
"Unable to parse date time value \"210302\" using configuration from",
"@org.springframework.format.annotation.DateTimeFormat",
"yyyy-MM-dd", "M/d/yy", "yyyyMMdd", "yyyy.MM.dd");
"yyyy-MM-dd", "M/d/yy", "yyyyMMdd", "yyyy.MM.dd")
.hasCauseInstanceOf(DateTimeParseException.class).getCause()
.hasMessageStartingWith("Text '210302'")
.hasNoCause();
}
}
}
public static class DateTimeBean {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -1261,6 +1261,23 @@ class CronExpressionTests {
assertThat(actual.getDayOfWeek()).isEqualTo(SUNDAY);
}
@Test
public void daylightSaving() {
CronExpression cronExpression = CronExpression.parse("0 0 9 * * *");
ZonedDateTime last = ZonedDateTime.parse("2021-03-27T09:00:00+01:00[Europe/Amsterdam]");
ZonedDateTime expected = ZonedDateTime.parse("2021-03-28T09:00:00+02:00[Europe/Amsterdam]");
ZonedDateTime actual = cronExpression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
last = ZonedDateTime.parse("2021-10-30T09:00:00+02:00[Europe/Amsterdam]");
expected = ZonedDateTime.parse("2021-10-31T09:00:00+01:00[Europe/Amsterdam]");
actual = cronExpression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
}
}
+3 -3
View File
@@ -8,7 +8,7 @@ apply plugin: "kotlin"
// cglib itself depends on asm and is therefore further transformed by the ShadowJar task to
// depend on org.springframework.asm; this avoids including two different copies of asm.
def cglibVersion = "3.3.0"
def objenesisVersion = "3.1"
def objenesisVersion = "3.2"
configurations {
cglib
@@ -70,8 +70,8 @@ dependencies {
jar {
reproducibleFileOrder = true
preserveFileTimestamps = false // maybe not necessary here, but good for reproducibility
manifest.attributes["Dependencies"] = "jdk.unsupported" // JBoss modules
preserveFileTimestamps = false // maybe not necessary here, but good for reproducibility
manifest.attributes["Dependencies"] = "jdk.unsupported" // for WildFly (-> Objenesis 3.2)
// Inline repackaged cglib classes directly into spring-core jar
dependsOn cglibRepackJar
@@ -163,7 +163,7 @@ public final class BridgeMethodResolver {
}
}
// A non-array type: compare the type itself.
if (!candidateParameter.equals(genericParameter.toClass())) {
if (!ClassUtils.resolvePrimitiveIfNecessary(candidateParameter).equals(ClassUtils.resolvePrimitiveIfNecessary(genericParameter.toClass()))) {
return false;
}
}
@@ -1,6 +1,6 @@
/**
* Spring's repackaging of
* <a href="http://objenesis.org">Objenesis 3.0</a>
* <a href="http://objenesis.org">Objenesis 3.2</a>
* (with SpringObjenesis entry point; for internal use only).
*
* <p>This repackaging technique avoids any potential conflicts with
@@ -0,0 +1,71 @@
/*
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.core
import org.assertj.core.api.Assertions
import org.junit.jupiter.api.Test
class KotlinBridgeMethodResolverTests {
@Test
fun findBridgedMethod() {
val unbridged = GenericRepository::class.java.getDeclaredMethod("delete", Int::class.java)
val bridged = GenericRepository::class.java.getDeclaredMethod("delete", Any::class.java)
Assertions.assertThat(unbridged.isBridge).isFalse
Assertions.assertThat(bridged.isBridge).isTrue
Assertions.assertThat(BridgeMethodResolver.findBridgedMethod(unbridged)).`as`("Unbridged method not returned directly").isEqualTo(unbridged)
Assertions.assertThat(BridgeMethodResolver.findBridgedMethod(bridged)).`as`("Incorrect bridged method returned").isEqualTo(unbridged)
}
@Test
fun findBridgedMethodWithArrays() {
val unbridged = GenericRepository::class.java.getDeclaredMethod("delete", Array<Int>::class.java)
val bridged = GenericRepository::class.java.getDeclaredMethod("delete", Array<Any>::class.java)
Assertions.assertThat(unbridged.isBridge).isFalse
Assertions.assertThat(bridged.isBridge).isTrue
Assertions.assertThat(BridgeMethodResolver.findBridgedMethod(unbridged)).`as`("Unbridged method not returned directly").isEqualTo(unbridged)
Assertions.assertThat(BridgeMethodResolver.findBridgedMethod(bridged)).`as`("Incorrect bridged method returned").isEqualTo(unbridged)
}
}
interface GenericInterface<ID> {
fun delete(id: ID)
fun delete(ids: Array<ID>)
}
abstract class AbstractGenericClass<ID> : GenericInterface<ID> {
override fun delete(id: ID) {
}
override fun delete(ids: Array<ID>) {
}
}
class GenericRepository : AbstractGenericClass<Int>() {
override fun delete(id: Int) {
error("gotcha")
}
override fun delete(ids: Array<Int>) {
error("gotcha")
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -20,6 +20,7 @@ import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.Instant;
@@ -89,8 +90,12 @@ public abstract class AbstractDataBufferAllocatingTests {
}
protected Consumer<DataBuffer> stringConsumer(String expected) {
return stringConsumer(expected, UTF_8);
}
protected Consumer<DataBuffer> stringConsumer(String expected, Charset charset) {
return dataBuffer -> {
String value = dataBuffer.toString(UTF_8);
String value = dataBuffer.toString(charset);
DataBufferUtils.release(dataBuffer);
assertThat(value).isEqualTo(expected);
};
@@ -136,6 +136,7 @@ public final class SpelCompiler implements Opcodes {
private Class<? extends CompiledExpression> createExpressionClass(SpelNodeImpl expressionToCompile) {
// Create class outline 'spel/ExNNN extends org.springframework.expression.spel.CompiledExpression'
String className = "spel/Ex" + getNextSuffix();
String evaluationContextClass = "org/springframework/expression/EvaluationContext";
ClassWriter cw = new ExpressionClassWriter();
cw.visit(V1_8, ACC_PUBLIC, className, null, "org/springframework/expression/spel/CompiledExpression", null);
@@ -151,7 +152,7 @@ public final class SpelCompiler implements Opcodes {
// Create getValue() method
mv = cw.visitMethod(ACC_PUBLIC, "getValue",
"(Ljava/lang/Object;Lorg/springframework/expression/EvaluationContext;)Ljava/lang/Object;", null,
"(Ljava/lang/Object;L" + evaluationContextClass + ";)Ljava/lang/Object;", null,
new String[] {"org/springframework/expression/EvaluationException"});
mv.visitCode();
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -224,8 +224,7 @@ public abstract class AbstractContainerEntityManagerFactoryIntegrationTests
q.setFlushMode(FlushModeType.AUTO);
List<Person> people = q.getResultList();
assertThat(people.size()).isEqualTo(0);
assertThatExceptionOfType(Exception.class).isThrownBy(() ->
q.getSingleResult())
assertThatExceptionOfType(Exception.class).isThrownBy(q::getSingleResult)
.withMessageContaining("closed");
// We would typically expect an IllegalStateException, but Hibernate throws a
// PersistenceException. So we assert the contents of the exception message instead.
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2017 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -34,6 +34,9 @@ import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.annotation.DirtiesContext.HierarchyMode;
import org.springframework.test.context.TestContextAnnotationUtils;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
@@ -105,6 +108,7 @@ abstract class AbstractExpressionEvaluatingCondition implements ExecutionConditi
boolean loadContext = loadContextExtractor.apply(annotation.get());
boolean evaluatedToTrue = evaluateExpression(expression, loadContext, annotationType, context);
ConditionEvaluationResult result;
if (evaluatedToTrue) {
String adjective = (enabledOnTrue ? "enabled" : "disabled");
@@ -114,7 +118,7 @@ abstract class AbstractExpressionEvaluatingCondition implements ExecutionConditi
if (logger.isInfoEnabled()) {
logger.info(reason);
}
return (enabledOnTrue ? ConditionEvaluationResult.enabled(reason)
result = (enabledOnTrue ? ConditionEvaluationResult.enabled(reason)
: ConditionEvaluationResult.disabled(reason));
}
else {
@@ -124,9 +128,26 @@ abstract class AbstractExpressionEvaluatingCondition implements ExecutionConditi
if (logger.isDebugEnabled()) {
logger.debug(reason);
}
return (enabledOnTrue ? ConditionEvaluationResult.disabled(reason) :
result = (enabledOnTrue ? ConditionEvaluationResult.disabled(reason) :
ConditionEvaluationResult.enabled(reason));
}
// If we eagerly loaded the ApplicationContext to evaluate SpEL expressions
// and the test class ends up being disabled, we have to check if the
// user asked for the ApplicationContext to be closed via @DirtiesContext,
// since the DirtiesContextTestExecutionListener will never be invoked for
// a disabled test class.
// See https://github.com/spring-projects/spring-framework/issues/26694
if (loadContext && result.isDisabled() && element instanceof Class) {
Class<?> testClass = (Class<?>) element;
DirtiesContext dirtiesContext = TestContextAnnotationUtils.findMergedAnnotation(testClass, DirtiesContext.class);
if (dirtiesContext != null) {
HierarchyMode hierarchyMode = dirtiesContext.hierarchyMode();
SpringExtension.getTestContextManager(context).getTestContext().markApplicationContextDirty(hierarchyMode);
}
}
return result;
}
private <A extends Annotation> boolean evaluateExpression(String expression, boolean loadContext,
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -287,7 +287,7 @@ public class SpringExtension implements BeforeAllCallback, AfterAllCallback, Tes
* Get the {@link TestContextManager} associated with the supplied {@code ExtensionContext}.
* @return the {@code TestContextManager} (never {@code null})
*/
private static TestContextManager getTestContextManager(ExtensionContext context) {
static TestContextManager getTestContextManager(ExtensionContext context) {
Assert.notNull(context, "ExtensionContext must not be null");
Class<?> testClass = context.getRequiredTestClass();
Store store = getStore(context);
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -557,10 +557,10 @@ class TestContextAnnotationUtilsTests {
@Target(ElementType.TYPE)
@interface MetaConfig {
static class DevConfig {
class DevConfig {
}
static class ProductionConfig {
class ProductionConfig {
}
@@ -607,11 +607,11 @@ class TestContextAnnotationUtilsTests {
}
@MetaConfig
class MetaConfigWithDefaultAttributesTestCase {
static class MetaConfigWithDefaultAttributesTestCase {
}
@MetaConfig(classes = TestContextAnnotationUtilsTests.class)
class MetaConfigWithOverriddenAttributesTestCase {
static class MetaConfigWithOverriddenAttributesTestCase {
}
// -------------------------------------------------------------------------
@@ -0,0 +1,107 @@
/*
* Copyright 2002-2021 the original author 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.test.context.junit.jupiter;
import java.util.concurrent.atomic.AtomicBoolean;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.platform.testkit.engine.EngineTestKit;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.annotation.DirtiesContext;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.fail;
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;
/**
* Integration tests which verify support for {@link DisabledIf @DisabledIf} in
* conjunction with {@link DirtiesContext @DirtiesContext} and the
* {@link SpringExtension} in a JUnit Jupiter environment.
*
* @author Sam Brannen
* @since 5.2.14
* @see EnabledIfAndDirtiesContextTests
*/
class DisabledIfAndDirtiesContextTests {
private static AtomicBoolean contextClosed = new AtomicBoolean();
@BeforeEach
void reset() {
contextClosed.set(false);
}
@Test
void contextShouldBeClosedForEnabledTestClass() {
assertThat(contextClosed).as("context closed").isFalse();
EngineTestKit.engine("junit-jupiter").selectors(
selectClass(EnabledAndDirtiesContextTestCase.class))//
.execute()//
.testEvents()//
.assertStatistics(stats -> stats.started(1).succeeded(1).failed(0));
assertThat(contextClosed).as("context closed").isTrue();
}
@Test
void contextShouldBeClosedForDisabledTestClass() {
assertThat(contextClosed).as("context closed").isFalse();
EngineTestKit.engine("junit-jupiter").selectors(
selectClass(DisabledAndDirtiesContextTestCase.class))//
.execute()//
.testEvents()//
.assertStatistics(stats -> stats.started(0).succeeded(0).failed(0));
assertThat(contextClosed).as("context closed").isTrue();
}
@SpringJUnitConfig(Config.class)
@DisabledIf(expression = "false", loadContext = true)
@DirtiesContext
static class EnabledAndDirtiesContextTestCase {
@Test
void test() {
/* no-op */
}
}
@SpringJUnitConfig(Config.class)
@DisabledIf(expression = "true", loadContext = true)
@DirtiesContext
static class DisabledAndDirtiesContextTestCase {
@Test
void test() {
fail("This test must be disabled");
}
}
@Configuration
static class Config {
@Bean
DisposableBean disposableBean() {
return () -> contextClosed.set(true);
}
}
}
@@ -0,0 +1,107 @@
/*
* Copyright 2002-2021 the original author 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.test.context.junit.jupiter;
import java.util.concurrent.atomic.AtomicBoolean;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.platform.testkit.engine.EngineTestKit;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.annotation.DirtiesContext;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.fail;
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;
/**
* Integration tests which verify support for {@link EnabledIf @EnabledIf} in
* conjunction with {@link DirtiesContext @DirtiesContext} and the
* {@link SpringExtension} in a JUnit Jupiter environment.
*
* @author Sam Brannen
* @since 5.2.14
* @see DisabledIfAndDirtiesContextTests
*/
class EnabledIfAndDirtiesContextTests {
private static AtomicBoolean contextClosed = new AtomicBoolean();
@BeforeEach
void reset() {
contextClosed.set(false);
}
@Test
void contextShouldBeClosedForEnabledTestClass() {
assertThat(contextClosed).as("context closed").isFalse();
EngineTestKit.engine("junit-jupiter").selectors(
selectClass(EnabledAndDirtiesContextTestCase.class))//
.execute()//
.testEvents()//
.assertStatistics(stats -> stats.started(1).succeeded(1).failed(0));
assertThat(contextClosed).as("context closed").isTrue();
}
@Test
void contextShouldBeClosedForDisabledTestClass() {
assertThat(contextClosed).as("context closed").isFalse();
EngineTestKit.engine("junit-jupiter").selectors(
selectClass(DisabledAndDirtiesContextTestCase.class))//
.execute()//
.testEvents()//
.assertStatistics(stats -> stats.started(0).succeeded(0).failed(0));
assertThat(contextClosed).as("context closed").isTrue();
}
@SpringJUnitConfig(Config.class)
@EnabledIf(expression = "true", loadContext = true)
@DirtiesContext
static class EnabledAndDirtiesContextTestCase {
@Test
void test() {
/* no-op */
}
}
@SpringJUnitConfig(Config.class)
@EnabledIf(expression = "false", loadContext = true)
@DirtiesContext
static class DisabledAndDirtiesContextTestCase {
@Test
void test() {
fail("This test must be disabled");
}
}
@Configuration
static class Config {
@Bean
DisposableBean disposableBean() {
return () -> contextClosed.set(true);
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -32,6 +32,7 @@ import org.springframework.test.web.servlet.client.MockMvcWebTestClient;
import org.springframework.util.concurrent.ListenableFuture;
import org.springframework.util.concurrent.ListenableFutureTask;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
@@ -99,7 +100,7 @@ public class AsyncTests {
}
@Test
public void deferredResultWithImmediateValue() throws Exception {
public void deferredResultWithImmediateValue() {
this.testClient.get()
.uri("/1?deferredResultWithImmediateValue=true")
.exchange()
@@ -109,7 +110,7 @@ public class AsyncTests {
}
@Test
public void deferredResultWithDelayedError() throws Exception {
public void deferredResultWithDelayedError() {
this.testClient.get()
.uri("/1?deferredResultWithDelayedError=true")
.exchange()
@@ -118,7 +119,7 @@ public class AsyncTests {
}
@Test
public void listenableFuture() throws Exception {
public void listenableFuture() {
this.testClient.get()
.uri("/1?listenableFuture=true")
.exchange()
@@ -142,17 +143,17 @@ public class AsyncTests {
@RequestMapping(path = "/{id}", produces = "application/json")
private static class AsyncController {
@RequestMapping(params = "callable")
@GetMapping(params = "callable")
public Callable<Person> getCallable() {
return () -> new Person("Joe");
}
@RequestMapping(params = "streaming")
@GetMapping(params = "streaming")
public StreamingResponseBody getStreaming() {
return os -> os.write("name=Joe".getBytes(StandardCharsets.UTF_8));
}
@RequestMapping(params = "streamingSlow")
@GetMapping(params = "streamingSlow")
public StreamingResponseBody getStreamingSlow() {
return os -> {
os.write("name=Joe".getBytes());
@@ -166,41 +167,41 @@ public class AsyncTests {
};
}
@RequestMapping(params = "streamingJson")
@GetMapping(params = "streamingJson")
public ResponseEntity<StreamingResponseBody> getStreamingJson() {
return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON)
.body(os -> os.write("{\"name\":\"Joe\",\"someDouble\":0.5}".getBytes(StandardCharsets.UTF_8)));
}
@RequestMapping(params = "deferredResult")
@GetMapping(params = "deferredResult")
public DeferredResult<Person> getDeferredResult() {
DeferredResult<Person> result = new DeferredResult<>();
delay(100, () -> result.setResult(new Person("Joe")));
return result;
}
@RequestMapping(params = "deferredResultWithImmediateValue")
@GetMapping(params = "deferredResultWithImmediateValue")
public DeferredResult<Person> getDeferredResultWithImmediateValue() {
DeferredResult<Person> result = new DeferredResult<>();
result.setResult(new Person("Joe"));
return result;
}
@RequestMapping(params = "deferredResultWithDelayedError")
@GetMapping(params = "deferredResultWithDelayedError")
public DeferredResult<Person> getDeferredResultWithDelayedError() {
DeferredResult<Person> result = new DeferredResult<>();
delay(100, () -> result.setErrorResult(new RuntimeException("Delayed Error")));
return result;
}
@RequestMapping(params = "listenableFuture")
@GetMapping(params = "listenableFuture")
public ListenableFuture<Person> getListenableFuture() {
ListenableFutureTask<Person> futureTask = new ListenableFutureTask<>(() -> new Person("Joe"));
delay(100, futureTask);
return futureTask;
}
@RequestMapping(params = "completableFutureWithImmediateValue")
@GetMapping(params = "completableFutureWithImmediateValue")
public CompletableFuture<Person> getCompletableFutureWithImmediateValue() {
CompletableFuture<Person> future = new CompletableFuture<>();
future.complete(new Person("Joe"));
@@ -0,0 +1,71 @@
/*
* Copyright 2002-2021 the original author 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.test.web.servlet.samples.client.standalone;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Flux;
import reactor.test.StepVerifier;
import org.springframework.test.web.Person;
import org.springframework.test.web.reactive.server.FluxExchangeResult;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.test.web.servlet.client.MockMvcWebTestClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import static java.time.Duration.ofMillis;
import static org.assertj.core.api.Assertions.assertThat;
/**
* SSE controller tests with MockMvc and WebTestClient.
*
* @author Rossen Stoyanchev
*/
public class SseTests {
private final WebTestClient testClient =
MockMvcWebTestClient.bindToController(new SseController()).build();
@Test
public void sse() {
FluxExchangeResult<Person> exchangeResult = this.testClient.get()
.uri("/persons")
.exchange()
.expectStatus().isOk()
.expectHeader().contentType("text/event-stream")
.returnResult(Person.class);
StepVerifier.create(exchangeResult.getResponseBody())
.expectNext(new Person("N0"), new Person("N1"), new Person("N2"))
.expectNextCount(4)
.consumeNextWith(person -> assertThat(person.getName()).endsWith("7"))
.thenCancel()
.verify();
}
@RestController
private static class SseController {
@GetMapping(path = "/persons", produces = "text/event-stream")
public Flux<Person> getPersonStream() {
return Flux.interval(ofMillis(100)).take(50).onBackpressureBuffer(50)
.map(index -> new Person("N" + index));
}
}
}
@@ -25,6 +25,7 @@
<Logger name="org.springframework.test.context.support.DelegatingSmartContextLoader" level="info" />
<Logger name="org.springframework.test.context.support.AbstractGenericContextLoader" level="info" />
<Logger name="org.springframework.test.context.support.AnnotationConfigContextLoader" level="info" />
<Logger name="org.springframework.test.context.support.AbstractDirtiesContextTestExecutionListener" level="warn" />
<Logger name="org.springframework.test.context.support.TestPropertySourceUtils" level="trace" />
<Logger name="org.springframework.beans" level="warn" />
<Logger name="org.springframework.test.web.servlet.result" level="debug" />
+2
View File
@@ -7,6 +7,7 @@ dependencies {
compile(project(":spring-beans"))
compile(project(":spring-core"))
compileOnly(project(":kotlin-coroutines"))
compileOnly("io.projectreactor.tools:blockhound")
optional(project(":spring-aop"))
optional(project(":spring-context"))
optional(project(":spring-oxm"))
@@ -75,6 +76,7 @@ dependencies {
testCompile("org.skyscreamer:jsonassert")
testCompile("org.xmlunit:xmlunit-assertj")
testCompile("org.xmlunit:xmlunit-matchers")
testCompile("io.projectreactor.tools:blockhound")
testRuntime("com.sun.mail:javax.mail")
testRuntime("com.sun.xml.bind:jaxb-core")
testRuntime("com.sun.xml.bind:jaxb-impl")
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -99,6 +99,12 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable
* @see <a href="https://tools.ietf.org/html/rfc7231#section-5.3.5">Section 5.3.5 of RFC 7231</a>
*/
public static final String ACCEPT_LANGUAGE = "Accept-Language";
/**
* The HTTP {@code Accept-Patch} header field name.
* @since 5.3.6
* @see <a href="https://tools.ietf.org/html/rfc5789#section-3.1">Section 3.1 of RFC 5789</a>
*/
public static final String ACCEPT_PATCH = "Accept-Patch";
/**
* The HTTP {@code Accept-Ranges} header field name.
* @see <a href="https://tools.ietf.org/html/rfc7233#section-2.3">Section 5.3.5 of RFC 7233</a>
@@ -525,6 +531,25 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable
.collect(Collectors.toList());
}
/**
* Set the list of acceptable {@linkplain MediaType media types} for
* {@code PATCH} methods, as specified by the {@code Accept-Patch} header.
* @since 5.3.6
*/
public void setAcceptPatch(List<MediaType> mediaTypes) {
set(ACCEPT_PATCH, MediaType.toString(mediaTypes));
}
/**
* Return the list of acceptable {@linkplain MediaType media types} for
* {@code PATCH} methods, as specified by the {@code Accept-Patch} header.
* <p>Returns an empty list when the acceptable media types are unspecified.
* @since 5.3.6
*/
public List<MediaType> getAcceptPatch() {
return MediaType.parseMediaTypes(get(ACCEPT_PATCH));
}
/**
* Set the (new) value of the {@code Access-Control-Allow-Credentials} response header.
*/
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -28,6 +28,7 @@ import java.util.Optional;
import org.springframework.core.io.Resource;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
@@ -65,6 +66,7 @@ public final class MediaTypeFactory {
*/
private static MultiValueMap<String, MediaType> parseMimeTypes() {
InputStream is = MediaTypeFactory.class.getResourceAsStream(MIME_TYPES_FILE_NAME);
Assert.state(is != null, MIME_TYPES_FILE_NAME + " not found in classpath");
try (BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.US_ASCII))) {
MultiValueMap<String, MediaType> result = new LinkedMultiValueMap<>();
String line;
@@ -82,7 +84,7 @@ public final class MediaTypeFactory {
return result;
}
catch (IOException ex) {
throw new IllegalStateException("Could not load '" + MIME_TYPES_FILE_NAME + "'", ex);
throw new IllegalStateException("Could not read " + MIME_TYPES_FILE_NAME, ex);
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -18,7 +18,6 @@ package org.springframework.http.codec;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@@ -171,12 +170,14 @@ public class ServerSentEventHttpMessageWriter implements HttpMessageWriter<Objec
if (this.encoder == null) {
throw new CodecException("No SSE encoder configured and the data is not String.");
}
DataBuffer buffer = ((Encoder<T>) this.encoder).encodeValue(data, factory, dataType, mediaType, hints);
Hints.touchDataBuffer(buffer, hints, logger);
return Flux.just(factory.join(Arrays.asList(
encodeText(eventContent, mediaType, factory),
buffer,
encodeText("\n\n", mediaType, factory))));
return Flux.defer(() -> {
DataBuffer startBuffer = encodeText(eventContent, mediaType, factory);
DataBuffer endBuffer = encodeText("\n\n", mediaType, factory);
DataBuffer dataBuffer = ((Encoder<T>) this.encoder).encodeValue(data, factory, dataType, mediaType, hints);
Hints.touchDataBuffer(dataBuffer, hints, logger);
return Flux.just(startBuffer, dataBuffer, endBuffer);
});
}
private void writeField(String fieldName, Object fieldValue, StringBuilder sb) {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,6 +17,7 @@
package org.springframework.http.codec.multipart;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -78,6 +79,8 @@ public class DefaultPartHttpMessageReader extends LoggingCodecSupport implements
private Mono<Path> fileStorageDirectory = Mono.defer(this::defaultFileStorageDirectory).cache();
private Charset headersCharset = StandardCharsets.UTF_8;
/**
* Configure the maximum amount of memory that is allowed per headers section of each part.
@@ -132,7 +135,7 @@ public class DefaultPartHttpMessageReader extends LoggingCodecSupport implements
}
/**
* Sets the directory used to store parts larger than
* Set the directory used to store parts larger than
* {@link #setMaxInMemorySize(int) maxInMemorySize}. By default, a directory
* named {@code spring-webflux-multipart} is created under the system
* temporary directory.
@@ -151,7 +154,7 @@ public class DefaultPartHttpMessageReader extends LoggingCodecSupport implements
}
/**
* Sets the Reactor {@link Scheduler} to be used for creating files and
* Set the Reactor {@link Scheduler} to be used for creating files and
* directories, and writing to files. By default,
* {@link Schedulers#boundedElastic()} is used, but this property allows for
* changing it to an externally managed scheduler.
@@ -171,13 +174,11 @@ public class DefaultPartHttpMessageReader extends LoggingCodecSupport implements
* in memory nor file.
* When {@code false}, parts are backed by
* in-memory and/or file storage. Defaults to {@code false}.
*
* <p><strong>NOTE</strong> that with streaming enabled, the
* {@code Flux<Part>} that is produced by this message reader must be
* consumed in the original order, i.e. the order of the HTTP message.
* Additionally, the {@linkplain Part#content() body contents} must either
* be completely consumed or canceled before moving to the next part.
*
* <p>Also note that enabling this property effectively ignores
* {@link #setMaxInMemorySize(int) maxInMemorySize},
* {@link #setMaxDiskUsagePerPart(long) maxDiskUsagePerPart},
@@ -188,6 +189,18 @@ public class DefaultPartHttpMessageReader extends LoggingCodecSupport implements
this.streaming = streaming;
}
/**
* Set the character set used to decode headers.
* Defaults to UTF-8 as per RFC 7578.
* @param headersCharset the charset to use for decoding headers
* @since 5.3.6
* @see <a href="https://tools.ietf.org/html/rfc7578#section-5.1">RFC-7578 Section 5.2</a>
*/
public void setHeadersCharset(Charset headersCharset) {
Assert.notNull(headersCharset, "HeadersCharset must not be null");
this.headersCharset = headersCharset;
}
@Override
public List<MediaType> getReadableMediaTypes() {
return Collections.singletonList(MediaType.MULTIPART_FORM_DATA);
@@ -214,7 +227,7 @@ public class DefaultPartHttpMessageReader extends LoggingCodecSupport implements
message.getHeaders().getContentType() + "\""));
}
Flux<MultipartParser.Token> tokens = MultipartParser.parse(message.getBody(), boundary,
this.maxHeadersSize);
this.maxHeadersSize, this.headersCharset);
return PartGenerator.createParts(tokens, this.maxParts, this.maxInMemorySize, this.maxDiskUsagePerPart,
this.streaming, this.fileStorageDirectory, this.blockingOperationScheduler);
@@ -222,7 +235,7 @@ public class DefaultPartHttpMessageReader extends LoggingCodecSupport implements
}
@Nullable
private static byte[] boundary(HttpMessage message) {
private byte[] boundary(HttpMessage message) {
MediaType contentType = message.getHeaders().getContentType();
if (contentType != null) {
String boundary = contentType.getParameter("boundary");
@@ -231,7 +244,7 @@ public class DefaultPartHttpMessageReader extends LoggingCodecSupport implements
if (len > 2 && boundary.charAt(0) == '"' && boundary.charAt(len - 1) == '"') {
boundary = boundary.substring(1, len - 1);
}
return boundary.getBytes(StandardCharsets.ISO_8859_1);
return boundary.getBytes(this.headersCharset);
}
}
return null;
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,7 +16,7 @@
package org.springframework.http.codec.multipart;
import java.nio.charset.StandardCharsets;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -69,11 +69,14 @@ final class MultipartParser extends BaseSubscriber<DataBuffer> {
private final AtomicBoolean requestOutstanding = new AtomicBoolean();
private final Charset headersCharset;
private MultipartParser(FluxSink<Token> sink, byte[] boundary, int maxHeadersSize) {
private MultipartParser(FluxSink<Token> sink, byte[] boundary, int maxHeadersSize, Charset headersCharset) {
this.sink = sink;
this.boundary = boundary;
this.maxHeadersSize = maxHeadersSize;
this.headersCharset = headersCharset;
this.state = new AtomicReference<>(new PreambleState());
}
@@ -82,11 +85,13 @@ final class MultipartParser extends BaseSubscriber<DataBuffer> {
* @param buffers the input buffers
* @param boundary the multipart boundary, as found in the {@code Content-Type} header
* @param maxHeadersSize the maximum buffered header size
* @param headersCharset the charset to use for decoding headers
* @return a stream of parsed tokens
*/
public static Flux<Token> parse(Flux<DataBuffer> buffers, byte[] boundary, int maxHeadersSize) {
public static Flux<Token> parse(Flux<DataBuffer> buffers, byte[] boundary, int maxHeadersSize,
Charset headersCharset) {
return Flux.create(sink -> {
MultipartParser parser = new MultipartParser(sink, boundary, maxHeadersSize);
MultipartParser parser = new MultipartParser(sink, boundary, maxHeadersSize, headersCharset);
sink.onCancel(parser::onSinkCancel);
sink.onRequest(n -> parser.requestBuffer());
buffers.subscribe(parser);
@@ -180,7 +185,7 @@ final class MultipartParser extends BaseSubscriber<DataBuffer> {
/**
* Represents the output of {@link #parse(Flux, byte[], int)}.
* Represents the output of {@link #parse(Flux, byte[], int, Charset)}.
*/
public abstract static class Token {
@@ -372,7 +377,6 @@ final class MultipartParser extends BaseSubscriber<DataBuffer> {
DataBufferUtils.release(buf);
emitHeaders(parseHeaders());
// TODO: no need to check result of changeState, no further statements
changeState(this, new BodyState(), bodyBuf);
}
else {
@@ -408,7 +412,7 @@ final class MultipartParser extends BaseSubscriber<DataBuffer> {
}
DataBuffer joined = this.buffers.get(0).factory().join(this.buffers);
this.buffers.clear();
String string = joined.toString(StandardCharsets.ISO_8859_1);
String string = joined.toString(MultipartParser.this.headersCharset);
DataBufferUtils.release(joined);
String[] lines = string.split(HEADER_ENTRY_SEPARATOR);
HttpHeaders result = new HttpHeaders();
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -20,6 +20,7 @@ import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.ByteBuffer;
import java.nio.channels.WritableByteChannel;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
@@ -53,7 +54,7 @@ import org.springframework.util.FastByteArrayOutputStream;
/**
* Subscribes to a token stream (i.e. the result of
* {@link MultipartParser#parse(Flux, byte[], int)}, and produces a flux of {@link Part} objects.
* {@link MultipartParser#parse(Flux, byte[], int, Charset)}, and produces a flux of {@link Part} objects.
*
* @author Arjen Poutsma
* @since 5.3
@@ -577,6 +578,9 @@ final class PartGenerator extends BaseSubscriber<MultipartParser.Token> {
private WritingFileState createFileState(Path directory) {
try {
if (!Files.exists(directory)) {
Files.createDirectory(directory);
}
Path tempFile = Files.createTempFile(directory, null, ".multipart");
if (logger.isTraceEnabled()) {
logger.trace("Storing multipart data in file " + tempFile);
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -28,7 +28,7 @@ import org.springframework.core.MethodParameter;
* @see MissingPathVariableException
*/
@SuppressWarnings("serial")
public class MissingMatrixVariableException extends ServletRequestBindingException {
public class MissingMatrixVariableException extends MissingRequestValueException {
private final String variableName;
@@ -41,7 +41,20 @@ public class MissingMatrixVariableException extends ServletRequestBindingExcepti
* @param parameter the method parameter
*/
public MissingMatrixVariableException(String variableName, MethodParameter parameter) {
super("");
this(variableName, parameter, false);
}
/**
* Constructor for use when a value was present but converted to {@code null}.
* @param variableName the name of the missing matrix variable
* @param parameter the method parameter
* @param missingAfterConversion whether the value became null after conversion
* @since 5.3.6
*/
public MissingMatrixVariableException(
String variableName, MethodParameter parameter, boolean missingAfterConversion) {
super("", missingAfterConversion);
this.variableName = variableName;
this.parameter = parameter;
}
@@ -49,8 +62,9 @@ public class MissingMatrixVariableException extends ServletRequestBindingExcepti
@Override
public String getMessage() {
return "Missing matrix variable '" + this.variableName +
"' for method parameter of type " + this.parameter.getNestedParameterType().getSimpleName();
return "Required matrix variable '" + this.variableName + "' for method parameter type " +
this.parameter.getNestedParameterType().getSimpleName() + " is " +
(isMissingAfterConversion() ? "present but converted to null" : "not present");
}
/**
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -30,7 +30,7 @@ import org.springframework.core.MethodParameter;
* @see MissingMatrixVariableException
*/
@SuppressWarnings("serial")
public class MissingPathVariableException extends ServletRequestBindingException {
public class MissingPathVariableException extends MissingRequestValueException {
private final String variableName;
@@ -43,7 +43,20 @@ public class MissingPathVariableException extends ServletRequestBindingException
* @param parameter the method parameter
*/
public MissingPathVariableException(String variableName, MethodParameter parameter) {
super("");
this(variableName, parameter, false);
}
/**
* Constructor for use when a value was present but converted to {@code null}.
* @param variableName the name of the missing path variable
* @param parameter the method parameter
* @param missingAfterConversion whether the value became null after conversion
* @since 5.3.6
*/
public MissingPathVariableException(
String variableName, MethodParameter parameter, boolean missingAfterConversion) {
super("", missingAfterConversion);
this.variableName = variableName;
this.parameter = parameter;
}
@@ -51,8 +64,9 @@ public class MissingPathVariableException extends ServletRequestBindingException
@Override
public String getMessage() {
return "Missing URI template variable '" + this.variableName +
"' for method parameter of type " + this.parameter.getNestedParameterType().getSimpleName();
return "Required URI template variable '" + this.variableName + "' for method parameter type " +
this.parameter.getNestedParameterType().getSimpleName() + " is " +
(isMissingAfterConversion() ? "present but converted to null" : "not present");
}
/**
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -28,7 +28,7 @@ import org.springframework.core.MethodParameter;
* @see MissingRequestHeaderException
*/
@SuppressWarnings("serial")
public class MissingRequestCookieException extends ServletRequestBindingException {
public class MissingRequestCookieException extends MissingRequestValueException {
private final String cookieName;
@@ -41,7 +41,20 @@ public class MissingRequestCookieException extends ServletRequestBindingExceptio
* @param parameter the method parameter
*/
public MissingRequestCookieException(String cookieName, MethodParameter parameter) {
super("");
this(cookieName, parameter, false);
}
/**
* Constructor for use when a value was present but converted to {@code null}.
* @param cookieName the name of the missing request cookie
* @param parameter the method parameter
* @param missingAfterConversion whether the value became null after conversion
* @since 5.3.6
*/
public MissingRequestCookieException(
String cookieName, MethodParameter parameter, boolean missingAfterConversion) {
super("", missingAfterConversion);
this.cookieName = cookieName;
this.parameter = parameter;
}
@@ -49,8 +62,9 @@ public class MissingRequestCookieException extends ServletRequestBindingExceptio
@Override
public String getMessage() {
return "Missing cookie '" + this.cookieName +
"' for method parameter of type " + this.parameter.getNestedParameterType().getSimpleName();
return "Required cookie '" + this.cookieName + "' for method parameter type " +
this.parameter.getNestedParameterType().getSimpleName() + " is " +
(isMissingAfterConversion() ? "present but converted to null" : "not present");
}
/**
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -28,7 +28,7 @@ import org.springframework.core.MethodParameter;
* @see MissingRequestCookieException
*/
@SuppressWarnings("serial")
public class MissingRequestHeaderException extends ServletRequestBindingException {
public class MissingRequestHeaderException extends MissingRequestValueException {
private final String headerName;
@@ -41,7 +41,20 @@ public class MissingRequestHeaderException extends ServletRequestBindingExceptio
* @param parameter the method parameter
*/
public MissingRequestHeaderException(String headerName, MethodParameter parameter) {
super("");
this(headerName, parameter, false);
}
/**
* Constructor for use when a value was present but converted to {@code null}.
* @param headerName the name of the missing request header
* @param parameter the method parameter
* @param missingAfterConversion whether the value became null after conversion
* @since 5.3.6
*/
public MissingRequestHeaderException(
String headerName, MethodParameter parameter, boolean missingAfterConversion) {
super("", missingAfterConversion);
this.headerName = headerName;
this.parameter = parameter;
}
@@ -49,8 +62,9 @@ public class MissingRequestHeaderException extends ServletRequestBindingExceptio
@Override
public String getMessage() {
return "Missing request header '" + this.headerName +
"' for method parameter of type " + this.parameter.getNestedParameterType().getSimpleName();
String typeName = this.parameter.getNestedParameterType().getSimpleName();
return "Required request header '" + this.headerName + "' for method parameter type " + typeName + " is " +
(isMissingAfterConversion() ? "present but converted to null" : "not present");
}
/**
@@ -0,0 +1,50 @@
/*
* Copyright 2002-2021 the original author 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.bind;
/**
* Base class for {@link ServletRequestBindingException} exceptions that could
* not bind because the request value is required but is either missing or
* otherwise resolves to {@code null} after conversion.
*
* @author Rossen Stoyanchev
* @since 5.3.6
*/
@SuppressWarnings("serial")
public class MissingRequestValueException extends ServletRequestBindingException {
private final boolean missingAfterConversion;
public MissingRequestValueException(String msg) {
this(msg, false);
}
public MissingRequestValueException(String msg, boolean missingAfterConversion) {
super(msg);
this.missingAfterConversion = missingAfterConversion;
}
/**
* Whether the request value was present but converted to {@code null}, e.g. via
* {@code org.springframework.core.convert.support.IdToEntityConverter}.
*/
public boolean isMissingAfterConversion() {
return this.missingAfterConversion;
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2012 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -23,7 +23,7 @@ package org.springframework.web.bind;
* @since 2.0.2
*/
@SuppressWarnings("serial")
public class MissingServletRequestParameterException extends ServletRequestBindingException {
public class MissingServletRequestParameterException extends MissingRequestValueException {
private final String parameterName;
@@ -36,7 +36,20 @@ public class MissingServletRequestParameterException extends ServletRequestBindi
* @param parameterType the expected type of the missing parameter
*/
public MissingServletRequestParameterException(String parameterName, String parameterType) {
super("");
this(parameterName, parameterType, false);
}
/**
* Constructor for use when a value was present but converted to {@code null}.
* @param parameterName the name of the missing parameter
* @param parameterType the expected type of the missing parameter
* @param missingAfterConversion whether the value became null after conversion
* @since 5.3.6
*/
public MissingServletRequestParameterException(
String parameterName, String parameterType, boolean missingAfterConversion) {
super("", missingAfterConversion);
this.parameterName = parameterName;
this.parameterType = parameterType;
}
@@ -44,7 +57,9 @@ public class MissingServletRequestParameterException extends ServletRequestBindi
@Override
public String getMessage() {
return "Required " + this.parameterType + " parameter '" + this.parameterName + "' is not present";
return "Required request parameter '" + this.parameterName + "' for method parameter type " +
this.parameterType + " is " +
(isMissingAfterConversion() ? "present but converted to null" : "not present");
}
/**
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -26,6 +26,7 @@ import java.util.Set;
import java.util.function.Predicate;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.lang.Nullable;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
@@ -69,7 +70,7 @@ public final class HandlerTypePredicate implements Predicate<Class<?>> {
@Override
public boolean test(Class<?> controllerType) {
public boolean test(@Nullable Class<?> controllerType) {
if (!hasSelectors()) {
return true;
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -24,6 +24,7 @@ import org.springframework.web.bind.MissingRequestCookieException;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.context.request.NativeWebRequest;
/**
* A base abstract class to resolve method arguments annotated with
@@ -70,6 +71,12 @@ public abstract class AbstractCookieValueMethodArgumentResolver extends Abstract
throw new MissingRequestCookieException(name, parameter);
}
@Override
protected void handleMissingValueAfterConversion(
String name, MethodParameter parameter, NativeWebRequest request) throws Exception {
throw new MissingRequestCookieException(name, parameter, true);
}
private static final class CookieValueNamedValueInfo extends NamedValueInfo {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -135,7 +135,7 @@ public abstract class AbstractNamedValueMethodArgumentResolver implements Handle
// Check for null value after conversion of incoming argument value
if (arg == null && namedValueInfo.defaultValue == null &&
namedValueInfo.required && !nestedParameter.isOptional()) {
handleMissingValue(namedValueInfo.name, nestedParameter, webRequest);
handleMissingValueAfterConversion(namedValueInfo.name, nestedParameter, webRequest);
}
}
@@ -237,6 +237,19 @@ public abstract class AbstractNamedValueMethodArgumentResolver implements Handle
"' for method parameter of type " + parameter.getNestedParameterType().getSimpleName());
}
/**
* Invoked when a named value is present but becomes {@code null} after conversion.
* @param name the name for the value
* @param parameter the method parameter
* @param request the current request
* @since 5.3.6
*/
protected void handleMissingValueAfterConversion(String name, MethodParameter parameter, NativeWebRequest request)
throws Exception {
handleMissingValue(name, parameter, request);
}
/**
* A {@code null} results in a {@code false} value for {@code boolean}s or an exception for other primitives.
*/
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -87,6 +87,12 @@ public class RequestHeaderMethodArgumentResolver extends AbstractNamedValueMetho
throw new MissingRequestHeaderException(name, parameter);
}
@Override
protected void handleMissingValueAfterConversion(
String name, MethodParameter parameter, NativeWebRequest request) throws Exception {
throw new MissingRequestHeaderException(name, parameter, true);
}
private static final class RequestHeaderNamedValueInfo extends NamedValueInfo {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -190,6 +190,20 @@ public class RequestParamMethodArgumentResolver extends AbstractNamedValueMethod
protected void handleMissingValue(String name, MethodParameter parameter, NativeWebRequest request)
throws Exception {
handleMissingValueInternal(name, parameter, request, false);
}
@Override
protected void handleMissingValueAfterConversion(
String name, MethodParameter parameter, NativeWebRequest request) throws Exception {
handleMissingValueInternal(name, parameter, request, true);
}
protected void handleMissingValueInternal(
String name, MethodParameter parameter, NativeWebRequest request, boolean missingAfterConversion)
throws Exception {
HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class);
if (MultipartResolutionDelegate.isMultipartArgument(parameter)) {
if (servletRequest == null || !MultipartResolutionDelegate.isMultipartRequest(servletRequest)) {
@@ -201,7 +215,7 @@ public class RequestParamMethodArgumentResolver extends AbstractNamedValueMethod
}
else {
throw new MissingServletRequestParameterException(name,
parameter.getNestedParameterType().getSimpleName());
parameter.getNestedParameterType().getSimpleName(), missingAfterConversion);
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -20,9 +20,12 @@ import java.util.Collections;
import java.util.List;
import org.springframework.core.ResolvableType;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.lang.Nullable;
import org.springframework.util.CollectionUtils;
/**
* Exception for errors that fit response status 415 (unsupported media type).
@@ -41,6 +44,9 @@ public class UnsupportedMediaTypeStatusException extends ResponseStatusException
@Nullable
private final ResolvableType bodyType;
@Nullable
private final HttpMethod method;
/**
* Constructor for when the specified Content-Type is invalid.
@@ -50,13 +56,14 @@ public class UnsupportedMediaTypeStatusException extends ResponseStatusException
this.contentType = null;
this.supportedMediaTypes = Collections.emptyList();
this.bodyType = null;
this.method = null;
}
/**
* Constructor for when the Content-Type can be parsed but is not supported.
*/
public UnsupportedMediaTypeStatusException(@Nullable MediaType contentType, List<MediaType> supportedTypes) {
this(contentType, supportedTypes, null);
this(contentType, supportedTypes, null, null);
}
/**
@@ -65,11 +72,30 @@ public class UnsupportedMediaTypeStatusException extends ResponseStatusException
*/
public UnsupportedMediaTypeStatusException(@Nullable MediaType contentType, List<MediaType> supportedTypes,
@Nullable ResolvableType bodyType) {
this(contentType, supportedTypes, bodyType, null);
}
/**
* Constructor that provides the HTTP method.
* @since 5.3.6
*/
public UnsupportedMediaTypeStatusException(@Nullable MediaType contentType, List<MediaType> supportedTypes,
@Nullable HttpMethod method) {
this(contentType, supportedTypes, null, method);
}
/**
* Constructor for when trying to encode from or decode to a specific Java type.
* @since 5.3.6
*/
public UnsupportedMediaTypeStatusException(@Nullable MediaType contentType, List<MediaType> supportedTypes,
@Nullable ResolvableType bodyType, @Nullable HttpMethod method) {
super(HttpStatus.UNSUPPORTED_MEDIA_TYPE, initReason(contentType, bodyType));
this.contentType = contentType;
this.supportedMediaTypes = Collections.unmodifiableList(supportedTypes);
this.bodyType = bodyType;
this.method = method;
}
private static String initReason(@Nullable MediaType contentType, @Nullable ResolvableType bodyType) {
@@ -107,4 +133,14 @@ public class UnsupportedMediaTypeStatusException extends ResponseStatusException
return this.bodyType;
}
@Override
public HttpHeaders getResponseHeaders() {
if (HttpMethod.PATCH != this.method || CollectionUtils.isEmpty(this.supportedMediaTypes) ) {
return HttpHeaders.EMPTY;
}
HttpHeaders headers = new HttpHeaders();
headers.setAcceptPatch(this.supportedMediaTypes);
return headers;
}
}
@@ -23,6 +23,9 @@ import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collectors;
import reactor.blockhound.BlockHound;
import reactor.blockhound.integration.BlockHoundIntegration;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.context.ApplicationContext;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
@@ -418,4 +421,20 @@ public final class WebHttpHandlerBuilder {
return new WebHttpHandlerBuilder(this);
}
/**
* {@code BlockHoundIntegration} for spring-web classes.
* @since 5.3.6
*/
public static class SpringWebBlockHoundIntegration implements BlockHoundIntegration {
@Override
public void applyTo(BlockHound.Builder builder) {
// Avoid hard references potentially anywhere in spring-web (no need for structural dependency)
builder.allowBlockingCallsInside("org.springframework.web.util.HtmlUtils", "<clinit>");
}
}
}
@@ -357,18 +357,28 @@ public class UriComponentsBuilder implements UriBuilder, Cloneable {
String value = matcher.group(1).trim();
String host = value;
int portSeparatorIdx = value.lastIndexOf(':');
if (portSeparatorIdx > value.lastIndexOf(']')) {
int squareBracketIdx = value.lastIndexOf(']');
if (portSeparatorIdx > squareBracketIdx) {
if (squareBracketIdx == -1 && value.indexOf(':') != portSeparatorIdx) {
throw new IllegalArgumentException("Invalid IPv4 address: " + value);
}
host = value.substring(0, portSeparatorIdx);
port = Integer.parseInt(value.substring(portSeparatorIdx + 1));
try {
port = Integer.parseInt(value.substring(portSeparatorIdx + 1));
}
catch (NumberFormatException ex) {
throw new IllegalArgumentException(
"Failed to parse a port from \"forwarded\"-type header value: " + value);
}
}
return new InetSocketAddress(host, port);
return InetSocketAddress.createUnresolved(host, port);
}
}
String forHeader = request.getHeaders().getFirst("X-Forwarded-For");
if (StringUtils.hasText(forHeader)) {
String host = StringUtils.tokenizeToStringArray(forHeader, ",")[0];
return new InetSocketAddress(host, port);
return InetSocketAddress.createUnresolved(host, port);
}
return null;
@@ -886,7 +896,11 @@ public class UriComponentsBuilder implements UriBuilder, Cloneable {
private void adaptForwardedHost(String rawValue) {
int portSeparatorIdx = rawValue.lastIndexOf(':');
if (portSeparatorIdx > rawValue.lastIndexOf(']')) {
int squareBracketIdx = rawValue.lastIndexOf(']');
if (portSeparatorIdx > squareBracketIdx) {
if (squareBracketIdx == -1 && rawValue.indexOf(':') != portSeparatorIdx) {
throw new IllegalArgumentException("Invalid IPv4 address: " + rawValue);
}
host(rawValue.substring(0, portSeparatorIdx));
port(Integer.parseInt(rawValue.substring(portSeparatorIdx + 1)));
}
@@ -0,0 +1,15 @@
# Copyright 2002-2021 the original author 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.
org.springframework.web.server.adapter.WebHttpHandlerBuilder$SpringWebBlockHoundIntegration
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -31,7 +31,7 @@ import static org.assertj.core.api.Assertions.assertThat;
public class HttpEntityTests {
@Test
public void noHeaders() {
void noHeaders() {
String body = "foo";
HttpEntity<String> entity = new HttpEntity<>(body);
assertThat(entity.getBody()).isSameAs(body);
@@ -39,7 +39,7 @@ public class HttpEntityTests {
}
@Test
public void httpHeaders() {
void httpHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.TEXT_PLAIN);
String body = "foo";
@@ -50,7 +50,7 @@ public class HttpEntityTests {
}
@Test
public void multiValueMap() {
void multiValueMap() {
MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
map.set("Content-Type", "text/plain");
String body = "foo";
@@ -61,30 +61,30 @@ public class HttpEntityTests {
}
@Test
public void testEquals() {
void testEquals() {
MultiValueMap<String, String> map1 = new LinkedMultiValueMap<>();
map1.set("Content-Type", "text/plain");
MultiValueMap<String, String> map2 = new LinkedMultiValueMap<>();
map2.set("Content-Type", "application/json");
assertThat(new HttpEntity<>().equals(new HttpEntity<Object>())).isTrue();
assertThat(new HttpEntity<>(map1).equals(new HttpEntity<Object>())).isFalse();
assertThat(new HttpEntity<>().equals(new HttpEntity<Object>(map2))).isFalse();
assertThat(new HttpEntity<>().equals(new HttpEntity<>())).isTrue();
assertThat(new HttpEntity<>(map1).equals(new HttpEntity<>())).isFalse();
assertThat(new HttpEntity<>().equals(new HttpEntity<>(map2))).isFalse();
assertThat(new HttpEntity<>(map1).equals(new HttpEntity<Object>(map1))).isTrue();
assertThat(new HttpEntity<>(map1).equals(new HttpEntity<Object>(map2))).isFalse();
assertThat(new HttpEntity<>(map1).equals(new HttpEntity<>(map1))).isTrue();
assertThat(new HttpEntity<>(map1).equals(new HttpEntity<>(map2))).isFalse();
assertThat(new HttpEntity<String>(null, null).equals(new HttpEntity<String>(null, null))).isTrue();
assertThat(new HttpEntity<>("foo", null).equals(new HttpEntity<String>(null, null))).isFalse();
assertThat(new HttpEntity<String>(null, null).equals(new HttpEntity<>("bar", null))).isFalse();
assertThat(new HttpEntity<>("foo", map1).equals(new HttpEntity<String>("foo", map1))).isTrue();
assertThat(new HttpEntity<>("foo", map1).equals(new HttpEntity<String>("bar", map1))).isFalse();
assertThat(new HttpEntity<>("foo", map1).equals(new HttpEntity<>("foo", map1))).isTrue();
assertThat(new HttpEntity<>("foo", map1).equals(new HttpEntity<>("bar", map1))).isFalse();
}
@Test
public void responseEntity() {
void responseEntity() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.TEXT_PLAIN);
String body = "foo";
@@ -104,7 +104,7 @@ public class HttpEntityTests {
}
@Test
public void requestEntity() throws Exception {
void requestEntity() throws Exception {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.TEXT_PLAIN);
String body = "foo";
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -150,8 +150,12 @@ class ServerSentEventHttpMessageWriterTests extends AbstractDataBufferAllocating
testWrite(source, outputMessage, Pojo.class);
StepVerifier.create(outputMessage.getBody())
.consumeNextWith(stringConsumer("data:{\"foo\":\"foofoo\",\"bar\":\"barbar\"}\n\n"))
.consumeNextWith(stringConsumer("data:{\"foo\":\"foofoofoo\",\"bar\":\"barbarbar\"}\n\n"))
.consumeNextWith(stringConsumer("data:"))
.consumeNextWith(stringConsumer("{\"foo\":\"foofoo\",\"bar\":\"barbar\"}"))
.consumeNextWith(stringConsumer("\n\n"))
.consumeNextWith(stringConsumer("data:"))
.consumeNextWith(stringConsumer("{\"foo\":\"foofoofoo\",\"bar\":\"barbarbar\"}"))
.consumeNextWith(stringConsumer("\n\n"))
.expectComplete()
.verify();
}
@@ -168,12 +172,16 @@ class ServerSentEventHttpMessageWriterTests extends AbstractDataBufferAllocating
testWrite(source, outputMessage, Pojo.class);
StepVerifier.create(outputMessage.getBody())
.consumeNextWith(stringConsumer("data:{\n" +
.consumeNextWith(stringConsumer("data:"))
.consumeNextWith(stringConsumer("{\n" +
"data: \"foo\" : \"foofoo\",\n" +
"data: \"bar\" : \"barbar\"\n" + "data:}\n\n"))
.consumeNextWith(stringConsumer("data:{\n" +
"data: \"bar\" : \"barbar\"\n" + "data:}"))
.consumeNextWith(stringConsumer("\n\n"))
.consumeNextWith(stringConsumer("data:"))
.consumeNextWith(stringConsumer("{\n" +
"data: \"foo\" : \"foofoofoo\",\n" +
"data: \"bar\" : \"barbarbar\"\n" + "data:}\n\n"))
"data: \"bar\" : \"barbarbar\"\n" + "data:}"))
.consumeNextWith(stringConsumer("\n\n"))
.expectComplete()
.verify();
}
@@ -190,11 +198,9 @@ class ServerSentEventHttpMessageWriterTests extends AbstractDataBufferAllocating
assertThat(outputMessage.getHeaders().getContentType()).isEqualTo(mediaType);
StepVerifier.create(outputMessage.getBody())
.consumeNextWith(dataBuffer -> {
String value = dataBuffer.toString(charset);
DataBufferUtils.release(dataBuffer);
assertThat(value).isEqualTo("data:{\"foo\":\"foo\uD834\uDD1E\",\"bar\":\"bar\uD834\uDD1E\"}\n\n");
})
.consumeNextWith(stringConsumer("data:", charset))
.consumeNextWith(stringConsumer("{\"foo\":\"foo\uD834\uDD1E\",\"bar\":\"bar\uD834\uDD1E\"}", charset))
.consumeNextWith(stringConsumer("\n\n", charset))
.expectComplete()
.verify();
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -23,6 +23,7 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;
@@ -251,6 +252,24 @@ public class DefaultPartHttpMessageReaderTests {
latch.await();
}
@ParameterizedDefaultPartHttpMessageReaderTest
public void utf8Headers(String displayName, DefaultPartHttpMessageReader reader) throws InterruptedException {
MockServerHttpRequest request = createRequest(
new ClassPathResource("utf8.multipart", getClass()), "\"simple-boundary\"");
Flux<Part> result = reader.read(forClass(Part.class), request, emptyMap());
CountDownLatch latch = new CountDownLatch(1);
StepVerifier.create(result)
.consumeNextWith(part -> {
assertThat(part.headers()).containsEntry("Føø", Collections.singletonList("Bår"));
testPart(part, null, "This is plain ASCII text.", latch);
})
.verifyComplete();
latch.await();
}
private void testBrowser(DefaultPartHttpMessageReader reader, Resource resource, String boundary)
throws InterruptedException {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -45,20 +45,20 @@ public class ServletServerHttpRequestTests {
@BeforeEach
public void create() {
void create() {
mockRequest = new MockHttpServletRequest();
request = new ServletServerHttpRequest(mockRequest);
}
@Test
public void getMethod() {
void getMethod() {
mockRequest.setMethod("POST");
assertThat(request.getMethod()).as("Invalid method").isEqualTo(HttpMethod.POST);
}
@Test
public void getUriForSimplePath() throws URISyntaxException {
void getUriForSimplePath() throws URISyntaxException {
URI uri = new URI("https://example.com/path");
mockRequest.setScheme(uri.getScheme());
mockRequest.setServerName(uri.getHost());
@@ -69,7 +69,7 @@ public class ServletServerHttpRequestTests {
}
@Test
public void getUriWithQueryString() throws URISyntaxException {
void getUriWithQueryString() throws URISyntaxException {
URI uri = new URI("https://example.com/path?query");
mockRequest.setScheme(uri.getScheme());
mockRequest.setServerName(uri.getHost());
@@ -80,7 +80,7 @@ public class ServletServerHttpRequestTests {
}
@Test // SPR-16414
public void getUriWithQueryParam() throws URISyntaxException {
void getUriWithQueryParam() throws URISyntaxException {
mockRequest.setScheme("https");
mockRequest.setServerPort(443);
mockRequest.setServerName("example.com");
@@ -90,7 +90,7 @@ public class ServletServerHttpRequestTests {
}
@Test // SPR-16414
public void getUriWithMalformedQueryParam() throws URISyntaxException {
void getUriWithMalformedQueryParam() throws URISyntaxException {
mockRequest.setScheme("https");
mockRequest.setServerPort(443);
mockRequest.setServerName("example.com");
@@ -100,7 +100,7 @@ public class ServletServerHttpRequestTests {
}
@Test // SPR-13876
public void getUriWithEncoding() throws URISyntaxException {
void getUriWithEncoding() throws URISyntaxException {
URI uri = new URI("https://example.com/%E4%B8%AD%E6%96%87" +
"?redirect=https%3A%2F%2Fgithub.com%2Fspring-projects%2Fspring-framework");
mockRequest.setScheme(uri.getScheme());
@@ -112,7 +112,7 @@ public class ServletServerHttpRequestTests {
}
@Test
public void getHeaders() {
void getHeaders() {
String headerName = "MyHeader";
String headerValue1 = "value1";
String headerValue2 = "value2";
@@ -132,7 +132,7 @@ public class ServletServerHttpRequestTests {
}
@Test
public void getHeadersWithEmptyContentTypeAndEncoding() {
void getHeadersWithEmptyContentTypeAndEncoding() {
String headerName = "MyHeader";
String headerValue1 = "value1";
String headerValue2 = "value2";
@@ -152,8 +152,8 @@ public class ServletServerHttpRequestTests {
}
@Test
public void getBody() throws IOException {
byte[] content = "Hello World".getBytes("UTF-8");
void getBody() throws IOException {
byte[] content = "Hello World".getBytes(StandardCharsets.UTF_8);
mockRequest.setContent(content);
byte[] result = FileCopyUtils.copyToByteArray(request.getBody());
@@ -161,16 +161,17 @@ public class ServletServerHttpRequestTests {
}
@Test
public void getFormBody() throws IOException {
void getFormBody() throws IOException {
// Charset (SPR-8676)
mockRequest.setContentType("application/x-www-form-urlencoded; charset=UTF-8");
mockRequest.setMethod("POST");
mockRequest.addParameter("name 1", "value 1");
mockRequest.addParameter("name 2", new String[] {"value 2+1", "value 2+2"});
mockRequest.addParameter("name 2", "value 2+1", "value 2+2");
mockRequest.addParameter("name 3", (String) null);
byte[] result = FileCopyUtils.copyToByteArray(request.getBody());
byte[] content = "name+1=value+1&name+2=value+2%2B1&name+2=value+2%2B2&name+3".getBytes("UTF-8");
byte[] content = "name+1=value+1&name+2=value+2%2B1&name+2=value+2%2B2&name+3".getBytes(
StandardCharsets.UTF_8);
assertThat(result).as("Invalid content returned").isEqualTo(content);
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -36,6 +36,7 @@ import org.springframework.web.testfixture.servlet.MockHttpServletRequest;
import org.springframework.web.testfixture.servlet.MockHttpServletResponse;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.Mockito.mock;
/**
@@ -440,7 +441,7 @@ public class ForwardedHeaderFilterTests {
request.addHeader(FORWARDED, "for=\"[2001:db8:cafe::17]\"");
HttpServletRequest actual = filterAndGetWrappedRequest();
assertThat(actual.getRemoteAddr()).isEqualTo(actual.getRemoteHost()).isEqualTo("2001:db8:cafe:0:0:0:0:17");
assertThat(actual.getRemoteAddr()).isEqualTo(actual.getRemoteHost()).isEqualTo("[2001:db8:cafe::17]");
assertThat(actual.getRemotePort()).isEqualTo(MockHttpServletRequest.DEFAULT_SERVER_PORT);
}
@@ -458,7 +459,7 @@ public class ForwardedHeaderFilterTests {
request.addHeader(FORWARDED, "For=\"[2001:db8:cafe::17]:47011\"");
HttpServletRequest actual = filterAndGetWrappedRequest();
assertThat(actual.getRemoteAddr()).isEqualTo(actual.getRemoteHost()).isEqualTo("2001:db8:cafe:0:0:0:0:17");
assertThat(actual.getRemoteAddr()).isEqualTo(actual.getRemoteHost()).isEqualTo("[2001:db8:cafe::17]");
assertThat(actual.getRemotePort()).isEqualTo(47011);
}
@@ -470,6 +471,13 @@ public class ForwardedHeaderFilterTests {
assertThat(actual.getRemoteAddr()).isEqualTo(actual.getRemoteHost()).isEqualTo("203.0.113.195");
assertThat(actual.getRemotePort()).isEqualTo(MockHttpServletRequest.DEFAULT_SERVER_PORT);
}
@Test // gh-26748
public void forwardedForInvalidIpV6Address() {
request.addHeader(FORWARDED, "for=\"2a02:918:175:ab60:45ee:c12c:dac1:808b\"");
assertThatIllegalArgumentException().isThrownBy(
ForwardedHeaderFilterTests.this::filterAndGetWrappedRequest);
}
}
@Nested
@@ -453,6 +453,21 @@ class UriComponentsBuilderTests {
assertThat(result.toString()).isEqualTo("http://[1abc:2abc:3abc::5ABC:6abc]:8080/mvc-showcase");
}
@Test // gh-26748
void fromHttpRequestWithForwardedInvalidIPv6Address() {
MockHttpServletRequest request = new MockHttpServletRequest();
request.setScheme("http");
request.setServerName("localhost");
request.setServerPort(-1);
request.setRequestURI("/mvc-showcase");
request.addHeader("X-Forwarded-Host", "2a02:918:175:ab60:45ee:c12c:dac1:808b");
HttpRequest httpRequest = new ServletServerHttpRequest(request);
assertThatIllegalArgumentException().isThrownBy(() ->
UriComponentsBuilder.fromHttpRequest(httpRequest).build());
}
@Test
void fromHttpRequestWithForwardedHost() {
MockHttpServletRequest request = new MockHttpServletRequest();
@@ -0,0 +1,5 @@
--simple-boundary
Føø: Bår
This is plain ASCII text.
--simple-boundary--
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -110,7 +110,7 @@ public class PathResourceResolver extends AbstractResourceResolver {
*/
protected Mono<Resource> getResource(String resourcePath, Resource location) {
try {
if (location instanceof ClassPathResource) {
if (!(location instanceof UrlResource)) {
resourcePath = UriUtils.decode(resourcePath, StandardCharsets.UTF_8);
}
Resource resource = location.createRelative(resourcePath);
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -37,6 +37,7 @@ import org.springframework.http.server.PathContainer;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.util.Assert;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.reactive.HandlerMapping;
import org.springframework.web.reactive.result.condition.NameValueExpression;
@@ -173,7 +174,8 @@ public abstract class RequestMappingInfoHandlerMapping extends AbstractHandlerMe
String httpMethod = request.getMethodValue();
Set<HttpMethod> methods = helper.getAllowedMethods();
if (HttpMethod.OPTIONS.matches(httpMethod)) {
HttpOptionsHandler handler = new HttpOptionsHandler(methods);
Set<MediaType> mediaTypes = helper.getConsumablePatchMediaTypes();
HttpOptionsHandler handler = new HttpOptionsHandler(methods, mediaTypes);
return new HandlerMethod(handler, HTTP_OPTIONS_HANDLE_METHOD);
}
throw new MethodNotAllowedException(httpMethod, methods);
@@ -188,7 +190,7 @@ public abstract class RequestMappingInfoHandlerMapping extends AbstractHandlerMe
catch (InvalidMediaTypeException ex) {
throw new UnsupportedMediaTypeStatusException(ex.getMessage());
}
throw new UnsupportedMediaTypeStatusException(contentType, new ArrayList<>(mediaTypes));
throw new UnsupportedMediaTypeStatusException(contentType, new ArrayList<>(mediaTypes), exchange.getRequest().getMethod());
}
if (helper.hasProducesMismatch()) {
@@ -301,6 +303,22 @@ public abstract class RequestMappingInfoHandlerMapping extends AbstractHandlerMe
collect(Collectors.toList());
}
/**
* Return declared "consumable" types but only among those that have
* PATCH specified, or that have no methods at all.
*/
public Set<MediaType> getConsumablePatchMediaTypes() {
Set<MediaType> result = new LinkedHashSet<>();
for (PartialMatch match : this.partialMatches) {
Set<RequestMethod> methods = match.getInfo().getMethodsCondition().getMethods();
if (methods.isEmpty() || methods.contains(RequestMethod.PATCH)) {
result.addAll(match.getInfo().getConsumesCondition().getConsumableMediaTypes());
}
}
return result;
}
/**
* Container for a RequestMappingInfo that matches the URL path at least.
@@ -367,8 +385,9 @@ public abstract class RequestMappingInfoHandlerMapping extends AbstractHandlerMe
private final HttpHeaders headers = new HttpHeaders();
public HttpOptionsHandler(Set<HttpMethod> declaredMethods) {
public HttpOptionsHandler(Set<HttpMethod> declaredMethods, Set<MediaType> acceptPatch) {
this.headers.setAllow(initAllowedHttpMethods(declaredMethods));
this.headers.setAcceptPatch(new ArrayList<>(acceptPatch));
}
private static Set<HttpMethod> initAllowedHttpMethods(Set<HttpMethod> declaredMethods) {
@@ -21,6 +21,7 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.reactive.asFlow
import kotlinx.coroutines.reactive.awaitSingle
import kotlinx.coroutines.reactive.awaitSingle
import kotlinx.coroutines.reactive.awaitSingleOrNull
import kotlinx.coroutines.reactor.asFlux
import kotlinx.coroutines.reactor.mono
import org.reactivestreams.Publisher
@@ -143,6 +144,18 @@ suspend inline fun <reified T : Any> WebClient.ResponseSpec.awaitBody() : T =
else -> bodyToMono<T>().awaitSingle()
}
/**
* Coroutines variant of [WebClient.ResponseSpec.bodyToMono].
*
* @author Valentin Shakhov
* @since 5.3.6
*/
suspend inline fun <reified T : Any> WebClient.ResponseSpec.awaitBodyOrNull() : T? =
when (T::class) {
Unit::class -> awaitBodilessEntity().let { Unit as T? }
else -> bodyToMono<T>().awaitSingleOrNull()
}
/**
* Coroutines variant of [WebClient.ResponseSpec.toBodilessEntity].
*/
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -32,6 +32,7 @@ import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.springframework.core.io.buffer.DataBuffer;
@@ -51,6 +52,7 @@ import org.springframework.web.server.ServerWebExchange;
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 org.springframework.web.util.UriUtils;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.assertj.core.api.Assertions.assertThat;
@@ -232,6 +234,25 @@ public class ResourceWebHandlerTests {
assertResponseBody(exchange, "foo bar foo bar foo bar");
}
@Test
public void getResourceFromFileSystem() throws Exception {
String path = new ClassPathResource("", getClass()).getFile().getCanonicalPath()
.replace('\\', '/').replace("classes/java", "resources") + "/";
ResourceWebHandler handler = new ResourceWebHandler();
handler.setLocations(Collections.singletonList(new FileSystemResource(path)));
handler.afterPropertiesSet();
MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get(""));
setPathWithinHandlerMapping(exchange, UriUtils.encodePath("test/foo with spaces.css", UTF_8));
handler.handle(exchange).block(TIMEOUT);
HttpHeaders headers = exchange.getResponse().getHeaders();
assertThat(headers.getContentType()).isEqualTo(MediaType.parseMediaType("text/css"));
assertThat(headers.getContentLength()).isEqualTo(17);
assertResponseBody(exchange, "h1 { color:red; }");
}
@Test // SPR-14577
public void getMediaTypeWithFavorPathExtensionOff() throws Exception {
List<Resource> paths = Collections.singletonList(new ClassPathResource("test/", getClass()));
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -37,6 +37,7 @@ import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Controller;
import org.springframework.util.ClassUtils;
import org.springframework.util.MultiValueMap;
@@ -44,6 +45,7 @@ import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.reactive.BindingContext;
import org.springframework.web.reactive.HandlerMapping;
@@ -192,10 +194,12 @@ public class RequestMappingInfoHandlerMappingTests {
List<HttpMethod> allMethodExceptTrace = new ArrayList<>(Arrays.asList(HttpMethod.values()));
allMethodExceptTrace.remove(HttpMethod.TRACE);
testHttpOptions("/foo", EnumSet.of(HttpMethod.GET, HttpMethod.HEAD, HttpMethod.OPTIONS));
testHttpOptions("/person/1", EnumSet.of(HttpMethod.PUT, HttpMethod.OPTIONS));
testHttpOptions("/persons", EnumSet.copyOf(allMethodExceptTrace));
testHttpOptions("/something", EnumSet.of(HttpMethod.PUT, HttpMethod.POST));
testHttpOptions("/foo", EnumSet.of(HttpMethod.GET, HttpMethod.HEAD, HttpMethod.OPTIONS), null);
testHttpOptions("/person/1", EnumSet.of(HttpMethod.PUT, HttpMethod.OPTIONS), null);
testHttpOptions("/persons", EnumSet.copyOf(allMethodExceptTrace), null);
testHttpOptions("/something", EnumSet.of(HttpMethod.PUT, HttpMethod.POST), null);
testHttpOptions("/qux", EnumSet.of(HttpMethod.PATCH,HttpMethod.GET,HttpMethod.HEAD,HttpMethod.OPTIONS),
new MediaType("foo", "bar"));
}
@Test
@@ -313,6 +317,26 @@ public class RequestMappingInfoHandlerMappingTests {
assertThat(uriVariables.get("cars")).isEqualTo("cars");
}
@Test
public void handlePatchUnsupportedMediaType() {
MockServerHttpRequest request = MockServerHttpRequest.patch("/qux")
.header("content-type", "application/xml")
.build();
ServerWebExchange exchange = MockServerWebExchange.from(request);
Mono<Object> mono = this.handlerMapping.getHandler(exchange);
StepVerifier.create(mono)
.expectErrorSatisfies(ex -> {
assertThat(ex).isInstanceOf(UnsupportedMediaTypeStatusException.class);
UnsupportedMediaTypeStatusException umtse = (UnsupportedMediaTypeStatusException) ex;
MediaType mediaType = new MediaType("foo", "bar");
assertThat(umtse.getSupportedMediaTypes()).containsExactly(mediaType);
assertThat(umtse.getResponseHeaders().getAcceptPatch()).containsExactly(mediaType);
})
.verify();
}
@SuppressWarnings("unchecked")
private <T> void assertError(Mono<Object> mono, final Class<T> exceptionClass, final Consumer<T> consumer) {
@@ -332,7 +356,7 @@ public class RequestMappingInfoHandlerMappingTests {
assertError(mono, UnsupportedMediaTypeStatusException.class, ex -> assertThat(ex.getSupportedMediaTypes()).as("Invalid supported consumable media types").isEqualTo(Collections.singletonList(new MediaType("application", "xml"))));
}
private void testHttpOptions(String requestURI, Set<HttpMethod> allowedMethods) {
private void testHttpOptions(String requestURI, Set<HttpMethod> allowedMethods, @Nullable MediaType acceptPatch) {
ServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.options(requestURI));
HandlerMethod handlerMethod = (HandlerMethod) this.handlerMapping.getHandler(exchange).block();
@@ -346,7 +370,13 @@ public class RequestMappingInfoHandlerMappingTests {
Object value = result.getReturnValue();
assertThat(value).isNotNull();
assertThat(value.getClass()).isEqualTo(HttpHeaders.class);
assertThat(((HttpHeaders) value).getAllow()).isEqualTo(allowedMethods);
HttpHeaders headers = (HttpHeaders) value;
assertThat(headers.getAllow()).hasSameElementsAs(allowedMethods);
if (acceptPatch != null && headers.getAllow().contains(HttpMethod.PATCH) ) {
assertThat(headers.getAcceptPatch()).containsExactly(acceptPatch);
}
}
private void testMediaTypeNotAcceptable(String url) {
@@ -430,6 +460,16 @@ public class RequestMappingInfoHandlerMappingTests {
return headers;
}
@RequestMapping(value = "/qux", method = RequestMethod.GET, produces = "application/xml")
public String getBaz() {
return "";
}
@RequestMapping(value = "/qux", method = RequestMethod.PATCH, consumes = "foo/bar")
public void patchBaz(String value) {
}
public void dummy() { }
}
@@ -136,6 +136,25 @@ class WebClientExtensionsTests {
}
}
@Test
fun awaitBodyOrNull() {
val spec = mockk<WebClient.ResponseSpec>()
every { spec.bodyToMono<String>() } returns Mono.just("foo")
runBlocking {
assertThat(spec.awaitBodyOrNull<String>()).isEqualTo("foo")
}
}
@Test
fun `awaitBodyOrNull of type Unit`() {
val spec = mockk<WebClient.ResponseSpec>()
val entity = mockk<ResponseEntity<Void>>()
every { spec.toBodilessEntity() } returns Mono.just(entity)
runBlocking {
assertThat(spec.awaitBodyOrNull<Unit>()).isEqualTo(Unit)
}
}
@Test
fun awaitBodilessEntity() {
val spec = mockk<WebClient.ResponseSpec>()
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -60,7 +60,7 @@ public class CorsRegistration {
/**
* Alternative to {@link #allowCredentials} that supports origins declared
* via wildcard patterns. Please, see
* @link CorsConfiguration#setAllowedOriginPatterns(List)} for details.
* {@link CorsConfiguration#setAllowedOriginPatterns(List)} for details.
* <p>By default this is not set.
* @since 5.3
*/
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -34,6 +34,7 @@ import org.springframework.web.util.ServletRequestPathUtils;
import org.springframework.web.util.UrlPathHelper;
import org.springframework.web.util.pattern.PathPattern;
import org.springframework.web.util.pattern.PathPatternParser;
import org.springframework.web.util.pattern.PatternParseException;
/**
* Wraps a {@link HandlerInterceptor} and uses URL patterns to determine whether
@@ -64,10 +65,10 @@ public final class MappedInterceptor implements HandlerInterceptor {
@Nullable
private final PathPattern[] includePatterns;
private final PatternAdapter[] includePatterns;
@Nullable
private final PathPattern[] excludePatterns;
private final PatternAdapter[] excludePatterns;
private PathMatcher pathMatcher = defaultPathMatcher;
@@ -88,21 +89,11 @@ public final class MappedInterceptor implements HandlerInterceptor {
public MappedInterceptor(@Nullable String[] includePatterns, @Nullable String[] excludePatterns,
HandlerInterceptor interceptor, @Nullable PathPatternParser parser) {
this.includePatterns = initPatterns(includePatterns, parser);
this.excludePatterns = initPatterns(excludePatterns, parser);
this.includePatterns = PatternAdapter.initPatterns(includePatterns, parser);
this.excludePatterns = PatternAdapter.initPatterns(excludePatterns, parser);
this.interceptor = interceptor;
}
@Nullable
private static PathPattern[] initPatterns(
@Nullable String[] patterns, @Nullable PathPatternParser parser) {
if (ObjectUtils.isEmpty(patterns)) {
return null;
}
parser = (parser != null ? parser : PathPatternParser.defaultInstance);
return Arrays.stream(patterns).map(parser::parse).toArray(PathPattern[]::new);
}
/**
* Variant of
@@ -151,7 +142,7 @@ public final class MappedInterceptor implements HandlerInterceptor {
@Nullable
public String[] getPathPatterns() {
return (!ObjectUtils.isEmpty(this.includePatterns) ?
Arrays.stream(this.includePatterns).map(PathPattern::getPatternString).toArray(String[]::new) :
Arrays.stream(this.includePatterns).map(PatternAdapter::getPatternString).toArray(String[]::new) :
null);
}
@@ -199,8 +190,8 @@ public final class MappedInterceptor implements HandlerInterceptor {
}
boolean isPathContainer = (path instanceof PathContainer);
if (!ObjectUtils.isEmpty(this.excludePatterns)) {
for (PathPattern pattern : this.excludePatterns) {
if (matchPattern(path, isPathContainer, pattern)) {
for (PatternAdapter adapter : this.excludePatterns) {
if (adapter.match(path, isPathContainer, this.pathMatcher)) {
return false;
}
}
@@ -208,20 +199,14 @@ public final class MappedInterceptor implements HandlerInterceptor {
if (ObjectUtils.isEmpty(this.includePatterns)) {
return true;
}
for (PathPattern pattern : this.includePatterns) {
if (matchPattern(path, isPathContainer, pattern)) {
for (PatternAdapter adapter : this.includePatterns) {
if (adapter.match(path, isPathContainer, this.pathMatcher)) {
return true;
}
}
return false;
}
private boolean matchPattern(Object path, boolean isPathContainer, PathPattern pattern) {
return (isPathContainer ?
pattern.matches((PathContainer) path) :
this.pathMatcher.match(pattern.getPatternString(), (String) path));
}
/**
* Determine a match for the given lookup path.
* @param lookupPath the current request path
@@ -233,8 +218,8 @@ public final class MappedInterceptor implements HandlerInterceptor {
public boolean matches(String lookupPath, PathMatcher pathMatcher) {
pathMatcher = (this.pathMatcher != defaultPathMatcher ? this.pathMatcher : pathMatcher);
if (!ObjectUtils.isEmpty(this.excludePatterns)) {
for (PathPattern pattern : this.excludePatterns) {
if (pathMatcher.match(pattern.getPatternString(), lookupPath)) {
for (PatternAdapter adapter : this.excludePatterns) {
if (pathMatcher.match(adapter.getPatternString(), lookupPath)) {
return false;
}
}
@@ -242,8 +227,8 @@ public final class MappedInterceptor implements HandlerInterceptor {
if (ObjectUtils.isEmpty(this.includePatterns)) {
return true;
}
for (PathPattern pattern : this.includePatterns) {
if (pathMatcher.match(pattern.getPatternString(), lookupPath)) {
for (PatternAdapter adapter : this.includePatterns) {
if (pathMatcher.match(adapter.getPatternString(), lookupPath)) {
return true;
}
}
@@ -274,4 +259,64 @@ public final class MappedInterceptor implements HandlerInterceptor {
this.interceptor.afterCompletion(request, response, handler, ex);
}
/**
* Contains both the parsed {@link PathPattern} and the raw String pattern,
* and uses the former when the cached path is {@link PathContainer} or the
* latter otherwise. If the pattern cannot be parsed due to unsupported
* syntax, then {@link PathMatcher} is used for all requests.
* @since 5.3.6
*/
private static class PatternAdapter {
private final String patternString;
@Nullable
private final PathPattern pathPattern;
public PatternAdapter(String pattern, @Nullable PathPatternParser parser) {
this.patternString = pattern;
this.pathPattern = initPathPattern(pattern, parser);
}
@Nullable
private static PathPattern initPathPattern(String pattern, @Nullable PathPatternParser parser) {
try {
return (parser != null ? parser : PathPatternParser.defaultInstance).parse(pattern);
}
catch (PatternParseException ex) {
return null;
}
}
public String getPatternString() {
return this.patternString;
}
public boolean match(Object path, boolean isPathContainer, PathMatcher pathMatcher) {
if (isPathContainer) {
PathContainer pathContainer = (PathContainer) path;
if (this.pathPattern != null) {
return this.pathPattern.matches(pathContainer);
}
String lookupPath = pathContainer.value();
path = UrlPathHelper.defaultInstance.removeSemicolonContent(lookupPath);
}
return pathMatcher.match(this.patternString, (String) path);
}
@Nullable
public static PatternAdapter[] initPatterns(
@Nullable String[] patterns, @Nullable PathPatternParser parser) {
if (ObjectUtils.isEmpty(patterns)) {
return null;
}
return Arrays.stream(patterns)
.map(pattern -> new PatternAdapter(pattern, parser))
.toArray(PatternAdapter[]::new);
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -72,7 +72,7 @@ public class AcceptHeaderLocaleResolver implements LocaleResolver {
/**
* Configure a fixed default locale to fall back on if the request does not
* have an "Accept-Language" header.
* <p>By default this is not set in which case when there is "Accept-Language"
* <p>By default this is not set in which case when there is no "Accept-Language"
* header, the default locale for the server is used as defined in
* {@link HttpServletRequest#getLocale()}.
* @param defaultLocale the default locale to use
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -244,7 +244,8 @@ public abstract class RequestMappingInfoHandlerMapping extends AbstractHandlerMe
if (helper.hasMethodsMismatch()) {
Set<String> methods = helper.getAllowedMethods();
if (HttpMethod.OPTIONS.matches(request.getMethod())) {
HttpOptionsHandler handler = new HttpOptionsHandler(methods);
Set<MediaType> mediaTypes = helper.getConsumablePatchMediaTypes();
HttpOptionsHandler handler = new HttpOptionsHandler(methods, mediaTypes);
return new HandlerMethod(handler, HTTP_OPTIONS_HANDLE_METHOD);
}
throw new HttpRequestMethodNotSupportedException(request.getMethod(), methods);
@@ -411,6 +412,21 @@ public abstract class RequestMappingInfoHandlerMapping extends AbstractHandlerMe
return result;
}
/**
* Return declared "consumable" types but only among those that have
* PATCH specified, or that have no methods at all.
*/
public Set<MediaType> getConsumablePatchMediaTypes() {
Set<MediaType> result = new LinkedHashSet<>();
for (PartialMatch match : this.partialMatches) {
Set<RequestMethod> methods = match.getInfo().getMethodsCondition().getMethods();
if (methods.isEmpty() || methods.contains(RequestMethod.PATCH)) {
result.addAll(match.getInfo().getConsumesCondition().getConsumableMediaTypes());
}
}
return result;
}
/**
* Container for a RequestMappingInfo that matches the URL path at least.
@@ -475,8 +491,9 @@ public abstract class RequestMappingInfoHandlerMapping extends AbstractHandlerMe
private final HttpHeaders headers = new HttpHeaders();
public HttpOptionsHandler(Set<String> declaredMethods) {
public HttpOptionsHandler(Set<String> declaredMethods, Set<MediaType> acceptPatch) {
this.headers.setAllow(initAllowedHttpMethods(declaredMethods));
this.headers.setAcceptPatch(new ArrayList<>(acceptPatch));
}
private static Set<HttpMethod> initAllowedHttpMethods(Set<String> declaredMethods) {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -126,6 +126,12 @@ public class MatrixVariableMethodArgumentResolver extends AbstractNamedValueMeth
throw new MissingMatrixVariableException(name, parameter);
}
@Override
protected void handleMissingValueAfterConversion(
String name, MethodParameter parameter, NativeWebRequest request) throws Exception {
throw new MissingMatrixVariableException(name, parameter, true);
}
private static final class MatrixVariableNamedValueInfo extends NamedValueInfo {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -97,7 +97,9 @@ import org.springframework.web.util.UriComponentsBuilder;
*/
public class MvcUriComponentsBuilder {
/** Well-known name for the {@link CompositeUriComponentsContributor} object in the bean factory. */
/**
* Well-known name for the {@link CompositeUriComponentsContributor} object in the bean factory.
*/
public static final String MVC_URI_COMPONENTS_CONTRIBUTOR_BEAN_NAME = "mvcUriComponentsContributor";
@@ -716,7 +718,7 @@ public class MvcUriComponentsBuilder {
@Override
@Nullable
public Object intercept(Object obj, Method method, Object[] args, @Nullable MethodProxy proxy) {
public Object intercept(@Nullable Object obj, Method method, Object[] args, @Nullable MethodProxy proxy) {
switch (method.getName()) {
case "getControllerType": return this.controllerType;
case "getControllerMethod": return this.controllerMethod;
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -101,6 +101,13 @@ public class PathVariableMethodArgumentResolver extends AbstractNamedValueMethod
throw new MissingPathVariableException(name, parameter);
}
@Override
protected void handleMissingValueAfterConversion(
String name, MethodParameter parameter, NativeWebRequest request) throws Exception {
throw new MissingPathVariableException(name, parameter, true);
}
@Override
@SuppressWarnings("unchecked")
protected void handleResolvedValue(@Nullable Object arg, String name, MethodParameter parameter,
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2017 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -57,7 +57,7 @@ public interface RequestBodyAdvice {
* @param targetType the target type, not necessarily the same as the method
* parameter type, e.g. for {@code HttpEntity<String>}.
* @param converterType the converter used to deserialize the body
* @return the input request or a new instance, never {@code null}
* @return the input request or a new instance (never {@code null})
*/
HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter,
Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException;
@@ -83,8 +83,8 @@ public interface RequestBodyAdvice {
* @param targetType the target type, not necessarily the same as the method
* parameter type, e.g. for {@code HttpEntity<String>}.
* @param converterType the selected converter type
* @return the value to use or {@code null} which may then raise an
* {@code HttpMessageNotReadableException} if the argument is required.
* @return the value to use, or {@code null} which may then raise an
* {@code HttpMessageNotReadableException} if the argument is required
*/
@Nullable
Object handleEmptyBody(@Nullable Object body, HttpInputMessage inputMessage, MethodParameter parameter,
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2015 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.servlet.mvc.method.annotation;
import java.io.IOException;
@@ -25,10 +26,10 @@ import org.springframework.lang.Nullable;
/**
* A convenient starting point for implementing
* {@link org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice
* ResponseBodyAdvice} with default method implementations.
* {@link org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdvice
* RequestBodyAdvice} with default method implementations.
*
* <p>Sub-classes are required to implement {@link #supports} to return true
* <p>Subclasses are required to implement {@link #supports} to return true
* depending on when the advice applies.
*
* @author Rossen Stoyanchev
@@ -41,8 +42,7 @@ public abstract class RequestBodyAdviceAdapter implements RequestBodyAdvice {
*/
@Override
public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter,
Type targetType, Class<? extends HttpMessageConverter<?>> converterType)
throws IOException {
Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
return inputMessage;
}
@@ -62,9 +62,8 @@ public abstract class RequestBodyAdviceAdapter implements RequestBodyAdvice {
*/
@Override
@Nullable
public Object handleEmptyBody(@Nullable Object body, HttpInputMessage inputMessage,
MethodParameter parameter, Type targetType,
Class<? extends HttpMessageConverter<?>> converterType) {
public Object handleEmptyBody(@Nullable Object body, HttpInputMessage inputMessage, MethodParameter parameter,
Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
return body;
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -231,6 +231,12 @@ public abstract class ResponseEntityExceptionHandler {
List<MediaType> mediaTypes = ex.getSupportedMediaTypes();
if (!CollectionUtils.isEmpty(mediaTypes)) {
headers.setAccept(mediaTypes);
if (request instanceof ServletWebRequest) {
ServletWebRequest servletWebRequest = (ServletWebRequest) request;
if (HttpMethod.PATCH.equals(servletWebRequest.getHttpMethod())) {
headers.setAcceptPatch(mediaTypes);
}
}
}
return handleExceptionInternal(ex, null, headers, status, request);
@@ -281,6 +281,9 @@ public class DefaultHandlerExceptionResolver extends AbstractHandlerExceptionRes
List<MediaType> mediaTypes = ex.getSupportedMediaTypes();
if (!CollectionUtils.isEmpty(mediaTypes)) {
response.setHeader("Accept", MediaType.toString(mediaTypes));
if (request.getMethod().equals("PATCH")) {
response.setHeader("Accept-Patch", MediaType.toString(mediaTypes));
}
}
response.sendError(HttpServletResponse.SC_UNSUPPORTED_MEDIA_TYPE);
return new ModelAndView();
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -33,9 +33,11 @@ import javax.servlet.http.HttpServletRequest;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.springframework.http.server.PathContainer;
import org.springframework.lang.Nullable;
import org.springframework.util.StringUtils;
import org.springframework.web.context.support.ServletContextResource;
import org.springframework.web.util.ServletRequestPathUtils;
import org.springframework.web.util.UriUtils;
import org.springframework.web.util.UrlPathHelper;
@@ -151,7 +153,7 @@ public class PathResourceResolver extends AbstractResourceResolver {
for (Resource location : locations) {
try {
String pathToUse = encodeIfNecessary(resourcePath, request, location);
String pathToUse = encodeOrDecodeIfNecessary(resourcePath, request, location);
Resource resource = getResource(pathToUse, location);
if (resource != null) {
return resource;
@@ -255,8 +257,11 @@ public class PathResourceResolver extends AbstractResourceResolver {
return (resourcePath.startsWith(locationPath) && !isInvalidEncodedPath(resourcePath));
}
private String encodeIfNecessary(String path, @Nullable HttpServletRequest request, Resource location) {
if (shouldEncodeRelativePath(location) && request != null) {
private String encodeOrDecodeIfNecessary(String path, @Nullable HttpServletRequest request, Resource location) {
if (shouldDecodeRelativePath(location, request)) {
return UriUtils.decode(path, StandardCharsets.UTF_8);
}
else if (shouldEncodeRelativePath(location) && request != null) {
Charset charset = this.locationCharsets.getOrDefault(location, StandardCharsets.UTF_8);
StringBuilder sb = new StringBuilder();
StringTokenizer tokenizer = new StringTokenizer(path, "/");
@@ -275,8 +280,15 @@ public class PathResourceResolver extends AbstractResourceResolver {
}
}
private boolean shouldDecodeRelativePath(Resource location, @Nullable HttpServletRequest request) {
return (!(location instanceof UrlResource) && request != null &&
ServletRequestPathUtils.hasCachedPath(request) &&
ServletRequestPathUtils.getCachedPath(request) instanceof PathContainer);
}
private boolean shouldEncodeRelativePath(Resource location) {
return (location instanceof UrlResource && this.urlPathHelper != null && this.urlPathHelper.isUrlDecode());
return (location instanceof UrlResource &&
this.urlPathHelper != null && this.urlPathHelper.isUrlDecode());
}
private boolean isInvalidEncodedPath(String resourcePath) {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -88,6 +88,15 @@ class MappedInterceptorTests {
assertThat(interceptor.matches(requestFactory.apply("/admin/foo"))).isFalse();
}
@PathPatternsParameterizedTest // gh-26690
void includePatternWithFallbackOnPathMatcher(Function<String, MockHttpServletRequest> requestFactory) {
MappedInterceptor interceptor = new MappedInterceptor(new String[] { "/path1/**/path2" }, null, delegate);
assertThat(interceptor.matches(requestFactory.apply("/path1/foo/bar/path2"))).isTrue();
assertThat(interceptor.matches(requestFactory.apply("/path1/foo/bar/path3"))).isFalse();
assertThat(interceptor.matches(requestFactory.apply("/path3/foo/bar/path2"))).isFalse();
}
@PathPatternsParameterizedTest
void customPathMatcher(Function<String, MockHttpServletRequest> requestFactory) {
MappedInterceptor interceptor = new MappedInterceptor(new String[] { "/foo/[0-9]*" }, null, delegate);
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -22,6 +22,7 @@ import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.servlet.http.HttpServletRequest;
@@ -31,8 +32,10 @@ import org.junit.jupiter.api.Test;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.server.RequestPath;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Controller;
import org.springframework.util.MultiValueMap;
import org.springframework.web.HttpMediaTypeNotAcceptableException;
@@ -177,10 +180,11 @@ class RequestMappingInfoHandlerMappingTests {
@PathPatternsParameterizedTest
void getHandlerHttpOptions(TestRequestMappingInfoHandlerMapping mapping) throws Exception {
testHttpOptions(mapping, "/foo", "GET,HEAD,OPTIONS");
testHttpOptions(mapping, "/person/1", "PUT,OPTIONS");
testHttpOptions(mapping, "/persons", "GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS");
testHttpOptions(mapping, "/something", "PUT,POST");
testHttpOptions(mapping, "/foo", "GET,HEAD,OPTIONS", null);
testHttpOptions(mapping, "/person/1", "PUT,OPTIONS", null);
testHttpOptions(mapping, "/persons", "GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS", null);
testHttpOptions(mapping, "/something", "PUT,POST", null);
testHttpOptions(mapping, "/qux", "PATCH,GET,HEAD,OPTIONS", new MediaType("foo", "bar"));
}
@PathPatternsParameterizedTest
@@ -401,8 +405,8 @@ class RequestMappingInfoHandlerMappingTests {
.satisfies(ex -> assertThat(ex.getSupportedMediaTypes()).containsExactly(MediaType.APPLICATION_XML));
}
private void testHttpOptions(
TestRequestMappingInfoHandlerMapping mapping, String requestURI, String allowHeader) throws Exception {
private void testHttpOptions(TestRequestMappingInfoHandlerMapping mapping, String requestURI,
String allowHeader, @Nullable MediaType acceptPatch) throws Exception {
MockHttpServletRequest request = new MockHttpServletRequest("OPTIONS", requestURI);
HandlerMethod handlerMethod = getHandler(mapping, request);
@@ -413,7 +417,15 @@ class RequestMappingInfoHandlerMappingTests {
assertThat(result).isNotNull();
assertThat(result.getClass()).isEqualTo(HttpHeaders.class);
assertThat(((HttpHeaders) result).getFirst("Allow")).isEqualTo(allowHeader);
HttpHeaders headers = (HttpHeaders) result;
Set<HttpMethod> allowedMethods = Arrays.stream(allowHeader.split(","))
.map(HttpMethod::valueOf)
.collect(Collectors.toSet());
assertThat(headers.getAllow()).hasSameElementsAs(allowedMethods);
if (acceptPatch != null && headers.getAllow().contains(HttpMethod.PATCH) ) {
assertThat(headers.getAcceptPatch()).containsExactly(acceptPatch);
}
}
private void testHttpMediaTypeNotAcceptableException(TestRequestMappingInfoHandlerMapping mapping, String url) {
@@ -502,6 +514,15 @@ class RequestMappingInfoHandlerMappingTests {
headers.add("Allow", "PUT,POST");
return headers;
}
@RequestMapping(value = "/qux", method = RequestMethod.GET, produces = "application/xml")
public String getBaz() {
return "";
}
@RequestMapping(value = "/qux", method = RequestMethod.PATCH, consumes = "foo/bar")
public void patchBaz(String value) {
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -113,6 +113,20 @@ public class ResponseEntityExceptionHandlerTests {
ResponseEntity<Object> responseEntity = testException(ex);
assertThat(responseEntity.getHeaders().getAccept()).isEqualTo(acceptable);
assertThat(responseEntity.getHeaders().getAcceptPatch()).isEmpty();
}
@Test
public void patchHttpMediaTypeNotSupported() {
this.servletRequest = new MockHttpServletRequest("PATCH", "/");
this.request = new ServletWebRequest(this.servletRequest, this.servletResponse);
List<MediaType> acceptable = Arrays.asList(MediaType.APPLICATION_ATOM_XML, MediaType.APPLICATION_XML);
Exception ex = new HttpMediaTypeNotSupportedException(MediaType.APPLICATION_JSON, acceptable);
ResponseEntity<Object> responseEntity = testException(ex);
assertThat(responseEntity.getHeaders().getAccept()).isEqualTo(acceptable);
assertThat(responseEntity.getHeaders().getAcceptPatch()).isEqualTo(acceptable);
}
@Test
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -976,6 +976,26 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl
assertThat(response.getHeader("Accept")).isEqualTo("text/plain");
}
@PathPatternsParameterizedTest
void unsupportedPatchBody(boolean usePathPatterns) throws Exception {
initDispatcherServlet(RequestResponseBodyController.class, usePathPatterns, wac -> {
RootBeanDefinition adapterDef = new RootBeanDefinition(RequestMappingHandlerAdapter.class);
StringHttpMessageConverter converter = new StringHttpMessageConverter();
converter.setSupportedMediaTypes(Collections.singletonList(MediaType.TEXT_PLAIN));
adapterDef.getPropertyValues().add("messageConverters", converter);
wac.registerBeanDefinition("handlerAdapter", adapterDef);
});
MockHttpServletRequest request = new MockHttpServletRequest("PATCH", "/something");
String requestBody = "Hello World";
request.setContent(requestBody.getBytes(StandardCharsets.UTF_8));
request.addHeader("Content-Type", "application/pdf");
MockHttpServletResponse response = new MockHttpServletResponse();
getServlet().service(request, response);
assertThat(response.getStatus()).isEqualTo(415);
assertThat(response.getHeader("Accept-Patch")).isEqualTo("text/plain");
}
@PathPatternsParameterizedTest
void responseBodyNoAcceptHeader(boolean usePathPatterns) throws Exception {
initDispatcherServlet(RequestResponseBodyController.class, usePathPatterns);
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -87,6 +87,18 @@ public class DefaultHandlerExceptionResolverTests {
assertThat(response.getHeader("Accept")).as("Invalid Accept header").isEqualTo("application/pdf");
}
@Test
public void patchHttpMediaTypeNotSupported() {
HttpMediaTypeNotSupportedException ex = new HttpMediaTypeNotSupportedException(new MediaType("text", "plain"),
Collections.singletonList(new MediaType("application", "pdf")));
MockHttpServletRequest request = new MockHttpServletRequest("PATCH", "/");
ModelAndView mav = exceptionResolver.resolveException(request, response, null, ex);
assertThat(mav).as("No ModelAndView returned").isNotNull();
assertThat(mav.isEmpty()).as("No Empty ModelAndView returned").isTrue();
assertThat(response.getStatus()).as("Invalid status code").isEqualTo(415);
assertThat(response.getHeader("Accept-Patch")).as("Invalid Accept header").isEqualTo("application/pdf");
}
@Test
public void handleMissingPathVariable() throws NoSuchMethodException {
Method method = getClass().getMethod("handle", String.class);
@@ -96,7 +108,8 @@ public class DefaultHandlerExceptionResolverTests {
assertThat(mav).as("No ModelAndView returned").isNotNull();
assertThat(mav.isEmpty()).as("No Empty ModelAndView returned").isTrue();
assertThat(response.getStatus()).as("Invalid status code").isEqualTo(500);
assertThat(response.getErrorMessage()).isEqualTo("Missing URI template variable 'foo' for method parameter of type String");
assertThat(response.getErrorMessage())
.isEqualTo("Required URI template variable 'foo' for method parameter type String is not present");
}
@Test
@@ -106,7 +119,8 @@ public class DefaultHandlerExceptionResolverTests {
assertThat(mav).as("No ModelAndView returned").isNotNull();
assertThat(mav.isEmpty()).as("No Empty ModelAndView returned").isTrue();
assertThat(response.getStatus()).as("Invalid status code").isEqualTo(400);
assertThat(response.getErrorMessage()).isEqualTo("Required bar parameter 'foo' is not present");
assertThat(response.getErrorMessage()).isEqualTo(
"Required request parameter 'foo' for method parameter type bar is not present");
}
@Test
@@ -0,0 +1,179 @@
/*
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.servlet.resource;
import java.io.IOException;
import java.net.MalformedURLException;
import java.nio.charset.StandardCharsets;
import java.util.stream.Stream;
import javax.servlet.ServletException;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.UrlResource;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.PathMatchConfigurer;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.testfixture.servlet.MockHttpServletRequest;
import org.springframework.web.testfixture.servlet.MockHttpServletResponse;
import org.springframework.web.testfixture.servlet.MockServletConfig;
import org.springframework.web.testfixture.servlet.MockServletContext;
import org.springframework.web.util.UriUtils;
import org.springframework.web.util.pattern.PathPatternParser;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.params.provider.Arguments.arguments;
/**
* Integration tests for static resource handling.
*
* @author Rossen Stoyanchev
*/
public class ResourceHttpRequestHandlerIntegrationTests {
private final MockServletContext servletContext = new MockServletContext();
private final MockServletConfig servletConfig = new MockServletConfig(this.servletContext);
public static Stream<Arguments> argumentSource() {
return Stream.of(
arguments(true, "/cp"),
arguments(true, "/fs"),
arguments(true, "/url"),
arguments(false, "/cp"),
arguments(false, "/fs"),
arguments(false, "/url")
);
}
@ParameterizedTest
@MethodSource("argumentSource")
void cssFile(boolean usePathPatterns, String pathPrefix) throws Exception {
MockHttpServletRequest request = initRequest(pathPrefix + "/test/foo.css");
MockHttpServletResponse response = new MockHttpServletResponse();
DispatcherServlet servlet = initDispatcherServlet(usePathPatterns, WebConfig.class);
servlet.service(request, response);
String description = "usePathPattern=" + usePathPatterns + ", prefix=" + pathPrefix;
assertThat(response.getStatus()).as(description).isEqualTo(200);
assertThat(response.getContentType()).as(description).isEqualTo("text/css");
assertThat(response.getContentAsString()).as(description).isEqualTo("h1 { color:red; }");
}
@ParameterizedTest
@MethodSource("argumentSource")
void classpathLocationWithEncodedPath(boolean usePathPatterns, String pathPrefix) throws Exception {
MockHttpServletRequest request = initRequest(pathPrefix + "/test/foo with spaces.css");
MockHttpServletResponse response = new MockHttpServletResponse();
DispatcherServlet servlet = initDispatcherServlet(usePathPatterns, WebConfig.class);
servlet.service(request, response);
String description = "usePathPattern=" + usePathPatterns + ", prefix=" + pathPrefix;
assertThat(response.getStatus()).as(description).isEqualTo(200);
assertThat(response.getContentType()).as(description).isEqualTo("text/css");
assertThat(response.getContentAsString()).as(description).isEqualTo("h1 { color:red; }");
}
private DispatcherServlet initDispatcherServlet(boolean usePathPatterns, Class<?>... configClasses)
throws ServletException {
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
context.register(configClasses);
if (usePathPatterns) {
context.register(PathPatternParserConfig.class);
}
context.setServletConfig(this.servletConfig);
context.refresh();
DispatcherServlet servlet = new DispatcherServlet();
servlet.setApplicationContext(context);
servlet.init(this.servletConfig);
return servlet;
}
private MockHttpServletRequest initRequest(String path) {
path = UriUtils.encodePath(path, StandardCharsets.UTF_8);
MockHttpServletRequest request = new MockHttpServletRequest("GET", path);
request.setCharacterEncoding(StandardCharsets.UTF_8.name());
return request;
}
@EnableWebMvc
static class WebConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
ClassPathResource classPathLocation = new ClassPathResource("", getClass());
String path = getPath(classPathLocation);
registerClasspathLocation("/cp/**", classPathLocation, registry);
registerFileSystemLocation("/fs/**", path, registry);
registerUrlLocation("/url/**", "file:" + path, registry);
}
protected void registerClasspathLocation(String pattern, ClassPathResource resource, ResourceHandlerRegistry registry) {
registry.addResourceHandler(pattern).addResourceLocations(resource);
}
protected void registerFileSystemLocation(String pattern, String path, ResourceHandlerRegistry registry) {
FileSystemResource fileSystemLocation = new FileSystemResource(path);
registry.addResourceHandler(pattern).addResourceLocations(fileSystemLocation);
}
protected void registerUrlLocation(String pattern, String path, ResourceHandlerRegistry registry) {
try {
UrlResource urlLocation = new UrlResource(path);
registry.addResourceHandler(pattern).addResourceLocations(urlLocation);
}
catch (MalformedURLException ex) {
throw new IllegalStateException(ex);
}
}
private String getPath(ClassPathResource resource) {
try {
return resource.getFile().getCanonicalPath().replace('\\', '/').replace("classes/java", "resources") + "/";
}
catch (IOException ex) {
throw new IllegalStateException(ex);
}
}
}
static class PathPatternParserConfig implements WebMvcConfigurer {
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
configurer.setPatternParser(new PathPatternParser());
}
}
}
+3 -2
View File
@@ -568,8 +568,9 @@ is a convenience mechanism that sets up a <<core.adoc#beans-factory-placeholderc
This element activates the Spring infrastructure to detect annotations in bean classes:
* Spring's <<core.adoc#beans-factory-metadata, `@Configuration`>> model
* <<core.adoc#beans-annotation-config, `@Autowired`/`@Inject`>> and `@Value`
* JSR-250's `@Resource`, `@PostConstruct` and `@PreDestroy` (if available)
* <<core.adoc#beans-annotation-config, `@Autowired`/`@Inject`>>, `@Value`, and `@Lookup`
* JSR-250's `@Resource`, `@PostConstruct`, and `@PreDestroy` (if available)
* JAX-WS's `@WebServiceRef` and EJB 3's `@EJB` (if available)
* JPA's `@PersistenceContext` and `@PersistenceUnit` (if available)
* Spring's <<core.adoc#context-functionality-events-annotation, `@EventListener`>>
+29 -28
View File
@@ -923,7 +923,7 @@ injection:
public class SimpleMovieLister {
// the SimpleMovieLister has a dependency on a MovieFinder
private MovieFinder movieFinder;
private final MovieFinder movieFinder;
// a constructor so that the Spring container can inject a MovieFinder
public SimpleMovieLister(MovieFinder movieFinder) {
@@ -943,7 +943,7 @@ injection:
----
Notice that there is nothing special about this class. It is a POJO that
has no dependencies on container specific interfaces, base classes or annotations.
has no dependencies on container specific interfaces, base classes, or annotations.
[[beans-factory-ctor-arguments-resolution]]
===== Constructor Argument Resolution
@@ -974,10 +974,10 @@ being instantiated. Consider the following class:
class ThingOne(thingTwo: ThingTwo, thingThree: ThingThree)
----
Assuming that `ThingTwo` and `ThingThree` classes are not related by inheritance, no potential
ambiguity exists. Thus, the following configuration works fine, and you do not need to specify
the constructor argument indexes or types explicitly in the `<constructor-arg/>`
element.
Assuming that the `ThingTwo` and `ThingThree` classes are not related by inheritance, no
potential ambiguity exists. Thus, the following configuration works fine, and you do not
need to specify the constructor argument indexes or types explicitly in the
`<constructor-arg/>` element.
[source,xml,indent=0,subs="verbatim,quotes"]
----
@@ -1006,10 +1006,10 @@ by type without help. Consider the following class:
public class ExampleBean {
// Number of years to calculate the Ultimate Answer
private int years;
private final int years;
// The Answer to Life, the Universe, and Everything
private String ultimateAnswer;
private final String ultimateAnswer;
public ExampleBean(int years, String ultimateAnswer) {
this.years = years;
@@ -1031,7 +1031,7 @@ by type without help. Consider the following class:
.[[beans-factory-ctor-arguments-type]]Constructor argument type matching
--
In the preceding scenario, the container can use type matching with simple types if
you explicitly specify the type of the constructor argument by using the `type` attribute.
you explicitly specify the type of the constructor argument by using the `type` attribute,
as the following example shows:
[source,xml,indent=0,subs="verbatim,quotes"]
@@ -1251,7 +1251,8 @@ visibility of some configuration issues is why `ApplicationContext` implementati
default pre-instantiate singleton beans. At the cost of some upfront time and memory to
create these beans before they are actually needed, you discover configuration issues
when the `ApplicationContext` is created, not later. You can still override this default
behavior so that singleton beans initialize lazily, rather than being pre-instantiated.
behavior so that singleton beans initialize lazily, rather than being eagerly
pre-instantiated.
If no circular dependencies exist, when one or more collaborating beans are being
injected into a dependent bean, each collaborating bean is totally configured prior
@@ -4606,8 +4607,8 @@ Annotation injection is performed before XML injection. Thus, the XML configurat
overrides the annotations for properties wired through both approaches.
====
As always, you can register them as individual bean definitions, but they can also be
implicitly registered by including the following tag in an XML-based Spring
As always, you can register the post-processors as individual bean definitions, but they
can also be implicitly registered by including the following tag in an XML-based Spring
configuration (notice the inclusion of the `context` namespace):
[source,xml,indent=0,subs="verbatim,quotes"]
@@ -4626,11 +4627,13 @@ configuration (notice the inclusion of the `context` namespace):
</beans>
----
(The implicitly registered post-processors include
{api-spring-framework}/beans/factory/annotation/AutowiredAnnotationBeanPostProcessor.html[`AutowiredAnnotationBeanPostProcessor`],
{api-spring-framework}/context/annotation/CommonAnnotationBeanPostProcessor.html[`CommonAnnotationBeanPostProcessor`],
{api-spring-framework}/orm/jpa/support/PersistenceAnnotationBeanPostProcessor.html[`PersistenceAnnotationBeanPostProcessor`], and
{api-spring-framework}/beans/factory/annotation/RequiredAnnotationBeanPostProcessor.html[`RequiredAnnotationBeanPostProcessor`].)
The `<context:annotation-config/>` element implicitly registers the following post-processors:
* {api-spring-framework}/context/annotation/ConfigurationClassPostProcessor.html[`ConfigurationClassPostProcessor`]
* {api-spring-framework}/beans/factory/annotation/AutowiredAnnotationBeanPostProcessor.html[`AutowiredAnnotationBeanPostProcessor`]
* {api-spring-framework}/context/annotation/CommonAnnotationBeanPostProcessor.html[`CommonAnnotationBeanPostProcessor`]
* {api-spring-framework}/orm/jpa/support/PersistenceAnnotationBeanPostProcessor.html[`PersistenceAnnotationBeanPostProcessor`]
* {api-spring-framework}/context/event/EventListenerMethodProcessor.html[`EventListenerMethodProcessor`]
[NOTE]
====
@@ -4687,10 +4690,7 @@ references and values even when you use the class outside of a container.
[TIP]
====
The {api-spring-framework}/beans/factory/annotation/RequiredAnnotationBeanPostProcessor.html[`RequiredAnnotationBeanPostProcessor`]
must be registered as a bean to enable support for the `@Required` annotation. Note,
however, that a `RequiredAnnotationBeanPostProcessor` bean is registered automatically
when using the `<context:annotation-config/>` or `<context:component-scan/>` XML
namespace elements.
must be registered as a bean to enable support for the `@Required` annotation.
====
[NOTE]
@@ -10652,9 +10652,8 @@ architectures that build upon the well-known Spring programming model.
[[context-functionality-events-annotation]]
==== Annotation-based Event Listeners
As of Spring 4.2, you can register an event listener on any public method of a managed
bean by using the `@EventListener` annotation. The `BlockedListNotifier` can be rewritten as
follows:
You can register an event listener on any method of a managed bean by using the
`@EventListener` annotation. The `BlockedListNotifier` can be rewritten as follows:
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
@@ -10792,9 +10791,9 @@ method signature to return the event that should be published, as the following
NOTE: This feature is not supported for
<<context-functionality-events-async, asynchronous listeners>>.
This new method publishes a new `ListUpdateEvent` for every `BlockedListEvent` handled by the
method above. If you need to publish several events, you can return a `Collection` of events
instead.
The `handleBlockedListEvent()` method publishes a new `ListUpdateEvent` for every
`BlockedListEvent` that it handles. If you need to publish several events, you can return
a `Collection` or an array of events instead.
[[context-functionality-events-async]]
@@ -10954,8 +10953,10 @@ location path as a classpath location. You can also use location paths (resource
with special prefixes to force loading of definitions from the classpath or a URL,
regardless of the actual context type.
[[context-functionality-startup]]
=== Application Startup tracking
=== Application Startup Tracking
The `ApplicationContext` manages the lifecycle of Spring applications and provides a rich
programming model around components. As a result, complex applications can have equally
+29 -6
View File
@@ -7471,12 +7471,35 @@ or reactive type such as Reactor `Mono`:
[[spring-mvc-test-vs-streaming-response]]
===== Streaming Responses
There are no options built into Spring MVC Test for container-less testing of streaming
responses. However you can test streaming requests through the <<WebTestClient>>.
This is also supported in Spring Boot where you can
{doc-spring-boot}/html/spring-boot-features.html#boot-features-testing-spring-boot-applications-testing-with-running-server[test a running server]
with `WebTestClient`. One extra advantage is the ability to use the `StepVerifier` from
project Reactor that allows declaring expectations on a stream of data.
The best way to test streaming responses such as Server-Sent Events is through the
<<WebTestClient>> which can be used as a test client to connect to a `MockMvc` instance
to perform tests on Spring MVC controllers without a running server. For example:
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
----
WebTestClient client = MockMvcWebTestClient.bindToController(new SseController()).build();
FluxExchangeResult<Person> exchangeResult = client.get()
.uri("/persons")
.exchange()
.expectStatus().isOk()
.expectHeader().contentType("text/event-stream")
.returnResult(Person.class);
// Use StepVerifier from Project Reactor to test the streaming response
StepVerifier.create(exchangeResult.getResponseBody())
.expectNext(new Person("N0"), new Person("N1"), new Person("N2"))
.expectNextCount(4)
.consumeNextWith(person -> assertThat(person.getName()).endsWith("7"))
.thenCancel()
.verify();
----
`WebTestClient` can also connect to a live server and perform full end-to-end integration
tests. This is also supported in Spring Boot where you can
{doc-spring-boot}/html/spring-boot-features.html#boot-features-testing-spring-boot-applications-testing-with-running-server[test a running server].
[[spring-mvc-test-server-filters]]
+1 -1
View File
@@ -1521,7 +1521,7 @@ segments. For example `/resources/{*path}` matches all files under `/resources/`
`"path"` variable captures the complete relative path.
The syntax `{varName:regex}` declares a URI variable with a regular expression that has the
syntax: `{varName:regex}`. For example, given a URL of `/spring-web-3.0.5 .jar`, the following method
syntax: `{varName:regex}`. For example, given a URL of `/spring-web-3.0.5.jar`, the following method
extracts the name, version, and file extension:
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
+1 -1
View File
@@ -1618,7 +1618,7 @@ leave that detail out if the names are the same and your code is compiled with d
information or with the `-parameters` compiler flag on Java 8.
The syntax `{varName:regex}` declares a URI variable with a regular expression that has
syntax of `{varName:regex}`. For example, given URL `"/spring-web-3.0.5 .jar"`, the following method
syntax of `{varName:regex}`. For example, given URL `"/spring-web-3.0.5.jar"`, the following method
extracts the name, version, and file extension:
[source,java,indent=0,subs="verbatim,quotes",role="primary"]