mirror of
https://github.com/spring-projects/spring-framework
synced 2026-06-08 17:33:33 +00:00
Compare commits
58 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 83c6d10095 | |||
| f31933e67e | |||
| c7989c78c8 | |||
| f03ccd5cc9 | |||
| bcc6171a72 | |||
| a80c5fcfaf | |||
| 04ce8e0ac4 | |||
| 74f7eb11be | |||
| e05b584623 | |||
| 29955a2898 | |||
| 4b6b12bf2f | |||
| ab0e8f0617 | |||
| 0a6a6d48e2 | |||
| a08593b44b | |||
| 7954dc7a00 | |||
| 22d9012081 | |||
| 327e761536 | |||
| 4e42b5dfbc | |||
| 432c720dd0 | |||
| f7606ba51b | |||
| 0f54585f05 | |||
| d3d6e100da | |||
| a48c5f6a0d | |||
| 35ea33e13a | |||
| a2d91a562d | |||
| 97f3846971 | |||
| 44e1d6d1bf | |||
| 7adac25e7e | |||
| 7dca6a36a6 | |||
| c5ac8e8ab6 | |||
| fa4c62cab2 | |||
| 8a1182a678 | |||
| 01bea34569 | |||
| aec151e014 | |||
| 58e40d1aa2 | |||
| 85dbd2a4c3 | |||
| eca87b87de | |||
| 5e1e29a2dd | |||
| 7dc3a55648 | |||
| f982fd99d7 | |||
| ab18ab6025 | |||
| d83fb09914 | |||
| b651c10e83 | |||
| 5651695fd1 | |||
| 1a9556925d | |||
| 2dc203ffdc | |||
| 610de3ae78 | |||
| 5b2ace5742 | |||
| 5b227e5677 | |||
| 41d5048280 | |||
| a931ff12f6 | |||
| e4f753e3e3 | |||
| 4982b5fcb9 | |||
| f7678cdcdd | |||
| 3ab39eda08 | |||
| 14a6909c4a | |||
| fd17738f76 | |||
| f674652dd0 |
+16
-16
@@ -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
@@ -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
@@ -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,7 +1,7 @@
|
||||
---
|
||||
platform: linux
|
||||
image_resource:
|
||||
type: docker-image
|
||||
type: registry-image
|
||||
source:
|
||||
repository: springio/github-changelog-generator
|
||||
tag: '0.0.6'
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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"
|
||||
}
|
||||
|
||||
|
||||
+10
-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 {
|
||||
|
||||
+21
-25
@@ -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();
|
||||
|
||||
+1
-1
@@ -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);
|
||||
|
||||
+35
-2
@@ -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 {
|
||||
|
||||
+18
-1
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
+71
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
+7
-2
@@ -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);
|
||||
};
|
||||
|
||||
+2
-1
@@ -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();
|
||||
|
||||
|
||||
+2
-3
@@ -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.
|
||||
|
||||
+24
-3
@@ -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,
|
||||
|
||||
+2
-2
@@ -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);
|
||||
|
||||
+5
-5
@@ -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 {
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
+107
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+107
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+14
-13
@@ -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"));
|
||||
|
||||
+71
@@ -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" />
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+9
-8
@@ -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) {
|
||||
|
||||
+21
-8
@@ -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;
|
||||
|
||||
+12
-8
@@ -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();
|
||||
|
||||
+6
-2
@@ -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);
|
||||
|
||||
+19
-5
@@ -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");
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+19
-5
@@ -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");
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+19
-5
@@ -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");
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+19
-5
@@ -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");
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+50
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
+19
-4
@@ -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;
|
||||
}
|
||||
|
||||
+8
-1
@@ -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 {
|
||||
|
||||
|
||||
+15
-2
@@ -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.
|
||||
*/
|
||||
|
||||
+7
-1
@@ -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 {
|
||||
|
||||
|
||||
+16
-2
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+38
-2
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+19
@@ -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)));
|
||||
}
|
||||
|
||||
+15
@@ -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";
|
||||
|
||||
+18
-12
@@ -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();
|
||||
}
|
||||
|
||||
+20
-1
@@ -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 {
|
||||
|
||||
+16
-15
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
+11
-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.
|
||||
@@ -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--
|
||||
+2
-2
@@ -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);
|
||||
|
||||
+23
-4
@@ -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) {
|
||||
|
||||
+13
@@ -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].
|
||||
*/
|
||||
|
||||
+22
-1
@@ -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()));
|
||||
|
||||
+47
-7
@@ -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() { }
|
||||
}
|
||||
|
||||
|
||||
+19
@@ -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
@@ -0,0 +1 @@
|
||||
h1 { color:red; }
|
||||
+2
-2
@@ -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
|
||||
*/
|
||||
|
||||
+75
-30
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+20
-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.
|
||||
@@ -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) {
|
||||
|
||||
+7
-1
@@ -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 {
|
||||
|
||||
|
||||
+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.
|
||||
@@ -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;
|
||||
|
||||
+8
-1
@@ -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,
|
||||
|
||||
+4
-4
@@ -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,
|
||||
|
||||
+8
-9
@@ -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;
|
||||
}
|
||||
|
||||
+7
-1
@@ -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);
|
||||
|
||||
+3
@@ -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();
|
||||
|
||||
+17
-5
@@ -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) {
|
||||
|
||||
+10
-1
@@ -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);
|
||||
|
||||
+29
-8
@@ -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) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
+15
-1
@@ -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
|
||||
|
||||
+21
-1
@@ -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);
|
||||
|
||||
+17
-3
@@ -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
|
||||
|
||||
+179
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
h1 { color:red; }
|
||||
@@ -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`>>
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]]
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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"]
|
||||
|
||||
Reference in New Issue
Block a user